fix: snapshot PR's .claude/ to .claude-pr/ before security restore (#1172)

When a PR modifies files under .claude/, the security restore in
restoreConfigFromBase() overwrites them with the base branch version —
correct for execution safety, but it means review agents never see what
the PR actually changes.

Before deleting the PR-controlled .claude/ tree, copy it to .claude-pr/.
Review agents can read .claude-pr/ to inspect the PR's hooks, MCP
configs, settings, and CLAUDE.md without those files ever being executed.
The snapshot is taken before the security delete so it captures the full
PR-authored version.

Fixes #1134.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Max Flanagan 2026-04-04 23:47:27 -04:00 committed by GitHub
parent eb8baa46af
commit 5150ea9643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,5 +1,5 @@
import { execFileSync } from "child_process"; import { execFileSync } from "child_process";
import { rmSync } from "fs"; import { cpSync, existsSync, rmSync } from "fs";
// Paths that are both PR-controllable and read from cwd at CLI startup. // Paths that are both PR-controllable and read from cwd at CLI startup.
// //
@ -44,6 +44,19 @@ export function restoreConfigFromBase(baseBranch: string): void {
`Restoring ${SENSITIVE_PATHS.join(", ")} from origin/${baseBranch} (PR head is untrusted)`, `Restoring ${SENSITIVE_PATHS.join(", ")} from origin/${baseBranch} (PR head is untrusted)`,
); );
// Snapshot the PR's .claude/ tree to .claude-pr/ before deleting it.
// This lets review agents inspect what the PR actually changes (CLAUDE.md,
// settings, hooks, MCP configs) without those files ever being executed.
// The snapshot is taken before the security delete so it captures the
// PR-authored version.
rmSync(".claude-pr", { recursive: true, force: true });
if (existsSync(".claude")) {
cpSync(".claude", ".claude-pr", { recursive: true });
console.log(
"Preserved PR's .claude/ → .claude-pr/ for review agents (not executed)",
);
}
// Delete PR-controlled versions BEFORE fetching so the attacker-controlled // Delete PR-controlled versions BEFORE fetching so the attacker-controlled
// .gitmodules is absent during the network operation. If git reads .gitmodules // .gitmodules is absent during the network operation. If git reads .gitmodules
// during fetch (fetch.recurseSubmodules=on-demand, the git default), it will // during fetch (fetch.recurseSubmodules=on-demand, the git default), it will