Skip to content

Commit fec554f

Browse files
yukukotaniclaude
andauthored
feat: add flexible bot access control with allowed_bots option (#117)
* feat: skip permission check for GitHub App bot users GitHub Apps (users ending with [bot]) now bypass permission checks as they have their own authorization mechanism. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add allow_bot_users option to control bot user access - Add allow_bot_users input parameter (default: false) - Modify checkHumanActor to optionally allow bot users - Add comprehensive tests for bot user handling - Improve security by blocking bot users by default This change prevents potential prompt injection attacks from bot users while providing flexibility for trusted bot integrations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: mark bot user support feature as completed in roadmap 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: move allowedBots parameter to context object Move allowedBots from function parameter to context.inputs to maintain consistency with other input handling throughout the codebase. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: update README for bot user support feature Add documentation for the new allowed_bots parameter that enables bot users to trigger Claude actions with granular control. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add missing allowedBots property in permissions test 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: update bot name format to include [bot] suffix in tests and docs - Update test cases to use correct bot actor names with [bot] suffix - Update documentation example to show correct bot name format - Align with GitHub's actual bot naming convention 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: normalize bot names for allowed_bots validation - Strip [bot] suffix from both actor names and allowed bot list for comparison - Allow both "dependabot" and "dependabot[bot]" formats in allowed_bots input - Display normalized bot names in error messages for consistency - Add comprehensive test coverage for both naming formats 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 59ca6e4 commit fec554f

12 files changed

Lines changed: 166 additions & 3 deletions

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
@@ -23,6 +23,10 @@ inputs:
2323
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
2424
required: false
2525
default: "claude/"
26+
allowed_bots:
27+
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
28+
required: false
29+
default: ""
2630

2731
# Mode configuration
2832
mode:
@@ -156,6 +160,7 @@ runs:
156160
OVERRIDE_PROMPT: ${{ inputs.override_prompt }}
157161
MCP_CONFIG: ${{ inputs.mcp_config }}
158162
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
163+
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
159164
GITHUB_RUN_ID: ${{ github.run_id }}
160165
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
161166
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}

docs/security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Access Control
44

55
- **Repository Access**: The action can only be triggered by users with write access to the repository
6-
- **No Bot Triggers**: GitHub Apps and bots cannot trigger this action
6+
- **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
77
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
88
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
99
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions

docs/usage.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
4343
# additional_permissions: |
4444
# actions: read
45+
# Optional: allow bot users to trigger the action
46+
# allowed_bots: "dependabot[bot],renovate[bot]"
4547
```
4648

4749
## Inputs
@@ -76,6 +78,7 @@ jobs:
7678
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
7779
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
7880
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
81+
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
7982

8083
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
8184

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+
allowedBots: string;
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+
allowedBots: process.env.ALLOWED_BOTS ?? "",
139141
},
140142
};
141143

src/github/validation/actor.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,42 @@ export async function checkHumanActor(
2121

2222
console.log(`Actor type: ${actorType}`);
2323

24+
// Check bot permissions if actor is not a User
2425
if (actorType !== "User") {
26+
const allowedBots = githubContext.inputs.allowedBots;
27+
28+
// Check if all bots are allowed
29+
if (allowedBots.trim() === "*") {
30+
console.log(
31+
`All bots are allowed, skipping human actor check for: ${githubContext.actor}`,
32+
);
33+
return;
34+
}
35+
36+
// Parse allowed bots list
37+
const allowedBotsList = allowedBots
38+
.split(",")
39+
.map((bot) =>
40+
bot
41+
.trim()
42+
.toLowerCase()
43+
.replace(/\[bot\]$/, ""),
44+
)
45+
.filter((bot) => bot.length > 0);
46+
47+
const botName = githubContext.actor.toLowerCase().replace(/\[bot\]$/, "");
48+
49+
// Check if specific bot is allowed
50+
if (allowedBotsList.includes(botName)) {
51+
console.log(
52+
`Bot ${botName} is in allowed list, skipping human actor check`,
53+
);
54+
return;
55+
}
56+
57+
// Bot not allowed
2558
throw new Error(
26-
`Workflow initiated by non-human actor: ${githubContext.actor} (type: ${actorType}).`,
59+
`Workflow initiated by non-human actor: ${botName} (type: ${actorType}). Add bot to allowed_bots list or use '*' to allow all bots.`,
2760
);
2861
}
2962

src/github/validation/permissions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export async function checkWritePermissions(
1717
try {
1818
core.info(`Checking permissions for actor: ${actor}`);
1919

20+
// Check if the actor is a GitHub App (bot user)
21+
if (actor.endsWith("[bot]")) {
22+
core.info(`Actor is a GitHub App: ${actor}`);
23+
return true;
24+
}
25+
2026
// Check permissions directly using the permission endpoint
2127
const response = await octokit.repos.getCollaboratorPermissionLevel({
2228
owner: repository.owner,

test/actor.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env bun
2+
3+
import { describe, test, expect } from "bun:test";
4+
import { checkHumanActor } from "../src/github/validation/actor";
5+
import type { Octokit } from "@octokit/rest";
6+
import { createMockContext } from "./mockContext";
7+
8+
function createMockOctokit(userType: string): Octokit {
9+
return {
10+
users: {
11+
getByUsername: async () => ({
12+
data: {
13+
type: userType,
14+
},
15+
}),
16+
},
17+
} as unknown as Octokit;
18+
}
19+
20+
describe("checkHumanActor", () => {
21+
test("should pass for human actor", async () => {
22+
const mockOctokit = createMockOctokit("User");
23+
const context = createMockContext();
24+
context.actor = "human-user";
25+
26+
await expect(
27+
checkHumanActor(mockOctokit, context),
28+
).resolves.toBeUndefined();
29+
});
30+
31+
test("should throw error for bot actor when not allowed", async () => {
32+
const mockOctokit = createMockOctokit("Bot");
33+
const context = createMockContext();
34+
context.actor = "test-bot[bot]";
35+
context.inputs.allowedBots = "";
36+
37+
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
38+
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
39+
);
40+
});
41+
42+
test("should pass for bot actor when all bots allowed", async () => {
43+
const mockOctokit = createMockOctokit("Bot");
44+
const context = createMockContext();
45+
context.actor = "test-bot[bot]";
46+
context.inputs.allowedBots = "*";
47+
48+
await expect(
49+
checkHumanActor(mockOctokit, context),
50+
).resolves.toBeUndefined();
51+
});
52+
53+
test("should pass for specific bot when in allowed list", async () => {
54+
const mockOctokit = createMockOctokit("Bot");
55+
const context = createMockContext();
56+
context.actor = "dependabot[bot]";
57+
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
58+
59+
await expect(
60+
checkHumanActor(mockOctokit, context),
61+
).resolves.toBeUndefined();
62+
});
63+
64+
test("should pass for specific bot when in allowed list (without [bot])", async () => {
65+
const mockOctokit = createMockOctokit("Bot");
66+
const context = createMockContext();
67+
context.actor = "dependabot[bot]";
68+
context.inputs.allowedBots = "dependabot,renovate";
69+
70+
await expect(
71+
checkHumanActor(mockOctokit, context),
72+
).resolves.toBeUndefined();
73+
});
74+
75+
test("should throw error for bot not in allowed list", async () => {
76+
const mockOctokit = createMockOctokit("Bot");
77+
const context = createMockContext();
78+
context.actor = "other-bot[bot]";
79+
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
80+
81+
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
82+
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
83+
);
84+
});
85+
86+
test("should throw error for bot not in allowed list (without [bot])", async () => {
87+
const mockOctokit = createMockOctokit("Bot");
88+
const context = createMockContext();
89+
context.actor = "other-bot[bot]";
90+
context.inputs.allowedBots = "dependabot,renovate";
91+
92+
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
93+
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
94+
);
95+
});
96+
});

test/install-mcp-server.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe("prepareMcpConfig", () => {
3737
useStickyComment: false,
3838
additionalPermissions: new Map(),
3939
useCommitSigning: false,
40+
allowedBots: "",
4041
},
4142
};
4243

test/mockContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const defaultInputs = {
2828
useStickyComment: false,
2929
additionalPermissions: new Map<string, string>(),
3030
useCommitSigning: false,
31+
allowedBots: "",
3132
};
3233

3334
const defaultRepository = {

0 commit comments

Comments
 (0)