From 33fbb80626625f2dfc689982c92e71fc7edd42ab Mon Sep 17 00:00:00 2001 From: dustin <112589654+an-dustin@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:04:11 -0700 Subject: [PATCH 1/8] docs: warn that allowed_bots can expose the action to external triggers (#1039) allowed_bots does not verify that a matching bot is installed on the repository or has write access. On a public repo, external GitHub Apps may be able to trigger workflow events (issues, comments, PR reviews). If the workflow listens on those events and allowed_bots is '*', an external App can invoke this action with a prompt it controls. Default config (allowed_bots: "") is unaffected. - docs/security.md: add warning and mitigation guidance - docs/usage.md: add inline warning to the allowed_bots input row - action.yml: add warning to the allowed_bots input description :house: Remote-Dev: homespace --- action.yml | 2 +- docs/security.md | 4 ++++ docs/usage.md | 58 ++++++++++++++++++++++++------------------------ 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/action.yml b/action.yml index aecd94e..d1b9ad0 100644 --- a/action.yml +++ b/action.yml @@ -28,7 +28,7 @@ inputs: required: false default: "" allowed_bots: - description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no 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." required: false default: "" allowed_non_write_users: diff --git a/docs/security.md b/docs/security.md index d36f061..eb3c69a 100644 --- a/docs/security.md +++ b/docs/security.md @@ -4,6 +4,10 @@ - **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 + - **⚠️ 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: - 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 diff --git a/docs/usage.md b/docs/usage.md index 3e55a3d..f2ceada 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -52,35 +52,35 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| `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\* | - | -| `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` | -| `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 | "" | -| `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` | -| `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_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 | - | -| `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` | -| `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 | "" | -| `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` | -| `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_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 | "" | -| `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 | "" | -| `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 | "" | -| `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 | "" | +| Input | Description | Required | Default | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | +| `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\* | - | +| `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` | +| `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 | "" | +| `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` | +| `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_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 | - | +| `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` | +| `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 | "" | +| `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` | +| `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_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. **⚠️ On public repos with `'*'`, external Apps may be able to invoke this action.** See [Security](./security.md) | 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 | "" | +| `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 | "" | +| `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 From eb99fb38f09dedf69f423f1315d6c0272ace56a0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 10 Mar 2026 00:49:35 +0000 Subject: [PATCH 2/8] chore: bump Claude Code to 2.1.72 and Agent SDK to 0.2.72 --- base-action/action.yml | 2 +- base-action/bun.lock | 4 ++-- base-action/package.json | 2 +- bun.lock | 4 ++-- package.json | 2 +- src/entrypoints/run.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/base-action/action.yml b/base-action/action.yml index 038be98..12b5115 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -124,7 +124,7 @@ runs: PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} run: | if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then - CLAUDE_CODE_VERSION="2.1.71" + CLAUDE_CODE_VERSION="2.1.72" echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." for attempt in 1 2 3; do echo "Installation attempt $attempt..." diff --git a/base-action/bun.lock b/base-action/bun.lock index b00a73c..c4882cd 100644 --- a/base-action/bun.lock +++ b/base-action/bun.lock @@ -6,7 +6,7 @@ "name": "@anthropic-ai/claude-code-base-action", "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.72", "shell-quote": "^1.8.3", }, "devDependencies": { @@ -27,7 +27,7 @@ "@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.71", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pIsQJnM7Y+cJHL7aFY6SCCW3FIni218gVEpPqG8XGowfYxboFNBbNssWiUNRwthT8bp9jypcX7q5kx0Xsw14xg=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.72", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-GR3QaLRCoWO5DkRknaaCH6zzmUNZ3E6VckEKNE7EO5R7qDBexQe9tDKag257pji2NenTrnBDMxznoZrhNCRTzA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/base-action/package.json b/base-action/package.json index d20f324..fd56b2e 100644 --- a/base-action/package.json +++ b/base-action/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.72", "shell-quote": "^1.8.3" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index 715b0f6..65fb7b8 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.72", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", @@ -37,7 +37,7 @@ "@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.71", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pIsQJnM7Y+cJHL7aFY6SCCW3FIni218gVEpPqG8XGowfYxboFNBbNssWiUNRwthT8bp9jypcX7q5kx0Xsw14xg=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.72", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-GR3QaLRCoWO5DkRknaaCH6zzmUNZ3E6VckEKNE7EO5R7qDBexQe9tDKag257pji2NenTrnBDMxznoZrhNCRTzA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/package.json b/package.json index e3f29c9..a99d661 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.72", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index d1be1ab..5280a29 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -51,7 +51,7 @@ async function installClaudeCode(): Promise { return; } - const claudeCodeVersion = "2.1.71"; + const claudeCodeVersion = "2.1.72"; console.log(`Installing Claude Code v${claudeCodeVersion}...`); for (let attempt = 1; attempt <= 3; attempt++) { From 567be3da98ac1cacd803477c9a738bec79484dc0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 11 Mar 2026 18:33:26 +0000 Subject: [PATCH 3/8] chore: bump Claude Code to 2.1.73 and Agent SDK to 0.2.73 --- base-action/action.yml | 2 +- base-action/bun.lock | 4 ++-- base-action/package.json | 2 +- bun.lock | 4 ++-- package.json | 2 +- src/entrypoints/run.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/base-action/action.yml b/base-action/action.yml index 12b5115..3b09717 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -124,7 +124,7 @@ runs: PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} run: | if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then - CLAUDE_CODE_VERSION="2.1.72" + CLAUDE_CODE_VERSION="2.1.73" echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." for attempt in 1 2 3; do echo "Installation attempt $attempt..." diff --git a/base-action/bun.lock b/base-action/bun.lock index c4882cd..c8c25a6 100644 --- a/base-action/bun.lock +++ b/base-action/bun.lock @@ -6,7 +6,7 @@ "name": "@anthropic-ai/claude-code-base-action", "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.72", + "@anthropic-ai/claude-agent-sdk": "^0.2.73", "shell-quote": "^1.8.3", }, "devDependencies": { @@ -27,7 +27,7 @@ "@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.72", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-GR3QaLRCoWO5DkRknaaCH6zzmUNZ3E6VckEKNE7EO5R7qDBexQe9tDKag257pji2NenTrnBDMxznoZrhNCRTzA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.73", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-JrHeMl93Q5ai9GMPAffQkSisbbDvD1skU2x6sf6WRzEZw0sK6aTG+XSiZHY2F5aSrfd4G2qUogLHEm6Y8obyOQ=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/base-action/package.json b/base-action/package.json index fd56b2e..6907010 100644 --- a/base-action/package.json +++ b/base-action/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.72", + "@anthropic-ai/claude-agent-sdk": "^0.2.73", "shell-quote": "^1.8.3" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index 65fb7b8..4edc2ad 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.72", + "@anthropic-ai/claude-agent-sdk": "^0.2.73", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", @@ -37,7 +37,7 @@ "@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.72", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-GR3QaLRCoWO5DkRknaaCH6zzmUNZ3E6VckEKNE7EO5R7qDBexQe9tDKag257pji2NenTrnBDMxznoZrhNCRTzA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.73", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-JrHeMl93Q5ai9GMPAffQkSisbbDvD1skU2x6sf6WRzEZw0sK6aTG+XSiZHY2F5aSrfd4G2qUogLHEm6Y8obyOQ=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/package.json b/package.json index a99d661..09cdff5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.72", + "@anthropic-ai/claude-agent-sdk": "^0.2.73", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index 5280a29..8a42776 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -51,7 +51,7 @@ async function installClaudeCode(): Promise { return; } - const claudeCodeVersion = "2.1.72"; + const claudeCodeVersion = "2.1.73"; console.log(`Installing Claude Code v${claudeCodeVersion}...`); for (let attempt = 1; attempt <= 3; attempt++) { From 5d0cc745cd0cce4c0e9e0b3511de26c3bc285eb5 Mon Sep 17 00:00:00 2001 From: kashyap murali Date: Thu, 12 Mar 2026 00:12:55 -0700 Subject: [PATCH 4/8] feat(inline-comment): add confirmed param + probe-pattern safety net (#1048) * feat(inline-comment): add confirmed param + probe-pattern safety net Subagents that inherit this tool sometimes probe it with test comments ('Test comment to see if I can create inline comments') after hitting unrelated errors elsewhere. Recurring issue across customer PRs. Adds two defenses: 1. confirmed param: set true to post (final review comments should pass this). When false, buffers to a JSONL file instead of posting. 2. Probe-pattern safety net: when confirmed is omitted (backward compat for existing prompts), the body is checked against obvious probe patterns ('test comment', 'can i', 'does this work', etc.). Matching calls are buffered instead of posted. A post-run step in action.yml reports the buffered call count and bodies as a workflow warning for diagnostics. Backward compatibility: - Existing single-agent prompts (no confirmed param) post normally unless the body happens to start with a probe phrase (unlikely for real review comments) - The code-review skill is being updated to pass confirmed: true in its final posting step - Subagent probes that would previously post now harmlessly buffer * refactor: replace probe-regex with Haiku classification in post-step The regex approach was narrow and could miss creative probe phrasings. Replaced with a batch Haiku classification that runs after the session completes. Flow: - MCP server: confirmed !== true -> buffer to JSONL (no classification in-band, no latency in the tool path) - Post-step (src/entrypoints/post-buffered-inline-comments.ts): reads buffer, sends all bodies to a single Haiku call, posts only those classified as real review comments - confirmed=false entries are never posted regardless of classification Fail-open: if ANTHROPIC_API_KEY is unavailable (Bedrock/Vertex users) or the classification call fails, posts all unconfirmed comments. This matches pre-PR behavior where all calls posted immediately. The post-step emits ::warning:: for each filtered comment so users can see what was dropped and why. * feat: add classify_inline_comments opt-out input New action input classify_inline_comments (default 'true'). Setting to 'false' restores pre-buffering behavior: all inline comment calls post immediately regardless of the confirmed param. Threads through: action input -> CLASSIFY_INLINE_COMMENTS env -> context.inputs.classifyInlineComments -> MCP server env -> CLASSIFY_ENABLED module const. Post-step is also gated on the input so it skips entirely when classification is disabled. * docs: document classify_inline_comments input and confirmed param - usage.md: add classify_inline_comments to inputs table - solutions.md: mention confirmed=true in the prompt example and explain buffering/classification in the tool permissions section --- action.yml | 17 ++ docs/solutions.md | 4 +- docs/usage.md | 59 ++--- src/entrypoints/collect-inputs.ts | 1 + .../post-buffered-inline-comments.ts | 233 ++++++++++++++++++ src/github/context.ts | 2 + src/mcp/github-inline-comment-server.ts | 60 ++++- src/mcp/install-mcp-server.ts | 3 + test/install-mcp-server.test.ts | 1 + test/mockContext.ts | 1 + test/modes/detector.test.ts | 1 + test/permissions.test.ts | 1 + test/trigger-validation.test.ts | 5 + 13 files changed, 354 insertions(+), 34 deletions(-) create mode 100644 src/entrypoints/post-buffered-inline-comments.ts diff --git a/action.yml b/action.yml index d1b9ad0..18806ab 100644 --- a/action.yml +++ b/action.yml @@ -89,6 +89,10 @@ inputs: description: "Use just one comment to deliver issue/PR comments" required: 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: description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands" required: false @@ -204,6 +208,7 @@ runs: EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }} GITHUB_RUN_ID: ${{ github.run_id }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} + CLASSIFY_INLINE_COMMENTS: ${{ inputs.classify_inline_comments }} DEFAULT_WORKFLOW_TOKEN: ${{ github.token }} USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }} SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }} @@ -282,6 +287,18 @@ runs: run: | bun 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 run ${GITHUB_ACTION_PATH}/src/entrypoints/post-buffered-inline-comments.ts + - name: Revoke app token if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' shell: bash diff --git a/docs/solutions.md b/docs/solutions.md index 98f5d72..dd7fcb5 100644 --- a/docs/solutions.md +++ b/docs/solutions.md @@ -55,7 +55,7 @@ jobs: Note: The PR branch is already checked out in the current working directory. Use `gh pr comment` for top-level feedback. - Use `mcp__github_inline_comment__create_inline_comment` to highlight specific code issues. + Use `mcp__github_inline_comment__create_inline_comment` (with `confirmed: true`) to highlight specific code issues. Only post GitHub comments - don't submit review text as messages. claude_args: | @@ -585,7 +585,7 @@ prompt: | ### Common Tool Permissions - **PR Comments**: `Bash(gh pr comment:*)` -- **Inline Comments**: `mcp__github_inline_comment__create_inline_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. - **File Operations**: `Read,Write,Edit` - **Git Operations**: `Bash(git:*)` diff --git a/docs/usage.md b/docs/usage.md index f2ceada..89677fa 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -52,35 +52,36 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| `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\* | - | -| `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` | -| `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 | "" | -| `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` | -| `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_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 | - | -| `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` | -| `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 | "" | -| `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` | -| `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_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. **⚠️ On public repos with `'*'`, external Apps may be able to invoke this action.** See [Security](./security.md) | 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 | "" | -| `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 | "" | -| `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 | "" | +| Input | Description | Required | Default | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | +| `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\* | - | +| `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` | +| `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 | "" | +| `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` | +| `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 | - | +| `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` | +| `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 | - | +| `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/` | +| `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 | "" | +| `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 | "" | +| `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]` | +| `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 | "" | +| `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 | "" | +| `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 | "" | +| `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 diff --git a/src/entrypoints/collect-inputs.ts b/src/entrypoints/collect-inputs.ts index a463743..079565c 100644 --- a/src/entrypoints/collect-inputs.ts +++ b/src/entrypoints/collect-inputs.ts @@ -23,6 +23,7 @@ export function collectActionInputsPresence(): string { github_token: "", max_turns: "", use_sticky_comment: "false", + classify_inline_comments: "true", use_commit_signing: "false", ssh_signing_key: "", }; diff --git a/src/entrypoints/post-buffered-inline-comments.ts b/src/entrypoints/post-buffered-inline-comments.ts new file mode 100644 index 0000000..763dae7 --- /dev/null +++ b/src/entrypoints/post-buffered-inline-comments.ts @@ -0,0 +1,233 @@ +#!/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 { + 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["rest"], + owner: string, + repo: string, + pull_number: number, + headSha: string, + c: BufferedComment, +): Promise { + const params: Parameters[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); +}); diff --git a/src/github/context.ts b/src/github/context.ts index 936e70e..a0b388c 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -90,6 +90,7 @@ type BaseContext = { branchPrefix: string; branchNameTemplate?: string; useStickyComment: boolean; + classifyInlineComments: boolean; useCommitSigning: boolean; sshSigningKey: string; botId: string; @@ -150,6 +151,7 @@ export function parseGitHubContext(): GitHubContext { branchPrefix: process.env.BRANCH_PREFIX ?? "claude/", branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE, useStickyComment: process.env.USE_STICKY_COMMENT === "true", + classifyInlineComments: process.env.CLASSIFY_INLINE_COMMENTS !== "false", useCommitSigning: process.env.USE_COMMIT_SIGNING === "true", sshSigningKey: process.env.SSH_SIGNING_KEY || "", botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts index 703cda2..535124f 100644 --- a/src/mcp/github-inline-comment-server.ts +++ b/src/mcp/github-inline-comment-server.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { appendFileSync } from "fs"; import { z } from "zod"; import { createOctokit } from "../github/api/client"; import { sanitizeContent } from "../github/utils/sanitizer"; @@ -10,6 +11,13 @@ const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; 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) { console.error( "Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required", @@ -67,8 +75,17 @@ server.tool( .describe( "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 }) => { + async ({ path, body, line, startLine, side, commit_id, confirmed }) => { try { const githubToken = process.env.GITHUB_TOKEN; @@ -80,8 +97,6 @@ server.tool( const repo = REPO_NAME; const pull_number = parseInt(PR_NUMBER, 10); - const octokit = createOctokit(githubToken).rest; - // Sanitize the comment body to remove any potential GitHub tokens const sanitizedBody = sanitizeContent(body); @@ -92,10 +107,49 @@ 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 both startLine and line are provided, it's a multi-line comment const isSingleLine = !startLine; + const octokit = createOctokit(githubToken).rest; + const pr = await octokit.pulls.get({ owner, repo, diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index d5fac34..e40b53f 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -152,6 +152,9 @@ export async function prepareMcpConfig( REPO_NAME: repo, PR_NUMBER: context.entityNumber?.toString() || "", GITHUB_API_URL: GITHUB_API_URL, + CLASSIFY_INLINE_COMMENTS: context.inputs.classifyInlineComments + ? "true" + : "false", }, }; } diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 152d2be..87c7513 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -32,6 +32,7 @@ describe("prepareMcpConfig", () => { labelTrigger: "", branchPrefix: "", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), diff --git a/test/mockContext.ts b/test/mockContext.ts index 19ab044..c02caa4 100644 --- a/test/mockContext.ts +++ b/test/mockContext.ts @@ -19,6 +19,7 @@ const defaultInputs = { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), diff --git a/test/modes/detector.test.ts b/test/modes/detector.test.ts index c8a6c75..3b58f9e 100644 --- a/test/modes/detector.test.ts +++ b/test/modes/detector.test.ts @@ -19,6 +19,7 @@ describe("detectMode with enhanced routing", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: "123456", diff --git a/test/permissions.test.ts b/test/permissions.test.ts index cf2efdb..1d65457 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -67,6 +67,7 @@ describe("checkWritePermissions", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), diff --git a/test/trigger-validation.test.ts b/test/trigger-validation.test.ts index 36c41f2..f235928 100644 --- a/test/trigger-validation.test.ts +++ b/test/trigger-validation.test.ts @@ -34,6 +34,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -62,6 +63,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -274,6 +276,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -303,6 +306,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -332,6 +336,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, From f956510b1afc643e768961656c10f7039534d553 Mon Sep 17 00:00:00 2001 From: kashyap murali Date: Thu, 12 Mar 2026 13:35:17 -0700 Subject: [PATCH 5/8] Harden tag mode tool permissions against prompt injection (#1002) Two defenses for tag mode where an attacker with repo write access could craft a prompt injection payload in an issue/PR to gain RCE on the Actions runner: 1. git-push wrapper (H1 #3556799) The Bash(git\ push:*) rule permitted arbitrary flags and remotes, including combinations that execute shell commands locally. Replaced with scripts/git-push.sh which allowlists exactly 'origin ' with no flags, validates the ref via check-ref-format. Same pattern as scripts/gh.sh. 2. acceptEdits instead of blanket Write/Edit (Asana 1213310082312048) Edit/MultiEdit/Write in allowedTools granted write access to the whole runner filesystem (~/.bashrc etc). Removed from allowedTools and set --permission-mode acceptEdits, which auto-accepts edits inside cwd ($GITHUB_WORKSPACE) and denies outside. Headless SDK has no prompt handler so 'ask' becomes deny. Also: - Noted that create-prompt/index.ts exports ALLOWED_TOOLS env var that nothing reads. The live path is modes/tag/index.ts. Mirrored the fix in both so the file the H1 report likely points to stays in sync. - Updated prompt text (3 callsites) to reference the wrapper. - Updated tests (4 prompt-content asserts, 7 tool-list asserts). --- scripts/git-push.sh | 36 ++++++++++++++++++++++++++++ src/create-prompt/index.ts | 28 ++++++++++------------ src/modes/tag/index.ts | 19 ++++++++------- test/create-prompt.test.ts | 41 ++++++++++++++++++-------------- test/pull-request-target.test.ts | 2 +- 5 files changed, 84 insertions(+), 42 deletions(-) create mode 100755 scripts/git-push.sh diff --git a/scripts/git-push.sh b/scripts/git-push.sh new file mode 100755 index 0000000..a8afcb1 --- /dev/null +++ b/scripts/git-push.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wrapper around `git push` that only allows `origin ` 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 " >&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" diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 4624c19..c21d831 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -23,19 +23,15 @@ import { GITHUB_SERVER_URL } from "../github/api/config"; import { extractUserRequest } from "../utils/extract-user-request"; 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 */ const USER_REQUEST_FILENAME = "claude-user-request.txt"; -// Tag mode defaults - these tools are needed for tag mode to function -const BASE_ALLOWED_TOOLS = [ - "Edit", - "MultiEdit", - "Glob", - "Grep", - "LS", - "Read", - "Write", -]; +// Tag mode defaults - these tools are needed for tag mode to function. +// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode +// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside it. +const BASE_ALLOWED_TOOLS = ["Glob", "Grep", "LS", "Read"]; export function buildAllowedToolsString( customAllowedTools?: string[], @@ -59,7 +55,7 @@ export function buildAllowedToolsString( baseTools.push( "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)", + `Bash(${GIT_PUSH_WRAPPER}:*)`, "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", @@ -434,7 +430,7 @@ function getCommitInstructions( Bash(git commit -m "\\n\\n${coAuthorLine}")` : "" } - - Push to the remote: Bash(git push origin HEAD)`; + - Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin HEAD)`; } else { const branchName = eventData.claudeBranch || eventData.baseBranch; return ` @@ -448,7 +444,7 @@ function getCommitInstructions( Bash(git commit -m "\\n\\n${coAuthorLine}")` : "" } - - Push to the remote: Bash(git push origin ${branchName})`; + - Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin ${branchName})`; } } } @@ -823,7 +819,7 @@ ${ : `- Use git commands via the Bash tool for version control (remember that you have access to these git commands): - Stage files: Bash(git add ) - Commit changes: Bash(git commit -m "") - - Push to remote: Bash(git push origin ) (NEVER force push) + - Push to remote: Bash(${GIT_PUSH_WRAPPER} origin ) - Delete files: Bash(git rm ) followed by commit and push - 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)` : ""}` @@ -977,7 +973,9 @@ export async function createPrompt( console.log("========================"); } - // Set allowed tools + // NOTE: these env var exports are dead — nothing reads ALLOWED_TOOLS / DISALLOWED_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 allAllowedTools = buildAllowedToolsString( diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index 0adf746..14af9af 100644 --- a/src/modes/tag/index.ts +++ b/src/modes/tag/index.ts @@ -114,16 +114,17 @@ export async function prepareTagMode({ tool.startsWith("mcp__github_"), ); - // Build claude_args for tag mode with required tools - // Tag mode REQUIRES these tools to function properly + const gitPushWrapper = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`; + + // 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 = [ - "Edit", - "MultiEdit", "Glob", "Grep", "LS", "Read", - "Write", "mcp__github_comment__update_claude_comment", "mcp__github_ci__get_ci_status", "mcp__github_ci__get_workflow_run_details", @@ -137,7 +138,7 @@ export async function prepareTagMode({ tagModeTools.push( "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)", + `Bash(${gitPushWrapper}:*)`, "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", @@ -171,8 +172,10 @@ export async function prepareTagMode({ const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''"); claudeArgs = `--mcp-config '${escapedOurConfig}'`; - // Add required tools for tag mode - claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`; + // Add required tools for tag mode. + // acceptEdits: file edits auto-allowed inside cwd ($GITHUB_WORKSPACE), denied outside. + // 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) if (userClaudeArgs) { diff --git a/test/create-prompt.test.ts b/test/create-prompt.test.ts index 0a5ea82..3bf8b6c 100644 --- a/test/create-prompt.test.ts +++ b/test/create-prompt.test.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { describe, test, expect } from "bun:test"; +import { describe, test, expect, beforeAll } from "bun:test"; import { generatePrompt, getEventTypeAndContext, @@ -9,6 +9,10 @@ import { } from "../src/create-prompt"; import type { PreparedContext } from "../src/create-prompt"; +beforeAll(() => { + process.env.GITHUB_ACTION_PATH = "/test/action/path"; +}); + describe("generatePrompt", () => { const mockGitHubData = { contextData: { @@ -505,7 +509,7 @@ describe("generatePrompt", () => { const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag"); // Should contain PR-specific instructions (git commands when not using signing) - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain( "Always push to the existing branch when triggered on a PR", ); @@ -643,7 +647,7 @@ describe("generatePrompt", () => { const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag"); // Should contain open PR instructions (git commands when not using signing) - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain( "Always push to the existing branch when triggered on a PR", ); @@ -757,7 +761,7 @@ describe("generatePrompt", () => { expect(prompt).toContain("Use git commands via the Bash tool"); expect(prompt).toContain("git add"); expect(prompt).toContain("git commit"); - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); // Should use the minimal comment tool expect(prompt).toContain("mcp__github_comment__update_claude_comment"); @@ -886,17 +890,18 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(); // The base tools should be in the result - expect(result).toContain("Edit"); + // Edit/MultiEdit/Write are NOT in allowedTools — acceptEdits permission mode handles them + expect(result).not.toContain("Edit"); + expect(result).not.toContain("Write"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); // Default is no commit signing, so should have specific Bash git commands expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git commit:*)"); - expect(result).toContain("Bash(git push:*)"); + expect(result).toContain("scripts/git-push.sh:*)"); expect(result).toContain("mcp__github_comment__update_claude_comment"); // Should not have commit signing tools @@ -908,12 +913,12 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], false, false); // The base tools should be in the result - expect(result).toContain("Edit"); + expect(result).not.toContain("Edit"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); + expect(result).not.toContain("Write"); // Should have specific Bash git commands for non-signing mode expect(result).toContain("Bash(git add:*)"); @@ -930,7 +935,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(customTools); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); expect(result).toContain("Glob"); // Custom tools should be appended @@ -950,7 +955,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], true); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); expect(result).toContain("Glob"); // GitHub Actions tools should be included @@ -964,7 +969,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(customTools, true); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); // Custom tools should be included expect(result).toContain("Tool1"); @@ -980,12 +985,12 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], false, true); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).not.toContain("Edit"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); + expect(result).not.toContain("Write"); // Commit signing tools should be included expect(result).toContain("mcp__github_file_ops__commit_files"); @@ -1001,17 +1006,17 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], false, false); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).not.toContain("Edit"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); + expect(result).not.toContain("Write"); // Specific Bash git commands should be included expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git commit:*)"); - expect(result).toContain("Bash(git push:*)"); + expect(result).toContain("scripts/git-push.sh:*)"); expect(result).toContain("Bash(git status:*)"); expect(result).toContain("Bash(git diff:*)"); expect(result).toContain("Bash(git log:*)"); @@ -1030,7 +1035,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(customTools, true, false); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); expect(result).toContain("Bash(git add:*)"); // Custom tools should be included diff --git a/test/pull-request-target.test.ts b/test/pull-request-target.test.ts index 59c747f..6d9cac0 100644 --- a/test/pull-request-target.test.ts +++ b/test/pull-request-target.test.ts @@ -135,7 +135,7 @@ describe("pull_request_target event support", () => { const prompt = generatePrompt(envVars, mockGitHubData, false, "tag"); // Should include git commands for non-commit-signing mode - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain( "Always push to the existing branch when triggered on a PR", ); From 0e80d3c5b80b88b63214c657709c731343e7c87b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 13 Mar 2026 17:07:33 +0000 Subject: [PATCH 6/8] chore: bump Claude Code to 2.1.75 and Agent SDK to 0.2.75 --- base-action/action.yml | 2 +- base-action/bun.lock | 4 ++-- base-action/package.json | 2 +- bun.lock | 4 ++-- package.json | 2 +- src/entrypoints/run.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/base-action/action.yml b/base-action/action.yml index 3b09717..9c38fad 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -124,7 +124,7 @@ runs: PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} run: | if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then - CLAUDE_CODE_VERSION="2.1.73" + CLAUDE_CODE_VERSION="2.1.75" echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." for attempt in 1 2 3; do echo "Installation attempt $attempt..." diff --git a/base-action/bun.lock b/base-action/bun.lock index c8c25a6..507775f 100644 --- a/base-action/bun.lock +++ b/base-action/bun.lock @@ -6,7 +6,7 @@ "name": "@anthropic-ai/claude-code-base-action", "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.73", + "@anthropic-ai/claude-agent-sdk": "^0.2.75", "shell-quote": "^1.8.3", }, "devDependencies": { @@ -27,7 +27,7 @@ "@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.73", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-JrHeMl93Q5ai9GMPAffQkSisbbDvD1skU2x6sf6WRzEZw0sK6aTG+XSiZHY2F5aSrfd4G2qUogLHEm6Y8obyOQ=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.75", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-BSF5m0z7mwP5zB0G9JOEhCBovPpRBTe69oMSdUOYvEX6rLcHM0lp38MC2mr81vqSFo91a21/hVxOeifZyn/qkw=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/base-action/package.json b/base-action/package.json index 6907010..78a6148 100644 --- a/base-action/package.json +++ b/base-action/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.73", + "@anthropic-ai/claude-agent-sdk": "^0.2.75", "shell-quote": "^1.8.3" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index 4edc2ad..e6bd746 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.73", + "@anthropic-ai/claude-agent-sdk": "^0.2.75", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", @@ -37,7 +37,7 @@ "@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.73", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-JrHeMl93Q5ai9GMPAffQkSisbbDvD1skU2x6sf6WRzEZw0sK6aTG+XSiZHY2F5aSrfd4G2qUogLHEm6Y8obyOQ=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.75", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-BSF5m0z7mwP5zB0G9JOEhCBovPpRBTe69oMSdUOYvEX6rLcHM0lp38MC2mr81vqSFo91a21/hVxOeifZyn/qkw=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/package.json b/package.json index 09cdff5..767b99f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.73", + "@anthropic-ai/claude-agent-sdk": "^0.2.75", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index 8a42776..a43a7d0 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -51,7 +51,7 @@ async function installClaudeCode(): Promise { return; } - const claudeCodeVersion = "2.1.73"; + const claudeCodeVersion = "2.1.75"; console.log(`Installing Claude Code v${claudeCodeVersion}...`); for (let attempt = 1; attempt <= 3; attempt++) { From cd77b50d2b0808657f8e6774085c8bf54484351c Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 14 Mar 2026 01:29:31 +0000 Subject: [PATCH 7/8] chore: bump Claude Code to 2.1.76 and Agent SDK to 0.2.76 --- base-action/action.yml | 2 +- base-action/bun.lock | 4 ++-- base-action/package.json | 2 +- bun.lock | 4 ++-- package.json | 2 +- src/entrypoints/run.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/base-action/action.yml b/base-action/action.yml index 9c38fad..31d7928 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -124,7 +124,7 @@ runs: PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} run: | if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then - CLAUDE_CODE_VERSION="2.1.75" + CLAUDE_CODE_VERSION="2.1.76" echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." for attempt in 1 2 3; do echo "Installation attempt $attempt..." diff --git a/base-action/bun.lock b/base-action/bun.lock index 507775f..8be9f43 100644 --- a/base-action/bun.lock +++ b/base-action/bun.lock @@ -6,7 +6,7 @@ "name": "@anthropic-ai/claude-code-base-action", "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.75", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "shell-quote": "^1.8.3", }, "devDependencies": { @@ -27,7 +27,7 @@ "@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.75", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-BSF5m0z7mwP5zB0G9JOEhCBovPpRBTe69oMSdUOYvEX6rLcHM0lp38MC2mr81vqSFo91a21/hVxOeifZyn/qkw=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/base-action/package.json b/base-action/package.json index 78a6148..2c72e73 100644 --- a/base-action/package.json +++ b/base-action/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.75", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "shell-quote": "^1.8.3" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index e6bd746..88e171c 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.75", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", @@ -37,7 +37,7 @@ "@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.75", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-BSF5m0z7mwP5zB0G9JOEhCBovPpRBTe69oMSdUOYvEX6rLcHM0lp38MC2mr81vqSFo91a21/hVxOeifZyn/qkw=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/package.json b/package.json index 767b99f..bbd8863 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.75", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index a43a7d0..62d22b0 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -51,7 +51,7 @@ async function installClaudeCode(): Promise { return; } - const claudeCodeVersion = "2.1.75"; + const claudeCodeVersion = "2.1.76"; console.log(`Installing Claude Code v${claudeCodeVersion}...`); for (let attempt = 1; attempt <= 3; attempt++) { From c80d3be21af7083e656c0c732b6054f731e9feab Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 16 Mar 2026 08:32:04 +0000 Subject: [PATCH 8/8] fix: handle malformed JSONL lines gracefully in post-buffered-inline-comments Wrap per-line JSON.parse in try-catch so a single truncated/malformed entry (e.g. from an OOM mid-write) skips with a warning instead of aborting the entire batch and dropping all comments. Co-authored-by: vtmocanu --- src/entrypoints/post-buffered-inline-comments.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/entrypoints/post-buffered-inline-comments.ts b/src/entrypoints/post-buffered-inline-comments.ts index 763dae7..aa960a6 100644 --- a/src/entrypoints/post-buffered-inline-comments.ts +++ b/src/entrypoints/post-buffered-inline-comments.ts @@ -155,7 +155,14 @@ async function main() { const comments: BufferedComment[] = raw .split("\n") .filter(Boolean) - .map((line) => JSON.parse(line)); + .flatMap((line) => { + try { + return [JSON.parse(line) as BufferedComment]; + } catch { + console.log(`::warning::Skipping malformed JSONL line: ${line.slice(0, 120)}`); + return []; + } + }); if (comments.length === 0) { console.log("No buffered inline comments");