Skip to content

No guard reads the merged compose mount graph: ADR-0001's "no shared credentials mount" is prose in four files and green under mutation #116

Description

@dlovell

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 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:

- ${HOME}/.claude/credentials:/home/vscode/.claude/credentials

bash tests/run-allexit 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 agentfeat(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:

  1. absence — no bind target under ~/.claude matching the credential store (ADR-0001)
  2. nesting — no bind nests under a recursive-chown root, except a listed exception carrying a reason (this is chown -R on a named volume crosses into a bind nested under it — the recursive branch is not mount-aware #115's detection half)
  3. 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.

Refs #115, #81, #89.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions