Compare commits

..

2 Commits

56 changed files with 251 additions and 1737 deletions

View File

@ -1,5 +1,5 @@
--- ---
allowed-tools: Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*) allowed-tools: Bash(gh label list:*),Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search:*)
description: Apply labels to GitHub issues description: Apply labels to GitHub issues
--- ---
@ -14,18 +14,17 @@ Issue Information:
TASK OVERVIEW: TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `./scripts/gh.sh label list`. Run exactly this command with nothing else. 1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use gh wrapper commands to get context about the issue: 2. Next, use gh commands to get context about the issue:
- Use `./scripts/gh.sh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details - Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
- Use `./scripts/gh.sh search issues` to find similar issues that might provide context for proper categorization - Use `gh search issues` to find similar issues that might provide context for proper categorization
- `./scripts/gh.sh` is a wrapper for `gh` CLI. Example commands: - You have access to these Bash commands:
- `./scripts/gh.sh label list` — fetch all available labels - Bash(gh label list:\*) - to get available labels
- `./scripts/gh.sh issue view 123` — view issue details - Bash(gh issue view:\*) - to view issue details
- `./scripts/gh.sh issue view 123 --comments` — view with comments - Bash(gh issue edit:\*) - to apply labels to the issue
- `./scripts/gh.sh search issues "query" --limit 10` — search for issues - Bash(gh search:\*) - to search for similar issues
- `./scripts/edit-issue-labels.sh` — apply labels to the issue
3. Analyze the issue content, considering: 3. Analyze the issue content, considering:
@ -40,12 +39,12 @@ TASK OVERVIEW:
- Choose labels that accurately reflect the issue's nature - Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive - Be specific but comprehensive
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from ./scripts/gh.sh label list - IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
- Consider platform labels (android, ios) if applicable - Consider platform labels (android, ios) if applicable
- If you find similar issues using ./scripts/gh.sh search, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. - If you find similar issues using gh search, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels: 5. Apply the selected labels:
- Use `./scripts/edit-issue-labels.sh --add-label LABEL1 --add-label LABEL2` to apply your selected labels (issue number is read from the workflow event) - Use `gh issue edit` to apply your selected labels
- DO NOT post any comments explaining your decision - DO NOT post any comments explaining your decision
- DO NOT communicate directly with users - DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels - If no labels are clearly applicable, do not apply any labels
@ -55,7 +54,7 @@ IMPORTANT GUIDELINES:
- Be thorough in your analysis - Be thorough in your analysis
- Only select labels from the provided list above - Only select labels from the provided list above
- DO NOT post any comments to the issue - DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using ./scripts/edit-issue-labels.sh - Your ONLY action should be to apply labels using gh issue edit
- It's okay to not add any labels if none are clearly applicable - It's okay to not add any labels if none are clearly applicable
--- ---

View File

@ -25,4 +25,4 @@ jobs:
prompt: "/review-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}" prompt: "/review-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}"
claude_args: | claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment" --allowedTools "mcp__github_inline_comment__create_inline_comment"
--model "claude-opus-4-7" --model "claude-opus-4-6"

View File

@ -19,9 +19,9 @@ jobs:
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: read
pull-requests: write pull-requests: read
issues: write issues: read
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
@ -31,9 +31,9 @@ jobs:
- name: Run Claude Code - name: Run Claude Code
id: claude id: claude
uses: anthropics/claude-code-action@main uses: anthropics/claude-code-action@v1
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: | claude_args: |
--allowedTools "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)" --allowedTools "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
--model "claude-opus-4-7" --model "claude-opus-4-6"

View File

@ -20,10 +20,8 @@ jobs:
- name: Run Claude Code for Issue Triage - name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@main uses: anthropics/claude-code-action@main
env:
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
with: with:
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}" prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,47 +0,0 @@
name: Non-write Users Check
on:
pull_request:
paths:
- ".github/**"
permissions:
contents: read
pull-requests: write
jobs:
allowed-non-write-check:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- run: |
DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" || true)
if ! echo "$DIFF" | grep -qE '^diff --git a/\.github/.*\.ya?ml'; then
exit 0
fi
MATCHES=$(echo "$DIFF" | grep "^+.*allowed_non_write_users" || true)
if [ -z "$MATCHES" ]; then
exit 0
fi
EXISTING=$(gh pr view "$PR_NUMBER" -R "$REPO" --json comments --jq '.comments[].body' \
| grep -c "<!-- non-write-users-check -->" || true)
if [ "$EXISTING" -gt 0 ]; then
exit 0
fi
gh pr comment "$PR_NUMBER" -R "$REPO" --body '<!-- non-write-users-check -->
**`allowed_non_write_users` detected**
This PR adds or modifies `allowed_non_write_users`, which allows users without write access to trigger Claude Code Action workflows. This can introduce security risks.
If this is a new flow, please make sure you actually need `allowed_non_write_users`. If you are editing an existing workflow, double check that you are not adding new Claude permissions which might lead to a vulnerability.
See existing workflows in this repo for safe usage examples, or contact the AppSec team.'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}

View File

@ -28,20 +28,11 @@ inputs:
required: false required: false
default: "" default: ""
allowed_bots: allowed_bots:
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots. WARNING: On public repos with '*', external Apps may be able to invoke this action with prompts they control. See docs/security.md." description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
required: false required: false
default: "" default: ""
allowed_non_write_users: allowed_non_write_users:
description: | description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
Comma-separated list of usernames to allow without write permissions, or '*' to allow all users.
Only works when github_token input is provided. WARNING: Use with extreme caution - this
bypasses security checks and should only be used for workflows with very limited permissions
(e.g., issue labeling).
SECURITY: Processing untrusted content exposes the workflow to prompt injection. When this
input is set, Claude does a best-effort scrub of Anthropic, cloud, and GitHub Actions secrets
from subprocess environments. This reduces but does not eliminate prompt injection risk -
only use for workflows with very limited permissions and validate all outputs.
required: false required: false
default: "" default: ""
include_comments_by_actor: include_comments_by_actor:
@ -98,10 +89,6 @@ inputs:
description: "Use just one comment to deliver issue/PR comments" description: "Use just one comment to deliver issue/PR comments"
required: false required: false
default: "false" default: "false"
classify_inline_comments:
description: "Buffer inline comments without confirmed=true and classify them (real review vs test/probe) before posting after the session ends. Set to 'false' to post all inline comments immediately (pre-buffering behavior)."
required: false
default: "true"
use_commit_signing: use_commit_signing:
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands" description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
required: false required: false
@ -134,10 +121,6 @@ inputs:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment." description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false required: false
default: "" default: ""
display_report:
description: "Whether to display the Claude Code Report in GitHub Step Summary. Set to 'false' to disable when using custom formatting solutions. WARNING: This outputs Claude-authored content in the GitHub Step Summary. This should only be used in cases where the action is used solely with trusted input."
required: false
default: "false"
show_full_output: show_full_output:
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments." description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
required: false required: false
@ -173,7 +156,7 @@ runs:
steps: steps:
- name: Install Bun - name: Install Bun
if: inputs.path_to_bun_executable == '' if: inputs.path_to_bun_executable == ''
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # https://github.com/oven-sh/setup-bun/releases/tag/v2.1.2
with: with:
bun-version: 1.3.6 bun-version: 1.3.6
token: ${{ inputs.github_token || github.token }} token: ${{ inputs.github_token || github.token }}
@ -195,56 +178,11 @@ runs:
cd ${GITHUB_ACTION_PATH} cd ${GITHUB_ACTION_PATH}
bun install --production bun install --production
- name: Install subprocess isolation dependencies
# Install subprocess isolation dependencies when processing content from non-write users.
# Best-effort: skips on non-Linux or when sudo/apt unavailable (self-hosted runners).
if: ${{ inputs.allowed_non_write_users != '' && runner.os == 'Linux' }}
continue-on-error: true
shell: bash
run: |
if [ "${CLAUDE_CODE_SUBPROCESS_ENV_SCRUB:-}" = "0" ]; then
echo "Subprocess isolation opted out via CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0"
exit 0
fi
if command -v apt-get >/dev/null && command -v sudo >/dev/null; then
for i in 1 2 3; do
sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break
echo "apt-get attempt $i failed, retrying..."
sleep 5
done
fi
# Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
# The sysctl doesn't exist on older kernels — that's fine.
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ] && command -v sudo >/dev/null; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Pin bun binary for post-steps
if: ${{ inputs.allowed_non_write_users != '' }}
continue-on-error: true
shell: bash
run: |
# Keep a copy of the bun binary alongside the action's own files so
# post-steps use the same version that was on PATH at action start.
mkdir -p "$GITHUB_ACTION_PATH/bin"
cp "$(command -v bun)" "$GITHUB_ACTION_PATH/bin/bun"
- name: Prepend system bin dirs to PATH
if: ${{ inputs.allowed_non_write_users != '' && runner.os != 'Windows' }}
continue-on-error: true
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
run: |
echo "/usr/bin" >> "$GITHUB_PATH"
echo "/bin" >> "$GITHUB_PATH"
- name: Run Claude Code Action - name: Run Claude Code Action
id: run id: run
shell: bash shell: bash
run: | run: |
bun --no-env-file \ bun run ${GITHUB_ACTION_PATH}/src/entrypoints/run.ts
--config="${GITHUB_ACTION_PATH}/bunfig.toml" \
--tsconfig-override="${GITHUB_ACTION_PATH}/tsconfig.json" \
run ${GITHUB_ACTION_PATH}/src/entrypoints/run.ts
env: env:
# Prepare inputs # Prepare inputs
MODE: ${{ inputs.mode }} MODE: ${{ inputs.mode }}
@ -258,13 +196,10 @@ runs:
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }} ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }} ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: ${{ env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB || (inputs.allowed_non_write_users != '' && '1') || '' }}
CLAUDE_CODE_SCRIPT_CAPS: ${{ env.CLAUDE_CODE_SCRIPT_CAPS || '' }}
INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }} INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }} EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
CLASSIFY_INLINE_COMMENTS: ${{ inputs.classify_inline_comments }}
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }} DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }} USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }} SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }}
@ -283,7 +218,6 @@ runs:
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }} INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
DISPLAY_REPORT: ${{ inputs.display_report }}
INPUT_PLUGINS: ${{ inputs.plugins }} INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }} INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
@ -326,15 +260,6 @@ runs:
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ env.ANTHROPIC_DEFAULT_HAIKU_MODEL }} ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ env.ANTHROPIC_DEFAULT_HAIKU_MODEL }}
ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }} ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }}
# MCP configuration — these env vars are read directly from process.env by the
# Claude CLI subprocess. They must be listed explicitly here because this step's
# env: block shadows the calling workflow's job-level env vars (GitHub Actions
# composite action behavior). Set these in your workflow's job-level env: or via
# a prior step that writes to $GITHUB_ENV.
MCP_TIMEOUT: ${{ env.MCP_TIMEOUT }}
MCP_TOOL_TIMEOUT: ${{ env.MCP_TOOL_TIMEOUT }}
MAX_MCP_OUTPUT_TOKENS: ${{ env.MAX_MCP_OUTPUT_TOKENS }}
# Telemetry configuration # Telemetry configuration
CLAUDE_CODE_ENABLE_TELEMETRY: ${{ env.CLAUDE_CODE_ENABLE_TELEMETRY }} CLAUDE_CODE_ENABLE_TELEMETRY: ${{ env.CLAUDE_CODE_ENABLE_TELEMETRY }}
OTEL_METRICS_EXPORTER: ${{ env.OTEL_METRICS_EXPORTER }} OTEL_METRICS_EXPORTER: ${{ env.OTEL_METRICS_EXPORTER }}
@ -346,62 +271,14 @@ runs:
OTEL_LOGS_EXPORT_INTERVAL: ${{ env.OTEL_LOGS_EXPORT_INTERVAL }} OTEL_LOGS_EXPORT_INTERVAL: ${{ env.OTEL_LOGS_EXPORT_INTERVAL }}
OTEL_RESOURCE_ATTRIBUTES: ${{ env.OTEL_RESOURCE_ATTRIBUTES }} OTEL_RESOURCE_ATTRIBUTES: ${{ env.OTEL_RESOURCE_ATTRIBUTES }}
- name: Re-prepend system bin dirs to PATH
if: ${{ always() && inputs.allowed_non_write_users != '' && runner.os != 'Windows' }}
continue-on-error: true
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
env:
BASH_ENV: ""
LD_PRELOAD: ""
LD_LIBRARY_PATH: ""
NODE_OPTIONS: ""
DYLD_INSERT_LIBRARIES: ""
DYLD_PRELOAD: ""
DYLD_LIBRARY_PATH: ""
DYLD_FRAMEWORK_PATH: ""
run: |
echo "/usr/bin" >> "$GITHUB_PATH"
echo "/bin" >> "$GITHUB_PATH"
{
echo "BASH_ENV="
echo "LD_PRELOAD="
echo "LD_LIBRARY_PATH="
echo "DYLD_INSERT_LIBRARIES="
echo "DYLD_PRELOAD="
echo "DYLD_LIBRARY_PATH="
echo "DYLD_FRAMEWORK_PATH="
} >> "$GITHUB_ENV"
- name: Cleanup SSH signing key - name: Cleanup SSH signing key
if: always() && inputs.ssh_signing_key != '' if: always() && inputs.ssh_signing_key != ''
shell: bash shell: bash
run: | run: |
BUN_BIN="${GITHUB_ACTION_PATH}/bin/bun" bun run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts
[ -x "$BUN_BIN" ] || BUN_BIN="bun"
"$BUN_BIN" --no-env-file \
--config="${GITHUB_ACTION_PATH}/bunfig.toml" \
--tsconfig-override="${GITHUB_ACTION_PATH}/tsconfig.json" \
run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts
- name: Post buffered inline comments
if: always() && inputs.classify_inline_comments != 'false'
shell: bash
env:
GITHUB_TOKEN: ${{ steps.run.outputs.github_token || inputs.github_token || github.token }}
REPO_OWNER: ${{ github.event.repository.owner.login }}
REPO_NAME: ${{ github.event.repository.name }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
run: |
BUN_BIN="${GITHUB_ACTION_PATH}/bin/bun"
[ -x "$BUN_BIN" ] || BUN_BIN="bun"
"$BUN_BIN" --no-env-file \
--config="${GITHUB_ACTION_PATH}/bunfig.toml" \
--tsconfig-override="${GITHUB_ACTION_PATH}/tsconfig.json" \
run ${GITHUB_ACTION_PATH}/src/entrypoints/post-buffered-inline-comments.ts
- name: Revoke app token - name: Revoke app token
if: always() && inputs.github_token == '' && steps.run.outputs.github_token != '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
shell: bash shell: bash
run: | run: |
curl -L \ curl -L \

View File

@ -4,14 +4,6 @@ This GitHub Action allows you to run [Claude Code](https://www.anthropic.com/cla
For simply tagging @claude in issues and PRs out of the box, [check out the Claude Code action and GitHub app](https://github.com/anthropics/claude-code-action). For simply tagging @claude in issues and PRs out of the box, [check out the Claude Code action and GitHub app](https://github.com/anthropics/claude-code-action).
## Trust model
This action is a thin wrapper that installs and runs Claude Code with the inputs you provide. It does **not** enforce any trust boundaries on its own. Running this action in a directory is equivalent to running Claude Code in that directory — Claude reads project-level configuration (`.claude/`, `CLAUDE.md`, `.mcp.json`, etc.) from the working directory, and the action's own setup steps run from there as well.
**The caller is responsible for ensuring the working directory and prompt are trusted.** If your workflow processes untrusted input (issues, fork pull requests, external comments), use [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) instead — it provides actor permission checks, restores project configuration from the base ref in PR contexts, and is the supported path for those scenarios.
See [Claude Code's security documentation](https://docs.anthropic.com/en/docs/claude-code/security) and the [GitHub Actions guidance on `pull_request_target`](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) for background.
## Usage ## Usage
Add the following to your workflow file: Add the following to your workflow file:

View File

@ -97,7 +97,7 @@ runs:
- name: Install Bun - name: Install Bun
if: inputs.path_to_bun_executable == '' if: inputs.path_to_bun_executable == ''
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0 uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # https://github.com/oven-sh/setup-bun/releases/tag/v2.1.2
with: with:
bun-version: 1.3.6 bun-version: 1.3.6
@ -124,7 +124,7 @@ runs:
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
run: | run: |
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
CLAUDE_CODE_VERSION="2.1.123" CLAUDE_CODE_VERSION="2.1.39"
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
for attempt in 1 2 3; do for attempt in 1 2 3; do
echo "Installation attempt $attempt..." echo "Installation attempt $attempt..."

View File

@ -6,7 +6,7 @@
"name": "@anthropic-ai/claude-code-base-action", "name": "@anthropic-ai/claude-code-base-action",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.123", "@anthropic-ai/claude-agent-sdk": "^0.2.39",
"shell-quote": "^1.8.3", "shell-quote": "^1.8.3",
}, },
"devDependencies": { "devDependencies": {
@ -27,33 +27,39 @@
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.123", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.123" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-a4TysYoR9DBdkM9Uwh4J5ub7TwKmRPe5hFiWh4En+IKC+qkk5UFkxFM22c//cZjYZKynHX0ah2t6LUqb+najYA=="], "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.39", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-wR1TBH62X6E1YwRnWa+A2Eau7AfpTWtfpnwQXO3yRY31FtmzOjPkQb93hbF3AkT0WL7YF9mxBBwJKUa3ZEc5+A=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.123", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tYAXCjlXZQklsUs0J//gip3fZQRzhlH5OCgvNXV70qe7A1iiwHqO2KPGvEHV1L+deEKQoMZmTaCOrQpN6zju3w=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.123", "", { "os": "darwin", "cpu": "x64" }, "sha512-AcUC6sTon6z6HculP87KsAOeTMRLBwpovdhcXUTjXUpo/8nplJ7lBEzWjZCHt8FF1KuN/WBy1Z4bDg/59TQDmA=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-7+GnbcF3/aZ8RJ1WmU/ogtPsOpknBAoUPer90MvZuFYBLPT9iI/U7f24gjrOHuYdcbDA5n7jFlhcfIO26F5DJQ=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-bYgRiaf2q+yVbGAoUluuhqrEW1zexL34+3HDmK9DneKXa2K2EJpw4M6Sq4XoBD/JezGaemoAP78Xv/M/QUS1OQ=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-Xi+Rwk8uP5vWEnawJOlsk179fr0ATLl5J90MlbLj+puKaX5svEq8ljS+P3zq6zHTJeKh9GKLzPf7bc5YJKwcew=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-IX95lFKhmmndY/YPfWPsVV+C3rLYJmuuq5wCS53p6jYIkCMxH1iGfhBGF1EUWcXO4Uc8yqXFmQ3aaxMzOOPrwA=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.123", "", { "os": "win32", "cpu": "arm64" }, "sha512-WDZmAQG1rOiqNLZlSXaCjSWmqJvLk2io+vFQWWqSy2b5HCk9pa3PadLiaLztiihyk81wPhH9Q/44kOxdyfEGMw=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.123", "", { "os": "win32", "cpu": "x64" }, "sha512-588xrd1i6d4kXQ6FqwL+cgBiN4evRQSi5DCtPa02CZ3VEbuVQBeFlyPlD8tfWtNNeGZ4NM8kjPNNzZz5omezPA=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
@ -63,202 +69,22 @@
"@types/shell-quote": ["@types/shell-quote@1.7.5", "", {}, "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw=="], "@types/shell-quote": ["@types/shell-quote@1.7.5", "", {}, "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
} }
} }

View File

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.123", "@anthropic-ai/claude-agent-sdk": "^0.2.39",
"shell-quote": "^1.8.3" "shell-quote": "^1.8.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -11,14 +11,6 @@ async function run() {
try { try {
validateEnvironmentVariables(); validateEnvironmentVariables();
// The composite action's "Install Claude Code" step writes the binary to
// ~/.local/bin/claude. Pass that path explicitly so the Agent SDK doesn't
// fall back to its bundled platform package, which bun may resolve to the
// wrong libc variant on Linux.
const claudeExecutable =
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE ||
`${process.env.HOME}/.local/bin/claude`;
await setupClaudeCodeSettings( await setupClaudeCodeSettings(
process.env.INPUT_SETTINGS, process.env.INPUT_SETTINGS,
undefined, // homeDir undefined, // homeDir
@ -28,7 +20,7 @@ async function run() {
await installPlugins( await installPlugins(
process.env.INPUT_PLUGIN_MARKETPLACES, process.env.INPUT_PLUGIN_MARKETPLACES,
process.env.INPUT_PLUGINS, process.env.INPUT_PLUGINS,
claudeExecutable, process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
); );
const promptConfig = await preparePrompt({ const promptConfig = await preparePrompt({
@ -46,7 +38,8 @@ async function run() {
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT, appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
fallbackModel: process.env.INPUT_FALLBACK_MODEL, fallbackModel: process.env.INPUT_FALLBACK_MODEL,
model: process.env.ANTHROPIC_MODEL, model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable: claudeExecutable, pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
}); });

View File

@ -79,20 +79,6 @@ function mergeMcpConfigs(configValues: string[]): string {
return JSON.stringify(merged); return JSON.stringify(merged);
} }
/**
* Strip comment lines from a shell argument string.
* Lines whose first non-whitespace character is `#` are removed entirely.
* Inline `#` within a line (e.g. inside a quoted value) is left untouched
* because shell-quote handles quoting we only need to remove full comment lines
* before shell-quote sees them.
*/
function stripShellComments(input: string): string {
return input
.split("\n")
.filter((line) => !line.trim().startsWith("#"))
.join("\n");
}
/** /**
* Parse claudeArgs string into extraArgs record for SDK pass-through * Parse claudeArgs string into extraArgs record for SDK pass-through
* The SDK/CLI will handle --mcp-config, --json-schema, etc. * The SDK/CLI will handle --mcp-config, --json-schema, etc.
@ -106,7 +92,7 @@ function parseClaudeArgsToExtraArgs(
if (!claudeArgs?.trim()) return {}; if (!claudeArgs?.trim()) return {};
const result: Record<string, string | null> = {}; const result: Record<string, string | null> = {};
const args = parseShellArgs(stripShellComments(claudeArgs)).filter( const args = parseShellArgs(claudeArgs).filter(
(arg): arg is string => typeof arg === "string", (arg): arg is string => typeof arg === "string",
); );
@ -229,12 +215,6 @@ export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions {
// Set the entrypoint for Claude Code to identify this as the GitHub Action // Set the entrypoint for Claude Code to identify this as the GitHub Action
env.CLAUDE_CODE_ENTRYPOINT = "claude-code-github-action"; env.CLAUDE_CODE_ENTRYPOINT = "claude-code-github-action";
// Remove OIDC token request variables so Claude cannot mint new tokens.
// These are only needed by the action itself (via @actions/core.getIDToken()),
// not by the Claude session.
delete env.ACTIONS_ID_TOKEN_REQUEST_URL;
delete env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
// Build system prompt option - default to claude_code preset // Build system prompt option - default to claude_code preset
let systemPrompt: SdkOptions["systemPrompt"]; let systemPrompt: SdkOptions["systemPrompt"];
if (options.systemPrompt) { if (options.systemPrompt) {

View File

@ -119,7 +119,7 @@ function sanitizeSdkOutput(
duration_ms: resultMsg.duration_ms, duration_ms: resultMsg.duration_ms,
num_turns: resultMsg.num_turns, num_turns: resultMsg.num_turns,
total_cost_usd: resultMsg.total_cost_usd, total_cost_usd: resultMsg.total_cost_usd,
permission_denials_count: resultMsg.permission_denials?.length ?? 0, permission_denials: resultMsg.permission_denials,
}, },
null, null,
2, 2,
@ -151,7 +151,7 @@ export async function runClaudeWithSdk(
console.log(`Running Claude with prompt from file: ${promptPath}`); console.log(`Running Claude with prompt from file: ${promptPath}`);
// Log SDK options without env (which could contain sensitive data) // Log SDK options without env (which could contain sensitive data)
const { env, extraArgs, ...optionsToLog } = sdkOptions; const { env, ...optionsToLog } = sdkOptions;
console.log("SDK options:", JSON.stringify(optionsToLog, null, 2)); console.log("SDK options:", JSON.stringify(optionsToLog, null, 2));
const messages: SDKMessage[] = []; const messages: SDKMessage[] = [];

View File

@ -313,40 +313,6 @@ describe("parseSdkOptions", () => {
}); });
}); });
describe("shell comment stripping", () => {
test("should parse flags before and after a comment line", () => {
const options: ClaudeOptions = {
claudeArgs: "--model 'claude-haiku'\n# comment\n--allowed-tools 'Edit'",
};
const result = parseSdkOptions(options);
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku");
expect(result.sdkOptions.allowedTools).toEqual(["Edit"]);
});
test("should parse flags correctly when no comments are present", () => {
const options: ClaudeOptions = {
claudeArgs: "--model 'claude-haiku'",
};
const result = parseSdkOptions(options);
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku");
});
test("should not strip inline # that appears inside a quoted value", () => {
const options: ClaudeOptions = {
claudeArgs: "--model 'claude-haiku' --prompt 'use color #ff0000'",
};
const result = parseSdkOptions(options);
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku");
expect(result.sdkOptions.extraArgs?.["prompt"]).toBe("use color #ff0000");
});
});
describe("environment variables passthrough", () => { describe("environment variables passthrough", () => {
test("should include OTEL environment variables in sdkOptions.env", () => { test("should include OTEL environment variables in sdkOptions.env", () => {
// Set up test environment variables // Set up test environment variables
@ -400,26 +366,5 @@ describe("parseSdkOptions", () => {
"claude-code-github-action", "claude-code-github-action",
); );
}); });
test("should strip ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN from env", () => {
const originalEnv = { ...process.env };
process.env.ACTIONS_ID_TOKEN_REQUEST_URL =
"https://token.actions.githubusercontent.com";
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "secret-token-value";
try {
const options: ClaudeOptions = {};
const result = parseSdkOptions(options);
expect(
result.sdkOptions.env?.ACTIONS_ID_TOKEN_REQUEST_URL,
).toBeUndefined();
expect(
result.sdkOptions.env?.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
).toBeUndefined();
} finally {
process.env = originalEnv;
}
});
}); });
}); });

108
bun.lock
View File

@ -7,7 +7,7 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.123", "@anthropic-ai/claude-agent-sdk": "^0.2.39",
"@modelcontextprotocol/sdk": "^1.11.0", "@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2", "@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1", "@octokit/rest": "^21.1.1",
@ -37,31 +37,39 @@
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.123", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.123" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-a4TysYoR9DBdkM9Uwh4J5ub7TwKmRPe5hFiWh4En+IKC+qkk5UFkxFM22c//cZjYZKynHX0ah2t6LUqb+najYA=="], "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.39", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-wR1TBH62X6E1YwRnWa+A2Eau7AfpTWtfpnwQXO3yRY31FtmzOjPkQb93hbF3AkT0WL7YF9mxBBwJKUa3ZEc5+A=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.123", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tYAXCjlXZQklsUs0J//gip3fZQRzhlH5OCgvNXV70qe7A1iiwHqO2KPGvEHV1L+deEKQoMZmTaCOrQpN6zju3w=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.123", "", { "os": "darwin", "cpu": "x64" }, "sha512-AcUC6sTon6z6HculP87KsAOeTMRLBwpovdhcXUTjXUpo/8nplJ7lBEzWjZCHt8FF1KuN/WBy1Z4bDg/59TQDmA=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-7+GnbcF3/aZ8RJ1WmU/ogtPsOpknBAoUPer90MvZuFYBLPT9iI/U7f24gjrOHuYdcbDA5n7jFlhcfIO26F5DJQ=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-bYgRiaf2q+yVbGAoUluuhqrEW1zexL34+3HDmK9DneKXa2K2EJpw4M6Sq4XoBD/JezGaemoAP78Xv/M/QUS1OQ=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-Xi+Rwk8uP5vWEnawJOlsk179fr0ATLl5J90MlbLj+puKaX5svEq8ljS+P3zq6zHTJeKh9GKLzPf7bc5YJKwcew=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-IX95lFKhmmndY/YPfWPsVV+C3rLYJmuuq5wCS53p6jYIkCMxH1iGfhBGF1EUWcXO4Uc8yqXFmQ3aaxMzOOPrwA=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.123", "", { "os": "win32", "cpu": "arm64" }, "sha512-WDZmAQG1rOiqNLZlSXaCjSWmqJvLk2io+vFQWWqSy2b5HCk9pa3PadLiaLztiihyk81wPhH9Q/44kOxdyfEGMw=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.123", "", { "os": "win32", "cpu": "x64" }, "sha512-588xrd1i6d4kXQ6FqwL+cgBiN4evRQSi5DCtPa02CZ3VEbuVQBeFlyPlD8tfWtNNeGZ4NM8kjPNNzZz5omezPA=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.16.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.16.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg=="],
@ -103,8 +111,6 @@
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
@ -175,8 +181,6 @@
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
@ -203,30 +207,20 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@ -273,8 +267,6 @@
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@ -305,8 +297,6 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
@ -335,8 +325,6 @@
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], "@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
"@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], "@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
@ -369,24 +357,12 @@
"accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@ -425,24 +401,12 @@
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="], "@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
@ -450,15 +414,5 @@
"@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser/qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
} }
} }

View File

@ -1,2 +0,0 @@
# Intentionally minimal. action.yml pins --config to this file so bun resolves
# its runtime config from the action directory rather than the workspace.

View File

@ -4,55 +4,15 @@
- **Repository Access**: The action can only be triggered by users with write access to the repository - **Repository Access**: The action can only be triggered by users with write access to the repository
- **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots - **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots
- **⚠️ Allowed bots are not checked for repository permissions.** A bot that matches an entry does **not** need to be installed on your repository or have write access. On a **public repository**, external parties — including GitHub Apps created by anyone — may be able to trigger workflow events such as opening issues, commenting, or reviewing pull requests. If your workflow listens on those events and `allowed_bots` is set to `'*'`, any such App can invoke this action with a prompt it controls.
- Prefer an explicit list over `'*'`
- Only list App names you trust
- If you need `'*'`, scope workflow `permissions:` to the minimum required
- **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature: - **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature:
- Only works when `github_token` is provided as input (not with GitHub App authentication) - Only works when `github_token` is provided as input (not with GitHub App authentication)
- Accepts either a comma-separated list of specific usernames or `*` to allow all users - Accepts either a comma-separated list of specific usernames or `*` to allow all users
- **Should be used with extreme caution** as it bypasses the primary security mechanism of this action - **Should be used with extreme caution** as it bypasses the primary security mechanism of this action
- Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope - Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope
- When set, Claude does a best-effort scrub of Anthropic, cloud, and GitHub Actions secrets from subprocess environments. On Linux runners with bubblewrap available, subprocesses additionally run with PID-namespace isolation. This reduces but does not eliminate prompt injection risk — keep workflow permissions minimal and validate all outputs. Set `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: 0` in your workflow or job `env:` block to opt out.
- Optionally set `CLAUDE_CODE_SCRIPT_CAPS` in your workflow `env:` block to limit how many times Claude can call specific scripts per run. Value is JSON: `{"script-name.sh": maxCalls}`. Example: `CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'` allows at most 2 calls to `edit-issue-labels.sh`. Useful for write-capable helper scripts.
- When using `allowed_non_write_users`, always pass `github_token: ${{ secrets.GITHUB_TOKEN }}`. The auto-generated workflow token is scoped to the job's declared permissions and expires when the job completes. **Do not use a personal access token** — a static token does not rotate between runs and could be partially or fully recovered over time via prompt injection. Restricting allowed tools via `claude_args` reduces the rate of recovery but may not eliminate the risk. We recommend restricting allowed tools (e.g. `claude_args: '--allowedTools "Bash(gh issue view:*)"'`) to the minimum required when using `allowed_non_write_users`.
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in - **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered - **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions - **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
## Using this action with `pull_request_target` or `workflow_run`
`pull_request_target` and `workflow_run` execute with the **base repository's secrets**. If your workflow checks out the PR head (`ref: ${{ github.event.pull_request.head.sha }}` for `pull_request_target`, `ref: ${{ github.event.workflow_run.head_sha }}` for `workflow_run`) into `$GITHUB_WORKSPACE` before this action, the action and Claude run with that checkout as the working directory.
**Do not check out an untrusted ref into the workspace root before this action.** Use one of these patterns instead:
```yaml
# Preferred — check out the base ref (default).
- uses: actions/checkout@v6 # no `ref:` → base branch
- uses: anthropics/claude-code-action@v1
```
```yaml
# If you need the PR's files locally — check out the base ref at the workspace
# root (this action expects a git repo there), then check out the head ref into
# a subdirectory and pass it via --add-dir.
- uses: actions/checkout@v6 # no `ref:` → base branch at workspace root
- uses: actions/checkout@v6
with:
# For workflow_run use: ${{ github.event.workflow_run.head_sha }}
ref: ${{ github.event.pull_request.head.sha }}
path: pr-head
- uses: anthropics/claude-code-action@v1
with:
claude_args: "--add-dir pr-head"
```
This is general guidance for these event types — see [GitHub's documentation](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/).
### `claude-code-action` vs `claude-code-base-action`
`claude-code-base-action` is a lower-level building block that installs and runs Claude Code with the inputs you provide. It does not perform actor permission checks or restore project configuration from the base ref. If you need those behaviors, use this action (`claude-code-action`). See the [base-action README](../base-action/README.md#trust-model) for details.
## Pull Request Creation ## Pull Request Creation
In its default configuration, **Claude does not create pull requests automatically** when responding to `@claude` mentions. Instead: In its default configuration, **Claude does not create pull requests automatically** when responding to `@claude` mentions. Instead:
@ -67,8 +27,6 @@ This design ensures that users retain full control over what pull requests are c
**Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it. **Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it.
On public repos, you can also use `include_comments_by_actor` to allowlist which users' comments are passed to Claude, reducing exposure to untrusted input. Use `exclude_comments_by_actor` to filter out noisy bot comments (e.g., `dependabot[bot]`, `renovate[bot]`). If an actor matches both lists, exclusion takes priority. See [Usage](./usage.md) for details.
## GitHub App Permissions ## GitHub App Permissions
The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions: The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions:

View File

@ -55,7 +55,7 @@ jobs:
Note: The PR branch is already checked out in the current working directory. Note: The PR branch is already checked out in the current working directory.
Use `gh pr comment` for top-level feedback. Use `gh pr comment` for top-level feedback.
Use `mcp__github_inline_comment__create_inline_comment` (with `confirmed: true`) to highlight specific code issues. Use `mcp__github_inline_comment__create_inline_comment` to highlight specific code issues.
Only post GitHub comments - don't submit review text as messages. Only post GitHub comments - don't submit review text as messages.
claude_args: | claude_args: |
@ -398,7 +398,6 @@ jobs:
issues: write issues: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1 - uses: anthropics/claude-code-action@v1
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
@ -415,19 +414,13 @@ jobs:
3. Suggest appropriate labels 3. Suggest appropriate labels
4. Check if it duplicates existing issues 4. Check if it duplicates existing issues
Use ./scripts/gh.sh to interact with GitHub:
- `./scripts/gh.sh issue view [number]` to view the issue
- `./scripts/gh.sh search issues "query"` to find similar issues
- `./scripts/gh.sh label list` to see available labels
Based on your analysis, add the appropriate labels using: Based on your analysis, add the appropriate labels using:
`./scripts/edit-issue-labels.sh --add-label "label1" --add-label "label2"` `gh issue edit [number] --add-label "label1,label2"`
(the issue number is read automatically from the workflow event)
If it appears to be a duplicate, post a comment mentioning the original issue. If it appears to be a duplicate, post a comment mentioning the original issue.
claude_args: | claude_args: |
--allowedTools "Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*)" --allowedTools "Bash(gh issue:*),Bash(gh search:*)"
``` ```
**Key Configuration:** **Key Configuration:**
@ -435,7 +428,6 @@ jobs:
- Triggered on new issues - Triggered on new issues
- Issue context in prompt - Issue context in prompt
- Label management capabilities - Label management capabilities
- Requires `scripts/gh.sh` and `scripts/edit-issue-labels.sh` in your repo (see this repo's `scripts/` directory for examples)
**Expected Output:** Automatically labeled and categorized issues. **Expected Output:** Automatically labeled and categorized issues.
@ -586,7 +578,7 @@ prompt: |
### Common Tool Permissions ### Common Tool Permissions
- **PR Comments**: `Bash(gh pr comment:*)` - **PR Comments**: `Bash(gh pr comment:*)`
- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment` — pass `confirmed: true` to post immediately. When omitted, the comment is buffered and classified after the session ends (real review comments post, test/probe comments are filtered). This prevents subagent test comments from reaching PRs. To disable classification entirely, set `classify_inline_comments: 'false'` on the action. - **Inline Comments**: `mcp__github_inline_comment__create_inline_comment`
- **File Operations**: `Read,Write,Edit` - **File Operations**: `Read,Write,Edit`
- **Git Operations**: `Bash(git:*)` - **Git Operations**: `Bash(git:*)`

View File

@ -52,38 +52,35 @@ jobs:
## Inputs ## Inputs
| Input | Description | Required | Default | | Input | Description | Required | Default |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | | `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | | `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | | `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | | `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` |
| `include_fix_links` | Include 'Fix this' links in PR code review feedback that open Claude Code with context to fix the identified issue | No | `true` | | `include_fix_links` | Include 'Fix this' links in PR code review feedback that open Claude Code with context to fix the identified issue | No | `true` |
| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | | `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | | `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | | `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
| `classify_inline_comments` | Buffer inline comments without `confirmed: true` and classify them (real review vs test/probe) via Haiku before posting after the session ends. Prevents subagent test comments. Set `'false'` to post all inline comments immediately | No | `true` | | `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | | `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | | `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | | `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | | `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | | `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | | `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | | `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | | `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | | `use_commit_signing` | Enable commit signing using GitHub's API. Simple but cannot perform complex git operations like rebasing. See [Security](./security.md#commit-signing) | No | `false` |
| `use_commit_signing` | Enable commit signing using GitHub's API. Simple but cannot perform complex git operations like rebasing. See [Security](./security.md#commit-signing) | No | `false` | | `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" |
| `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" | | `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` |
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` | | `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | No | `claude[bot]` |
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | No | `claude[bot]` | | `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
| `include_comments_by_actor` | Comma-separated list of actor usernames to INCLUDE in comments. Supports the `*[bot]` wildcard to match all bot accounts. Empty (default) includes all actors | No | "" | | `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" |
| `exclude_comments_by_actor` | Comma-separated list of actor usernames to EXCLUDE from comments. Supports the `*[bot]` wildcard to match all bot accounts. If an actor matches both lists, exclusion takes priority | No | "" | | `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" |
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots. **⚠️ On public repos with `'*'`, external Apps may be able to invoke this action.** See [Security](./security.md) | No | "" | | `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | | `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" |
| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" | | `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" |
| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
| `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" |
| `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" |
### Deprecated Inputs ### Deprecated Inputs

View File

@ -1,21 +1,5 @@
name: Auto Fix CI Failures name: Auto Fix CI Failures
# ⚠️ SECURITY NOTE
#
# This workflow checks out the PR branch and runs build/test commands
# (npm, bun, etc.) against it with elevated permissions (contents:write,
# id-token:write). This means code from the PR branch executes in a
# trusted context with access to secrets and the ability to push to the
# repository.
#
# Only use this workflow in repositories where everyone with write access
# is fully trusted with these permissions. Do not use this in repositories
# that accept contributions from untrusted or semi-trusted collaborators.
#
# The pull_requests[0] check below limits this to same-repo PRs (fork PRs
# are excluded), but anyone who can push a branch to this repository can
# control what code runs here.
on: on:
workflow_run: workflow_run:
workflows: ["CI"] workflows: ["CI"]
@ -51,14 +35,10 @@ jobs:
- name: Create fix branch - name: Create fix branch
id: branch id: branch
env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
RUN_ID: ${{ github.run_id }}
run: | run: |
SAFE_BRANCH=$(printf '%s' "$HEAD_BRANCH" | tr -cd 'a-zA-Z0-9/_.-') BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
BRANCH_NAME="claude-auto-fix-ci-${SAFE_BRANCH}-${RUN_ID}"
git checkout -b "$BRANCH_NAME" git checkout -b "$BRANCH_NAME"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Get CI failure details - name: Get CI failure details
id: failure_details id: failure_details

View File

@ -21,8 +21,8 @@ jobs:
- name: Run Claude Code for Issue Triage - name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@v1 uses: anthropics/claude-code-action@v1
with: with:
# NOTE: /label-issue requires .claude/commands/label-issue.md and scripts/edit-issue-labels.sh in your repo (see this repo for examples) # NOTE: /label-issue here requires a .claude/commands/label-issue.md file in your repo (see this repo's .claude directory for an example)
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}" prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues

View File

@ -53,8 +53,6 @@ jobs:
fromJSON(steps.detect.outputs.structured_output).confidence >= 0.7 fromJSON(steps.detect.outputs.structured_output).confidence >= 0.7
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: | run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}' OUTPUT='${{ steps.detect.outputs.structured_output }}'
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence') CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
@ -65,7 +63,8 @@ jobs:
echo "" echo ""
echo "Triggering automatic retry..." echo "Triggering automatic retry..."
gh workflow run "$WORKFLOW_NAME" --ref "$HEAD_BRANCH" gh workflow run "${{ github.event.workflow_run.name }}" \
--ref "${{ github.event.workflow_run.head_branch }}"
# Low confidence flaky detection - skip retry # Low confidence flaky detection - skip retry
- name: Low confidence detection - name: Low confidence detection
@ -84,14 +83,13 @@ jobs:
if: github.event.workflow_run.event == 'pull_request' if: github.event.workflow_run.event == 'pull_request'
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: | run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}' OUTPUT='${{ steps.detect.outputs.structured_output }}'
IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky') IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky')
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence') CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary') SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
pr_number=$(gh pr list --head "$HEAD_BRANCH" --json number --jq '.[0].number') pr_number=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --json number --jq '.[0].number')
if [ -n "$pr_number" ]; then if [ -n "$pr_number" ]; then
if [ "$IS_FLAKY" = "true" ]; then if [ "$IS_FLAKY" = "true" ]; then

View File

@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.123", "@anthropic-ai/claude-agent-sdk": "^0.2.39",
"@modelcontextprotocol/sdk": "^1.11.0", "@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2", "@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1", "@octokit/rest": "^21.1.1",

View File

@ -1,83 +0,0 @@
#!/usr/bin/env bash
#
# Edits labels on a GitHub issue.
# Usage: ./scripts/edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged
#
# The issue number is read from the workflow event payload.
#
set -euo pipefail
# Read from event payload so the issue number is bound to the triggering event
ISSUE=$(jq -r '.issue.number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
echo "Error: no issue number in event payload" >&2
exit 1
fi
ADD_LABELS=()
REMOVE_LABELS=()
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--add-label)
ADD_LABELS+=("$2")
shift 2
;;
--remove-label)
REMOVE_LABELS+=("$2")
shift 2
;;
*)
echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
exit 1
;;
esac
done
if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
exit 1
fi
# Fetch valid labels from the repo
VALID_LABELS=$(gh label list --limit 500 --json name --jq '.[].name')
# Filter to only labels that exist in the repo
FILTERED_ADD=()
for label in "${ADD_LABELS[@]}"; do
if echo "$VALID_LABELS" | grep -qxF "$label"; then
FILTERED_ADD+=("$label")
fi
done
FILTERED_REMOVE=()
for label in "${REMOVE_LABELS[@]}"; do
if echo "$VALID_LABELS" | grep -qxF "$label"; then
FILTERED_REMOVE+=("$label")
fi
done
if [[ ${#FILTERED_ADD[@]} -eq 0 && ${#FILTERED_REMOVE[@]} -eq 0 ]]; then
exit 0
fi
# Build gh command arguments
GH_ARGS=("issue" "edit" "$ISSUE")
for label in "${FILTERED_ADD[@]}"; do
GH_ARGS+=("--add-label" "$label")
done
for label in "${FILTERED_REMOVE[@]}"; do
GH_ARGS+=("--remove-label" "$label")
done
gh "${GH_ARGS[@]}"
if [[ ${#FILTERED_ADD[@]} -gt 0 ]]; then
echo "Added: ${FILTERED_ADD[*]}"
fi
if [[ ${#FILTERED_REMOVE[@]} -gt 0 ]]; then
echo "Removed: ${FILTERED_REMOVE[*]}"
fi

View File

@ -1,96 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Wrapper around gh CLI that only allows specific subcommands and flags.
# All commands are scoped to the current repository via GH_REPO or GITHUB_REPOSITORY.
#
# Usage:
# ./scripts/gh.sh issue view 123
# ./scripts/gh.sh issue view 123 --comments
# ./scripts/gh.sh issue list --state open --limit 20
# ./scripts/gh.sh search issues "search query" --limit 10
# ./scripts/gh.sh label list --limit 100
export GH_HOST=github.com
REPO="${GH_REPO:-${GITHUB_REPOSITORY:-}}"
if [[ -z "$REPO" || "$REPO" == */*/* || "$REPO" != */* ]]; then
echo "Error: GH_REPO or GITHUB_REPOSITORY must be set to owner/repo format (e.g., GITHUB_REPOSITORY=anthropics/claude-code)" >&2
exit 1
fi
export GH_REPO="$REPO"
ALLOWED_FLAGS=(--comments --state --limit --label)
FLAGS_WITH_VALUES=(--state --limit --label)
SUB1="${1:-}"
SUB2="${2:-}"
CMD="$SUB1 $SUB2"
case "$CMD" in
"issue view"|"issue list"|"search issues"|"label list")
;;
*)
echo "Error: only 'issue view', 'issue list', 'search issues', 'label list' are allowed (e.g., ./scripts/gh.sh issue view 123)" >&2
exit 1
;;
esac
shift 2
# Separate flags from positional arguments
POSITIONAL=()
FLAGS=()
skip_next=false
for arg in "$@"; do
if [[ "$skip_next" == true ]]; then
FLAGS+=("$arg")
skip_next=false
elif [[ "$arg" == -* ]]; then
flag="${arg%%=*}"
matched=false
for allowed in "${ALLOWED_FLAGS[@]}"; do
if [[ "$flag" == "$allowed" ]]; then
matched=true
break
fi
done
if [[ "$matched" == false ]]; then
echo "Error: only --comments, --state, --limit, --label flags are allowed (e.g., ./scripts/gh.sh issue list --state open --limit 20)" >&2
exit 1
fi
FLAGS+=("$arg")
# If flag expects a value and isn't using = syntax, skip next arg
if [[ "$arg" != *=* ]]; then
for vflag in "${FLAGS_WITH_VALUES[@]}"; do
if [[ "$flag" == "$vflag" ]]; then
skip_next=true
break
fi
done
fi
else
POSITIONAL+=("$arg")
fi
done
if [[ "$CMD" == "search issues" ]]; then
QUERY="${POSITIONAL[0]:-}"
QUERY_LOWER=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]')
if [[ "$QUERY_LOWER" == *"repo:"* || "$QUERY_LOWER" == *"org:"* || "$QUERY_LOWER" == *"user:"* ]]; then
echo "Error: search query must not contain repo:, org:, or user: qualifiers (e.g., ./scripts/gh.sh search issues \"bug report\" --limit 10)" >&2
exit 1
fi
gh "$SUB1" "$SUB2" "$QUERY" --repo "$REPO" "${FLAGS[@]}"
elif [[ "$CMD" == "issue view" ]]; then
if [[ ${#POSITIONAL[@]} -ne 1 ]] || ! [[ "${POSITIONAL[0]}" =~ ^[0-9]+$ ]]; then
echo "Error: issue view requires exactly one numeric issue number (e.g., ./scripts/gh.sh issue view 123)" >&2
exit 1
fi
gh "$SUB1" "$SUB2" "${POSITIONAL[0]}" "${FLAGS[@]}"
else
if [[ ${#POSITIONAL[@]} -ne 0 ]]; then
echo "Error: issue list and label list do not accept positional arguments (e.g., ./scripts/gh.sh issue list --state open, ./scripts/gh.sh label list --limit 100)" >&2
exit 1
fi
gh "$SUB1" "$SUB2" "${FLAGS[@]}"
fi

View File

@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Wrapper around `git push` that only allows `origin <ref>` with no flags.
# Defends against --receive-pack / --exec RCE and arbitrary-remote exfiltration
# (H1 #3556799). `git push:*` in allowedTools permits `git push --receive-pack='sh -c ...' ext::sh`
# which runs arbitrary shell on the Actions runner. This wrapper closes that.
#
# Usage:
# git-push.sh origin HEAD
# git-push.sh origin claude/issue-123-20260304
if [[ $# -ne 2 ]]; then
echo "Error: exactly two arguments required: origin <ref>" >&2
exit 1
fi
for arg in "$@"; do
if [[ "$arg" == -* ]]; then
echo "Error: flags are not allowed (got: $arg)" >&2
exit 1
fi
done
if [[ "$1" != "origin" ]]; then
echo "Error: remote must be 'origin' (got: $1)" >&2
exit 1
fi
REF="$2"
if [[ "$REF" != "HEAD" ]] && ! git check-ref-format --branch "$REF" >/dev/null 2>&1; then
echo "Error: invalid ref: $REF" >&2
exit 1
fi
exec git push origin "$REF"

View File

@ -23,15 +23,19 @@ import { GITHUB_SERVER_URL } from "../github/api/config";
import { extractUserRequest } from "../utils/extract-user-request"; import { extractUserRequest } from "../utils/extract-user-request";
export type { CommonFields, PreparedContext } from "./types"; export type { CommonFields, PreparedContext } from "./types";
const GIT_PUSH_WRAPPER = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`;
/** Filename for the user request file, read by the SDK runner */ /** Filename for the user request file, read by the SDK runner */
const USER_REQUEST_FILENAME = "claude-user-request.txt"; const USER_REQUEST_FILENAME = "claude-user-request.txt";
// Tag mode defaults - these tools are needed for tag mode to function. // Tag mode defaults - these tools are needed for tag mode to function
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode const BASE_ALLOWED_TOOLS = [
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside it. "Edit",
const BASE_ALLOWED_TOOLS = ["Glob", "Grep", "LS", "Read"]; "MultiEdit",
"Glob",
"Grep",
"LS",
"Read",
"Write",
];
export function buildAllowedToolsString( export function buildAllowedToolsString(
customAllowedTools?: string[], customAllowedTools?: string[],
@ -53,10 +57,13 @@ export function buildAllowedToolsString(
} else { } else {
// When not using commit signing, add specific Bash git commands // When not using commit signing, add specific Bash git commands
baseTools.push( baseTools.push(
"Bash(git add:*)", "Bash(git add *)",
"Bash(git commit:*)", "Bash(git commit *)",
`Bash(${GIT_PUSH_WRAPPER}:*)`, "Bash(git push *)",
"Bash(git rm:*)", "Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git rm *)",
); );
} }
@ -427,7 +434,7 @@ function getCommitInstructions(
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")` Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
: "" : ""
} }
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin HEAD)`; - Push to the remote: Bash(git push origin HEAD)`;
} else { } else {
const branchName = eventData.claudeBranch || eventData.baseBranch; const branchName = eventData.claudeBranch || eventData.baseBranch;
return ` return `
@ -441,7 +448,7 @@ function getCommitInstructions(
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")` Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
: "" : ""
} }
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin ${branchName})`; - Push to the remote: Bash(git push origin ${branchName})`;
} }
} }
} }
@ -816,7 +823,7 @@ ${
: `- Use git commands via the Bash tool for version control (remember that you have access to these git commands): : `- Use git commands via the Bash tool for version control (remember that you have access to these git commands):
- Stage files: Bash(git add <files>) - Stage files: Bash(git add <files>)
- Commit changes: Bash(git commit -m "<message>") - Commit changes: Bash(git commit -m "<message>")
- Push to remote: Bash(${GIT_PUSH_WRAPPER} origin <branch>) - Push to remote: Bash(git push origin <branch>) (NEVER force push)
- Delete files: Bash(git rm <files>) followed by commit and push - Delete files: Bash(git rm <files>) followed by commit and push
- Check status: Bash(git status) - Check status: Bash(git status)
- View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}` - View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}`
@ -970,9 +977,7 @@ export async function createPrompt(
console.log("========================"); console.log("========================");
} }
// NOTE: these env var exports are dead — nothing reads ALLOWED_TOOLS / DISALLOWED_TOOLS. // Set allowed tools
// The live path is modes/tag/index.ts which builds --allowedTools into claudeArgs directly.
// Kept only so the H1 report's pointed-to file stays in sync with the live fix.
const hasActionsReadPermission = false; const hasActionsReadPermission = false;
const allAllowedTools = buildAllowedToolsString( const allAllowedTools = buildAllowedToolsString(

View File

@ -23,7 +23,6 @@ export function collectActionInputsPresence(): string {
github_token: "", github_token: "",
max_turns: "", max_turns: "",
use_sticky_comment: "false", use_sticky_comment: "false",
classify_inline_comments: "true",
use_commit_signing: "false", use_commit_signing: "false",
ssh_signing_key: "", ssh_signing_key: "",
}; };

View File

@ -1,233 +0,0 @@
#!/usr/bin/env bun
/**
* Reads buffered inline-comment calls from /tmp/inline-comments-buffer.jsonl,
* classifies each as "real review" vs "test/probe" using Haiku, and posts
* only the real ones. Calls with confirmed=false are never posted.
*
* If the Anthropic API is unavailable (Bedrock/Vertex users without a direct
* key), falls back to posting everything with confirmed !== false. This
* preserves backward compatibility before this change, all unconfirmed
* calls posted immediately.
*/
import { readFileSync } from "fs";
import { createOctokit } from "../github/api/client";
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
type BufferedComment = {
ts: string;
path: string;
line?: number;
startLine?: number;
side?: "LEFT" | "RIGHT";
commit_id?: string;
body: string;
confirmed?: boolean;
};
const CLASSIFICATION_PROMPT = `You are classifying PR inline comments as either REAL code review feedback or TEST/PROBE calls.
A TEST/PROBE call is when an automated agent is checking whether a commenting tool works. These typically:
- Start with phrases like "Test comment", "Testing if", "Can I", "Does this work", "Checking if"
- Have generic/placeholder content not specific to any code
- Exist to verify tool functionality, not to provide review feedback
A REAL review comment:
- Discusses specific code, logic, bugs, or style
- Provides actionable feedback for the PR author
- References concrete aspects of the change
For each numbered comment body below, respond with ONLY a JSON array of booleans where true = REAL review comment, false = test/probe. No other text.
Comments:
`;
async function classifyComments(bodies: string[]): Promise<boolean[] | null> {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.log(
"ANTHROPIC_API_KEY not set — skipping classification, posting all unconfirmed comments",
);
return null;
}
const prompt =
CLASSIFICATION_PROMPT +
bodies.map((b, i) => `${i + 1}. ${JSON.stringify(b)}`).join("\n");
try {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
}),
});
if (!res.ok) {
console.log(
`Classification API returned ${res.status} — posting all unconfirmed comments`,
);
return null;
}
const data = (await res.json()) as {
content: { type: string; text: string }[];
};
const text = data.content.find((c) => c.type === "text")?.text ?? "";
const match = text.match(/\[[\s\S]*\]/);
if (!match) {
console.log(
"Could not parse classification response — posting all unconfirmed comments",
);
return null;
}
const parsed = JSON.parse(match[0]);
if (
!Array.isArray(parsed) ||
parsed.length !== bodies.length ||
!parsed.every((v) => typeof v === "boolean")
) {
console.log(
"Classification response shape mismatch — posting all unconfirmed comments",
);
return null;
}
return parsed;
} catch (e) {
console.log(
`Classification failed (${e instanceof Error ? e.message : String(e)}) — posting all unconfirmed comments`,
);
return null;
}
}
async function postComment(
octokit: ReturnType<typeof createOctokit>["rest"],
owner: string,
repo: string,
pull_number: number,
headSha: string,
c: BufferedComment,
): Promise<boolean> {
const params: Parameters<typeof octokit.rest.pulls.createReviewComment>[0] = {
owner,
repo,
pull_number,
body: c.body,
path: c.path,
side: c.side || "RIGHT",
commit_id: c.commit_id || headSha,
};
if (c.startLine) {
params.start_line = c.startLine;
params.start_side = c.side || "RIGHT";
params.line = c.line;
} else {
params.line = c.line;
}
try {
await octokit.rest.pulls.createReviewComment(params);
return true;
} catch (e) {
console.log(
` failed ${c.path}:${c.line}: ${e instanceof Error ? e.message : String(e)}`,
);
return false;
}
}
async function main() {
let raw: string;
try {
raw = readFileSync(BUFFER_PATH, "utf8");
} catch {
console.log("No buffered inline comments");
return;
}
const comments: BufferedComment[] = raw
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
if (comments.length === 0) {
console.log("No buffered inline comments");
return;
}
console.log(`Found ${comments.length} buffered inline comment(s)`);
const githubToken = process.env.GITHUB_TOKEN;
const owner = process.env.REPO_OWNER;
const repo = process.env.REPO_NAME;
const prNumber = process.env.PR_NUMBER;
if (!githubToken || !owner || !repo || !prNumber) {
console.log(
"::warning::Missing GITHUB_TOKEN/REPO_OWNER/REPO_NAME/PR_NUMBER — cannot post buffered comments",
);
return;
}
// Partition: confirmed=false are never posted; the rest are candidates
const neverPost = comments.filter((c) => c.confirmed === false);
const candidates = comments.filter((c) => c.confirmed !== false);
if (neverPost.length > 0) {
console.log(` ${neverPost.length} with confirmed=false — not posting`);
}
if (candidates.length === 0) {
return;
}
// Classify candidates
const verdicts = await classifyComments(candidates.map((c) => c.body));
const toPost =
verdicts === null
? candidates
: candidates.filter((_, i) => verdicts[i] === true);
const filtered =
verdicts === null ? [] : candidates.filter((_, i) => verdicts[i] === false);
if (filtered.length > 0) {
console.log(
`::warning::${filtered.length} buffered comment(s) classified as test/probe — NOT posted:`,
);
for (const c of filtered) {
console.log(` [${c.path}:${c.line}] ${c.body.slice(0, 120)}`);
}
}
if (toPost.length === 0) {
console.log("No real comments to post");
return;
}
const octokit = createOctokit(githubToken).rest;
const pull_number = parseInt(prNumber, 10);
const pr = await octokit.pulls.get({ owner, repo, pull_number });
const headSha = pr.data.head.sha;
console.log(`Posting ${toPost.length} classified-as-real comment(s)`);
let posted = 0;
for (const c of toPost) {
if (await postComment(octokit, owner, repo, pull_number, headSha, c)) {
console.log(` posted ${c.path}:${c.line}`);
posted++;
}
}
console.log(`Posted ${posted}/${toPost.length}`);
}
main().catch((e) => {
console.error("post-buffered-inline-comments failed:", e);
process.exit(1);
});

View File

@ -15,20 +15,12 @@ import { setupGitHubToken, WorkflowValidationSkipError } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions"; import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import type { Octokits } from "../github/api/client"; import type { Octokits } from "../github/api/client";
import { import { parseGitHubContext, isEntityContext } from "../github/context";
parseGitHubContext,
isEntityContext,
isPullRequestEvent,
isPullRequestReviewEvent,
isPullRequestReviewCommentEvent,
} from "../github/context";
import type { GitHubContext } from "../github/context"; import type { GitHubContext } from "../github/context";
import { detectMode } from "../modes/detector"; import { detectMode } from "../modes/detector";
import { prepareTagMode } from "../modes/tag"; import { prepareTagMode } from "../modes/tag";
import { prepareAgentMode } from "../modes/agent"; import { prepareAgentMode } from "../modes/agent";
import { checkContainsTrigger } from "../github/validation/trigger"; import { checkContainsTrigger } from "../github/validation/trigger";
import { restoreConfigFromBase } from "../github/operations/restore-config";
import { validateBranchName } from "../github/operations/branch";
import { collectActionInputsPresence } from "./collect-inputs"; import { collectActionInputsPresence } from "./collect-inputs";
import { updateCommentLink } from "./update-comment-link"; import { updateCommentLink } from "./update-comment-link";
import { formatTurnsFromData } from "./format-turns"; import { formatTurnsFromData } from "./format-turns";
@ -43,16 +35,10 @@ import type { ClaudeRunResult } from "../../base-action/src/run-claude-sdk";
/** /**
* Install Claude Code CLI, handling retry logic and custom executable paths. * Install Claude Code CLI, handling retry logic and custom executable paths.
* Returns the absolute path to the claude executable.
*/ */
async function installClaudeCode(): Promise<string> { async function installClaudeCode(): Promise<void> {
const customExecutable = process.env.PATH_TO_CLAUDE_CODE_EXECUTABLE; const customExecutable = process.env.PATH_TO_CLAUDE_CODE_EXECUTABLE;
if (customExecutable) { if (customExecutable) {
if (/[\x00-\x1f\x7f]/.test(customExecutable)) {
throw new Error(
"PATH_TO_CLAUDE_CODE_EXECUTABLE contains control characters (e.g. newlines), which is not allowed",
);
}
console.log(`Using custom Claude Code executable: ${customExecutable}`); console.log(`Using custom Claude Code executable: ${customExecutable}`);
const claudeDir = dirname(customExecutable); const claudeDir = dirname(customExecutable);
// Add to PATH by appending to GITHUB_PATH // Add to PATH by appending to GITHUB_PATH
@ -62,10 +48,10 @@ async function installClaudeCode(): Promise<string> {
} }
// Also add to current process PATH // Also add to current process PATH
process.env.PATH = `${claudeDir}:${process.env.PATH}`; process.env.PATH = `${claudeDir}:${process.env.PATH}`;
return customExecutable; return;
} }
const claudeCodeVersion = "2.1.123"; const claudeCodeVersion = "2.1.31";
console.log(`Installing Claude Code v${claudeCodeVersion}...`); console.log(`Installing Claude Code v${claudeCodeVersion}...`);
for (let attempt = 1; attempt <= 3; attempt++) { for (let attempt = 1; attempt <= 3; attempt++) {
@ -94,7 +80,7 @@ async function installClaudeCode(): Promise<string> {
await appendFile(githubPath, `${homeBin}\n`); await appendFile(githubPath, `${homeBin}\n`);
} }
process.env.PATH = `${homeBin}:${process.env.PATH}`; process.env.PATH = `${homeBin}:${process.env.PATH}`;
return `${homeBin}/claude`; return;
} catch (error) { } catch (error) {
if (attempt === 3) { if (attempt === 3) {
throw new Error( throw new Error(
@ -105,7 +91,6 @@ async function installClaudeCode(): Promise<string> {
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
} }
} }
throw new Error("unreachable");
} }
/** /**
@ -222,7 +207,7 @@ async function run() {
prepareCompleted = true; prepareCompleted = true;
// Phase 2: Install Claude Code CLI // Phase 2: Install Claude Code CLI
const claudeExecutable = await installClaudeCode(); await installClaudeCode();
// Phase 3: Run Claude (import base-action directly) // Phase 3: Run Claude (import base-action directly)
// Set env vars needed by the base-action code // Set env vars needed by the base-action code
@ -232,36 +217,12 @@ async function run() {
validateEnvironmentVariables(); validateEnvironmentVariables();
// On PRs, .claude/ and .mcp.json in the checkout are attacker-controlled.
// Restore them from the base branch before the CLI reads them.
//
// We read pull_request.base.ref from the payload directly because agent
// mode's branchInfo.baseBranch defaults to the repo's default branch rather
// than the PR's actual target (agent/index.ts). For issue_comment on a PR the payload
// lacks base.ref, so we fall back to the mode-provided value — tag mode
// fetches it from GraphQL; agent mode on issue_comment is an edge case
// that at worst restores from the wrong trusted branch (still secure).
if (isEntityContext(context) && context.isPR) {
let restoreBase = baseBranch;
if (
isPullRequestEvent(context) ||
isPullRequestReviewEvent(context) ||
isPullRequestReviewCommentEvent(context)
) {
restoreBase = context.payload.pull_request.base.ref;
validateBranchName(restoreBase);
}
if (restoreBase) {
restoreConfigFromBase(restoreBase);
}
}
await setupClaudeCodeSettings(process.env.INPUT_SETTINGS); await setupClaudeCodeSettings(process.env.INPUT_SETTINGS);
await installPlugins( await installPlugins(
process.env.INPUT_PLUGIN_MARKETPLACES, process.env.INPUT_PLUGIN_MARKETPLACES,
process.env.INPUT_PLUGINS, process.env.INPUT_PLUGINS,
claudeExecutable, process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
); );
const promptFile = const promptFile =
@ -276,7 +237,8 @@ async function run() {
claudeArgs: prepareResult.claudeArgs, claudeArgs: prepareResult.claudeArgs,
appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT, appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT,
model: process.env.ANTHROPIC_MODEL, model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable: claudeExecutable, pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT, showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
}); });
@ -318,7 +280,7 @@ async function run() {
commentId, commentId,
githubToken, githubToken,
claudeBranch, claudeBranch,
baseBranch: baseBranch || context.repository.default_branch || "main", baseBranch: baseBranch || "main",
triggerUsername: context.actor, triggerUsername: context.actor,
context, context,
octokit, octokit,
@ -333,12 +295,8 @@ async function run() {
} }
} }
// Write step summary (unless display_report is set to false) // Write step summary
if ( if (executionFile && existsSync(executionFile)) {
executionFile &&
existsSync(executionFile) &&
process.env.DISPLAY_REPORT !== "false"
) {
await writeStepSummary(executionFile); await writeStepSummary(executionFile);
} }

View File

@ -253,8 +253,7 @@ async function run() {
commentId: parseInt(process.env.CLAUDE_COMMENT_ID!), commentId: parseInt(process.env.CLAUDE_COMMENT_ID!),
githubToken, githubToken,
claudeBranch: process.env.CLAUDE_BRANCH, claudeBranch: process.env.CLAUDE_BRANCH,
baseBranch: baseBranch: process.env.BASE_BRANCH || "main",
process.env.BASE_BRANCH || context.repository.default_branch || "main",
triggerUsername: process.env.TRIGGER_USERNAME, triggerUsername: process.env.TRIGGER_USERNAME,
context, context,
octokit, octokit,

View File

@ -12,13 +12,6 @@ export const PR_QUERY = `
baseRefName baseRefName
headRefName headRefName
headRefOid headRefOid
isCrossRepository
headRepository {
owner {
login
}
name
}
createdAt createdAt
updatedAt updatedAt
lastEditedAt lastEditedAt

View File

@ -79,7 +79,6 @@ type BaseContext = {
owner: string; owner: string;
repo: string; repo: string;
full_name: string; full_name: string;
default_branch?: string;
}; };
actor: string; actor: string;
inputs: { inputs: {
@ -91,7 +90,6 @@ type BaseContext = {
branchPrefix: string; branchPrefix: string;
branchNameTemplate?: string; branchNameTemplate?: string;
useStickyComment: boolean; useStickyComment: boolean;
classifyInlineComments: boolean;
useCommitSigning: boolean; useCommitSigning: boolean;
sshSigningKey: string; sshSigningKey: string;
botId: string; botId: string;
@ -141,7 +139,6 @@ export function parseGitHubContext(): GitHubContext {
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
full_name: `${context.repo.owner}/${context.repo.repo}`, full_name: `${context.repo.owner}/${context.repo.repo}`,
default_branch: context.payload.repository?.default_branch,
}, },
actor: context.actor, actor: context.actor,
inputs: { inputs: {
@ -153,7 +150,6 @@ export function parseGitHubContext(): GitHubContext {
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/", branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE, branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
useStickyComment: process.env.USE_STICKY_COMMENT === "true", useStickyComment: process.env.USE_STICKY_COMMENT === "true",
classifyInlineComments: process.env.CLASSIFY_INLINE_COMMENTS !== "false",
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true", useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
sshSigningKey: process.env.SSH_SIGNING_KEY || "", sshSigningKey: process.env.SSH_SIGNING_KEY || "",
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),

View File

@ -299,7 +299,7 @@ export async function fetchGitHubData({
includeCommentsByActor, includeCommentsByActor,
excludeCommentsByActor, excludeCommentsByActor,
); );
reviewData = pullRequest.reviews || { nodes: [] }; reviewData = pullRequest.reviews || [];
console.log(`Successfully fetched PR #${prNumber} data`); console.log(`Successfully fetched PR #${prNumber} data`);
} else { } else {

View File

@ -28,7 +28,7 @@ function extractFirstLabel(githubData: FetchDataResult): string | undefined {
* *
* Valid branch names: * Valid branch names:
* - Start with alphanumeric character (not dash, to prevent option injection) * - Start with alphanumeric character (not dash, to prevent option injection)
* - Contain only alphanumeric, forward slash, hyphen, underscore, period, or hash (#) * - Contain only alphanumeric, forward slash, hyphen, underscore, or period
* - Do not start or end with a period * - Do not start or end with a period
* - Do not end with a slash * - Do not end with a slash
* - Do not contain '..' (path traversal) * - Do not contain '..' (path traversal)
@ -58,16 +58,12 @@ export function validateBranchName(branchName: string): void {
); );
} }
// Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period/hash/plus. // Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period
// # is valid per git-check-ref-format and commonly used in branch names like "fix/#123-description". const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$/;
// + is valid per git-check-ref-format and generated by Claude Code's EnterWorktree tool when
// converting worktree names containing "/" (e.g. "feat/foo" becomes "worktree-feat+foo").
// All git calls use execFileSync (not shell interpolation), so neither # nor + carries injection risk.
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.#+-]*$/;
if (!validPattern.test(branchName)) { if (!validPattern.test(branchName)) {
throw new Error( throw new Error(
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), or plus signs (+).`, `Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, or periods.`,
); );
} }
@ -122,7 +118,7 @@ export function validateBranchName(branchName: string): void {
* @param args - Git command arguments (e.g., ["checkout", "branch-name"]) * @param args - Git command arguments (e.g., ["checkout", "branch-name"])
*/ */
function execGit(args: string[]): void { function execGit(args: string[]): void {
execFileSync("git", args, { stdio: "inherit", env: process.env }); execFileSync("git", args, { stdio: "inherit" });
} }
export type BranchInfo = { export type BranchInfo = {
@ -168,23 +164,15 @@ export async function setupBranch(
// Validate branch names before use to prevent command injection // Validate branch names before use to prevent command injection
validateBranchName(branchName); validateBranchName(branchName);
// For cross-repository (fork) PRs, fetch via the pull ref since the // Execute git commands to checkout PR branch (dynamic depth based on PR size)
// branch only exists on the fork's remote, not on origin. // Using execFileSync instead of shell template literals for security
if (prData.isCrossRepository) { // Use GitHub's PR ref which works for both same-repo and fork PRs
console.log( execGit([
`PR #${entityNumber} is from a fork, fetching via refs/pull/${entityNumber}/head...`, "fetch",
); "origin",
execGit([ `--depth=${fetchDepth}`,
"fetch", `pull/${entityNumber}/head:${branchName}`,
"origin", ]);
`--depth=${fetchDepth}`,
`pull/${entityNumber}/head:${branchName}`,
]);
} else {
// Execute git commands to checkout PR branch (dynamic depth based on PR size)
// Using execFileSync instead of shell template literals for security
execGit(["fetch", "origin", `--depth=${fetchDepth}`, branchName]);
}
execGit(["checkout", branchName, "--"]); execGit(["checkout", branchName, "--"]);
console.log(`Successfully checked out PR branch for PR #${entityNumber}`); console.log(`Successfully checked out PR branch for PR #${entityNumber}`);

View File

@ -51,34 +51,11 @@ export async function configureGitAuth(
console.log("No existing authentication headers to remove"); console.log("No existing authentication headers to remove");
} }
if (process.env.ALLOWED_NON_WRITE_USERS) { // Update the remote URL to include the token for authentication
// When processing content from non-write users, use a credential helper console.log("Updating remote URL with authentication...");
// instead of embedding the token in the remote URL. The helper script reads const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
// from GH_TOKEN at auth time, so .git/config stays token-free. Written as a await $`git remote set-url origin ${remoteUrl}`;
// file to avoid shell-escaping the helper body; placed under console.log("✓ Updated remote URL with authentication token");
// GITHUB_ACTION_PATH so it sits alongside the action source.
console.log("Configuring git credential helper...");
process.env.GH_TOKEN = githubToken;
const helperPath = join(
process.env.GITHUB_ACTION_PATH || homedir(),
".git-credential-gh-token",
);
await writeFile(
helperPath,
'#!/bin/sh\necho username=x-access-token\necho password="$GH_TOKEN"\n',
{ mode: 0o700 },
);
const cleanUrl = `https://${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
await $`git remote set-url origin ${cleanUrl}`;
await $`git config credential.helper ${helperPath}`;
console.log("✓ Configured credential helper");
} else {
// Update the remote URL to include the token for authentication
console.log("Updating remote URL with authentication...");
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
await $`git remote set-url origin ${remoteUrl}`;
console.log("✓ Updated remote URL with authentication token");
}
console.log("Git authentication configured successfully"); console.log("Git authentication configured successfully");
} }

View File

@ -1,109 +0,0 @@
import { execFileSync } from "child_process";
import { cpSync, existsSync, rmSync } from "fs";
// Paths that are both PR-controllable and read from cwd at CLI startup.
//
// Deliberately excluded from the CLI's broader auto-edit blocklist:
// .git/ — not tracked by git; PR commits cannot place files there.
// Restoring it would also undo the PR checkout entirely.
// .gitconfig — git reads ~/.gitconfig and .git/config, never cwd/.gitconfig.
// .bashrc etc. — shells source these from $HOME; checkout cannot reach $HOME.
// .vscode/.idea— IDE config; nothing in the CLI's startup path reads them.
const SENSITIVE_PATHS = [
".claude",
".mcp.json",
".claude.json",
".gitmodules",
".ripgreprc",
"CLAUDE.md",
"CLAUDE.local.md",
".husky",
];
/**
* Restores security-sensitive config paths from the PR base branch.
*
* The CLI's non-interactive mode trusts cwd: it reads `.mcp.json`,
* `.claude/settings.json`, and `.claude/settings.local.json` from the working
* directory and acts on them before any tool-permission gating executing
* hooks (including SessionStart), setting env vars (NODE_OPTIONS, LD_PRELOAD,
* PATH), running apiKeyHelper/awsAuthRefresh shell commands, and auto-approving
* MCP servers. When this action checks out a PR head, all of these are
* attacker-controlled.
*
* Rather than enumerate every dangerous key, this replaces the entire `.claude/`
* tree and `.mcp.json` with the versions from the PR base branch, which a
* maintainer has reviewed and merged. Paths absent on base are deleted.
*
* Known limitation: if a PR legitimately modifies `.claude/` and the CLI later
* commits with `git add -A`, the revert will be included in that commit. This
* is a narrow UX tradeoff for closing the RCE surface.
*
* @param baseBranch - PR base branch name. Must be pre-validated (branch.ts
* calls validateBranchName on it before returning).
*/
export function restoreConfigFromBase(baseBranch: string): void {
console.log(
`Restoring ${SENSITIVE_PATHS.join(", ")} from origin/${baseBranch} (PR head is untrusted)`,
);
// Snapshot every PR-authored sensitive path into .claude-pr/ before deletion
// so review agents can inspect what the PR changes without those files ever
// being executed. Captured before the security delete so it reflects the
// PR-authored version.
rmSync(".claude-pr", { recursive: true, force: true });
for (const p of SENSITIVE_PATHS) {
if (existsSync(p)) {
cpSync(p, `.claude-pr/${p}`, { recursive: true });
}
}
if (existsSync(".claude-pr")) {
console.log(
"Preserved PR's sensitive paths → .claude-pr/ for review agents (not executed)",
);
}
// Delete PR-controlled versions BEFORE fetching so the attacker-controlled
// .gitmodules is absent during the network operation. If git reads .gitmodules
// during fetch (fetch.recurseSubmodules=on-demand, the git default), it will
// attempt to fetch submodule objects and block on credential prompts in CI —
// causing an indefinite hang. Deleting first closes that window.
//
// If the restore below fails for a given path, that path stays deleted —
// the safe fallback (no attacker-controlled config). A bare `git checkout`
// alone wouldn't remove files the PR added, so nuke first.
for (const p of SENSITIVE_PATHS) {
rmSync(p, { recursive: true, force: true });
}
// --no-recurse-submodules: explicitly suppress submodule fetching regardless of
// fetch.recurseSubmodules config. Defense-in-depth alongside the delete above.
execFileSync(
"git",
["fetch", "origin", baseBranch, "--depth=1", "--no-recurse-submodules"],
{
stdio: "inherit",
env: process.env,
},
);
for (const p of SENSITIVE_PATHS) {
try {
execFileSync("git", ["checkout", `origin/${baseBranch}`, "--", p], {
stdio: "pipe",
});
} catch {
// Path doesn't exist on base — it stays deleted.
}
}
// `git checkout <ref> -- <path>` stages the restored files. Unstage so the
// revert doesn't silently leak into commits the CLI makes later.
try {
execFileSync("git", ["reset", "--", ...SENSITIVE_PATHS], {
stdio: "pipe",
});
} catch {
// Nothing was staged, or paths don't exist on HEAD — either is fine.
}
}

View File

@ -141,14 +141,10 @@ export async function setupGitHubToken(): Promise<string> {
const permissions = parseAdditionalPermissions(); const permissions = parseAdditionalPermissions();
console.log("Exchanging OIDC token for app token..."); console.log("Exchanging OIDC token for app token...");
const appToken = await retryWithBackoff( const appToken = await retryWithBackoff(() =>
() => exchangeForAppToken(oidcToken, permissions), exchangeForAppToken(oidcToken, permissions),
{
shouldRetry: (error) => !(error instanceof WorkflowValidationSkipError),
},
); );
console.log("App token successfully obtained"); console.log("App token successfully obtained");
core.setSecret(appToken);
console.log("Using GITHUB_TOKEN from OIDC"); console.log("Using GITHUB_TOKEN from OIDC");
return appToken; return appToken;

View File

@ -57,13 +57,6 @@ export type GitHubPullRequest = {
baseRefName: string; baseRefName: string;
headRefName: string; headRefName: string;
headRefOid: string; headRefOid: string;
isCrossRepository: boolean;
headRepository: {
owner: {
login: string;
};
name: string;
} | null;
createdAt: string; createdAt: string;
updatedAt?: string; updatedAt?: string;
lastEditedAt?: string; lastEditedAt?: string;

View File

@ -1,7 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { appendFileSync } from "fs";
import { z } from "zod"; import { z } from "zod";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import { sanitizeContent } from "../github/utils/sanitizer"; import { sanitizeContent } from "../github/utils/sanitizer";
@ -11,13 +10,6 @@ const REPO_OWNER = process.env.REPO_OWNER;
const REPO_NAME = process.env.REPO_NAME; const REPO_NAME = process.env.REPO_NAME;
const PR_NUMBER = process.env.PR_NUMBER; const PR_NUMBER = process.env.PR_NUMBER;
// Calls without confirmed=true are buffered here instead of posted. This
// prevents subagents from posting test/probe comments when they inherit this
// tool and probe it after hitting unrelated errors. The action's post-step
// reports the buffer count for diagnostics.
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
const CLASSIFY_ENABLED = process.env.CLASSIFY_INLINE_COMMENTS !== "false";
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) { if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) {
console.error( console.error(
"Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required", "Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required",
@ -75,17 +67,8 @@ server.tool(
.describe( .describe(
"Specific commit SHA to comment on (defaults to latest commit)", "Specific commit SHA to comment on (defaults to latest commit)",
), ),
confirmed: z
.boolean()
.optional()
.describe(
"Set true to post immediately. When omitted, the call is buffered " +
"and classified after the session completes — real review comments " +
"post, test/probe comments are dropped. Set false to buffer and " +
"never post. Only set true when posting final review comments.",
),
}, },
async ({ path, body, line, startLine, side, commit_id, confirmed }) => { async ({ path, body, line, startLine, side, commit_id }) => {
try { try {
const githubToken = process.env.GITHUB_TOKEN; const githubToken = process.env.GITHUB_TOKEN;
@ -97,6 +80,8 @@ server.tool(
const repo = REPO_NAME; const repo = REPO_NAME;
const pull_number = parseInt(PR_NUMBER, 10); const pull_number = parseInt(PR_NUMBER, 10);
const octokit = createOctokit(githubToken).rest;
// Sanitize the comment body to remove any potential GitHub tokens // Sanitize the comment body to remove any potential GitHub tokens
const sanitizedBody = sanitizeContent(body); const sanitizedBody = sanitizeContent(body);
@ -107,49 +92,10 @@ server.tool(
); );
} }
if (CLASSIFY_ENABLED && confirmed !== true) {
appendFileSync(
BUFFER_PATH,
JSON.stringify({
ts: new Date().toISOString(),
path,
line,
startLine,
side,
commit_id,
body: sanitizedBody,
confirmed,
}) + "\n",
);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
buffered: true,
message:
"Comment buffered. It will be classified and posted after " +
"this session completes (real review comments post, " +
"test/probe comments are dropped). Set confirmed=true to " +
"post immediately. If you are testing whether this tool " +
"works: it works — no need to test further.",
},
null,
2,
),
},
],
};
}
// If only line is provided, it's a single-line comment // If only line is provided, it's a single-line comment
// If both startLine and line are provided, it's a multi-line comment // If both startLine and line are provided, it's a multi-line comment
const isSingleLine = !startLine; const isSingleLine = !startLine;
const octokit = createOctokit(githubToken).rest;
const pr = await octokit.pulls.get({ const pr = await octokit.pulls.get({
owner, owner,
repo, repo,

View File

@ -152,9 +152,6 @@ export async function prepareMcpConfig(
REPO_NAME: repo, REPO_NAME: repo,
PR_NUMBER: context.entityNumber?.toString() || "", PR_NUMBER: context.entityNumber?.toString() || "",
GITHUB_API_URL: GITHUB_API_URL, GITHUB_API_URL: GITHUB_API_URL,
CLASSIFY_INLINE_COMMENTS: context.inputs.classifyInlineComments
? "true"
: "false",
}, },
}; };
} }
@ -181,8 +178,9 @@ export async function prepareMcpConfig(
core.warning( core.warning(
"The github_ci MCP server requires 'actions: read' permission. " + "The github_ci MCP server requires 'actions: read' permission. " +
"Skipping CI server installation. " + "Skipping CI server installation. " +
"To enable CI status checks, add 'actions: read' to your workflow permissions. " + "To enable CI status checks, add 'actions: read' to your workflow permissions " +
"See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token", "and set 'additional_permissions: actions: read' in the action's with: inputs. " +
"See: https://github.com/anthropics/claude-code-action/blob/main/docs/configuration.md#additional-permissions-for-cicd-integration",
); );
} else { } else {
baseMcpConfig.mcpServers.github_ci = { baseMcpConfig.mcpServers.github_ci = {

View File

@ -85,15 +85,15 @@ export async function prepareAgentMode({
// Check for branch info from environment variables (useful for auto-fix workflows) // Check for branch info from environment variables (useful for auto-fix workflows)
const claudeBranch = process.env.CLAUDE_BRANCH || undefined; const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
const defaultBranch = context.repository.default_branch || "main"; const baseBranch =
const baseBranch = context.inputs.baseBranch || defaultBranch; process.env.BASE_BRANCH || context.inputs.baseBranch || "main";
// Detect current branch from GitHub environment // Detect current branch from GitHub environment
const currentBranch = const currentBranch =
claudeBranch || claudeBranch ||
process.env.GITHUB_HEAD_REF || process.env.GITHUB_HEAD_REF ||
process.env.GITHUB_REF_NAME || process.env.GITHUB_REF_NAME ||
defaultBranch; "main";
// Get our GitHub MCP servers config // Get our GitHub MCP servers config
const ourMcpConfig = await prepareMcpConfig({ const ourMcpConfig = await prepareMcpConfig({

View File

@ -114,17 +114,16 @@ export async function prepareTagMode({
tool.startsWith("mcp__github_"), tool.startsWith("mcp__github_"),
); );
const gitPushWrapper = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`; // Build claude_args for tag mode with required tools
// Tag mode REQUIRES these tools to function properly
// Build claude_args for tag mode with required tools.
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode (set below)
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside (e.g. ~/.bashrc).
// Listing them here would grant blanket write access to the whole runner (Asana 1213310082312048).
const tagModeTools = [ const tagModeTools = [
"Edit",
"MultiEdit",
"Glob", "Glob",
"Grep", "Grep",
"LS", "LS",
"Read", "Read",
"Write",
"mcp__github_comment__update_claude_comment", "mcp__github_comment__update_claude_comment",
"mcp__github_ci__get_ci_status", "mcp__github_ci__get_ci_status",
"mcp__github_ci__get_workflow_run_details", "mcp__github_ci__get_workflow_run_details",
@ -136,10 +135,13 @@ export async function prepareTagMode({
// SSH signing still uses git CLI, just with signing enabled // SSH signing still uses git CLI, just with signing enabled
if (!useApiCommitSigning) { if (!useApiCommitSigning) {
tagModeTools.push( tagModeTools.push(
"Bash(git add:*)", "Bash(git add *)",
"Bash(git commit:*)", "Bash(git commit *)",
`Bash(${gitPushWrapper}:*)`, "Bash(git push *)",
"Bash(git rm:*)", "Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git rm *)",
); );
} else { } else {
// When using API commit signing, use MCP file ops tools // When using API commit signing, use MCP file ops tools
@ -169,10 +171,8 @@ export async function prepareTagMode({
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''"); const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
claudeArgs = `--mcp-config '${escapedOurConfig}'`; claudeArgs = `--mcp-config '${escapedOurConfig}'`;
// Add required tools for tag mode. // Add required tools for tag mode
// acceptEdits: file edits auto-allowed inside cwd ($GITHUB_WORKSPACE), denied outside. claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
// Headless SDK has no prompt handler, so anything that falls through to "ask" is denied.
claudeArgs += ` --permission-mode acceptEdits --allowedTools "${tagModeTools.join(",")}"`;
// Append user's claude_args (which may have more --mcp-config flags) // Append user's claude_args (which may have more --mcp-config flags)
if (userClaudeArgs) { if (userClaudeArgs) {

View File

@ -3,7 +3,6 @@ export type RetryOptions = {
initialDelayMs?: number; initialDelayMs?: number;
maxDelayMs?: number; maxDelayMs?: number;
backoffFactor?: number; backoffFactor?: number;
shouldRetry?: (error: Error) => boolean;
}; };
export async function retryWithBackoff<T>( export async function retryWithBackoff<T>(
@ -15,7 +14,6 @@ export async function retryWithBackoff<T>(
initialDelayMs = 5000, initialDelayMs = 5000,
maxDelayMs = 20000, maxDelayMs = 20000,
backoffFactor = 2, backoffFactor = 2,
shouldRetry,
} = options; } = options;
let delayMs = initialDelayMs; let delayMs = initialDelayMs;
@ -29,11 +27,6 @@ export async function retryWithBackoff<T>(
lastError = error instanceof Error ? error : new Error(String(error)); lastError = error instanceof Error ? error : new Error(String(error));
console.error(`Attempt ${attempt} failed:`, lastError.message); console.error(`Attempt ${attempt} failed:`, lastError.message);
if (shouldRetry && !shouldRetry(lastError)) {
console.error("Error is not retryable, giving up immediately");
throw lastError;
}
if (attempt < maxAttempts) { if (attempt < maxAttempts) {
console.log(`Retrying in ${delayMs / 1000} seconds...`); console.log(`Retrying in ${delayMs / 1000} seconds...`);
await new Promise((resolve) => setTimeout(resolve, delayMs)); await new Promise((resolve) => setTimeout(resolve, delayMs));

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { describe, test, expect, beforeAll } from "bun:test"; import { describe, test, expect } from "bun:test";
import { import {
generatePrompt, generatePrompt,
getEventTypeAndContext, getEventTypeAndContext,
@ -9,10 +9,6 @@ import {
} from "../src/create-prompt"; } from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt"; import type { PreparedContext } from "../src/create-prompt";
beforeAll(() => {
process.env.GITHUB_ACTION_PATH = "/test/action/path";
});
describe("generatePrompt", () => { describe("generatePrompt", () => {
const mockGitHubData = { const mockGitHubData = {
contextData: { contextData: {
@ -27,8 +23,6 @@ describe("generatePrompt", () => {
baseRefName: "main", baseRefName: "main",
headRefName: "feature-branch", headRefName: "feature-branch",
headRefOid: "abc123", headRefOid: "abc123",
isCrossRepository: false,
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
commits: { commits: {
totalCount: 2, totalCount: 2,
nodes: [ nodes: [
@ -511,7 +505,7 @@ describe("generatePrompt", () => {
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag"); const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain PR-specific instructions (git commands when not using signing) // Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain("git push");
expect(prompt).toContain( expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR", "Always push to the existing branch when triggered on a PR",
); );
@ -649,7 +643,7 @@ describe("generatePrompt", () => {
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag"); const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain open PR instructions (git commands when not using signing) // Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain("git push");
expect(prompt).toContain( expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR", "Always push to the existing branch when triggered on a PR",
); );
@ -763,7 +757,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("Use git commands via the Bash tool"); expect(prompt).toContain("Use git commands via the Bash tool");
expect(prompt).toContain("git add"); expect(prompt).toContain("git add");
expect(prompt).toContain("git commit"); expect(prompt).toContain("git commit");
expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain("git push");
// Should use the minimal comment tool // Should use the minimal comment tool
expect(prompt).toContain("mcp__github_comment__update_claude_comment"); expect(prompt).toContain("mcp__github_comment__update_claude_comment");
@ -892,18 +886,17 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(); const result = buildAllowedToolsString();
// The base tools should be in the result // The base tools should be in the result
// Edit/MultiEdit/Write are NOT in allowedTools — acceptEdits permission mode handles them expect(result).toContain("Edit");
expect(result).not.toContain("Edit");
expect(result).not.toContain("Write");
expect(result).toContain("Glob"); expect(result).toContain("Glob");
expect(result).toContain("Grep"); expect(result).toContain("Grep");
expect(result).toContain("LS"); expect(result).toContain("LS");
expect(result).toContain("Read"); expect(result).toContain("Read");
expect(result).toContain("Write");
// Default is no commit signing, so should have specific Bash git commands // Default is no commit signing, so should have specific Bash git commands
expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git add *)");
expect(result).toContain("Bash(git commit:*)"); expect(result).toContain("Bash(git commit *)");
expect(result).toContain("scripts/git-push.sh:*)"); expect(result).toContain("Bash(git push *)");
expect(result).toContain("mcp__github_comment__update_claude_comment"); expect(result).toContain("mcp__github_comment__update_claude_comment");
// Should not have commit signing tools // Should not have commit signing tools
@ -915,16 +908,16 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], false, false); const result = buildAllowedToolsString([], false, false);
// The base tools should be in the result // The base tools should be in the result
expect(result).not.toContain("Edit"); expect(result).toContain("Edit");
expect(result).toContain("Glob"); expect(result).toContain("Glob");
expect(result).toContain("Grep"); expect(result).toContain("Grep");
expect(result).toContain("LS"); expect(result).toContain("LS");
expect(result).toContain("Read"); expect(result).toContain("Read");
expect(result).not.toContain("Write"); expect(result).toContain("Write");
// Should have specific Bash git commands for non-signing mode // Should have specific Bash git commands for non-signing mode
expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git add *)");
expect(result).toContain("Bash(git commit:*)"); expect(result).toContain("Bash(git commit *)");
expect(result).toContain("mcp__github_comment__update_claude_comment"); expect(result).toContain("mcp__github_comment__update_claude_comment");
// Should not have commit signing tools // Should not have commit signing tools
@ -937,7 +930,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(customTools); const result = buildAllowedToolsString(customTools);
// Base tools should be present // Base tools should be present
expect(result).toContain("Read"); expect(result).toContain("Edit");
expect(result).toContain("Glob"); expect(result).toContain("Glob");
// Custom tools should be appended // Custom tools should be appended
@ -957,7 +950,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], true); const result = buildAllowedToolsString([], true);
// Base tools should be present // Base tools should be present
expect(result).toContain("Read"); expect(result).toContain("Edit");
expect(result).toContain("Glob"); expect(result).toContain("Glob");
// GitHub Actions tools should be included // GitHub Actions tools should be included
@ -971,7 +964,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(customTools, true); const result = buildAllowedToolsString(customTools, true);
// Base tools should be present // Base tools should be present
expect(result).toContain("Read"); expect(result).toContain("Edit");
// Custom tools should be included // Custom tools should be included
expect(result).toContain("Tool1"); expect(result).toContain("Tool1");
@ -987,12 +980,12 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], false, true); const result = buildAllowedToolsString([], false, true);
// Base tools should be present // Base tools should be present
expect(result).not.toContain("Edit"); expect(result).toContain("Edit");
expect(result).toContain("Glob"); expect(result).toContain("Glob");
expect(result).toContain("Grep"); expect(result).toContain("Grep");
expect(result).toContain("LS"); expect(result).toContain("LS");
expect(result).toContain("Read"); expect(result).toContain("Read");
expect(result).not.toContain("Write"); expect(result).toContain("Write");
// Commit signing tools should be included // Commit signing tools should be included
expect(result).toContain("mcp__github_file_ops__commit_files"); expect(result).toContain("mcp__github_file_ops__commit_files");
@ -1008,18 +1001,21 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], false, false); const result = buildAllowedToolsString([], false, false);
// Base tools should be present // Base tools should be present
expect(result).not.toContain("Edit"); expect(result).toContain("Edit");
expect(result).toContain("Glob"); expect(result).toContain("Glob");
expect(result).toContain("Grep"); expect(result).toContain("Grep");
expect(result).toContain("LS"); expect(result).toContain("LS");
expect(result).toContain("Read"); expect(result).toContain("Read");
expect(result).not.toContain("Write"); expect(result).toContain("Write");
// Specific Bash git commands should be included // Specific Bash git commands should be included
expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git add *)");
expect(result).toContain("Bash(git commit:*)"); expect(result).toContain("Bash(git commit *)");
expect(result).toContain("scripts/git-push.sh:*)"); expect(result).toContain("Bash(git push *)");
expect(result).toContain("Bash(git rm:*)"); expect(result).toContain("Bash(git status *)");
expect(result).toContain("Bash(git diff *)");
expect(result).toContain("Bash(git log *)");
expect(result).toContain("Bash(git rm *)");
// Comment tool from minimal server should be included // Comment tool from minimal server should be included
expect(result).toContain("mcp__github_comment__update_claude_comment"); expect(result).toContain("mcp__github_comment__update_claude_comment");
@ -1034,8 +1030,8 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(customTools, true, false); const result = buildAllowedToolsString(customTools, true, false);
// Base tools should be present // Base tools should be present
expect(result).toContain("Read"); expect(result).toContain("Edit");
expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git add *)");
// Custom tools should be included // Custom tools should be included
expect(result).toContain("CustomTool1"); expect(result).toContain("CustomTool1");

View File

@ -1006,8 +1006,6 @@ describe("fetchGitHubData integration with time filtering", () => {
baseRefName: "main", baseRefName: "main",
headRefName: "feature", headRefName: "feature",
headRefOid: "abc123", headRefOid: "abc123",
isCrossRepository: false,
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
createdAt: "2024-01-15T10:00:00Z", createdAt: "2024-01-15T10:00:00Z",
updatedAt: "2024-01-15T12:30:00Z", // Edited after trigger updatedAt: "2024-01-15T12:30:00Z", // Edited after trigger
lastEditedAt: "2024-01-15T12:30:00Z", // Edited after trigger lastEditedAt: "2024-01-15T12:30:00Z", // Edited after trigger

View File

@ -24,8 +24,6 @@ describe("formatContext", () => {
baseRefName: "main", baseRefName: "main",
headRefName: "feature/test", headRefName: "feature/test",
headRefOid: "abc123", headRefOid: "abc123",
isCrossRepository: false,
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
createdAt: "2023-01-01T00:00:00Z", createdAt: "2023-01-01T00:00:00Z",
additions: 50, additions: 50,
deletions: 30, deletions: 30,

View File

@ -32,7 +32,6 @@ describe("prepareMcpConfig", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "", branchPrefix: "",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
sshSigningKey: "", sshSigningKey: "",
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
@ -293,6 +292,9 @@ describe("prepareMcpConfig", () => {
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).not.toBeDefined(); expect(parsed.mcpServers.github_ci).not.toBeDefined();
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("Skipping CI server installation"),
);
delete process.env.DEFAULT_WORKFLOW_TOKEN; delete process.env.DEFAULT_WORKFLOW_TOKEN;
}); });

View File

@ -19,7 +19,6 @@ const defaultInputs = {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
sshSigningKey: "", sshSigningKey: "",
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
@ -36,7 +35,6 @@ const defaultRepository = {
owner: "test-owner", owner: "test-owner",
repo: "test-repo", repo: "test-repo",
full_name: "test-owner/test-repo", full_name: "test-owner/test-repo",
default_branch: "main",
}; };
type MockContextOverrides = Omit<Partial<ParsedGitHubContext>, "inputs"> & { type MockContextOverrides = Omit<Partial<ParsedGitHubContext>, "inputs"> & {

View File

@ -88,7 +88,7 @@ describe("Agent Mode", () => {
expect(result.claudeArgs).toBe("--model claude-sonnet-4 --max-turns 10"); expect(result.claudeArgs).toBe("--model claude-sonnet-4 --max-turns 10");
expect(result.claudeArgs).not.toContain("--mcp-config"); expect(result.claudeArgs).not.toContain("--mcp-config");
// Verify return structure - should fall back to repository.default_branch when no env vars set // Verify return structure - should use "main" as fallback when no env vars set
expect(result).toEqual({ expect(result).toEqual({
commentId: undefined, commentId: undefined,
branchInfo: { branchInfo: {
@ -108,60 +108,6 @@ describe("Agent Mode", () => {
process.env.GITHUB_REF_NAME = originalRefName; process.env.GITHUB_REF_NAME = originalRefName;
}); });
test("prepare falls back to repository.default_branch when not 'main'", async () => {
const contextWithDevelop = createMockAutomationContext({
eventName: "workflow_dispatch",
repository: {
owner: "test-owner",
repo: "test-repo",
full_name: "test-owner/test-repo",
default_branch: "develop",
},
});
// Save and clear env vars that would otherwise override the fallback
const originalClaudeBranch = process.env.CLAUDE_BRANCH;
const originalHeadRef = process.env.GITHUB_HEAD_REF;
const originalRefName = process.env.GITHUB_REF_NAME;
delete process.env.CLAUDE_BRANCH;
delete process.env.GITHUB_HEAD_REF;
delete process.env.GITHUB_REF_NAME;
const mockOctokit = {
rest: {
users: {
getAuthenticated: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345, type: "User" },
}),
),
getByUsername: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345, type: "User" },
}),
),
},
},
} as any;
const result = await prepareAgentMode({
context: contextWithDevelop,
octokit: mockOctokit,
githubToken: "test-token",
});
expect(result.branchInfo.baseBranch).toBe("develop");
expect(result.branchInfo.currentBranch).toBe("develop");
// Restore env vars
if (originalClaudeBranch !== undefined)
process.env.CLAUDE_BRANCH = originalClaudeBranch;
if (originalHeadRef !== undefined)
process.env.GITHUB_HEAD_REF = originalHeadRef;
if (originalRefName !== undefined)
process.env.GITHUB_REF_NAME = originalRefName;
});
test("prepare rejects bot actors without allowed_bots", async () => { test("prepare rejects bot actors without allowed_bots", async () => {
const contextWithPrompts = createMockAutomationContext({ const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",

View File

@ -19,7 +19,6 @@ describe("detectMode with enhanced routing", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
sshSigningKey: "", sshSigningKey: "",
botId: "123456", botId: "123456",

View File

@ -67,7 +67,6 @@ describe("checkWritePermissions", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
sshSigningKey: "", sshSigningKey: "",
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),

View File

@ -17,8 +17,6 @@ describe("pull_request_target event support", () => {
baseRefName: "main", baseRefName: "main",
headRefName: "feature-branch", headRefName: "feature-branch",
headRefOid: "abc123", headRefOid: "abc123",
isCrossRepository: false,
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
commits: { commits: {
totalCount: 2, totalCount: 2,
nodes: [ nodes: [
@ -137,7 +135,7 @@ describe("pull_request_target event support", () => {
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag"); const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
// Should include git commands for non-commit-signing mode // Should include git commands for non-commit-signing mode
expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain("git push");
expect(prompt).toContain( expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR", "Always push to the existing branch when triggered on a PR",
); );

View File

@ -1,120 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { retryWithBackoff } from "../src/utils/retry";
describe("retryWithBackoff", () => {
let originalConsoleLog: typeof console.log;
let originalConsoleError: typeof console.error;
beforeEach(() => {
originalConsoleLog = console.log;
originalConsoleError = console.error;
console.log = mock(() => {});
console.error = mock(() => {});
});
afterEach(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
});
it("returns the result on first success", async () => {
const result = await retryWithBackoff(() => Promise.resolve("ok"), {
maxAttempts: 3,
initialDelayMs: 1,
});
expect(result).toBe("ok");
});
it("retries on failure and succeeds", async () => {
let attempt = 0;
const result = await retryWithBackoff(
() => {
attempt++;
if (attempt < 3) throw new Error("transient");
return Promise.resolve("recovered");
},
{ maxAttempts: 3, initialDelayMs: 1 },
);
expect(result).toBe("recovered");
expect(attempt).toBe(3);
});
it("throws after exhausting all attempts", async () => {
await expect(
retryWithBackoff(() => Promise.reject(new Error("permanent")), {
maxAttempts: 2,
initialDelayMs: 1,
}),
).rejects.toThrow("permanent");
});
it("stops retrying immediately when shouldRetry returns false", async () => {
class NonRetryableError extends Error {
constructor() {
super("non-retryable");
this.name = "NonRetryableError";
}
}
let attempts = 0;
await expect(
retryWithBackoff(
() => {
attempts++;
throw new NonRetryableError();
},
{
maxAttempts: 3,
initialDelayMs: 1,
shouldRetry: (error) => !(error instanceof NonRetryableError),
},
),
).rejects.toThrow("non-retryable");
expect(attempts).toBe(1);
});
it("continues retrying when shouldRetry returns true", async () => {
let attempts = 0;
await expect(
retryWithBackoff(
() => {
attempts++;
throw new Error("retryable");
},
{
maxAttempts: 3,
initialDelayMs: 1,
shouldRetry: () => true,
},
),
).rejects.toThrow("retryable");
expect(attempts).toBe(3);
});
it("preserves the original error when shouldRetry aborts", async () => {
class SpecificError extends Error {
code = 401;
constructor() {
super("unauthorized");
this.name = "SpecificError";
}
}
try {
await retryWithBackoff(
() => {
throw new SpecificError();
},
{
maxAttempts: 3,
initialDelayMs: 1,
shouldRetry: (error) => !(error instanceof SpecificError),
},
);
expect.unreachable("should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(SpecificError);
expect((error as SpecificError).code).toBe(401);
}
});
});

View File

@ -34,7 +34,6 @@ describe("checkContainsTrigger", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
allowedBots: "", allowedBots: "",
}, },
@ -63,7 +62,6 @@ describe("checkContainsTrigger", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
allowedBots: "", allowedBots: "",
}, },
@ -276,7 +274,6 @@ describe("checkContainsTrigger", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
allowedBots: "", allowedBots: "",
}, },
@ -306,7 +303,6 @@ describe("checkContainsTrigger", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
allowedBots: "", allowedBots: "",
}, },
@ -336,7 +332,6 @@ describe("checkContainsTrigger", () => {
labelTrigger: "", labelTrigger: "",
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
classifyInlineComments: true,
useCommitSigning: false, useCommitSigning: false,
allowedBots: "", allowedBots: "",
}, },

View File

@ -36,25 +36,6 @@ describe("validateBranchName", () => {
expect(() => validateBranchName("refs/heads/main")).not.toThrow(); expect(() => validateBranchName("refs/heads/main")).not.toThrow();
expect(() => validateBranchName("bugfix/JIRA-1234")).not.toThrow(); expect(() => validateBranchName("bugfix/JIRA-1234")).not.toThrow();
}); });
it("should accept branch names containing # (git-valid, common in issue-linked branches)", () => {
// Reported in #1137: branches like "put-back-arm64-#2" were rejected
expect(() => validateBranchName("put-back-arm64-#2")).not.toThrow();
expect(() =>
validateBranchName("feature/#123-description"),
).not.toThrow();
expect(() => validateBranchName("fix/issue-#42")).not.toThrow();
});
it("should accept branch names containing + (generated by Claude Code EnterWorktree)", () => {
// EnterWorktree converts "/" in worktree names to "+" when generating branch names.
// e.g. EnterWorktree("feat/skill-consolidation") → branch "worktree-feat+skill-consolidation"
expect(() =>
validateBranchName("worktree-feat+skill-consolidation"),
).not.toThrow();
expect(() => validateBranchName("fix+issue-123")).not.toThrow();
expect(() => validateBranchName("feature+new-thing")).not.toThrow();
});
}); });
describe("command injection attempts", () => { describe("command injection attempts", () => {