Skip to content

Scope project-settings EROFS claim to in-place writes; rename bypasses it (refs #286) - #293

Merged
vyskocilm merged 1 commit into
mainfrom
issue-286-settings-claim
Aug 21, 2026
Merged

Scope project-settings EROFS claim to in-place writes; rename bypasses it (refs #286)#293
vyskocilm merged 1 commit into
mainfrom
issue-286-settings-claim

Conversation

@vyskocilm

Copy link
Copy Markdown
Contributor

What

Commit 433401e claimed stageProjectClaudeSettings closes 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).

AccessRO mount pins one inode at one PATH, not the NAME. Target bind is rw by design, so payload runs mv .claude .claudeOLD; mkdir .claude and writes a fresh settings.json to 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:

  • OUTBOUND bullet in stageProjectClaudeSettings doc comment → scoped to in-place, names the rename.
  • THE RESIDUAL paragraph → exists case reaches the create-case residual by rename; INBOUND (shipped hooks reinterpreted+dropped) unaffected, stays the sharper closed half.
  • project-scope paragraph in claudeGuidance → "cannot overwrite in place — EROFS", not fully closed outbound.

Test

test/integration/claudeprojectsettings_test.go — real @claude sandbox over a target shipping .claude/settings.json:

  • in-place write is EROFS (control, proves the projection is mounted; INPLACE-WROTE fatals);
  • mv .claude .claudeOLD; mkdir .claude; write hooks persists to host {target}/.claude/settings.json (load-bearing);
  • original dragged into .claudeOLD/settings.json unchanged.

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).

… 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>
@vyskocilm
vyskocilm merged commit 932f64c into main Aug 21, 2026
5 checks passed
@vyskocilm
vyskocilm deleted the issue-286-settings-claim branch August 21, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant