Two defenses for tag mode where an attacker with repo write access could
craft a prompt injection payload in an issue/PR to gain RCE on the
Actions runner:
1. git-push wrapper (H1 #3556799)
The Bash(git\ push:*) rule permitted arbitrary flags and remotes,
including combinations that execute shell commands locally. Replaced
with scripts/git-push.sh which allowlists exactly 'origin <ref>' with
no flags, validates the ref via check-ref-format. Same pattern as
scripts/gh.sh.
2. acceptEdits instead of blanket Write/Edit (Asana 1213310082312048)
Edit/MultiEdit/Write in allowedTools granted write access to the
whole runner filesystem (~/.bashrc etc). Removed from allowedTools
and set --permission-mode acceptEdits, which auto-accepts edits
inside cwd ($GITHUB_WORKSPACE) and denies outside. Headless SDK has
no prompt handler so 'ask' becomes deny.
Also:
- Noted that create-prompt/index.ts exports ALLOWED_TOOLS env var that
nothing reads. The live path is modes/tag/index.ts. Mirrored the fix
in both so the file the H1 report likely points to stays in sync.
- Updated prompt text (3 callsites) to reference the wrapper.
- Updated tests (4 prompt-content asserts, 7 tool-list asserts).
* feat(inline-comment): add confirmed param + probe-pattern safety net
Subagents that inherit this tool sometimes probe it with test comments
('Test comment to see if I can create inline comments') after hitting
unrelated errors elsewhere. Recurring issue across customer PRs.
Adds two defenses:
1. confirmed param: set true to post (final review comments should pass
this). When false, buffers to a JSONL file instead of posting.
2. Probe-pattern safety net: when confirmed is omitted (backward compat
for existing prompts), the body is checked against obvious probe
patterns ('test comment', 'can i', 'does this work', etc.). Matching
calls are buffered instead of posted.
A post-run step in action.yml reports the buffered call count and bodies
as a workflow warning for diagnostics.
Backward compatibility:
- Existing single-agent prompts (no confirmed param) post normally unless
the body happens to start with a probe phrase (unlikely for real
review comments)
- The code-review skill is being updated to pass confirmed: true in its
final posting step
- Subagent probes that would previously post now harmlessly buffer
* refactor: replace probe-regex with Haiku classification in post-step
The regex approach was narrow and could miss creative probe phrasings.
Replaced with a batch Haiku classification that runs after the session
completes.
Flow:
- MCP server: confirmed !== true -> buffer to JSONL (no classification
in-band, no latency in the tool path)
- Post-step (src/entrypoints/post-buffered-inline-comments.ts): reads
buffer, sends all bodies to a single Haiku call, posts only those
classified as real review comments
- confirmed=false entries are never posted regardless of classification
Fail-open: if ANTHROPIC_API_KEY is unavailable (Bedrock/Vertex users)
or the classification call fails, posts all unconfirmed comments. This
matches pre-PR behavior where all calls posted immediately.
The post-step emits :⚠️: for each filtered comment so users can
see what was dropped and why.
* feat: add classify_inline_comments opt-out input
New action input classify_inline_comments (default 'true'). Setting to
'false' restores pre-buffering behavior: all inline comment calls post
immediately regardless of the confirmed param.
Threads through: action input -> CLASSIFY_INLINE_COMMENTS env ->
context.inputs.classifyInlineComments -> MCP server env ->
CLASSIFY_ENABLED module const.
Post-step is also gated on the input so it skips entirely when
classification is disabled.
* docs: document classify_inline_comments input and confirmed param
- usage.md: add classify_inline_comments to inputs table
- solutions.md: mention confirmed=true in the prompt example and explain
buffering/classification in the tool permissions section
Add a `display_report` input parameter (default: "true") that controls
whether the Claude Code Report is written to the GitHub Step Summary.
Setting it to "false" allows users with custom formatting solutions to
avoid duplicate output in the step summary.
Closes#206
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Ashwin Bhat <ashwin-ant@users.noreply.github.com>
Reverts the following commits:
- f669191 fix: use unique local branch names for PR checkout to avoid conflicts (#931)
- 21e3fe0 Fix PR checkout to support fork PRs (#851)
Simplifies PR branch checkout back to using headRefName directly instead
of the pr-{number} local branch naming scheme introduced in #931 and the
GitHub pull ref fetch approach introduced in #851.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The previous implementation used the PR's original branch name when
fetching, which could conflict with existing local or remote branches
of the same name. This caused checkout failures for PRs with common
branch names like 'main' or 'feature/xyz'.
Changes:
- Use 'pr-{number}' format for local branch names (e.g., pr-385)
- Preserve original branch name for logging purposes
- Add detailed logging showing original -> local branch mapping
This ensures uniqueness since PR numbers are unique per repository,
while maintaining support for both same-repo and fork PRs via
GitHub's pull/{number}/head refs.
Fixes the issue introduced in #851 where fork PR support was added.
Co-authored-by: Yi-Cheng Wang <yicheng.wang@heph-ai.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace `Bash(git add:*)` syntax with `Bash(git add *)` in default
tool permissions for tag mode and create-prompt. The colon-prefixed
wildcard syntax is deprecated and causes SDK validation errors.
Closes#856
Co-authored-by: Dave-London <hello@os4us.org>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use GitHub's PR refs (pull/NUMBER/head) instead of fetching branch
by name. This works for both same-repo and fork PRs because GitHub
automatically creates these refs in the base repository for all PRs.
The branch name doesn't exist on origin for fork PRs, causing:
fatal: couldn't find remote ref <branch-name>
Using pull/${entityNumber}/head:${branchName} fetches the PR head
and creates a local branch with the correct name.
Fixes issues with tag mode failing on fork PRs.
Replace the over-engineered Mode interface/registry/detector pattern with
straightforward inline logic. There are only 2 modes (tag and agent) and
the complexity wasn't justified.
- Delete Mode interface, registry, and prepare pass-through modules
- Export prepareTagMode() and prepareAgentMode() as standalone functions
- Inline trigger checking and mode dispatch in run.ts/prepare.ts
- Change generatePrompt/createPrompt to take modeName string instead of Mode
- Remove dead code (extractGitHubContext, unused detector helpers)
- Update CLAUDE.md to reflect new architecture
* refactor: unify action into single composite step with run.ts entrypoint
Consolidate the prepare and base-action phases into a single composite
step that runs src/entrypoints/run.ts. This simplifies the action.yml
from multiple steps to one execution step, while keeping the same
behavior.
Key changes:
- Add src/entrypoints/run.ts as unified entrypoint
- Simplify action.yml to single 'Run Claude Code Action' step
- Pass all inputs via environment variables
- Update base-action to accept inputs via env vars
- Support agent mode auto-detection from prompt input
* refactor: keep SSH signing cleanup and token revocation as separate action steps
Move SSH signing key cleanup and app token revocation back to separate
composite action steps in action.yml with always() conditions, rather
than handling them inside run.ts. This keeps these cleanup concerns
as independently visible steps in the workflow.
* fix: address PR review feedback
- Use path.dirname() instead of manual string slicing for executable path
- Differentiate prepare vs execution errors in catch block so tracking
comment accurately reflects which phase failed
- Update CLAUDE.md architecture docs to reflect unified run.ts entrypoint
and four-phase design
* fix: address PR review feedback
- Use path.dirname() instead of manual string slicing for executable path
- Differentiate prepare vs execution errors in catch block so tracking
comment accurately reflects which phase failed
- Rewrite CLAUDE.md to focus on mental model, key concepts, and gotchas
instead of exhaustive file listings
- Introduced `include_comments_by_actor` and `exclude_comments_by_actor` inputs in action.yml to allow filtering of comments based on actor usernames.
- Updated context parsing to handle new input fields.
- Implemented `filterCommentsByActor` function to filter comments according to specified inclusion and exclusion patterns.
- Modified `fetchGitHubData` to apply actor filters when retrieving comments from pull requests and issues.
- Added comprehensive tests for the new filtering functionality.
This enhancement provides more control over which comments are processed based on the actor, improving the flexibility of the workflow.
* feat: send additional_permissions in token exchange request
Parse the ADDITIONAL_PERMISSIONS env var and send it as a JSON body
in the OIDC token exchange request. Permissions are merged on top of
the standard defaults (contents: write, pull_requests: write,
issues: write).
* docs: list specific available additional permissions
ssh-keygen requires a trailing newline to parse private keys correctly.
Without it, git signing fails with the confusing error:
'Couldn't load public key: No such file or directory?'
This normalizes the key to always end with a newline before writing.
Fixes issue #641 where users were getting banned due to rapid successive
Claude runs triggered by the synchronize event.
Changes:
- Add checkHumanActor call to agent mode's prepare() method to reject
bot-triggered workflows unless explicitly allowed via allowed_bots
- Update checkHumanActor to accept GitHubContext (union type) instead
of just ParsedGitHubContext
- Add tests for bot rejection/allowance in agent mode
Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 3
Claude-Escapes: 0
The parseAllowedTools() function previously used .match() which only
returns the first match. This caused tools specified in subsequent
--allowed-tools flags to be ignored during MCP server initialization.
Changes:
- Add /g flag to regex patterns for global matching
- Use matchAll() to find all occurrences
- Deduplicate tools while preserving order
- Make unquoted pattern not match quoted values
Fixes#800
#vibe
Co-authored-by: Claude <noreply@anthropic.com>
* Add branch-name-template config option
* Logging
* Use branch name template
* Add label to template variables
* Add description template variable
* More concise description for branch_name_template
* Remove more granular time template variables
* Only fetch first label
* Add check for empty template-generated name
* Clean up comments, docstrings
* Merge createBranchTemplateVariables into generateBranchName
* Still replace undefined values
* Fall back to default on duplicate branch
* Parameterize description wordcount
* Remove some over-explanatory comments
* NUM_DESCRIPTION_WORDS: 3 -> 5
Add validatePathWithinRepo helper to ensure file paths resolve within the repository root directory. This hardens the commit_files tool by validating paths before file operations.
Changes:
- Add src/mcp/path-validation.ts with async path validation using realpath
- Update commit_files to validate all paths before reading files
- Prevent symlink-based path escapes by resolving real paths
- Add comprehensive test coverage including symlink attack scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: use original title from webhook payload instead of fetched title
- Add extractOriginalTitle() helper to extract title from webhook payload
- Add originalTitle parameter to fetchGitHubData()
- Update tag mode to pass original title from webhook context
- Add tests for extractOriginalTitle and originalTitle parameter
This ensures the title used in prompts is the one that existed when the
trigger event occurred, rather than a potentially modified title fetched
later via GraphQL.
* fix: add title sanitization and explicit TOCTOU test
- Apply sanitizeContent() to titles in formatContext() for defense-in-depth
- Add explicit test documenting TOCTOU prevention for title handling
* feat: send user request as separate content block for slash command support
When in tag mode with the SDK path, extracts the user's request from the
trigger comment (text after @claude) and sends it as a separate content
block. This enables the CLI to process slash commands like "/review-pr".
- Add extract-user-request utility to parse trigger comments
- Write user request to separate file during prompt generation
- Send multi-block SDKUserMessage when user request file exists
- Add tests for the extraction utility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address PR feedback
- Fix potential ReDoS vulnerability by using string operations instead of regex
- Remove unused extractUserRequestFromEvent function and tests
- Extract USER_REQUEST_FILENAME to shared constants
- Conditionally log user request based on showFullOutput setting
- Add JSDoc documentation to extractUserRequestFromContext
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add ssh_signing_key input for SSH commit signing
Add a new ssh_signing_key input that allows passing an SSH signing key
for commit signing, as an alternative to the existing use_commit_signing
(which uses GitHub API-based commits).
When ssh_signing_key is provided:
- Git is configured to use SSH signing (gpg.format=ssh, commit.gpgsign=true)
- The key is written to ~/.ssh/claude_signing_key with 0600 permissions
- Git CLI commands are used (not MCP file ops)
- The key is cleaned up in a post step for security
Behavior matrix:
| ssh_signing_key | use_commit_signing | Result |
|-----------------|-------------------|--------|
| not set | false | Regular git, no signing |
| not set | true | GitHub API (MCP), verified commits |
| set | false | Git CLI with SSH signing |
| set | true | Git CLI with SSH signing (ssh_signing_key takes precedence)
* docs: add SSH signing key documentation
- Update security.md with detailed setup instructions for both signing options
- Explain that ssh_signing_key enables full git CLI operations (rebasing, etc.)
- Add ssh_signing_key to inputs table in usage.md
- Update bot_id/bot_name descriptions to note they're needed for verified commits
* fix: address security review feedback for SSH signing
- Write SSH key atomically with mode 0o600 (fixes TOCTOU race condition)
- Create .ssh directory with mode 0o700 (SSH best practices)
- Add input validation for SSH key format
- Remove unused chmod import
- Add tests for validation logic
* feat: add "Fix this" links to PR code reviews
When Claude reviews PRs and identifies fixable issues, it now includes
inline links that open Claude Code with the fix request pre-loaded.
Format: [Fix this →](https://claude.ai/code?q=<URI_ENCODED_INSTRUCTIONS>&repo=<REPO>)
This enables one-click fix requests directly from code review comments.
* feat: add include_fix_links input to control Fix this links
Adds a configurable input to enable/disable the "Fix this →" links
in PR code reviews. Defaults to true for backwards compatibility.
* fix: Prevent command injection in branch operations
Replace Bun shell template literals with Node.js execFileSync to prevent
command injection attacks via malicious branch names. Branch names from
PR data (headRefName) are now validated against a strict whitelist pattern
before use in git commands.
Changes:
- Add validateBranchName() function with strict character whitelist
- Replace $`git ...` shell templates with execGit() using execFileSync
- Validate all branch names before use in git operations
* fix: Address review comments for branch validation security
- Enhanced execGit JSDoc to explain security benefits of execFileSync
- Added comprehensive branch name validation:
- Leading dash check (prevents option injection)
- Control characters and special git characters (~^:?*[\])
- Leading/trailing period checks
- Trailing slash and consecutive slash checks
- Added -- separator to git checkout commands
- Added 30 unit tests for validateBranchName covering:
- Valid branch names
- Command injection attempts
- Option injection attempts
- Path traversal attempts
- Git-specific invalid patterns
- Control characters and edge cases
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Adds a shorter, more concise prompt for tag mode that trusts the model
to figure out details. Opt-in via USE_SIMPLE_PROMPT=true. The simplified
prompt keeps all context data but reduces instructions from ~250 to ~70 lines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>