Skip to content

Commit 4ea9e38

Browse files
committed
feat: add allow_bot_actor parameter for automated workflows
- Add allow_bot_actor parameter to enable GitHub bots to trigger Claude Code Action - Implement robust bot write permission validation - Use repo.permissions for comprehensive access checks - Handle both collaborator and installation permissions - Add comprehensive test coverage for bot scenarios - Update documentation with security considerations This enables automated workflows like documentation updates, CI-triggered code reviews, and scheduled maintenance while maintaining security through explicit opt-in and proper permission validation.
1 parent 0d9513b commit 4ea9e38

10 files changed

Lines changed: 1805 additions & 81 deletions

File tree

README.md

Lines changed: 891 additions & 12 deletions
Large diffs are not rendered by default.

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Thank you for trying out the beta of our GitHub Action! This document outlines o
1010
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
1111
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
1212
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
13-
- **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude
13+
- ~**Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude~
1414
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
1515

1616
---

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ inputs:
6060
description: "Complete replacement of Claude's prompt with custom template (supports variable substitution)"
6161
required: false
6262
default: ""
63+
allow_bot_actor:
64+
description: "Allow bot actors to trigger the action. Default is false for security reasons."
65+
required: false
66+
default: "false"
6367
mcp_config:
6468
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
6569
additional_permissions:
@@ -154,6 +158,7 @@ runs:
154158
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
155159
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
156160
OVERRIDE_PROMPT: ${{ inputs.override_prompt }}
161+
ALLOW_BOT_ACTOR: ${{ inputs.allow_bot_actor }}
157162
MCP_CONFIG: ${{ inputs.mcp_config }}
158163
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
159164
GITHUB_RUN_ID: ${{ github.run_id }}

docs/faq.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ This FAQ addresses common questions and gotchas when using the Claude Code GitHu
66

77
### Why doesn't tagging @claude from my automated workflow work?
88

9-
The `github-actions` user cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user, or use a separate app token of your own. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
9+
By default, bots cannot trigger Claude for security reasons. With `allow_bot_actor: true`, you can enable bot triggers, but there are important distinctions:
10+
11+
1. **GitHub Apps** (recommended): Create a GitHub App, use app tokens, and set `allow_bot_actor: true`. The app needs write permissions.
12+
2. **Personal Access Tokens**: Use a PAT instead of `GITHUB_TOKEN` in your workflows with `allow_bot_actor: true`.
13+
3. **github-actions[bot]**: Can trigger Claude with `allow_bot_actor: true`, BUT due to GitHub's security, responses won't trigger subsequent workflows.
14+
15+
**Important**: With `allow_bot_actor: true`, `github-actions[bot]` CAN trigger Claude initially. However, Claude's responses (when using `GITHUB_TOKEN`) cannot trigger subsequent workflows due to GitHub's anti-loop security feature.
1016

1117
### Why does Claude say I don't have permission to trigger it?
1218

src/entrypoints/prepare.ts

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,60 +7,40 @@
77

88
import * as core from "@actions/core";
99
import { setupGitHubToken } from "../github/token";
10+
import { checkHumanActor } from "../github/validation/actor";
1011
import { checkWritePermissions } from "../github/validation/permissions";
12+
import { createInitialComment } from "../github/operations/comments/create-initial";
13+
import { setupBranch } from "../github/operations/branch";
14+
import { configureGitAuth } from "../github/operations/git-config";
15+
import { prepareMcpConfig } from "../mcp/install-mcp-server";
1116
import { createOctokit } from "../github/api/client";
12-
import { parseGitHubContext, isEntityContext } from "../github/context";
13-
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
14-
import type { ModeName } from "../modes/types";
15-
import { prepare } from "../prepare";
17+
import { fetchGitHubData } from "../github/data/fetcher";
18+
import { parseGitHubContext } from "../github/context";
19+
import { getMode } from "../modes/registry";
20+
import { createPrompt } from "../create-prompt";
1621

1722
async function run() {
1823
try {
19-
// Step 1: Get mode first to determine authentication method
20-
const modeInput = process.env.MODE || DEFAULT_MODE;
21-
22-
// Validate mode input
23-
if (!isValidMode(modeInput)) {
24-
throw new Error(`Invalid mode: ${modeInput}`);
25-
}
26-
const validatedMode: ModeName = modeInput;
27-
28-
// Step 2: Setup GitHub token based on mode
29-
let githubToken: string;
30-
if (validatedMode === "experimental-review") {
31-
// For experimental-review mode, use the default GitHub Action token
32-
githubToken = process.env.DEFAULT_WORKFLOW_TOKEN || "";
33-
if (!githubToken) {
34-
throw new Error(
35-
"DEFAULT_WORKFLOW_TOKEN not found for experimental-review mode",
36-
);
37-
}
38-
console.log("Using default GitHub Action token for review mode");
39-
core.setOutput("GITHUB_TOKEN", githubToken);
40-
} else {
41-
// For other modes, use the existing token exchange
42-
githubToken = await setupGitHubToken();
43-
}
24+
// Step 1: Setup GitHub token
25+
const githubToken = await setupGitHubToken();
4426
const octokit = createOctokit(githubToken);
4527

4628
// Step 2: Parse GitHub context (once for all operations)
4729
const context = parseGitHubContext();
4830

49-
// Step 3: Check write permissions (only for entity contexts)
50-
if (isEntityContext(context)) {
51-
const hasWritePermissions = await checkWritePermissions(
52-
octokit.rest,
53-
context,
31+
// Step 3: Check write permissions
32+
const hasWritePermissions = await checkWritePermissions(
33+
octokit.rest,
34+
context,
35+
);
36+
if (!hasWritePermissions) {
37+
throw new Error(
38+
"Actor does not have write permissions to the repository",
5439
);
55-
if (!hasWritePermissions) {
56-
throw new Error(
57-
"Actor does not have write permissions to the repository",
58-
);
59-
}
6040
}
6141

6242
// Step 4: Get mode and check trigger conditions
63-
const mode = getMode(validatedMode, context);
43+
const mode = getMode(context.inputs.mode);
6444
const containsTrigger = mode.shouldTrigger(context);
6545

6646
// Set output for action.yml to check
@@ -71,16 +51,65 @@ async function run() {
7151
return;
7252
}
7353

74-
// Step 5: Use the new modular prepare function
75-
const result = await prepare({
76-
context,
77-
octokit,
78-
mode,
79-
githubToken,
54+
// Step 5: Check if actor is human (unless bot actors are allowed)
55+
await checkHumanActor(octokit.rest, context);
56+
57+
// Step 6: Create initial tracking comment (mode-aware)
58+
// Some modes (e.g., agent mode) may not need tracking comments
59+
let commentId: number | undefined;
60+
let commentData:
61+
| Awaited<ReturnType<typeof createInitialComment>>
62+
| undefined;
63+
if (mode.shouldCreateTrackingComment()) {
64+
commentData = await createInitialComment(octokit.rest, context);
65+
commentId = commentData.id;
66+
}
67+
68+
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
69+
const githubData = await fetchGitHubData({
70+
octokits: octokit,
71+
repository: `${context.repository.owner}/${context.repository.repo}`,
72+
prNumber: context.entityNumber.toString(),
73+
isPR: context.isPR,
74+
triggerUsername: context.actor,
8075
});
8176

82-
// Set the MCP config output
83-
core.setOutput("mcp_config", result.mcpConfig);
77+
// Step 8: Setup branch
78+
const branchInfo = await setupBranch(octokit, githubData, context);
79+
80+
// Step 9: Configure git authentication if not using commit signing
81+
if (!context.inputs.useCommitSigning) {
82+
try {
83+
await configureGitAuth(githubToken, context, commentData?.user || null);
84+
} catch (error) {
85+
console.error("Failed to configure git authentication:", error);
86+
throw error;
87+
}
88+
}
89+
90+
// Step 10: Create prompt file
91+
const modeContext = mode.prepareContext(context, {
92+
commentId,
93+
baseBranch: branchInfo.baseBranch,
94+
claudeBranch: branchInfo.claudeBranch,
95+
});
96+
97+
await createPrompt(mode, modeContext, githubData, context);
98+
99+
// Step 11: Get MCP configuration
100+
const additionalMcpConfig = process.env.MCP_CONFIG || "";
101+
const mcpConfig = await prepareMcpConfig({
102+
githubToken,
103+
owner: context.repository.owner,
104+
repo: context.repository.repo,
105+
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
106+
baseBranch: branchInfo.baseBranch,
107+
additionalMcpConfig,
108+
claudeCommentId: commentId?.toString() || "",
109+
allowedTools: context.inputs.allowedTools,
110+
context,
111+
});
112+
core.setOutput("mcp_config", mcpConfig);
84113
} catch (error) {
85114
const errorMessage = error instanceof Error ? error.message : String(error);
86115
core.setFailed(`Prepare step failed with error: ${errorMessage}`);

src/github/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ type BaseContext = {
7777
useStickyComment: boolean;
7878
additionalPermissions: Map<string, string>;
7979
useCommitSigning: boolean;
80+
allowBotActor: boolean;
8081
};
8182
};
8283

@@ -136,6 +137,7 @@ export function parseGitHubContext(): GitHubContext {
136137
process.env.ADDITIONAL_PERMISSIONS ?? "",
137138
),
138139
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
140+
allowBotActor: process.env.ALLOW_BOT_ACTOR === "true",
139141
},
140142
};
141143

src/github/validation/actor.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,47 @@
55
* Prevents automated tools or bots from triggering Claude
66
*/
77

8+
import * as core from "@actions/core";
89
import type { Octokit } from "@octokit/rest";
910
import type { ParsedGitHubContext } from "../context";
1011

12+
/**
13+
* Get the GitHub actor type (User, Bot, Organization, etc.)
14+
*/
15+
async function getActorType(
16+
octokit: Octokit,
17+
actor: string,
18+
): Promise<string | null> {
19+
try {
20+
const { data } = await octokit.users.getByUsername({ username: actor });
21+
return data.type;
22+
} catch (error) {
23+
core.warning(`Failed to get user data for ${actor}: ${error}`);
24+
return null;
25+
}
26+
}
27+
1128
export async function checkHumanActor(
1229
octokit: Octokit,
1330
githubContext: ParsedGitHubContext,
1431
) {
15-
// Fetch user information from GitHub API
16-
const { data: userData } = await octokit.users.getByUsername({
17-
username: githubContext.actor,
18-
});
32+
const actorType = await getActorType(octokit, githubContext.actor);
1933

20-
const actorType = userData.type;
34+
if (!actorType) {
35+
throw new Error(
36+
`Could not determine actor type for: ${githubContext.actor}`,
37+
);
38+
}
2139

2240
console.log(`Actor type: ${actorType}`);
2341

42+
if (githubContext.inputs.allowBotActor && actorType === "Bot") {
43+
console.log(
44+
`Bot actor allowed, skipping human actor check for: ${githubContext.actor}`,
45+
);
46+
return;
47+
}
48+
2449
if (actorType !== "User") {
2550
throw new Error(
2651
`Workflow initiated by non-human actor: ${githubContext.actor} (type: ${actorType}).`,

0 commit comments

Comments
 (0)