Claude Code's EnterWorktree tool converts "/" to "+" when generating
branch names from worktree names (e.g. EnterWorktree("feat/foo") creates
branch "worktree-feat+foo"). The strict whitelist in validateBranchName
rejected these names, causing claude-code-action to fail on any PR opened
from an EnterWorktree-generated branch.
Since all git calls use execFileSync (not shell interpolation), "+" carries
no command injection risk — the same rationale used for allowing "#".
Git itself permits "+" in branch names per git-check-ref-format.
Fixes: https://github.com/anthropics/claude-code-action/issues/1244
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Agent SDK 0.2.113 dropped vendor/ripgrep and now ships native binaries
via per-platform optionalDependencies. Two breakages:
- action.yml chmod'd vendor/ripgrep which no longer exists, failing the
Install Dependencies step with find exit 1.
- The SDK auto-resolves its bundled binary by trying the -musl platform
package before the glibc one. bun install does not respect the
package.json libc field and installs both on glibc Linux, so the SDK
picks the musl binary and spawn fails with ENOENT.
Remove the obsolete ripgrep chmod. Make installClaudeCode() return the
install.sh binary path and pass it explicitly as
pathToClaudeCodeExecutable so the SDK skips auto-resolution entirely.
When a PR originates from a fork, `git fetch origin <branch>` fails
because the branch only exists on the fork's remote.
Fix: detect cross-repository PRs via the `isCrossRepository` GraphQL
field and fetch using `pull/<number>/head:<branch>` refspec instead,
which is the standard GitHub mechanism for accessing fork PR branches.
Changes:
- Add `isCrossRepository` and `headRepository` to PR GraphQL query
- Add corresponding fields to GitHubPullRequest type
- Branch checkout uses pull ref for fork PRs
- Update test fixtures with new fields
Co-authored-by: User <user@example.com>
dirname() preserves embedded newlines, so a value like
`/usr/bin/claude\n/attacker/path` writes two lines to GITHUB_PATH,
injecting an attacker-controlled directory into PATH for all subsequent
workflow steps.
Validate the input immediately after reading it and throw if it
contains any control characters (0x00-0x1f, 0x7f). This is fail-closed
rather than silent stripping — a path with control characters is always
misconfigured or malicious.
Fixes#1160
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
`validateBranchName` used a strict whitelist that excluded `#`,
causing the action to fail on PRs from branches like `put-back-arm64-#2`
with "Invalid branch name" — even though the branch already exists in
git and `#` is permitted by git-check-ref-format.
The validation was designed to prevent command injection. However, every
git call in the action uses `execFileSync`, which bypasses the shell
entirely and passes arguments directly to the kernel's execve. There is
no shell to interpret `#` as a metacharacter, so the strict whitelist was
over-blocking valid names with no security benefit.
Add `#` to the whitelist pattern, and update the JSDoc and error message
to reflect the allowed character set.
Fixes#1137.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add shouldRetry predicate to RetryOptions so callers can abort retries
for errors that will never succeed (e.g. 401 WorkflowValidationSkipError).
Previously, retryWithBackoff retried all errors blindly, wasting ~35s on
deterministic failures like workflow validation 401s.
Fixes#1081
Co-authored-by: Claude <noreply@anthropic.com>
The reviewData variable is typed as `{ nodes: GitHubReview[] } | null`,
but the fallback value was `[]` (a plain array). When
`pullRequest.reviews` is null/undefined, `reviewData` becomes `[]`,
causing `reviewData.nodes` to return `undefined` instead of `[]`.
This leads to silent failures in downstream code that iterates over
`reviewData.nodes`, such as `filterReviewsToTriggerTime` and
`filterCommentsByActor`.
Co-authored-by: Claude Opus 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
* fix: fall back to repo default_branch instead of hardcoded "main"
When no explicit base_branch input is provided, the action previously
fell back to a hardcoded "main", which fails on repositories whose
default branch is named differently (e.g. "master", "develop").
This reads repository.default_branch from the GitHub event payload
(populated once in parseGitHubContext) and uses it as the fallback in
all three callsites: agent/index.ts, run.ts, and update-comment-link.ts.
Explicit env/input precedence is preserved; "main" remains only as a
last-resort defensive fallback if the payload somehow lacks the field.
* test: drop unused BASE_BRANCH env handling from default_branch test
agent/index.ts no longer reads process.env.BASE_BRANCH directly (it now
goes through context.inputs.baseBranch which is set on the mock context),
so saving/clearing/restoring that env var in the regression test is dead
code.
* Revert "chore: bump Claude Code to 2.1.89 and Agent SDK to 0.2.89"
This reverts commit bee87b3258c251f9279e5371b0cc3660f37f3f77.
* Revert "chore: bump Claude Code to 2.1.88 and Agent SDK to 0.2.88"
This reverts commit 7225f045c6219dd201504adc5534baf31024db31.
* 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).