From 5150ea9643a49aa8186b9ceb0caccdc480997e7b Mon Sep 17 00:00:00 2001 From: Max Flanagan Date: Sat, 4 Apr 2026 23:47:27 -0400 Subject: [PATCH] fix: snapshot PR's .claude/ to .claude-pr/ before security restore (#1172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/github/operations/restore-config.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/github/operations/restore-config.ts b/src/github/operations/restore-config.ts index 0806787..c1ff004 100644 --- a/src/github/operations/restore-config.ts +++ b/src/github/operations/restore-config.ts @@ -1,5 +1,5 @@ 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. // @@ -44,6 +44,19 @@ export function restoreConfigFromBase(baseBranch: string): void { `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 // .gitmodules is absent during the network operation. If git reads .gitmodules // during fetch (fetch.recurseSubmodules=on-demand, the git default), it will