mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a017b830c0 | ||
|
|
75f52e56b2 | ||
|
|
1bbc9e7ff7 | ||
|
|
625ea1519c | ||
|
|
a9171f0ced | ||
|
|
4778aeae4c | ||
|
|
b6e5a9f27a | ||
|
|
5d91d7d217 | ||
|
|
90006bcae7 | ||
|
|
005436f51d |
90
.github/workflows/release.yml
vendored
90
.github/workflows/release.yml
vendored
@@ -109,48 +109,48 @@ jobs:
|
||||
|
||||
echo "Updated $major_version tag to point to $next_version"
|
||||
|
||||
release-base-action:
|
||||
needs: create-release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout base-action repo
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: anthropics/claude-code-base-action
|
||||
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Create and push tag
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# # Create the version tag
|
||||
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||
# git push origin "$next_version"
|
||||
|
||||
# # Update the beta tag
|
||||
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
||||
# git push origin beta --force
|
||||
|
||||
# - name: Create GitHub release
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
|
||||
# # Create the release
|
||||
# gh release create "$next_version" \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --title "$next_version" \
|
||||
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
|
||||
# --latest=false
|
||||
|
||||
# # Update beta release to be latest
|
||||
# gh release edit beta \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --latest
|
||||
# release-base-action:
|
||||
# needs: create-release
|
||||
# if: ${{ !inputs.dry_run }}
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: production
|
||||
# steps:
|
||||
# - name: Checkout base-action repo
|
||||
# uses: actions/checkout@v5
|
||||
# with:
|
||||
# repository: anthropics/claude-code-base-action
|
||||
# token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
# fetch-depth: 0
|
||||
#
|
||||
# - name: Create and push tag
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
#
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
#
|
||||
# # Create the version tag
|
||||
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||
# git push origin "$next_version"
|
||||
#
|
||||
# # Update the beta tag
|
||||
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
||||
# git push origin beta --force
|
||||
#
|
||||
# - name: Create GitHub release
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
#
|
||||
# # Create the release
|
||||
# gh release create "$next_version" \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --title "$next_version" \
|
||||
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
|
||||
# --latest=false
|
||||
#
|
||||
# # Update beta release to be latest
|
||||
# gh release edit beta \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --latest
|
||||
|
||||
@@ -213,7 +213,7 @@ runs:
|
||||
|
||||
# Install Claude Code if no custom executable is provided
|
||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||
CLAUDE_CODE_VERSION="2.1.1"
|
||||
CLAUDE_CODE_VERSION="2.1.9"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
|
||||
@@ -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.1"
|
||||
CLAUDE_CODE_VERSION="2.1.9"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"name": "@anthropic-ai/claude-code-base-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.9",
|
||||
"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.1", "", { "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-ZJO/TWcrFHGQTGHJDJl03mWozirWMBqdNpbuAgxZpLaHj2N5vyMxoeYiJC+7M0+gOSs7bjwKJLKTZcHGtGa34g=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.9", "", { "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-b4JD6ZKCZeVDqpWBnb+zJISWi3HzlweNlV7Oy/uo5G2XAfUV2M5AJ/tomKZCvZsvmr1fYbmmfyde3GL2h0pksA=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.9",
|
||||
"shell-quote": "^1.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
4
bun.lock
4
bun.lock
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.9",
|
||||
"@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.1", "", { "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-ZJO/TWcrFHGQTGHJDJl03mWozirWMBqdNpbuAgxZpLaHj2N5vyMxoeYiJC+7M0+gOSs7bjwKJLKTZcHGtGa34g=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.9", "", { "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-b4JD6ZKCZeVDqpWBnb+zJISWi3HzlweNlV7Oy/uo5G2XAfUV2M5AJ/tomKZCvZsvmr1fYbmmfyde3GL2h0pksA=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
- **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
|
||||
|
||||
## Pull Request Creation
|
||||
|
||||
In its default configuration, **Claude does not create pull requests automatically** when responding to `@claude` mentions. Instead:
|
||||
|
||||
- Claude commits code changes to a new branch
|
||||
- Claude provides a **link to the GitHub PR creation page** in its response
|
||||
- **The user must click the link and create the PR themselves**, ensuring human oversight before any code is proposed for merging
|
||||
|
||||
This design ensures that users retain full control over what pull requests are created and can review the changes before initiating the PR workflow.
|
||||
|
||||
## ⚠️ Prompt Injection Risks
|
||||
|
||||
**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.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.9",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
*/
|
||||
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import type { GitHubContext } from "../context";
|
||||
|
||||
export async function checkHumanActor(
|
||||
octokit: Octokit,
|
||||
githubContext: ParsedGitHubContext,
|
||||
githubContext: GitHubContext,
|
||||
) {
|
||||
// Fetch user information from GitHub API
|
||||
const { data: userData } = await octokit.users.getByUsername({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
configureGitAuth,
|
||||
setupSshSigning,
|
||||
} from "../../github/operations/git-config";
|
||||
import { checkHumanActor } from "../../github/validation/actor";
|
||||
import type { GitHubContext } from "../../github/context";
|
||||
import { isEntityContext } from "../../github/context";
|
||||
|
||||
@@ -80,7 +81,14 @@ export const agentMode: Mode = {
|
||||
return false;
|
||||
},
|
||||
|
||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||
async prepare({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
}: ModeOptions): Promise<ModeResult> {
|
||||
// Check if actor is human (prevents bot-triggered loops)
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
// Configure git authentication for agent mode (same as tag mode)
|
||||
// SSH signing takes precedence if provided
|
||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
export function parseAllowedTools(claudeArgs: string): string[] {
|
||||
// Match --allowedTools or --allowed-tools followed by the value
|
||||
// Handle both quoted and unquoted values
|
||||
// Use /g flag to find ALL occurrences, not just the first one
|
||||
const patterns = [
|
||||
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/, // Double quoted
|
||||
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/, // Single quoted
|
||||
/--(?:allowedTools|allowed-tools)\s+([^\s]+)/, // Unquoted
|
||||
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/g, // Double quoted
|
||||
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/g, // Single quoted
|
||||
/--(?:allowedTools|allowed-tools)\s+([^'"\s][^\s]*)/g, // Unquoted (must not start with quote)
|
||||
];
|
||||
|
||||
const tools: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = claudeArgs.match(pattern);
|
||||
if (match && match[1]) {
|
||||
// Don't return if the value starts with -- (another flag)
|
||||
if (match[1].startsWith("--")) {
|
||||
return [];
|
||||
for (const match of claudeArgs.matchAll(pattern)) {
|
||||
if (match[1]) {
|
||||
// Don't add if the value starts with -- (another flag)
|
||||
if (match[1].startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
for (const tool of match[1].split(",")) {
|
||||
const trimmed = tool.trim();
|
||||
if (trimmed && !seen.has(trimmed)) {
|
||||
seen.add(trimmed);
|
||||
tools.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return match[1].split(",").map((t) => t.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
return tools;
|
||||
}
|
||||
|
||||
@@ -145,12 +145,12 @@ describe("Agent Mode", () => {
|
||||
users: {
|
||||
getAuthenticated: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -187,6 +187,65 @@ describe("Agent Mode", () => {
|
||||
process.env.GITHUB_REF_NAME = originalRefName;
|
||||
});
|
||||
|
||||
test("prepare method rejects bot actors without allowed_bots", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
contextWithPrompts.actor = "claude[bot]";
|
||||
contextWithPrompts.inputs.allowedBots = "";
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
users: {
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "claude[bot]", id: 12345, type: "Bot" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
await expect(
|
||||
agentMode.prepare({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Workflow initiated by non-human actor: claude (type: Bot)",
|
||||
);
|
||||
});
|
||||
|
||||
test("prepare method allows bot actors when in allowed_bots list", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
contextWithPrompts.actor = "dependabot[bot]";
|
||||
contextWithPrompts.inputs.allowedBots = "dependabot";
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
users: {
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "dependabot[bot]", id: 12345, type: "Bot" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
// Should not throw - bot is in allowed list
|
||||
await expect(
|
||||
agentMode.prepare({
|
||||
context: contextWithPrompts,
|
||||
octokit: mockOctokit,
|
||||
githubToken: "test-token",
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test("prepare method creates prompt file with correct content", async () => {
|
||||
const contextWithPrompts = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
@@ -199,12 +258,12 @@ describe("Agent Mode", () => {
|
||||
users: {
|
||||
getAuthenticated: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
getByUsername: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { login: "test-user", id: 12345 },
|
||||
data: { login: "test-user", id: 12345, type: "User" },
|
||||
}),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -35,12 +35,44 @@ describe("parseAllowedTools", () => {
|
||||
expect(parseAllowedTools("")).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles duplicate --allowedTools flags", () => {
|
||||
test("handles --allowedTools followed by another --allowedTools flag", () => {
|
||||
const args = "--allowedTools --allowedTools mcp__github__*";
|
||||
// Should not match the first one since the value is another flag
|
||||
// The second --allowedTools is consumed as a value of the first, then skipped.
|
||||
// This is an edge case with malformed input - returns empty.
|
||||
expect(parseAllowedTools(args)).toEqual([]);
|
||||
});
|
||||
|
||||
test("parses multiple separate --allowed-tools flags", () => {
|
||||
const args =
|
||||
"--allowed-tools 'mcp__context7__*' --allowed-tools 'Read,Glob' --allowed-tools 'mcp__github_inline_comment__*'";
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__context7__*",
|
||||
"Read",
|
||||
"Glob",
|
||||
"mcp__github_inline_comment__*",
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses multiple --allowed-tools flags on separate lines", () => {
|
||||
const args = `--model 'claude-haiku'
|
||||
--allowed-tools 'mcp__context7__*'
|
||||
--allowed-tools 'Read,Glob,Grep'
|
||||
--allowed-tools 'mcp__github_inline_comment__create_inline_comment'`;
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__context7__*",
|
||||
"Read",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"mcp__github_inline_comment__create_inline_comment",
|
||||
]);
|
||||
});
|
||||
|
||||
test("deduplicates tools from multiple flags", () => {
|
||||
const args =
|
||||
"--allowed-tools 'Read,Glob' --allowed-tools 'Glob,Grep' --allowed-tools 'Read'";
|
||||
expect(parseAllowedTools(args)).toEqual(["Read", "Glob", "Grep"]);
|
||||
});
|
||||
|
||||
test("handles typo --alloedTools", () => {
|
||||
const args = "--alloedTools mcp__github__*";
|
||||
expect(parseAllowedTools(args)).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user