You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ADR-0001's central decision is a fact about docker-compose.yml: the container is not bind-mounted to the host credential store. That fact is restated in four places and enforced in none.
docker-compose.yml:38-39 — "No shared credentials mount: each container refreshes its own token."
setup-claude.py:11 — "The container is NOT bind-mounted to the host credential file"
dev/devcontainer:1252 — "there is no shared mount to point back at"
lib/host-bridge.sh:393 — the same premise, load-bearing for the chown above it
There is no CLAUDE.md invariant for it, and ADR-0001 names no guard.
Mutation
Re-adding the exact line ADR-0001 exists to remove, on a scratch copy of fix/bind-mount-point-chown:
bash tests/run-all → exit 0, every suite green. Re-introducing the shared-credential inode that ADR-0001 was written to eliminate is completely invisible to the suite.
A second, independent mutation — ${HOME}/.mut-newbind:/home/vscode/.claude/newbind, a bind directly under the volume that #115's chown -R is rooted at — is also exit 0, green.
Root cause: nothing reads the merged mount graph
dev/devcontainer's mount_point_targets reads it at runtime via dc config. The hermetic suite reads docker-compose.yml only as text: tests/test-claude-logs.sh and tests/test-image-fingerprint.sh both grep for literals. So existence is assertable; nesting and absence are not assertable at all.
That is the substrate behind more than one rule. The chown-safety premise has now been stated, lost and re-derived five times across #48 → #58 → #61 → #113 → #115: #48 encoded "there is no bind under ~/.claude" as a comment, #58 added the transcript bind and falsified it — in a diff that rendered the falsified comment three lines below its own edit — and #61 then added a second, recursive, bind-unaware chown in a different file. PR #81 is about to add two more binds under ~/.claude without referencing #115.
Every existing mechanism is structurally unable to catch this:
the invariant ledger records couplings between two encodings of one value; this is a conditional precondition ("X is safe provided topology Y"), which has no row shape
ADR-0005 operates on facts already promoted to invariants; it has no step asking what unstated preconditions a destructive primitive has
the drift-guard convention triggers on authorial recognition, and the whole failure mode is that the coupling is invisible from the side that changes — docker-compose.yml carries no marker saying a recursive chown depends on its shape
Sweep the 58 rule-asserting comments in non-test sources: promote, guard, or delete #89's sweep used a deontic regex (must|never|in sync|lockstep|mirror); these comments are indicative ("there is no longer a credentials/ bind mount"), so they were never in its population — and its manual pass then named lib/volume-perms.sh and lib/host-bridge.sh in its dropped list as "local invariants enforced by the code itself. No action."
review, human or agent — feat(devcontainer): keep Claude session transcripts on the host #58 had both halves on one screen and it was still missed. Diff scope was not the limiter; salience was. A premise reads as justification for local behaviour, not as a claim about the file being edited.
Proposed guard (test:, derive-not-restate per ADR-0005)
A tests/lib/compose-topology.sh that parses the compose file set into (kind, target) pairs — YAML plus env stubs, no docker daemon needed — supporting:
absence — no bind target under ~/.claude matching the credential store (ADR-0001)
coverage — every ~/.claude mount is named by an invariant
The decisive property: it fails from the side that changes.#58 and #81 both edit docker-compose.yml, and a topology guard goes red in the PR that adds the mount — the only place the fix is cheap. Every mechanism above requires the person editing compose to already know about a constraint written in a different file.
tests/test-volume-chown-guard.sh (PR #113) already lifts dev/devcontainer's compose-query snippet out and runs it against a document — but a synthetic one. Pointing that same technique at the repo's own compose files is most of the change. tests/lib/workflow-paths.sh is the precedent this repo already paid for the same class of problem.
Scope note: mount_point_targets reads only services.app.volumes, so the top-level tmpfs: key (/home/vscode/.claude/shell-snapshots, a third mount under the volume) is invisible to it. Harmless today — tmpfs is container-local — but a topology guard should read tmpfs too, or it inherits the same partial view.
Summary
ADR-0001's central decision is a fact about
docker-compose.yml: the container is not bind-mounted to the host credential store. That fact is restated in four places and enforced in none.docker-compose.yml:38-39— "No shared credentials mount: each container refreshes its own token."setup-claude.py:11— "The container is NOT bind-mounted to the host credential file"dev/devcontainer:1252— "there is no shared mount to point back at"lib/host-bridge.sh:393— the same premise, load-bearing for the chown above itThere is no
CLAUDE.mdinvariant for it, and ADR-0001 names no guard.Mutation
Re-adding the exact line ADR-0001 exists to remove, on a scratch copy of
fix/bind-mount-point-chown:- ${HOME}/.claude/credentials:/home/vscode/.claude/credentialsbash tests/run-all→ exit 0, every suite green. Re-introducing the shared-credential inode that ADR-0001 was written to eliminate is completely invisible to the suite.A second, independent mutation —
${HOME}/.mut-newbind:/home/vscode/.claude/newbind, a bind directly under the volume that #115'schown -Ris rooted at — is also exit 0, green.Root cause: nothing reads the merged mount graph
dev/devcontainer'smount_point_targetsreads it at runtime viadc config. The hermetic suite readsdocker-compose.ymlonly as text:tests/test-claude-logs.shandtests/test-image-fingerprint.shbothgrepfor literals. So existence is assertable; nesting and absence are not assertable at all.That is the substrate behind more than one rule. The chown-safety premise has now been stated, lost and re-derived five times across #48 → #58 → #61 → #113 → #115: #48 encoded "there is no bind under
~/.claude" as a comment, #58 added the transcript bind and falsified it — in a diff that rendered the falsified comment three lines below its own edit — and #61 then added a second, recursive, bind-unaware chown in a different file. PR #81 is about to add two more binds under~/.claudewithout referencing #115.Every existing mechanism is structurally unable to catch this:
docker-compose.ymlcarries no marker saying a recursive chown depends on its shapemust|never|in sync|lockstep|mirror); these comments are indicative ("there is no longer a credentials/ bind mount"), so they were never in its population — and its manual pass then namedlib/volume-perms.shandlib/host-bridge.shin its dropped list as "local invariants enforced by the code itself. No action."Proposed guard (
test:, derive-not-restate per ADR-0005)A
tests/lib/compose-topology.shthat parses the compose file set into(kind, target)pairs — YAML plus env stubs, no docker daemon needed — supporting:~/.claudematching the credential store (ADR-0001)~/.claudemount is named by an invariantThe decisive property: it fails from the side that changes. #58 and #81 both edit
docker-compose.yml, and a topology guard goes red in the PR that adds the mount — the only place the fix is cheap. Every mechanism above requires the person editing compose to already know about a constraint written in a different file.tests/test-volume-chown-guard.sh(PR #113) already liftsdev/devcontainer's compose-query snippet out and runs it against a document — but a synthetic one. Pointing that same technique at the repo's own compose files is most of the change.tests/lib/workflow-paths.shis the precedent this repo already paid for the same class of problem.Scope note:
mount_point_targetsreads onlyservices.app.volumes, so the top-leveltmpfs:key (/home/vscode/.claude/shell-snapshots, a third mount under the volume) is invisible to it. Harmless today — tmpfs is container-local — but a topology guard should read tmpfs too, or it inherits the same partial view.Refs #115, #81, #89.