Compare commits

..

1 Commits

Author SHA1 Message Date
Claude
9e388ead50 Add Australian programming joke to README
Added a light-hearted programming joke with an Australian twist to the README,
referencing both coding bugs and drop bears for some local flavor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 22:27:45 +00:00
33 changed files with 65 additions and 830 deletions

View File

@@ -9,7 +9,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
@@ -24,7 +24,7 @@ jobs:
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
@@ -39,7 +39,7 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:

View File

@@ -13,7 +13,7 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -25,7 +25,7 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -19,7 +19,7 @@ jobs:
next_version: ${{ steps.next_version.outputs.next_version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -91,7 +91,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -116,7 +116,7 @@ jobs:
environment: production
steps:
- name: Checkout base-action repo
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: anthropics/claude-code-base-action
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}

View File

@@ -2,6 +2,8 @@
# Claude Code Action
> *Why do Aussie programmers prefer dark mode? Because the light brings in drop bears and bugs, mate!* 🐨🐛
A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs and issues that can answer questions and implement code changes. This action intelligently detects when to activate based on your workflow context—whether responding to @claude mentions, issue assignments, or executing automation tasks with explicit prompts. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI.
## Features

View File

@@ -101,10 +101,6 @@ inputs:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
plugins:
description: "Comma-separated list of Claude Code plugin names to install (e.g., 'plugin1,plugin2,plugin3')"
required: false
default: ""
outputs:
execution_file:
@@ -167,7 +163,6 @@ runs:
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }}
ALL_INPUTS: ${{ toJson(inputs) }}
PLUGINS: ${{ inputs.plugins }}
- name: Install Base Action Dependencies
if: steps.prepare.outputs.contains_trigger == 'true'
@@ -182,7 +177,7 @@ runs:
# Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.27
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.9
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
@@ -218,7 +213,6 @@ runs:
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_PLUGINS: ${{ inputs.plugins }}
# Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}

View File

@@ -336,7 +336,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -55,10 +55,6 @@ inputs:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
plugins:
description: "Comma-separated list of Claude Code plugin names to install (e.g., 'plugin1,plugin2,plugin3')"
required: false
default: ""
outputs:
conclusion:
@@ -103,7 +99,7 @@ runs:
run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.27
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.9
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH
@@ -130,7 +126,6 @@ runs:
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_PLUGINS: ${{ inputs.plugins }}
# Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}

View File

@@ -32,7 +32,7 @@ jobs:
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-23fa0dd"
"ghcr.io/github/github-mcp-server:sha-7aced2b"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -5,7 +5,6 @@ import { preparePrompt } from "./prepare-prompt";
import { runClaude } from "./run-claude";
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
import { validateEnvironmentVariables } from "./validate-env";
import { installPlugins } from "./install-plugins";
async function run() {
try {
@@ -16,12 +15,6 @@ async function run() {
undefined, // homeDir
);
// Install Claude Code plugins if specified
await installPlugins(
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
);
const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",
promptFile: process.env.INPUT_PROMPT_FILE || "",

View File

@@ -1,155 +0,0 @@
import { spawn, ChildProcess } from "child_process";
const PLUGIN_NAME_REGEX = /^[@a-zA-Z0-9_\-\/\.]+$/;
const MAX_PLUGIN_NAME_LENGTH = 512;
const CLAUDE_CODE_MARKETPLACE_URL =
"https://github.com/anthropics/claude-code.git";
const PATH_TRAVERSAL_REGEX =
/\.\.\/|\/\.\.|\.\/|\/\.|(?:^|\/)\.\.$|(?:^|\/)\.$|\.\.(?![0-9])/;
/**
* Validates a plugin name for security issues
* @param pluginName - The plugin name to validate
* @throws {Error} If the plugin name is invalid
*/
function validatePluginName(pluginName: string): void {
// Normalize Unicode to prevent homoglyph attacks (e.g., fullwidth dots, Unicode slashes)
const normalized = pluginName.normalize("NFC");
if (normalized.length > MAX_PLUGIN_NAME_LENGTH) {
throw new Error(`Plugin name too long: ${normalized.substring(0, 50)}...`);
}
if (!PLUGIN_NAME_REGEX.test(normalized)) {
throw new Error(`Invalid plugin name format: ${pluginName}`);
}
// Prevent path traversal attacks with single efficient regex check
if (PATH_TRAVERSAL_REGEX.test(normalized)) {
throw new Error(`Invalid plugin name format: ${pluginName}`);
}
}
/**
* Parse a comma-separated list of plugin names and return an array of trimmed, non-empty plugin names
* Validates plugin names to prevent command injection and path traversal attacks
* Allows: letters, numbers, @, -, _, /, . (common npm/scoped package characters)
* Disallows: path traversal (../, ./), shell metacharacters, and consecutive dots
*/
function parsePlugins(plugins?: string): string[] {
const trimmedPlugins = plugins?.trim();
if (!trimmedPlugins) {
return [];
}
// Split by comma and process each plugin
return trimmedPlugins
.split(",")
.map((p) => p.trim())
.filter((p) => {
if (p.length === 0) return false;
validatePluginName(p);
return true;
});
}
/**
* Executes a Claude Code CLI command with proper error handling
* @param claudeExecutable - Path to the Claude executable
* @param args - Command arguments to pass to the executable
* @param errorContext - Context string for error messages (e.g., "Failed to install plugin 'foo'")
* @returns Promise that resolves when the command completes successfully
* @throws {Error} If the command fails to execute
*/
async function executeClaudeCommand(
claudeExecutable: string,
args: string[],
errorContext: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const childProcess: ChildProcess = spawn(claudeExecutable, args, {
stdio: "inherit",
});
childProcess.on("close", (code: number | null) => {
if (code === 0) {
resolve();
} else if (code === null) {
reject(new Error(`${errorContext}: process terminated by signal`));
} else {
reject(new Error(`${errorContext} (exit code: ${code})`));
}
});
childProcess.on("error", (err: Error) => {
reject(new Error(`${errorContext}: ${err.message}`));
});
});
}
/**
* Installs a single Claude Code plugin
*/
async function installPlugin(
pluginName: string,
claudeExecutable: string,
): Promise<void> {
return executeClaudeCommand(
claudeExecutable,
["plugin", "install", pluginName],
`Failed to install plugin '${pluginName}'`,
);
}
/**
* Adds the Claude Code marketplace
* @param claudeExecutable - Path to the Claude executable
* @returns Promise that resolves when the marketplace add command completes
* @throws {Error} If the command fails to execute
*/
async function addMarketplace(claudeExecutable: string): Promise<void> {
console.log("Adding Claude Code marketplace...");
return executeClaudeCommand(
claudeExecutable,
["plugin", "marketplace", "add", CLAUDE_CODE_MARKETPLACE_URL],
"Failed to add marketplace",
);
}
/**
* Installs Claude Code plugins from a comma-separated list
* @param pluginsInput - Comma-separated list of plugin names, or undefined/empty to skip installation
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
* @returns Promise that resolves when all plugins are installed
* @throws {Error} If any plugin fails validation or installation (stops on first error)
*/
export async function installPlugins(
pluginsInput: string | undefined,
claudeExecutable?: string,
): Promise<void> {
const plugins = parsePlugins(pluginsInput);
if (plugins.length === 0) {
console.log("No plugins to install");
return;
}
// Resolve executable path with explicit fallback
const resolvedExecutable = claudeExecutable || "claude";
// Add marketplace before installing plugins
await addMarketplace(resolvedExecutable);
console.log(`Installing ${plugins.length} plugin(s)...`);
for (const plugin of plugins) {
console.log(`Installing plugin: ${plugin}`);
await installPlugin(plugin, resolvedExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
}
console.log("All plugins installed successfully");
}

View File

@@ -1,449 +0,0 @@
#!/usr/bin/env bun
import { describe, test, expect, mock, spyOn, afterEach } from "bun:test";
import { installPlugins } from "../src/install-plugins";
import * as childProcess from "child_process";
describe("installPlugins", () => {
let spawnSpy: ReturnType<typeof spyOn> | undefined;
afterEach(() => {
// Restore original spawn after each test
if (spawnSpy) {
spawnSpy.mockRestore();
}
});
function createMockSpawn(
exitCode: number | null = 0,
shouldError: boolean = false,
) {
const mockProcess = {
on: mock((event: string, handler: Function) => {
if (event === "close" && !shouldError) {
// Simulate successful close
setTimeout(() => handler(exitCode), 0);
} else if (event === "error" && shouldError) {
// Simulate error
setTimeout(() => handler(new Error("spawn error")), 0);
}
return mockProcess;
}),
};
spawnSpy = spyOn(childProcess, "spawn").mockImplementation(
() => mockProcess as any,
);
return spawnSpy;
}
test("should not call spawn when no plugins are specified", async () => {
const spy = createMockSpawn();
await installPlugins("");
expect(spy).not.toHaveBeenCalled();
});
test("should not call spawn when plugins is undefined", async () => {
const spy = createMockSpawn();
await installPlugins(undefined);
expect(spy).not.toHaveBeenCalled();
});
test("should not call spawn when plugins is only whitespace", async () => {
const spy = createMockSpawn();
await installPlugins(" ");
expect(spy).not.toHaveBeenCalled();
});
test("should install a single plugin with default executable", async () => {
const spy = createMockSpawn();
await installPlugins("test-plugin");
expect(spy).toHaveBeenCalledTimes(2);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
// Second call: install plugin
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should install multiple plugins sequentially", async () => {
const spy = createMockSpawn();
await installPlugins("plugin1,plugin2,plugin3");
expect(spy).toHaveBeenCalledTimes(4);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
// Subsequent calls: install plugins
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
4,
"claude",
["plugin", "install", "plugin3"],
{ stdio: "inherit" },
);
});
test("should use custom claude executable path when provided", async () => {
const spy = createMockSpawn();
await installPlugins("test-plugin", "/custom/path/to/claude");
expect(spy).toHaveBeenCalledTimes(2);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"/custom/path/to/claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
// Second call: install plugin
expect(spy).toHaveBeenNthCalledWith(
2,
"/custom/path/to/claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should trim whitespace from plugin names before installation", async () => {
const spy = createMockSpawn();
await installPlugins(" plugin1 , plugin2 ");
expect(spy).toHaveBeenCalledTimes(3);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
});
test("should skip empty entries in plugin list", async () => {
const spy = createMockSpawn();
await installPlugins("plugin1,,plugin2");
expect(spy).toHaveBeenCalledTimes(3);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
});
test("should handle plugin installation error and throw", async () => {
createMockSpawn(1, false); // Exit code 1
await expect(installPlugins("failing-plugin")).rejects.toThrow(
"Failed to add marketplace (exit code: 1)",
);
});
test("should handle null exit code (process terminated by signal)", async () => {
createMockSpawn(null, false); // Exit code null (terminated by signal)
await expect(installPlugins("terminated-plugin")).rejects.toThrow(
"Failed to add marketplace: process terminated by signal",
);
});
test("should stop installation on first error", async () => {
const spy = createMockSpawn(1, false); // Exit code 1
await expect(installPlugins("plugin1,plugin2,plugin3")).rejects.toThrow(
"Failed to add marketplace (exit code: 1)",
);
// Should only try to add marketplace before failing
expect(spy).toHaveBeenCalledTimes(1);
});
test("should handle plugins with special characters in names", async () => {
const spy = createMockSpawn();
await installPlugins("org/plugin-name,@scope/plugin");
expect(spy).toHaveBeenCalledTimes(3);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "org/plugin-name"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "@scope/plugin"],
{ stdio: "inherit" },
);
});
test("should handle spawn errors", async () => {
createMockSpawn(0, true); // Trigger error event
await expect(installPlugins("test-plugin")).rejects.toThrow(
"Failed to add marketplace: spawn error",
);
});
test("should install plugins with custom executable and multiple plugins", async () => {
const spy = createMockSpawn();
await installPlugins("plugin-a,plugin-b", "/usr/local/bin/claude-custom");
expect(spy).toHaveBeenCalledTimes(3);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"/usr/local/bin/claude-custom",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"/usr/local/bin/claude-custom",
["plugin", "install", "plugin-a"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"/usr/local/bin/claude-custom",
["plugin", "install", "plugin-b"],
{ stdio: "inherit" },
);
});
test("should reject plugin names with command injection attempts", async () => {
const spy = createMockSpawn();
// Should throw due to invalid characters (semicolon and spaces)
await expect(installPlugins("plugin-name; rm -rf /")).rejects.toThrow(
"Invalid plugin name format",
);
// Mock should never be called because validation fails first
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with path traversal using ../", async () => {
const spy = createMockSpawn();
await expect(installPlugins("../../../malicious-plugin")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with path traversal using ./", async () => {
const spy = createMockSpawn();
await expect(installPlugins("./../../@scope/package")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with consecutive dots", async () => {
const spy = createMockSpawn();
await expect(installPlugins(".../.../package")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with hidden path traversal", async () => {
const spy = createMockSpawn();
await expect(installPlugins("package/../other")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should accept plugin names with single dots in version numbers", async () => {
const spy = createMockSpawn();
await installPlugins("plugin-v1.0.2");
expect(spy).toHaveBeenCalledTimes(2);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin-v1.0.2"],
{ stdio: "inherit" },
);
});
test("should accept plugin names with multiple dots in semantic versions", async () => {
const spy = createMockSpawn();
await installPlugins("@scope/plugin-v1.0.0-beta.1");
expect(spy).toHaveBeenCalledTimes(2);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/anthropics/claude-code.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "@scope/plugin-v1.0.0-beta.1"],
{ stdio: "inherit" },
);
});
test("should reject Unicode homoglyph path traversal attempts", async () => {
const spy = createMockSpawn();
// Using fullwidth dots (U+FF0E) and fullwidth solidus (U+FF0F)
await expect(installPlugins("malicious")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject path traversal at end of path", async () => {
const spy = createMockSpawn();
await expect(installPlugins("package/..")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject single dot directory reference", async () => {
const spy = createMockSpawn();
await expect(installPlugins("package/.")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject path traversal in middle of path", async () => {
const spy = createMockSpawn();
await expect(installPlugins("package/../other")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
});

View File

@@ -127,7 +127,7 @@ For performance, Claude uses shallow clones:
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
```
- uses: actions/checkout@v5
- uses: actions/checkout@v4
depth: 0 # will fetch full repo history
```

View File

@@ -19,22 +19,11 @@
## GitHub App Permissions
The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions:
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
### Currently Used Permissions
- **Contents** (Read & Write): For reading repository files and creating branches
- **Pull Requests** (Read & Write): For reading PR data and creating/updating pull requests
- **Issues** (Read & Write): For reading issue data and updating issue comments
### Permissions for Future Features
The following permissions are requested but not yet actively used. These will enable planned features in future releases:
- **Discussions** (Read & Write): For interaction with GitHub Discussions
- **Actions** (Read): For accessing workflow run data and logs
- **Checks** (Read): For reading check run results
- **Workflows** (Read & Write): For triggering and managing GitHub Actions workflows
- **Pull Requests**: Read and write to create PRs and push changes
- **Issues**: Read and write to respond to issues
- **Contents**: Read and write to modify repository files
## Commit Signing

View File

@@ -35,7 +35,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -89,7 +89,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -153,7 +153,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -211,7 +211,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -268,7 +268,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -344,7 +344,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -456,7 +456,7 @@ jobs:
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
@@ -513,7 +513,7 @@ jobs:
security-events: write
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -32,9 +32,6 @@ jobs:
# --max-turns 10
# --model claude-4-0-sonnet-20250805
# Optional: install Claude Code plugins
# plugins: "plugin1,plugin2,plugin3"
# Optional: add custom trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: add assignee trigger for issues
@@ -50,33 +47,32 @@ 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` |
| `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 | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | 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 | "" |
| `plugins` | Comma-separated list of Claude Code plugin names to install (e.g., `plugin1,plugin2,plugin3`). 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` |
| `claude_args` | Additional arguments to pass directly to Claude CLI (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 | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | 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 | "" |
### Deprecated Inputs

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_branch }}
fetch-depth: 0

View File

@@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need at least 2 commits to analyze the latest

View File

@@ -16,7 +16,7 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -18,7 +18,7 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -19,7 +19,7 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -684,7 +684,7 @@ ${
- Display the todo list as a checklist in the GitHub comment and mark things off as you go.
- REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively.
- Use h3 headers (###) for section titles in your comments, not h1 headers (#).
- Your comment must always include the job run link in the format "[View job run](${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID})" at the bottom of your response (branch link if there is one should also be included there).
- Your comment must always include the job run link (and branch link if there is one) at the bottom.
CAPABILITIES AND LIMITATIONS:
When users ask you to do something, be aware of what you can and cannot do. This section helps you understand how to respond when users request actions outside your scope.

View File

@@ -95,7 +95,6 @@ type BaseContext = {
allowedBots: string;
allowedNonWriteUsers: string;
trackProgress: boolean;
plugins: string[];
};
};
@@ -151,10 +150,6 @@ export function parseGitHubContext(): GitHubContext {
allowedBots: process.env.ALLOWED_BOTS ?? "",
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
trackProgress: process.env.TRACK_PROGRESS === "true",
plugins: (process.env.PLUGINS || "")
.split(",")
.map((p) => p.trim())
.filter((p) => p.length > 0),
},
};

View File

@@ -134,17 +134,11 @@ export async function prepareMcpConfig(
};
}
// Check if code-review plugin is in the plugins list
const hasCodeReviewPlugin = context.inputs.plugins.includes(
"code-review@claude-code-plugins",
);
// Include inline comment server for PRs when requested via allowed tools
// or when code-review plugin is specified (needs inline comment access for reviews)
if (
isEntityContext(context) &&
context.isPR &&
(hasGitHubMcpTools || hasInlineCommentTools || hasCodeReviewPlugin)
(hasGitHubMcpTools || hasInlineCommentTools)
) {
baseMcpConfig.mcpServers.github_inline_comment = {
command: "bun",
@@ -215,7 +209,7 @@ export async function prepareMcpConfig(
"GITHUB_PERSONAL_ACCESS_TOKEN",
"-e",
"GITHUB_HOST",
"ghcr.io/github/github-mcp-server:sha-23fa0dd", // https://github.com/github/github-mcp-server/releases/tag/v0.17.1
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,

View File

@@ -114,14 +114,6 @@ export const agentMode: Mode = {
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
const allowedTools = parseAllowedTools(userClaudeArgs);
// Add inline comment tool if code-review plugin is present
const hasCodeReviewPlugin = context.inputs.plugins.includes(
"code-review@claude-code-plugins",
);
if (hasCodeReviewPlugin && isEntityContext(context) && context.isPR) {
allowedTools.push("mcp__github_inline_comment__create_inline_comment");
}
// Check for branch info from environment variables (useful for auto-fix workflows)
const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
const baseBranch =

View File

@@ -37,7 +37,6 @@ describe("prepareMcpConfig", () => {
allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false,
plugins: [],
},
};
@@ -277,111 +276,4 @@ describe("prepareMcpConfig", () => {
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).not.toBeDefined();
});
test("should include inline comment server in agent mode when code-review plugin is specified", async () => {
const contextWithCodeReviewPlugin: ParsedGitHubContext = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
plugins: ["code-review@claude-code-plugins"],
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
mode: "agent",
context: contextWithCodeReviewPlugin,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_inline_comment).toBeDefined();
expect(parsed.mcpServers.github_inline_comment.env.GITHUB_TOKEN).toBe(
"test-token",
);
});
test("should not include inline comment server in agent mode when code-review plugin is not specified", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
mode: "agent",
context: mockPRContext,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_inline_comment).not.toBeDefined();
});
test("should include inline comment server in agent mode when code-review plugin is in a list of plugins", async () => {
const contextWithMultiplePlugins: ParsedGitHubContext = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
plugins: ["plugin1", "code-review@claude-code-plugins", "plugin2"],
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
mode: "agent",
context: contextWithMultiplePlugins,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_inline_comment).toBeDefined();
});
test("should not include inline comment server in agent mode when plugins contain similar but not exact match", async () => {
const contextWithSimilarPlugin: ParsedGitHubContext = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
plugins: ["code-review-other", "review@claude-code-plugins"],
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
mode: "agent",
context: contextWithSimilarPlugin,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_inline_comment).not.toBeDefined();
});
test("should include inline comment server in agent mode when explicit inline comment tools are provided (backward compatibility)", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: ["mcp__github_inline_comment__create_inline_comment"],
mode: "agent",
context: mockPRContext,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_inline_comment).toBeDefined();
});
});

View File

@@ -25,7 +25,6 @@ const defaultInputs = {
allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false,
plugins: [],
};
const defaultRepository = {

View File

@@ -25,7 +25,6 @@ describe("detectMode with enhanced routing", () => {
allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false,
plugins: [],
},
};

View File

@@ -73,7 +73,6 @@ describe("checkWritePermissions", () => {
allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false,
plugins: [],
},
});