Scope project-settings EROFS claim to in-place writes; rename bypasses it (refs #286) - #293
Merged
Merged
Conversation
… bypasses it (refs #286) Commit 433401e claimed stageProjectClaudeSettings closes the OUTBOUND direction: a hook written into an EXISTING {target}/.claude/settings.json "does not survive to run on the host later". True in place, false by rename. The AccessRO mount pins one inode at one PATH, not the NAME. The target bind is rw by design, so a payload runs `mv .claude .claudeOLD; mkdir .claude` and writes a fresh settings.json to the host at the original path. The RO mount follows the parent into .claudeOLD/ (EBUSY-pinned there), so the attack is git-visible, not silent, and equals the create-case residual the commit already discloses. What was wrong is the SCOPING of the claim, not a new capability. Three claim sites corrected in internal/cli/claude.go: the OUTBOUND bullet and THE RESIDUAL paragraph in stageProjectClaudeSettings's doc comment, and the project-scope paragraph in claudeGuidance. Each now scopes the EROFS guarantee to in-place writes and names the rename as reducing the exists case to the create-case residual. INBOUND (a hostile repo shipping hooks is reinterpreted and dropped) is unaffected by rename and stays the sharper, closed half. test/integration/claudeprojectsettings_test.go pins the boundary in a real @claude sandbox: the in-place write is EROFS (control, proving the projection is mounted) and the rename+recreate persists to the host (the residual). If a later change closes the outbound direction fully, the test fails and directs the author to widen the claim back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Commit 433401e claimed
stageProjectClaudeSettingscloses the OUTBOUND direction for{target}/.claude/settings.json: a hook written into an EXISTING file "does not survive to run on the host later". True in place, false by rename (redteam round, 2026-08-21, #286).AccessROmount pins one inode at one PATH, not the NAME. Target bind isrwby design, so payload runsmv .claude .claudeOLD; mkdir .claudeand writes a freshsettings.jsonto the host at the original path. RO mount follows the parent into.claudeOLD/(EBUSY-pinned), so attack is git-visible, not silent, and equals the create-case residual the commit already discloses. Wrong thing is the SCOPING of the claim, not a new capability. sev:low.Change
Claim-scoping only, no behavior change. Three sites in
internal/cli/claude.go:stageProjectClaudeSettingsdoc comment → scoped to in-place, names the rename.claudeGuidance→ "cannot overwrite in place — EROFS", not fully closed outbound.Test
test/integration/claudeprojectsettings_test.go— real@claudesandbox over a target shipping.claude/settings.json:mv .claude .claudeOLD; mkdir .claude; write hookspersists to host{target}/.claude/settings.json(load-bearing);.claudeOLD/settings.jsonunchanged.If a later change closes outbound fully, the test fails and directs the author to widen the claim back.
Measured:
PASS 0.26s. gofmt/vet/build clean.Not in scope
The create-case residual itself (an absent settings.json the payload creates) — unchanged and still disclosed; closing it needs a host write snug refuses (
rejectGeneratedOntoHost, #186).