`cpSync` defaults to `dereference: false`, which means it tries to
recreate symlinks at the destination rather than copying file contents.
When a sensitive path (e.g. CLAUDE.md) is a symlink, this fails with
`ENOENT: no such file or directory, symlink` because `cpSync` attempts
to call `symlink()` without ensuring the parent `.claude-pr/` directory
exists first.
Adding `dereference: true` fixes this by following symlinks and copying
the actual file contents, which is also the correct semantic behavior —
review agents need to inspect the real content, not a symlink that may
not resolve correctly from `.claude-pr/`.
Fixes the action crash when repositories use symlinked CLAUDE.md
(e.g. CLAUDE.md -> AGENTS.md).
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>
When a PR head contains `.gitmodules`, git's default
`fetch.recurseSubmodules=on-demand` config causes `git fetch` to attempt
submodule object fetches. In CI (no credentials), this blocks indefinitely
waiting for auth — producing ~4-hour hangs reported in #1088.
Two changes, both defence-in-depth:
1. Delete SENSITIVE_PATHS *before* fetching. The attacker-controlled
`.gitmodules` is absent during the network operation, so git never
sees a submodule config to follow regardless of git settings.
2. Pass `--no-recurse-submodules` to the fetch. Suppresses submodule
fetching explicitly, independent of any git config on the runner.
The original order (fetch-then-delete) was a brief window where
`.gitmodules` from the PR head could influence the fetch. Reordering
also tightens the security property: if `git checkout` below fails, the
attacker-controlled file is already gone rather than present during fetch.
Fixes#1088.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Bun's execFileSync without an explicit env option spawns with the process
startup environment, dropping runtime process.env mutations. The credential
helper reads GH_TOKEN which is set at runtime, so git fetch in the
restore-config path failed with empty password.
Fixes#1139🏠 Remote-Dev: homespace
* Restore .claude/ and .mcp.json from PR base branch before CLI runs
The CLI's non-interactive mode trusts cwd: it reads .mcp.json and
.claude/settings{,.local}.json from the working directory and acts on
them before any tool-permission gating — executing hooks, setting env
vars (NODE_OPTIONS, LD_PRELOAD), running apiKeyHelper shell commands,
and auto-approving MCP servers. When this action checks out a PR head,
these files are attacker-controlled.
Rather than enumerate dangerous keys, replace the entire .claude/ tree
and .mcp.json with the versions from the PR base branch (which a
maintainer has reviewed). Paths absent on base are deleted. Uses local
git state, so no TOCTOU against the GitHub API.
* Read PR base ref from payload for config restore in agent mode
Agent mode's branchInfo.baseBranch defaults to "main" (or env/input
override) instead of the PR's actual target branch — it doesn't query
prData.baseRefName like tag mode does. This meant a PR targeting
develop would get .claude/ restored from main.
Fix by reading pull_request.base.ref directly from the webhook payload
for pull_request, pull_request_review, and pull_request_review_comment
events. For issue_comment on a PR (no base.ref in payload), fall back
to the mode-provided value — tag mode's value is correct (from GraphQL);
agent mode on issue_comment is an edge case that at worst restores from
the wrong trusted branch, which is still secure.
The payload value passes through validateBranchName for defense-in-depth
(GitHub enforces valid branch names server-side, but we validate anyway).
* Extend restored paths to .gitmodules, .ripgreprc, .claude.json
.gitmodules defines submodule URLs and paths; path-confusion attacks
against git submodule operations can write into .git/hooks. .ripgreprc
can set --pre (arbitrary command on each file) if RIPGREP_CONFIG_PATH
points at it. .claude.json is cheap defense-in-depth.
Documented why .git/ is excluded (not trackable in commits, and
restoring it would undo the PR checkout), along with .gitconfig
(git never reads it from cwd) and shell rc files (sourced from $HOME,
not cwd — checkout cannot reach $HOME).