13. Deliver the integrity guard globally via managed-settings#

Date: 2026-06-14

Status#

Accepted

Context#

A guard must assert “Claude is actually inside the shadow” in every folder — even ones with no project .claude/. Two earlier designs failed: per-repo project .claude/ hooks left un-promoted folders unguarded; an intermediate user-scope ~/.claude/settings.json design was silently removable by editing that shared, cross-container file.

Decision#

Deliver the guard through Claude Code’s managed-settings layer (/etc/claude-code/managed-settings.json) — the highest-precedence tier, which a user cannot override or remove from user-scope — with two hooks whose scripts live in /usr/libexec/claude-sandbox/ (root-owned, off-PATH, ro inside the sandbox):

  • SessionStart sandbox-verify.sh: advisory full integrity battery, warns loudly when unwrapped (SessionStart cannot block).

  • UserPromptSubmit sandbox-gate.sh: fail-closed, exit 2 unless IS_SANDBOX=1. Escape hatch CLAUDE_SANDBOX_ALLOW_UNWRAPPED=1; both skip on CLAUDE_CODE_REMOTE=true.

This is the same “security inputs live outside the rw workspace” discipline as 12. Treat the read-write workspace as untrusted: default to $PWD, source config from /etc: under ~/.claude the scripts would be rw-bound and a compromised session could rewrite the gate to exit 0.

Consequences#

  • Editing ~/.claude/settings.json cannot disable the guard — only root editing /etc or a deliberate ./install can.

  • We deliberately do not set allowManagedHooksOnly (managed hooks are additive — the owner’s own hooks still run). wire_user_statusline prunes any guard hooks an earlier user-scope install left behind, so the guard has one authoritative home.

  • The same managed layer also hard-disables the in-container auto-updater (DISABLE_AUTOUPDATER=1 + autoUpdates:false; first shipped in this work, commit f4928d4). This is root-cause removal of a PATH-bypass re-arm: the updater otherwise re-creates ~/.local/bin/claude unwrapped on a version bump (see 9. Relocate the real Claude binary off PATH so the shadow always wins), so updates become a deliberate ./install that re-relocates the binary and re-asserts the shadow. autoUpdatesChannel:"stable" only slows updates and would not fix this.

  • Refuse: moving the guard back to per-repo/user-scope; putting the scripts in the sandbox rw set; setting allowManagedHooksOnly; or hard-failing install over a non-JSON settings file we don’t exclusively own (warn-and-skip instead). Fleet-wide enforcement against accidental unwrapped host claude (issue #41) extends this layer.