Skip to content

Commit 8fc8dec

Browse files
authored
Merge pull request #5 from semgrep/austin/update_default_tools
update default tools
2 parents db18f50 + 1b0bb95 commit 8fc8dec

File tree

5 files changed

+176
-8
lines changed

5 files changed

+176
-8
lines changed

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ pre_action_steps:
4646
# the claude-code-action configuration
4747
action:
4848
prompt: "Check this PR for linting and style issues."
49-
claude_args: "--timeout 300"
5049
plugins: "my-plugin"
50+
# additional tools beyond the defaults (see below)
51+
allowed_tools:
52+
- Edit
53+
- Write
54+
- "Bash(npm test*)"
5155

5256
# steps that run after claude-code-action
5357
post_action_steps:
@@ -74,14 +78,32 @@ You are a code review assistant. Follow these guidelines:
7478
| `description` | string | no | Human-readable description (used as the job name in the dispatcher) |
7579
| `action` | object | yes | Claude code action configuration |
7680
| `action.prompt` | string | yes | The prompt sent to Claude |
77-
| `action.claude_args` | string | no | Extra CLI args for Claude |
7881
| `action.plugins` | string | no | Plugins to enable |
82+
| `action.allowed_tools` | string[] | no | Additional tools to allow beyond the defaults (see below) |
7983
| `checkout` | object | no | Args passed to `actions/checkout@v4` |
8084
| `pre_action_steps` | array | no | GHA steps to run before the action |
8185
| `post_action_steps` | array | no | GHA steps to run after the action |
8286

8387
Pre/post action steps are validated against the [GitHub Actions step schema](https://json.schemastore.org/github-workflow.json) — each step must have exactly one of `uses` or `run`, and `shell`/`working-directory` are only valid with `run`.
8488

89+
### Default Allowed Tools
90+
91+
Every generated workflow automatically allows a set of safe, non-mutating tools via `--allowedTools`. These are passed as CLI args to Claude and do not need to be specified in your spec:
92+
93+
| Category | Tools |
94+
|---|---|
95+
| File reading / search | `Read`, `Grep`, `Glob` |
96+
| Subagents & task management | `Agent`, `Skill`, `TaskCreate`, `TaskGet`, `TaskList`, `TaskOutput`, `TaskStop`, `TaskUpdate`, `TodoWrite` |
97+
| Worktree management | `EnterWorktree`, `ExitWorktree` |
98+
| Scheduling | `CronCreate`, `CronDelete`, `CronList` |
99+
| Tool discovery | `ToolSearch` |
100+
| Code intelligence | `LSP` |
101+
| MCP resources | `ListMcpResourcesTool`, `ReadMcpResourceTool` |
102+
| Plan mode | `EnterPlanMode`, `ExitPlanMode` |
103+
| Scoped Bash | `Bash(git diff*)`, `Bash(git log*)`, `Bash(git show*)`, `Bash(gh pr *)` |
104+
105+
To allow additional tools (e.g. mutation tools like `Edit`, `Write`, or scoped `Bash` patterns), use `action.allowed_tools` in your spec. Duplicates with the defaults are automatically removed.
106+
85107
## CLI Usage
86108

87109
```bash
@@ -175,3 +197,4 @@ src/
175197
- **Strict schemas**: The root spec and `action` block use strict Zod schemas (reject unknown keys) to catch typos. Pre/post steps are validated against the GHA step spec from SchemaStore.
176198
- **`secrets: inherit`**: Reusable workflows get secrets from the dispatcher, keeping configuration simple.
177199
- **Hardcoded permissions**: Reusable workflows set `contents: write`, `pull-requests: write`, `issues: write`, `id-token: write` — what claude-code-action needs.
200+
- **Default allowed tools**: Every workflow gets a safe baseline set of non-mutating tools. Users can extend this via `action.allowed_tools` without needing to re-specify the defaults.

src/generator/workflow.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,74 @@ function stepToYaml(step: GhaStep): Record<string, unknown> {
3030
/**
3131
* Builds the claude-code-action step.
3232
*/
33+
/**
34+
* Default tools that are always allowed. These are all safe, non-mutating
35+
* tools (no permission required) plus scoped Bash patterns for git/gh.
36+
*/
37+
const DEFAULT_ALLOWED_TOOLS = [
38+
// File reading / search
39+
"Read",
40+
"Grep",
41+
"Glob",
42+
// Subagents & task management
43+
"Agent",
44+
"Skill",
45+
"TaskCreate",
46+
"TaskGet",
47+
"TaskList",
48+
"TaskOutput",
49+
"TaskStop",
50+
"TaskUpdate",
51+
"TodoWrite",
52+
// Worktree management
53+
"EnterWorktree",
54+
"ExitWorktree",
55+
// Scheduling
56+
"CronCreate",
57+
"CronDelete",
58+
"CronList",
59+
// Tool discovery
60+
"ToolSearch",
61+
// Code intelligence
62+
"LSP",
63+
// MCP resources
64+
"ListMcpResourcesTool",
65+
"ReadMcpResourceTool",
66+
// Plan mode
67+
"EnterPlanMode",
68+
"ExitPlanMode",
69+
// Scoped Bash for git/gh
70+
"Bash(git diff*)",
71+
"Bash(git log*)",
72+
"Bash(git show*)",
73+
"Bash(gh pr *)",
74+
];
75+
3376
function buildActionStep(
3477
prompt: string,
3578
systemPrompt: string | null,
3679
options: {
3780
claude_args?: string;
3881
plugins?: string;
82+
allowed_tools?: string[];
3983
actionVersion: string;
4084
}
4185
): Record<string, unknown> {
86+
const allowedTools = [
87+
...new Set([...DEFAULT_ALLOWED_TOOLS, ...(options.allowed_tools || [])]),
88+
];
89+
4290
const withBlock: Record<string, unknown> = {
4391
anthropic_api_key: "${{ secrets.ANTHROPIC_API_KEY }}",
44-
use_sticky_comment: true,
92+
github_token: "${{ secrets.GITHUB_TOKEN }}",
93+
track_progress: true,
4594
prompt,
4695
};
4796

48-
let claudeArgs = options.claude_args || "";
97+
const toolArgs = allowedTools.map((t) => `--allowedTools '${t}'`).join(" ");
98+
let claudeArgs = options.claude_args
99+
? `${options.claude_args} ${toolArgs}`
100+
: toolArgs;
49101

50102
if (systemPrompt) {
51103
const escaped = systemPrompt.replace(/'/g, "'\\''");
@@ -98,6 +150,7 @@ export function generateWorkflow(
98150
buildActionStep(spec.spec.action.prompt, systemPrompt, {
99151
claude_args: spec.spec.action.claude_args,
100152
plugins: spec.spec.action.plugins,
153+
allowed_tools: spec.spec.action.allowed_tools,
101154
actionVersion,
102155
})
103156
);

src/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const ActionSchema = z
2929
prompt: z.string().min(1, "Action prompt cannot be empty"),
3030
claude_args: z.string().optional(),
3131
plugins: z.string().optional(),
32+
allowed_tools: z.array(z.string()).optional(),
3233
})
3334
.strict();
3435

test/generator/workflow.test.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe("generateWorkflow", () => {
4444
expect(steps[0].uses).toBe("actions/checkout@v4");
4545
});
4646

47-
test("includes claude-code-action step", () => {
47+
test("includes claude-code-action step with required fields", () => {
4848
const result = generateWorkflow(minimalSpec, null);
4949
const jobs = result.content.jobs as any;
5050
const steps = jobs.task.steps;
@@ -53,6 +53,77 @@ describe("generateWorkflow", () => {
5353
);
5454
expect(actionStep).toBeTruthy();
5555
expect(actionStep.with.prompt).toBe("Review this code");
56+
expect(actionStep.with.github_token).toBe("${{ secrets.GITHUB_TOKEN }}");
57+
expect(actionStep.with.track_progress).toBe(true);
58+
// default allowed tools passed via claude_args
59+
const args: string = actionStep.with.claude_args;
60+
for (const tool of [
61+
"Read", "Grep", "Glob", "Agent", "Skill",
62+
"TaskCreate", "TaskGet", "TaskList", "TaskOutput", "TaskStop", "TaskUpdate",
63+
"TodoWrite", "EnterWorktree", "ExitWorktree",
64+
"CronCreate", "CronDelete", "CronList",
65+
"ToolSearch", "LSP", "ListMcpResourcesTool", "ReadMcpResourceTool",
66+
"EnterPlanMode", "ExitPlanMode",
67+
]) {
68+
expect(args).toContain(`--allowedTools '${tool}'`);
69+
}
70+
expect(args).toContain("--allowedTools 'Bash(git diff*)'");
71+
expect(args).toContain("--allowedTools 'Bash(git log*)'");
72+
expect(args).toContain("--allowedTools 'Bash(git show*)'");
73+
expect(args).toContain("--allowedTools 'Bash(gh pr *)'");
74+
});
75+
76+
test("merges user-specified allowed_tools with defaults", () => {
77+
const spec: ValidatedSpec = {
78+
filename: "test.yml",
79+
taskName: "test",
80+
spec: {
81+
name: "test",
82+
action: {
83+
prompt: "Test",
84+
allowed_tools: ["Edit", "Write", "Bash(npm test*)"],
85+
},
86+
},
87+
};
88+
const result = generateWorkflow(spec, null);
89+
const jobs = result.content.jobs as any;
90+
const actionStep = jobs.task.steps.find(
91+
(s: any) => s.uses === "anthropics/claude-code-action@v1"
92+
);
93+
const args: string = actionStep.with.claude_args;
94+
// defaults still present
95+
expect(args).toContain("--allowedTools 'Read'");
96+
expect(args).toContain("--allowedTools 'Agent'");
97+
// user tools appended
98+
expect(args).toContain("--allowedTools 'Edit'");
99+
expect(args).toContain("--allowedTools 'Write'");
100+
expect(args).toContain("--allowedTools 'Bash(npm test*)'");
101+
});
102+
103+
test("deduplicates user-specified tools that overlap with defaults", () => {
104+
const spec: ValidatedSpec = {
105+
filename: "test.yml",
106+
taskName: "test",
107+
spec: {
108+
name: "test",
109+
action: {
110+
prompt: "Test",
111+
allowed_tools: ["Read", "Grep", "Edit"],
112+
},
113+
},
114+
};
115+
const result = generateWorkflow(spec, null);
116+
const jobs = result.content.jobs as any;
117+
const actionStep = jobs.task.steps.find(
118+
(s: any) => s.uses === "anthropics/claude-code-action@v1"
119+
);
120+
const args: string = actionStep.with.claude_args;
121+
const readMatches = args.match(/--allowedTools 'Read'/g);
122+
expect(readMatches).toHaveLength(1);
123+
const grepMatches = args.match(/--allowedTools 'Grep'/g);
124+
expect(grepMatches).toHaveLength(1);
125+
// non-default tool still added
126+
expect(args).toContain("--allowedTools 'Edit'");
56127
});
57128

58129
test("passes system prompt via --append-system-prompt", () => {
@@ -62,7 +133,8 @@ describe("generateWorkflow", () => {
62133
(s: any) => s.uses === "anthropics/claude-code-action@v1"
63134
);
64135
expect(actionStep.with.prompt).toBe("Review this code");
65-
expect(actionStep.with.claude_args).toBe("--append-system-prompt 'Be helpful.'");
136+
expect(actionStep.with.claude_args).toContain("--append-system-prompt 'Be helpful.'");
137+
expect(actionStep.with.claude_args).toContain("--allowedTools");
66138
});
67139

68140
test("appends system prompt to existing claude_args", () => {
@@ -79,7 +151,9 @@ describe("generateWorkflow", () => {
79151
const actionStep = jobs.task.steps.find(
80152
(s: any) => s.uses === "anthropics/claude-code-action@v1"
81153
);
82-
expect(actionStep.with.claude_args).toBe("--timeout 600 --append-system-prompt 'Be helpful.'");
154+
expect(actionStep.with.claude_args).toContain("--timeout 600");
155+
expect(actionStep.with.claude_args).toContain("--append-system-prompt 'Be helpful.'");
156+
expect(actionStep.with.claude_args).toContain("--allowedTools");
83157
});
84158

85159
test("includes pre and post action steps", () => {
@@ -136,7 +210,8 @@ describe("generateWorkflow", () => {
136210
const actionStep = jobs.task.steps.find(
137211
(s: any) => s.uses === "anthropics/claude-code-action@v1"
138212
);
139-
expect(actionStep.with.claude_args).toBe("--timeout 600");
213+
expect(actionStep.with.claude_args).toContain("--timeout 600");
214+
expect(actionStep.with.claude_args).toContain("--allowedTools");
140215
expect(actionStep.with.plugins).toBe("my-plugin");
141216
});
142217
});

test/schema.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ describe("ActionSchema", () => {
3737
});
3838
expect(result.success).toBe(true);
3939
});
40+
41+
test("accepts allowed_tools as string array", () => {
42+
const result = ActionSchema.safeParse({
43+
prompt: "hello",
44+
allowed_tools: ["Edit", "Write", "Bash(npm test*)"],
45+
});
46+
expect(result.success).toBe(true);
47+
});
48+
49+
test("rejects allowed_tools as non-array", () => {
50+
const result = ActionSchema.safeParse({
51+
prompt: "hello",
52+
allowed_tools: "Edit",
53+
});
54+
expect(result.success).toBe(false);
55+
});
4056
});
4157

4258
describe("GhaStepSchema", () => {

0 commit comments

Comments
 (0)