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.
* test: cover prepareContext validation error branches
create-prompt.test.ts exercised only happy paths; the ~20 validation
guards in prepareContext (missing PR_NUMBER, unsupported event type,
unsupported issue action, missing claude branch, etc.) had no coverage.
Adds a "prepareContext validation errors" block asserting the thrown
messages for the reachable guards, using the existing createMockContext
helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: cover comments/common link and body builders
`src/github/operations/comments/common.ts` had no direct test coverage,
though its exports are live code used by create-initial.ts and
update-with-branch.ts. This adds unit tests for all four exports:
SPINNER_HTML, createJobRunLink, createBranchLink, and createCommentBody.
Assertions are built from the imported GITHUB_SERVER_URL so they hold on
GHES as well as github.com. Pure test additions — no production changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
Treat subtype success with is_error:true as a failed run so CI does not
show a misleading green check when the review never actually ran.
Fixes#1495
Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
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>
* Add agent-approval-check composite action
Require N human approvals on PRs that contain agent-authored commits.
Posts an agent-approval-check commit status that repos mark as a
required check on protected branches.
This is a sanitized port of the check Anthropic runs internally on
every agent-authored PR — same detection rules, /approve <sha>
comment flow, sibling-PR-same-SHA guard, and fail-closed semantics,
with the Anthropic-specific path exemptions and kill-switch removed
and config moved to action inputs.
Co-Authored-By: Claude <noreply@anthropic.com>
* Drop stray internal acronym from comment
* agent-approval-check: require write-access approvers, pin deps, pagination + doc fixes
🏠 Remote-Dev: homespace
* agent-approval-check: prettier
🏠 Remote-Dev: homespace
* agent-approval-check: verify approver write permission via REST; commits(last:100); docstring
🏠 Remote-Dev: homespace
* agent-approval-check: use headRefOid; drop pull_request_review trigger and correct threat-model docs
🏠 Remote-Dev: homespace
* agent-approval-check: stale-notification wording, no-retry-on-4xx, docstring API-call count
🏠 Remote-Dev: homespace
* agent-approval-check: fail-closed sibling guard on commits-ordering edge; drop stale 'reviewed' from README
🏠 Remote-Dev: homespace
* agent-approval-check: drop hardcoded API-call counts from logs; clarify author write-access requirement in README
🏠 Remote-Dev: homespace
* agent-approval-check: count all agent-email commits (close-reopen bypass); validate REQUIRED_APPROVALS>=1; exempt_head_branches warning
🏠 Remote-Dev: homespace
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Octavian Guzu <oct@anthropic.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>
Adds unit tests for previously-uncovered branches in
src/entrypoints/format-turns.ts:
- detectContentType: malformed-JSON fall-through (objects and arrays)
and the default python classification for non-python/non-js code
- formatResultContent: non-string inputs (number, plain object)
- groupTurnsNaturally / formatGroupedContent: the system_other path
for non-init system turns
Tests only; no source changes. format-turns.ts line coverage rises
from ~86% and the file's non-CLI logic is now fully exercised.
Co-authored-by: hk <solanamobilech@gmail.com>
Co-authored-by: Claude Opus 4.8 <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>
* fix(parse-sdk-options): prevent shell-quote from collapsing unquoted Bash(X:*) rules to bare Bash
shell-quote's parse() tokenizes unquoted `(`, `)` as control operators
and barewords containing `*` as glob ops, all returned as non-string
objects. parseClaudeArgsToExtraArgs filtered those out, so an unquoted
`--allowedTools View,Bash(gh:*),Bash(cat:*)` collapsed to bare `Bash` —
silently widening scoped permission rules to unrestricted Bash(*).
Escape shell control metachars to Unicode private-use placeholders
before parse() and restore after; extract .pattern from glob ops.
Preserves existing quote/whitespace handling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: retrigger
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>