fix: restoreConfigFromBase covers nested CLAUDE.md (#1270)#1271
Open
mahangu wants to merge 1 commit intoanthropics:mainfrom
Open
fix: restoreConfigFromBase covers nested CLAUDE.md (#1270)#1271mahangu wants to merge 1 commit intoanthropics:mainfrom
mahangu wants to merge 1 commit intoanthropics:mainfrom
Conversation
Closes anthropics#1270. Extends restoreConfigFromBase to revert PR-controlled nested CLAUDE.md / CLAUDE.local.md files (auto-loaded by Claude Code from cwd at any depth), not just the root entries in SENSITIVE_PATHS. Discovery uses `git ls-files -z` on PR head and `git ls-tree -r -z` on origin/<base>, filtered by basename. Discovery failures fail closed (re-throw) rather than silently leaving nested PR-controlled instruction files on disk. Opt out with `DISABLE_NESTED_CLAUDE_MD_RESTORE=true` (re-enables the gap). Snapshot defenses (so .claude-pr/ can't itself be loaded or used to exfiltrate target file contents): - Auto-load basenames are renamed with `.pr-snapshot` suffix at both the top level (`snapshotDest`) and after the recursive copy (`suffixAutoLoadedBasenames` walk catches files inside .claude/). - Symlinks are skipped at the top level (lstatSync) and inside recursive directory copies (cpSync filter), so a PR can't point CLAUDE.md or .claude/hooks/leak.txt at /etc/passwd and have the target read through .claude-pr/. Reviewed by claude-architect, superpowers code-reviewer, codex, agent-skills security-auditor, unit-tests auditor, and a docs- accuracy reviewer; each round caught a distinct class of bug (symlink exfiltration, recursive snapshot survival, leading-dash bypass, nested-symlink exfiltration). 15 tests cover root restore, nested restore both-sides, base-only / PR-only nested, snapshot encoding, leading-dash dirs, top-level + nested symlinks, and the opt-out flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Hi @ashwin-ant! 👋🏾 Given your work on the No rush — happy to iterate on anything that doesn't sit right. Thanks! |
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.
Fixes: #1270
Problem
restoreConfigFromBasereverts PR-controlled config from the trusted base before the Claude Code CLI starts. Its allowlist was path-literal and root-only:Root
CLAUDE.mdwas added in #1174 because PR-modified instruction files at startup are a prompt-injection surface. The same surface exists for nestedCLAUDE.mdfiles (Claude Code auto-loads them from cwd at any depth), but the literal list cannot matchpackages/foo/CLAUDE.md,apps/web/CLAUDE.md, etc.Root cause
SENSITIVE_PATHSis iterated as literal paths against the working tree — there is no recursive discovery for basenames Claude Code auto-loads at depth.Fix
A recursive pass that runs alongside the existing
SENSITIVE_PATHSloop:CLAUDE.md/CLAUDE.local.mdon PR head viagit ls-files -zand onorigin/<base>viagit ls-tree -r -z --name-only origin/<base>, filtered to exact basename matches..claude-pr/for review-agent inspection (with the encoding defenses below).git fetch.git checkout origin/<base> -- <path>.git reset --so the revert doesn't leak into later commits.Two defenses guard the snapshot itself, since
.claude-pr/<path>/CLAUDE.mdwould otherwise be a nestedCLAUDE.mdunder cwd:.pr-snapshotsuffix — both at the top level (snapshotDest) and after the recursive copy (asuffixAutoLoadedBasenameswalk catches files brought in bycpSyncof.claude/).lstatSyncshort-circuit) and inside recursively-copied directories (cpSyncfilter). Without the second guard, a PR could plant.claude/hooks/leak.txt → /etc/passwd(file or dir symlink) and have a review agent silently follow it.Every git invocation that takes a discovered path uses
--. The discovery filter rejects absolute paths,..segments, and NUL bytes (belt-and-braces; git enforces these on tracked paths). A leading-is intentionally not rejected — that would let an attacker hide-pkg/CLAUDE.mdfrom the discovery + delete loop.DISABLE_NESTED_CLAUDE_MD_RESTORE=trueopts out and reverts to the prior root-only behavior (re-opens this vulnerability; included for emergency rollback only).Testing
15 tests in
test/restore-config.test.tsexerciserestoreConfigFromBaseagainst a real temporary git repo (bare remote + base/head branches):Root
CLAUDE.md/CLAUDE.local.mdrestore (regression)Nested restore for files present on both PR + base, base-only, and PR-only
Basename-prefix non-match (
docs/CLAUDE.md.notesis left alone)Snapshot encoding:
.pr-snapshotsuffix at top level and inside recursively-copied.claude/, with verbatim CLAUDE.md basenames absent from.claude-pr/Symlink defenses: top-level CLAUDE.md → secret, root SENSITIVE_PATHS entry → secret, and nested symlinks (
.claude/hooks/leak.txt→ file,.claude/leaky→ dir) — assertions confirm no secret content reaches.claude-pr/Leading-dash directory carve-out (
-pkg/CLAUDE.mdis found and deleted)Opt-out flag (
DISABLE_NESTED_CLAUDE_MD_RESTORE=true)Restored paths are unstaged (no leak into auto-commits)
bun testpasses (680/680)bun run typecheckpassesbun run format:checkpassesRelated / supersedes
.claude-pr/snapshots. The two-layer symlink defense in this PR (top-levellstatSyncskip +cpSyncfilter for nested entries) covers the same surface and extends it to symlinks brought in by recursive directory copies.cpSyncENOENT crash on symlinked sensitive paths. Resolved by the samecpSyncfilter, which skips symlinks before they reach the copy.