launcher: refuse to start on a settings.json that doesn't parse #52

Merged
branderson merged 1 commits from validate-host-settings into main 2026-08-01 13:51:19 -07:00
Owner

Summary generated by LLM agent

Found by a live failure: after #49 and #51 merged, a sandbox came up warning about bypass-permissions mode and with ~/.claude/settings.json reported malformed inside the container.

What actually happened

The host's real ~/.claude/settings.json had a stray x on the line after the closing brace — 110 lines, object ends at 109. Nothing in dejima put it there; the file inside the container is byte-identical to the host's, because #51 stages it verbatim.

It only became visible after the merge. Pre-#49, the sandbox settings were generated by running the host file through jq '.sandbox.enabled = false'. jq parses a stream of values, so it wrote the complete valid object to the temp file and only then hit the trailing garbage and exited 4 — the container got clean JSON and the corruption stayed invisible. Since the override layer became a jq -n delta and the real file is staged as-is, nothing on the host reads it any more.

What the operator sees now is a lot worse than a parse error. Claude Code ignores an unparseable settings.json wholesale, with only a startup warning — so the session loses every permission, hook and plugin, and skipDangerousModePermissionPrompt goes with them. The first symptom is a bypass-mode warning that says nothing about settings at all, in a container, a long way from the file that caused it. Theme and editor mode being gone too was the tell.

The change

bin/dejima runs jq over ~/.claude/settings.json after the pre-create and refuses to launch if it doesn't parse, printing jq's own error and the path. This restores what the old generator gave for free — fail loudly, on the host, where the file can be fixed — without reintroducing a dependency on reading that file for anything else. jq is already a host requirement (the override layer and the remote-notify hooks both use it).

Testing

New test in project.bats, on the existing launcher harness: drives the real bin/dejima against a throwaway $HOME whose settings.json carries a trailing x, asserts a non-zero exit with the message and that docker is never invoked, then swaps in a valid file and asserts it launches. It fails without the guard.

One existing static test had to be reworked. "the generated override layer is a delta, not a copy of the operator's settings" asserted that $HOME/.claude/settings.json appears exactly once in bin/dejima — a mention count, which any second mention breaks regardless of what it does. It now asserts the property that test exists to protect: the override layer is built with jq -n, and the operator's settings.json is never an input to $SANDBOX_SETTINGS_FILE.

394 tests green across static, remote, audit, project, proxy. test/runtime.bats was not run — this work was done inside a dejima sandbox, which has no Docker — but nothing here touches the image, the entrypoint or the mounts.

**Summary generated by LLM agent** Found by a live failure: after #49 and #51 merged, a sandbox came up warning about bypass-permissions mode and with `~/.claude/settings.json` reported malformed inside the container. ## What actually happened The host's real `~/.claude/settings.json` had a stray `x` on the line after the closing brace — 110 lines, object ends at 109. Nothing in dejima put it there; the file inside the container is byte-identical to the host's, because #51 stages it verbatim. It only became visible after the merge. Pre-#49, the sandbox settings were generated by running the host file through `jq '.sandbox.enabled = false'`. jq parses a stream of values, so it wrote the complete valid object to the temp file and only *then* hit the trailing garbage and exited 4 — the container got clean JSON and the corruption stayed invisible. Since the override layer became a `jq -n` delta and the real file is staged as-is, nothing on the host reads it any more. What the operator sees now is a lot worse than a parse error. Claude Code ignores an unparseable settings.json **wholesale**, with only a startup warning — so the session loses every permission, hook and plugin, and `skipDangerousModePermissionPrompt` goes with them. The first symptom is a bypass-mode warning that says nothing about settings at all, in a container, a long way from the file that caused it. Theme and editor mode being gone too was the tell. ## The change `bin/dejima` runs `jq` over `~/.claude/settings.json` after the pre-create and refuses to launch if it doesn't parse, printing jq's own error and the path. This restores what the old generator gave for free — fail loudly, on the host, where the file can be fixed — without reintroducing a dependency on reading that file for anything else. jq is already a host requirement (the override layer and the remote-notify hooks both use it). ## Testing New test in `project.bats`, on the existing launcher harness: drives the real `bin/dejima` against a throwaway `$HOME` whose settings.json carries a trailing `x`, asserts a non-zero exit with the message and that docker is never invoked, then swaps in a valid file and asserts it launches. It fails without the guard. One existing static test had to be reworked. `"the generated override layer is a delta, not a copy of the operator's settings"` asserted that `$HOME/.claude/settings.json` appears exactly once in `bin/dejima` — a mention count, which any second mention breaks regardless of what it does. It now asserts the property that test exists to protect: the override layer is built with `jq -n`, and the operator's settings.json is never an input to `$SANDBOX_SETTINGS_FILE`. 394 tests green across `static`, `remote`, `audit`, `project`, `proxy`. `test/runtime.bats` was not run — this work was done inside a dejima sandbox, which has no Docker — but nothing here touches the image, the entrypoint or the mounts.
branderson added 1 commit 2026-08-01 13:44:05 -07:00
launcher: refuse to start on a settings.json that doesn't parse
tests / fast (pull_request) Successful in 5m35s
tests / runtime (pull_request) Successful in 7m54s
9eb9b9cd30
Claude Code ignores an unparseable settings.json wholesale, with only a
startup warning, so the sandbox comes up with none of the operator's
permissions, hooks or plugins - and, because
skipDangerousModePermissionPrompt goes with them, the first visible
symptom is an unexpected bypass-mode warning that says nothing about
settings at all.

This used to be caught by accident. The sandbox settings copy was
generated by running the host file through jq, so a malformed file
failed there and set -e stopped the launch. Since the override layer
became a jq -n delta (#47) and the real file is staged verbatim (#48),
nothing reads it on the host any more and the breakage surfaces inside
the container instead, far from the file that caused it.

Check it explicitly at launch, where the fix is at hand, and print jq's
parse error with the path. Found by a stray character appended to a real
~/.claude/settings.json, which cost a session its whole config.

The delta-not-a-copy invariant is retested rather than counting
mentions of the operator's settings.json: it now asserts the override
layer is built with jq -n and that the operator's file is never an input
to it, which is the property that actually matters.
branderson merged commit 00136eae66 into main 2026-08-01 13:51:19 -07:00
Sign in to join this conversation.