openclaw - ✅(Solved) Fix [Bug]: `JSON.stringify` silently drops UUID keys from array-typed `pending.json`, breaking all device pairing [4 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#63035Fetched 2026-04-09 07:59:14
View on GitHub
Comments
2
Participants
2
Timeline
14
Reactions
1
Author
Timeline (top)
cross-referenced ×4referenced ×4commented ×2labeled ×2

pending.json and paired.json in the devices state directory can end up containing [] (empty JSON arrays) instead of {} (empty objects). When loadState reads these files, the pending ?? {} fallback passes [] through because arrays are truthy. Subsequent code assigns UUID-keyed pairing requests onto the array object, which JavaScript accepts silently — but JSON.stringify drops all non-numeric keys from arrays. Every persist cycle writes [] back to disk, silently destroying all pending pairing state. This causes approveDevicePairing to return null on every silent auto-approval attempt, breaking CLI Docker pairing and potentially all device pairing flows.

Workaround: replace both files with {} and restart the gateway.

Suggested fix: guard 'loadState' against array-typed JSON with 'Array.isArray()' coercion.

Related

  • PR #55113 — fixes CLI Docker pairing locality detection. That fix is working correctly. This bug is downstream in the device pairing state layer where loadState does not guard against pending.json / paired.json containing [] instead of {}.

Error Message

  1. Observe error: gateway closed (1008): pairing required Severity: Blocks workflow. CLI commands are completely unusable from Docker containers. No error message indicates the root cause — the gateway reports pairing required which leads users toward auth and network debugging rather than state file corruption. Consequence: Complete loss of CLI access in Docker deployments. Users cannot run openclaw cron list, openclaw devices list, or any other CLI command through the container. The misleading error message makes diagnosis extremely difficult without source-level instrumentation. Workaround is non-obvious — manually replacing file contents with {}.

Root Cause

pending.json and paired.json in the devices state directory can end up containing [] (empty JSON arrays) instead of {} (empty objects). When loadState reads these files, the pending ?? {} fallback passes [] through because arrays are truthy. Subsequent code assigns UUID-keyed pairing requests onto the array object, which JavaScript accepts silently — but JSON.stringify drops all non-numeric keys from arrays. Every persist cycle writes [] back to disk, silently destroying all pending pairing state. This causes approveDevicePairing to return null on every silent auto-approval attempt, breaking CLI Docker pairing and potentially all device pairing flows.

Fix Action

Fix / Workaround

Workaround: replace both files with {} and restart the gateway.

Consequence: Complete loss of CLI access in Docker deployments. Users cannot run openclaw cron list, openclaw devices list, or any other CLI command through the container. The misleading error message makes diagnosis extremely difficult without source-level instrumentation. Workaround is non-obvious — manually replacing file contents with {}.

PR fix notes

PR #63072: fix: guard loadState against array-typed pending/paired JSON files

Description (problem / solution / changelog)

Summary

  • Problem: pending.json / paired.json can contain [] instead of {}. The nullish coalescing operator (??) passes arrays through because they are truthy.
  • Why it matters: UUID-keyed pairing requests assigned to arrays are silently dropped by JSON.stringify on persist, breaking all device and node pairing flows. CLI commands fail with misleading "pairing required" error.
  • What changed: Added Array.isArray() guards in loadState for both device-pairing.ts and node-pairing.ts to coerce arrays to empty objects.
  • What did NOT change (scope boundary): No changes to persistState, readJsonFile, or any other pairing logic. Only the deserialization guard.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #63035
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: loadState uses pending ?? {} which passes [] through since arrays are truthy. JSON.stringify silently drops non-numeric keys from arrays, so UUID-keyed entries are lost on every persist cycle.
  • Missing detection / guardrail: No type guard for array-vs-object on deserialized JSON state files.
  • Contributing context (if known): State files can end up as [] from previous version migration or corrupted init.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
  • Target test or file: src/infra/pairing-pending.test.ts or a new device-pairing.test.ts
  • Scenario the test should lock in: loadState returns {} when state file contains []
  • Why this is the smallest reliable guardrail: Tests the exact deserialization boundary where corruption enters
  • If no new test is added, why not: The fix is a 2-line guard; happy to add a test if maintainers prefer

User-visible / Behavior Changes

None — this fix restores expected behavior. Device pairing works correctly when state files contain [].

Diagram (if applicable)

Before:
[readJsonFile → []] -> [pending ?? {} → []] -> [assign UUID key → silent] -> [JSON.stringify → []] -> data lost

After:
[readJsonFile → []] -> [Array.isArray check → {}] -> [assign UUID key → works] -> [JSON.stringify → {uuid: ...}] -> data persisted

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Ubuntu 24.04 (Azure Linux)
  • Runtime/container: Node.js v22.22.0
  • Model/provider: N/A (infrastructure bug)

Steps

  1. Ensure devices/pending.json contains []
  2. Attempt CLI connection or device pairing
  3. Observe "pairing required" rejection

Expected

  • Pairing request is stored and approved

Actual

  • Pairing request is silently dropped on persist, approval returns null

Evidence

  • Trace/log snippets (see issue #63035 for detailed gateway logs)

Human Verification (required)

  • Verified scenarios: Code review of both changed files confirms guard matches issue description
  • Edge cases checked: null, undefined, [], {}, valid object — all produce correct output
  • What you did not verify: Did not reproduce in Docker environment (no Docker setup locally)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: None — strictly additive guard, no behavior change for valid {} state files.

Changed files

  • src/infra/device-pairing.ts (modified, +2/-2)
  • src/infra/node-pairing.ts (modified, +2/-2)

PR #63078: fix: guard loadState against array-typed state files

Description (problem / solution / changelog)

Summary

  • Problem: loadState in device-pairing.ts uses pending ?? {} to fall back to an empty object, but when pending.json or paired.json contains [] (an empty array), ?? does not trigger because arrays are truthy. UUID-keyed entries assigned to the resulting array are silently dropped by JSON.stringify, breaking device pairing approval.
  • Why it matters: Docker Compose deployments where state files get initialized as [] cannot complete device pairing — approveDevicePairing always returns null.
  • What changed: Replaced pending ?? {} with pending && !Array.isArray(pending) ? pending : {} (same for paired) so that array-shaped state files are normalized to {} on load. Added a regression test that seeds both files with [] and verifies the full request → approve → persist round-trip.
  • What did NOT change (scope boundary): No changes to readJsonFile, writeJsonAtomic, or any other state file handling. Only the loadState function's fallback logic was tightened.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #63035
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: The nullish coalescing operator (??) only guards against null/undefined, not against unexpected value types like arrays. [] is truthy so pending ?? {} passes the array through.
  • Missing detection / guardrail: No type assertion or shape validation on the parsed JSON before assigning to the typed state object.
  • Contributing context: State files can end up as [] in Docker Compose deployments when the volume is initialized or reset.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/infra/device-pairing.test.ts
  • Scenario the test should lock in: When pending.json and paired.json both contain [], requestDevicePairingapproveDevicePairing should succeed, and persisted files should contain objects (not arrays).
  • Why this is the smallest reliable guardrail: A unit test at the loadState consumer level directly exercises the buggy code path with minimal setup.
  • Existing test that already covers this: None.

User-visible / Behavior Changes

Device pairing in Docker Compose deployments with array-typed state files will now succeed instead of silently failing.

Diagram (if applicable)

Before:
[readJsonFile returns []] -> [pending ?? {} → []] -> [UUID keys on array] -> [JSON.stringify drops keys] -> [approval fails]

After:
[readJsonFile returns []] -> [Array.isArray check → {}] -> [UUID keys on object] -> [JSON.stringify preserves keys] -> [approval succeeds]

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS (also affects Linux Docker)
  • Runtime/container: Node.js / Docker Compose

Steps

  1. Ensure devices/pending.json and devices/paired.json contain []
  2. Run requestDevicePairing followed by approveDevicePairing
  3. Observe result

Expected

  • approveDevicePairing returns { status: 'approved', ... }

Actual (before fix)

  • approveDevicePairing returns null

Evidence

  • Failing test/log before + passing after
  • All 32 tests pass including the new regression test

Human Verification (required)

  • Verified scenarios: Full request → approve → persist round-trip with []-typed state files
  • Edge cases checked: Verified that normal {} state files still work (all 31 existing tests pass)
  • What you did not verify: Production Docker Compose deployment end-to-end

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: If a state file somehow contains a non-array, non-object truthy value (e.g., a string), the Array.isArray check alone won't catch it.
    • Mitigation: readJsonFile is typed to return Record<string, T> | null, so non-object returns are already outside the expected contract. The Array.isArray guard handles the most likely failure mode.

<sub>🔧 Generated by issue-to-pr</sub>

Changed files

  • src/infra/device-pairing.test.ts (modified, +39/-1)
  • src/infra/device-pairing.ts (modified, +2/-2)

PR #63081: fix(gateway): guard loadState against array-typed pairing state files

Description (problem / solution / changelog)

loadState reads pending.json and paired.json via pending ?? {}, which passes through [] since arrays are truthy. UUID keys set on an array are silently dropped by JSON.stringify, destroying all pairing state on every persist cycle.

Applies the same guard to both device-pairing and node-pairing.

Closes #63035

Summary

  • Problem: loadState in device-pairing.ts and node-pairing.ts uses pending ?? {} which does not guard against []. Arrays are truthy, so [] passes through. UUID-keyed entries are set on the array in memory but JSON.stringify silently drops non-numeric array keys, writing [] back to disk on every persist.
  • Why it matters: All device pairing operations fail silently. Silent auto-approval returns null, causing CLI Docker connections to be rejected with pairing required (1008) despite valid token auth and correct locality detection. Affects any deployment where state files contain [] instead of {}.
  • What changed: Added Array.isArray() guards in loadState for both device-pairing.ts and node-pairing.ts so that [] is coerced to {} on read.
  • What did NOT change (scope boundary): No changes to persistState, approveDevicePairing, requestDevicePairing, pairing locality logic, or auth validation. Write path is unmodified — the fix is read-side only.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #63035
  • Related #55113
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: loadState assigns pending ?? {} without checking the type. readJsonFile can return [] (a valid JSON parse result), which is truthy and passes through the nullish coalescing operator. UUID keys are then set on an array object. JSON.stringify serializes arrays by numeric index only, silently dropping all string-keyed entries on persist.
  • Missing detection / guardrail: No type assertion or Array.isArray check on the parsed JSON before assigning to pendingById / pairedByDeviceId / pairedByNodeId. No test coverage for malformed state file recovery.
  • Contributing context (if known): Unknown what originally wrote [] to the state files — possibly an older version migration, a failed init path, or a race condition.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/infra/device-pairing.test.ts
  • Scenario the test should lock in: loadState recovers gracefully when pending.json or paired.json contain [] instead of {}. A pairing request created after recovery should be findable by approveDevicePairing.
  • Why this is the smallest reliable guardrail: Directly exercises the read path with the known bad input.
  • Existing test that already covers this (if any): None.
  • If no new test is added, why not: Test should be added. I did not have a working test harness to validate against — happy to add one if a maintainer can point to the existing test setup pattern for device-pairing.ts.

User-visible / Behavior Changes

  • Device pairing silent auto-approval works correctly when state files contain [] instead of {}
  • CLI Docker commands no longer fail with pairing required when state files are corrupted

Diagram (if applicable)

Before:
readJsonFile(pending.json) -> [] -> pendingById = [] -> UUID key set on array -> JSON.stringify -> [] -> data lost

After:
readJsonFile(pending.json) -> [] -> Array.isArray check -> pendingById = {} -> UUID key set on object -> JSON.stringify -> {"uuid": ...} -> data preserved

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Ubuntu (headless)
  • Runtime/container: Docker Engine + Compose v2, host networking, network_mode: "service:openclaw-gateway" for CLI
  • Model/provider: Not model-specific
  • Integration/channel (if any): N/A
  • Relevant config (redacted): gateway.auth.mode: "token", gateway.bind: "lan", standard docker-compose.yml from repo

Steps

  1. Deploy with Docker Compose (gateway + CLI containers sharing network namespace)
  2. Ensure devices/pending.json and devices/paired.json contain [] instead of {}
  3. Run: docker exec openclaw-openclaw-cli-1 /usr/local/bin/openclaw cron list

Expected

  • CLI command succeeds, cron jobs listed

Actual

  • gateway closed (1008): pairing required
  • approveDevicePairing returns null because loadState reads [] and finds no pending entry

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Debug instrumentation confirmed:

[PERSIST] {"pendingKeys":["4bf6458c-631f-44b2-ba9e-d27f3aa96e09"],"pairedKeys":[],"baseDir":"default"}
[APPROVE-STATE] {"requestId":"4bf6458c-631f-44b2-ba9e-d27f3aa96e09","pendingKeys":[],"pairedKeys":[]}

persistState writes with the UUID key present, but loadState immediately reads back empty — because JSON.stringify on an array drops string keys.

After replacing state files with {} and restarting, CLI commands work immediately.

Human Verification (required)

  • Verified scenarios: Replaced [] with {} in both state files, restarted gateway, CLI cron list returned 14 jobs successfully.
  • Edge cases checked: Both device-pairing.ts and node-pairing.ts patched with the same guard.
  • What you did not verify: Did not run the full test suite (no local test harness). Did not verify node-pairing.ts end-to-end (no node pairing test environment). Did not identify what originally wrote [] to the state files.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: If a future code path intentionally writes [] to state files expecting array semantics, this guard would silently convert it to {}.
    • Mitigation: The type annotations (Record<string, ...>) on pendingById and pairedByDeviceId explicitly expect objects, not arrays. Array state files are always invalid.

Changed files

  • src/infra/device-pairing.ts (modified, +2/-2)
  • src/infra/node-pairing.ts (modified, +2/-2)

PR #63086: fix(device-pairing): coerce array-typed state files to plain objects in loadState

Description (problem / solution / changelog)

Summary

  • Problem: loadState() in src/infra/device-pairing.ts:111 and src/infra/node-pairing.ts:141 uses nullish coalescing (pending ?? {}, paired ?? {}) to default the return value of readJsonFile(). When pending.json or paired.json contains a JSON array ([]) instead of an object, the array passes through because arrays are truthy in JavaScript. UUID-keyed pairing requests assigned onto the array are then silently dropped by JSON.stringify() on the next persistState() call (line 119), permanently destroying all device/node pairing state. Every Docker CLI command fails with gateway closed (1008): pairing required.

  • Root Cause: readJsonFile<T>() in src/infra/json-files.ts:26 is a generic JSON deserializer that returns whatever JSON.parse() produces — including arrays and primitives. The TypeScript generic T provides zero runtime safety (it uses as T cast). The ?? {} guard in loadState() only catches null and undefined, not type mismatches. When the persisted file contains [], the array is truthy, so ?? does not trigger. JavaScript silently accepts UUID string-key assignments on arrays (arr["some-uuid"] = value works in memory), but JSON.stringify() only serializes numeric-index entries for arrays, discarding all string-keyed data. This creates a permanent self-amplifying data-loss loop: persistState() writes [] back to disk, and every subsequent loadState() reads [] again. The sibling module src/infra/device-bootstrap.ts (line 136) already guards against this exact scenario with an explicit Array.isArray() check, but the pairing loaders were missing this validation.

  • Fix: Introduce a shared, exported coerceToRecord<T>(value: unknown): Record<string, T> helper in src/infra/pairing-files.ts that rejects null, undefined, arrays, and primitive values, returning {} for any non-plain-object input. Replace ?? {} with coerceToRecord() in both device-pairing.ts and node-pairing.ts loadState() functions. This approach avoids side effects because: (1) it follows the existing project pattern in device-bootstrap.ts; (2) it does not modify the generic readJsonFile() or writeJsonAtomic() utilities used by 20+ callers; (3) for valid Record<string, T> inputs it returns the original object reference with zero overhead; (4) the fix is self-healing — the first loadState() coerces [] to {}, and the next persistState() writes a valid object back to disk, permanently repairing the corrupted file.

  • What changed:

    • src/infra/pairing-files.ts — added exported coerceToRecord<T>() helper with JSDoc (+16 lines)
    • src/infra/device-pairing.ts — imported coerceToRecord, replaced pending ?? {} and paired ?? {} with coerceToRecord(pending) and coerceToRecord(paired) in loadState() (+3/-2 lines)
    • src/infra/node-pairing.ts — identical change to its loadState() (+3/-2 lines)
    • src/infra/pairing-files.test.ts — added 7 unit tests for coerceToRecord covering null, undefined, empty array, non-empty array, primitives, plain object pass-through, empty object pass-through (+43 lines)
    • src/infra/device-pairing.test.ts — added 1 integration test that seeds [] into both state files and verifies full request-approve-persist round-trip with JSON round-trip validation (+38 lines)
  • What did NOT change (scope boundary): readJsonFile() and writeJsonAtomic() in json-files.ts are untouched — they are generic utilities and type validation is the caller's responsibility. persistState() is unchanged. No changes to gateway handshake logic, CLI, auth flow, or any module outside src/infra/. No changes to device-bootstrap.ts (it already has its own guard). Total: 5 files changed, +103/-4 lines.

Reproduction

  1. Deploy OpenClaw v2026.4.8 via Docker Compose with gateway and CLI containers sharing a network namespace
  2. Ensure ~/.openclaw/devices/pending.json and paired.json contain [] instead of {} (can occur from version migration or corrupted init)
  3. Run: docker exec openclaw-openclaw-cli-1 /usr/local/bin/openclaw cron list
  4. Observe error: gateway closed (1008): pairing required
  5. Gateway log shows: closed before connect remote=127.0.0.1 code=1008 reason=pairing required
  6. Replace both files with {} and restart gateway — CLI works immediately (confirming root cause)

Risk / Mitigation

  • Risk: If any legitimate use case stores arrays in pending.json or paired.json, the data would be silently converted to an empty object.

  • Mitigation: The pairing state schema (DevicePairingStateFile, NodePairingStateFile) explicitly defines pendingById and pairedByDeviceId/pairedByNodeId as Record<string, T>. Arrays are never a valid state format. The sibling device-bootstrap.ts already applies the same coercion pattern in production without issues.

  • Risk: node-pairing.ts change is not covered by a dedicated integration test.

  • Mitigation: The shared coerceToRecord() function is thoroughly unit-tested with 7 cases. The node-pairing.ts loadState() is structurally identical to device-pairing.ts and calls the same shared helper. The device-pairing integration test validates the full pipeline end-to-end.

  • Risk: Test coverage — 43 tests all pass (32 device-pairing + 11 pairing-files), zero regressions confirmed via npx vitest run --config vitest.infra.config.ts.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway / orchestration
  • Auth / tokens
  • Memory / storage

Linked Issue/PR

Fixes #63035

Changed files

  • src/infra/device-pairing.test.ts (modified, +38/-0)
  • src/infra/device-pairing.ts (modified, +3/-2)
  • src/infra/node-pairing.ts (modified, +3/-2)
  • src/infra/pairing-files.test.ts (modified, +43/-0)
  • src/infra/pairing-files.ts (modified, +16/-0)

Code Example

See additional information below for logs.

POTENTIAL FIX

In 'loadState' in 'device-pairing-C2yQw1er.js', change:


pendingById: pending ?? {},
pairedByDeviceId: paired ?? {}

to:

pendingById: (pending && !Array.isArray(pending)) ? pending : {},
pairedByDeviceId: (paired && !Array.isArray(paired)) ? paired : {}

---

[ws] closed before connect conn=1fcb55a9-f186-4918-b33f-ee7283113ae6 remote=127.0.0.1 fwd=n/a origin=n/a host=127.0.0.1:18789 ua=n/a code=1008 reason=pairing required

---

[PAIR-REJECT] {"locality":"direct_local","skipBackend":false,"skipCtrlUi":false,"clientId":"cli","clientMode":"cli","isLocal":true,"hasDevice":true,"authOk":true,"authMethod":"token","sharedAuthOk":true}

---

[PAIR-REJECT-DETAIL] {"silent":true,"resolvedByConcurrent":false,"allowSilentLocal":true,"reason":"not-paired"}

---

[PAIR-APPROVE-RESULT] {"approved":"null","pairing":"{\"requestId\":\"05a77604-dc84-474b-bc4d-abf0ac144edf\",\"silent\":true}"}

---

[PERSIST] {"pendingKeys":["4bf6458c-631f-44b2-ba9e-d27f3aa96e09"],"pairedKeys":[],"baseDir":"default"}
[APPROVE-STATE] {"requestId":"4bf6458c-631f-44b2-ba9e-d27f3aa96e09","pendingKeys":[],"pairedKeys":[]}

---

$ docker exec openclaw-openclaw-gateway-1 cat /home/node/.openclaw/devices/pending.json
[]
$ docker exec openclaw-openclaw-gateway-1 cat /home/node/.openclaw/devices/paired.json
[]

---

$ docker exec openclaw-openclaw-cli-1 /usr/local/bin/openclaw cron list
ID                                   Name                     Schedule          ...
dc3bc3e5-fe8d-42f1-be13-bf888a70ef69 proactive-surprise-check cron 24 */4 * * * ...
(14 jobs listed successfully)
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

pending.json and paired.json in the devices state directory can end up containing [] (empty JSON arrays) instead of {} (empty objects). When loadState reads these files, the pending ?? {} fallback passes [] through because arrays are truthy. Subsequent code assigns UUID-keyed pairing requests onto the array object, which JavaScript accepts silently — but JSON.stringify drops all non-numeric keys from arrays. Every persist cycle writes [] back to disk, silently destroying all pending pairing state. This causes approveDevicePairing to return null on every silent auto-approval attempt, breaking CLI Docker pairing and potentially all device pairing flows.

Workaround: replace both files with {} and restart the gateway.

Suggested fix: guard 'loadState' against array-typed JSON with 'Array.isArray()' coercion.

Related

  • PR #55113 — fixes CLI Docker pairing locality detection. That fix is working correctly. This bug is downstream in the device pairing state layer where loadState does not guard against pending.json / paired.json containing [] instead of {}.

Steps to reproduce

  1. Deploy OpenClaw via Docker Compose with gateway and CLI containers sharing a network namespace
  2. Ensure gateway auth mode is set to token with a valid OPENCLAW_GATEWAY_TOKEN
  3. Confirm that devices/pending.json and devices/paired.json contain [] instead of {} (this can occur from a previous version migration or corrupted init)
  4. Run: docker exec openclaw-openclaw-cli-1 /usr/local/bin/openclaw cron list
  5. Observe error: gateway closed (1008): pairing required
  6. The gateway log shows: closed before connect remote=127.0.0.1 code=1008 reason=pairing required
  7. Silent auto-approval is attempted (pairing.request.silent=true, allowSilentLocalPairing=true) but approveDevicePairing returns null because loadState reads [] and finds no pending entry despite one having just been written
  8. Replace both files with {} and restart the gateway — CLI commands work immediately

Expected behavior

approveDevicePairing should find the pending request created by requestDevicePairing and return status: "approved", allowing the CLI connection to complete without manual pairing intervention. loadState should treat [] the same as a missing or empty file and coerce it to {}.

Actual behavior

loadState reads [] from pending.json and assigns it directly to pendingById. UUID-keyed pairing requests are set on the array in memory but silently dropped by JSON.stringify on persist. approveDevicePairing loads the file, finds no matching request, and returns null. The gateway rejects the CLI connection with close(1008, "pairing required") despite valid token auth, correct client identity, and locality: "direct_local".

OpenClaw version

2026.4.8

Operating system

Ubuntu 24.04.4 LTS

Install method

Docker

Model

anthropic/claude-sonnet-4.6

Provider / routing chain

Not model-specific — this is a gateway infrastructure bug in the device pairing state layer. Reproduces regardless of provider or model configuration.

Additional provider/model setup details

Gateway auth mode: token. Gateway bind: lan. CLI container shares gateway network namespace via network_mode: "service:openclaw-gateway". Config delivered via mounted OPENCLAW_CONFIG_DIR volume. Token passed to CLI container via OPENCLAW_GATEWAY_TOKEN environment variable. Not relevant to reproduction — the bug is in the device pairing state layer and is provider-independent.

Logs, screenshots, and evidence

See additional information below for logs.

POTENTIAL FIX

In 'loadState' in 'device-pairing-C2yQw1er.js', change:


pendingById: pending ?? {},
pairedByDeviceId: paired ?? {}

to:

pendingById: (pending && !Array.isArray(pending)) ? pending : {},
pairedByDeviceId: (paired && !Array.isArray(paired)) ? paired : {}

Impact and severity

Affected users/systems/channels: Any Docker deployment where devices/pending.json or devices/paired.json contain [] instead of {}. Affects all device pairing operations — CLI Docker pairing, and potentially Control UI and node pairing flows that rely on approveDevicePairing.

Severity: Blocks workflow. CLI commands are completely unusable from Docker containers. No error message indicates the root cause — the gateway reports pairing required which leads users toward auth and network debugging rather than state file corruption.

Frequency: Always, once the state files are in the corrupted [] format. Every CLI connection attempt fails deterministically. The corruption persists across gateway restarts since each persist cycle writes [] back to disk.

Consequence: Complete loss of CLI access in Docker deployments. Users cannot run openclaw cron list, openclaw devices list, or any other CLI command through the container. The misleading error message makes diagnosis extremely difficult without source-level instrumentation. Workaround is non-obvious — manually replacing file contents with {}.

Additional information

Gateway log showing rejection:

[ws] closed before connect conn=1fcb55a9-f186-4918-b33f-ee7283113ae6 remote=127.0.0.1 fwd=n/a origin=n/a host=127.0.0.1:18789 ua=n/a code=1008 reason=pairing required

Debug instrumentation at rejection point:

[PAIR-REJECT] {"locality":"direct_local","skipBackend":false,"skipCtrlUi":false,"clientId":"cli","clientMode":"cli","isLocal":true,"hasDevice":true,"authOk":true,"authMethod":"token","sharedAuthOk":true}

Silent approval attempted but failed:

[PAIR-REJECT-DETAIL] {"silent":true,"resolvedByConcurrent":false,"allowSilentLocal":true,"reason":"not-paired"}

approveDevicePairing returns null — no pending request found:

[PAIR-APPROVE-RESULT] {"approved":"null","pairing":"{\"requestId\":\"05a77604-dc84-474b-bc4d-abf0ac144edf\",\"silent\":true}"}

persistState writes the request but loadState reads empty:

[PERSIST] {"pendingKeys":["4bf6458c-631f-44b2-ba9e-d27f3aa96e09"],"pairedKeys":[],"baseDir":"default"}
[APPROVE-STATE] {"requestId":"4bf6458c-631f-44b2-ba9e-d27f3aa96e09","pendingKeys":[],"pairedKeys":[]}

Root cause — state files contain [] instead of {}:

$ docker exec openclaw-openclaw-gateway-1 cat /home/node/.openclaw/devices/pending.json
[]
$ docker exec openclaw-openclaw-gateway-1 cat /home/node/.openclaw/devices/paired.json
[]

After replacing both files with {} and restarting, CLI works immediately:

$ docker exec openclaw-openclaw-cli-1 /usr/local/bin/openclaw cron list
ID                                   Name                     Schedule          ...
dc3bc3e5-fe8d-42f1-be13-bf888a70ef69 proactive-surprise-check cron 24 */4 * * * ...
(14 jobs listed successfully)

extent analysis

TL;DR

The most likely fix is to modify the loadState function to guard against array-typed JSON by using Array.isArray() coercion, ensuring that pending.json and paired.json files are treated as objects instead of arrays.

Guidance

  • Verify that the devices/pending.json and devices/paired.json files contain [] instead of {} by checking the file contents using docker exec.
  • Modify the loadState function to use Array.isArray() coercion to ensure that array-typed JSON is handled correctly, as suggested in the potential fix.
  • Replace the existing pending ?? {} and paired ?? {} lines with the suggested fix: pendingById: (pending && !Array.isArray(pending)) ? pending : {} and pairedByDeviceId: (paired && !Array.isArray(paired)) ? paired : {}.
  • Restart the gateway after making the changes to ensure that the new configuration is applied.
  • Test the CLI commands again to verify that the issue is resolved.

Example

// Modified loadState function
pendingById: (pending && !Array.isArray(pending)) ? pending : {},
pairedByDeviceId: (paired && !Array.isArray(paired)) ? paired : {}

Notes

This fix assumes that the issue is caused by the loadState function not handling array-typed JSON correctly. If the issue persists after applying this fix, further debugging may be necessary to identify the root cause.

Recommendation

Apply the suggested workaround by replacing the pending.json and paired.json files with {} and restarting the gateway, and then modify the loadState function to use Array.isArray() coercion to prevent similar issues in the future. This will ensure that the CLI commands work correctly and that the device pairing state is handled correctly.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

approveDevicePairing should find the pending request created by requestDevicePairing and return status: "approved", allowing the CLI connection to complete without manual pairing intervention. loadState should treat [] the same as a missing or empty file and coerce it to {}.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING