* fix: share one exchanged WIF credential across spawned Claude processes
GitHub OIDC tokens are single-use at the Anthropic token-exchange
endpoint (the same jti cannot be exchanged twice). With plugins
configured, the action spawns several short-lived claude processes
(plugin marketplace add, one plugin install per plugin, then the main
query). Each resolved federation from bare env vars and exchanged the
same identity-token file independently: the first exchange succeeded
and every later process got 401 (jti_reused), which the main query
retried for ~3 minutes before failing the job.
The SDK only enables its on-disk credentials cache when federation is
loaded from a profile config file, not from bare env vars. Write a
profile pointing at the identity-token file and select it via
ANTHROPIC_CONFIG_DIR / ANTHROPIC_PROFILE so the first process exchanges
once and the rest reuse the cached access token. The env vars are kept
as a fallback for CLIs that predate profile support.
* fix: scope the WIF credential cache per federation config
Address review feedback on the shared-credentials-cache fix:
- Embed a fingerprint of the federation inputs (rule, org, service
account, workspace, base URL, scope) in the config dir name. The SDK
cache reuses a token on expires_at alone and RUNNER_TEMP is per-job,
so a later step with different federation inputs would silently reuse
the first step's token. service_account_id and scope are included
beyond the reviewed list because both are sent in the exchange
request body and change which credential is minted.
- Skip the action-managed profile with a warning when the operator has
already set ANTHROPIC_CONFIG_DIR or ANTHROPIC_PROFILE.
- Shrink the profile to the minimal file-backed form; the CLI's bundled
SDK gap-fills the federation fields from the env vars the action
already exports (verified against the pinned 2.1.173 binary).
- Remove the token dir in stop() so the identity token and the cached
exchanged credential don't outlive the step.
- Document that cache sharing relies on the plugin subprocesses
spawning sequentially.
GitHub's GraphQL author field is null when the account behind a
comment, review, PR, or issue has been deleted (the ghost user). The
action typed author as non-null and read author.login directly, so a
single comment from a deleted account threw and was swallowed into a
generic 'Failed to fetch PR/issue data', failing the entire run.
Make author nullable on the four affected types and fall back to
'ghost' at each login read. With the type nullable, tsc flags every
dereference, so all sites are covered.
Branch names starting with an underscore (e.g. _release/v1.2.3) are valid
per git check-ref-format but were rejected by validateBranchName's
first-character whitelist. Since setupBranch validates a PR's baseRefName
after checkout, the action failed on every open PR targeting such a
branch. A leading underscore carries no option-injection risk (only a
leading dash does, which is still rejected separately).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
stripMarkdownImageAltText removed alt text from inline images
() but not reference-style images (![alt][ref]), because the
regex requires the "](" of the inline form. Alt text is a
hidden-instruction channel that reaches the prompt via sanitizeContent,
so the reference-style form let it survive.
Add a matching replace for the reference-style form (![alt][ref] ->
![][ref]), preserving the [ref] label so the image definition still
resolves. Adds regression tests.
Co-authored-by: Contributor <you@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A scoped label like area:permissions was substituted into the branch
name verbatim, producing a ":" that validateBranchName rejects. Because
the branch setup block catches that error and calls process.exit(1), the
whole run died. {{description}} was already sanitized via extractDescription;
{{label}} was the only free-text variable that skipped it.
Add a sanitizeLabel helper (replaces invalid-char runs with a hyphen so
scoped labels stay readable) and apply it before substitution, falling back
to entityType when a label sanitizes to empty. Adds regression tests that
also assert the result passes validateBranchName.
redactGitHubTokens covers ghp_, gho_, ghs_, ghr_, and github_pat_
tokens but misses ghu_ (GitHub App user-to-server tokens), one of the
documented GitHub token prefixes. A ghu_ token appearing in issue or
PR content passed through sanitization unredacted.
Add the ghu_ pattern, mirroring the existing 40-character token
patterns, with unit tests including the git-credential URL form.
installClaudeCode() pipes `curl -fsSL | bash -s --`. Bash exits with
the status of the last command, so when curl fails (429 rate limit,
403, or connection error) `bash -s` still exits 0 on empty stdin and
the action logs "Claude Code installed successfully". The 3-attempt
retry loop never triggers because the first attempt looks successful,
and the run later dies with "Executable not found in $PATH: claude".
Prefix the pipeline with `set -o pipefail;` so curl's non-zero exit
propagates through the pipe and the retry loop can actually kick in.
Extracted into buildInstallCommand() with regression tests covering
both the old buggy shape and the fixed one.
When snapshotting PR-authored sensitive paths into .claude-pr/, cpSync with
dereference:true throws ENOENT if a symlink target is missing on the PR head
(e.g. .claude/CLAUDE.md -> ../AGENTS.md). Fall back to copying the symlink
itself so restoreConfigFromBase can continue and restore trusted base versions.
Fixes#1398
When classify_inline_comments is enabled, create_inline_comment buffers calls
without confirmed=true. The model frequently re-issues the call with confirmed=true
after reading the buffered reply, which posts the comment live but leaves the
original buffered entry behind. The post-session replay step then posts it again,
so every inline comment lands twice.
Reconcile the buffer on a live post: after a confirmed comment is created, remove
any buffered entry matching the same path, line, startLine and body so it cannot be
replayed. Extracts the reconciliation into src/mcp/inline-comment-buffer.ts (the MCP
server module starts a server on import) and adds unit tests.
Co-authored-by: archievi <13202986+archievi@users.noreply.github.com>
`validateBranchName` rejects branch names containing `@`, even though
`git check-ref-format` permits `@` and GitHub itself accepts such
branches. PRs whose head or base branch contains an `@` fail validation
in-process before any git operation, so the action errors out
immediately.
Branch names with `@` show up in real workflows: ticket conventions
like "TICKET-123@add-feature" (#998), leading-prefix conventions like
"@hotfix/...", and agent tooling that appends "@<sessionid>" (#1305).
There is no workaround other than renaming the branch, which is often
not under the user's control.
Branch names are never passed through a shell (git calls use
execFileSync argv arrays), so `@` carries no injection risk. This is
the same reasoning used to add `#` in #1167, `+` in #1248, and `,` in
#1310. The bare name "@" (HEAD shorthand in git revision syntax) and
the "@{" reflog sequence are still rejected.
- Add `@` to the validateBranchName whitelist regex, including the
leading position (the leading-character rule blocks option injection
via `-`, which `@` cannot cause)
- Reject the bare name "@" with a dedicated check
- Update the surrounding comment, JSDoc, and error message to match
- Add test cases for @-containing names and bare "@"
Fixes#998
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Issue/PR comments (#512) and the issue/PR body (#710) are filtered to the
trigger timestamp so content created or edited after an authorized trigger
cannot be injected into Claude's prompt (TOCTOU protection). Reviews and
inline review comments were not: fetchGitHubData returned reviewData filtered
by actor only, and formatReviewComments renders it into the prompt, so a
review submitted or edited after the trigger reached Claude verbatim.
filterReviewsToTriggerTime already existed (added alongside the comment filter
in #512) but was only wired to the image-download list, never to the returned
reviewData.
Filter reviewData.nodes through filterReviewsToTriggerTime and each review's
inline comments through filterCommentsToTriggerTime, alongside the existing
actor filter, then build the review image-processing lists from those
already-filtered nodes (removing a now-redundant second filter pass).
Strengthen the two integration tests to assert post-trigger and edited-after
reviews/comments are dropped.
parseAllowedTools (used to decide which GitHub MCP servers to install)
hand-rolled a regex parse of claude_args, while the tools actually
granted to Claude are parsed by parseClaudeArgsToExtraArgs in
base-action/src/parse-sdk-options.ts using shell-quote. The two parsers
diverged on two inputs (#1357):
- Multiple values after a single flag: for
`--allowedTools "Read" "Grep" "mcp__github__get_commit"` the regex
captured only "Read", so the github MCP server was not installed even
though mcp__github__get_commit was granted — tool calls then failed.
- Commented-out lines: the regex counted tools on `#`-prefixed lines
that the SDK parser strips, installing servers that were never used.
Reimplement parseAllowedTools on the same shell-quote tokenizer and the
same "accumulating flag consumes all consecutive non-flag values"
semantics, stripping comment lines first, so the install decision agrees
with the tools that are actually granted. Unquoted glob patterns (e.g.
`mcp__github__*`), which shell-quote yields as glob objects, are
recovered to their literal text to preserve existing behavior.
Closes#1357
Co-authored-by: bymle <229636660+bymle@users.noreply.github.com>