-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: respect --allowedTools when building disallowed tools list #1033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,8 +84,11 @@ describe("Agent Mode", () => { | |
| githubToken: "test-token", | ||
| }); | ||
|
|
||
| // Verify claude_args includes user args (no MCP config in agent mode without allowed tools) | ||
| expect(result.claudeArgs).toBe("--model claude-sonnet-4 --max-turns 10"); | ||
| // Verify claude_args includes disallowed tools and user args | ||
| expect(result.claudeArgs).toContain("--disallowedTools"); | ||
| expect(result.claudeArgs).toContain("WebSearch"); | ||
| expect(result.claudeArgs).toContain("WebFetch"); | ||
| expect(result.claudeArgs).toContain("--model claude-sonnet-4 --max-turns 10"); | ||
| expect(result.claudeArgs).not.toContain("--mcp-config"); | ||
|
|
||
| // Verify return structure - should use "main" as fallback when no env vars set | ||
|
|
@@ -97,7 +100,7 @@ describe("Agent Mode", () => { | |
| claudeBranch: undefined, | ||
| }, | ||
| mcpConfig: expect.any(String), | ||
| claudeArgs: "--model claude-sonnet-4 --max-turns 10", | ||
| claudeArgs: expect.stringContaining("--disallowedTools"), | ||
| }); | ||
|
|
||
| // Clean up | ||
|
|
@@ -203,4 +206,103 @@ describe("Agent Mode", () => { | |
| // Should be empty or just whitespace when no MCP servers are included | ||
| expect(result.claudeArgs).not.toContain("--mcp-config"); | ||
| }); | ||
|
|
||
|
|
||
| test("--allowedTools WebSearch removes WebSearch from disallowed list", async () => { | ||
| const context = createMockAutomationContext({ | ||
| eventName: "workflow_dispatch", | ||
| }); | ||
|
|
||
| const originalHeadRef = process.env.GITHUB_HEAD_REF; | ||
| const originalRefName = process.env.GITHUB_REF_NAME; | ||
| delete process.env.GITHUB_HEAD_REF; | ||
| delete process.env.GITHUB_REF_NAME; | ||
|
|
||
| // Set CLAUDE_ARGS with --allowedTools WebSearch | ||
| process.env.CLAUDE_ARGS = '--allowedTools "WebSearch"'; | ||
|
|
||
| const mockOctokit = { | ||
| rest: { | ||
| users: { | ||
| getAuthenticated: mock(() => | ||
| Promise.resolve({ | ||
| data: { login: "test-user", id: 12345, type: "User" }, | ||
| }), | ||
| ), | ||
| getByUsername: mock(() => | ||
| Promise.resolve({ | ||
| data: { login: "test-user", id: 12345, type: "User" }, | ||
| }), | ||
| ), | ||
| }, | ||
| }, | ||
| } as any; | ||
|
|
||
| const result = await prepareAgentMode({ | ||
| context, | ||
| octokit: mockOctokit, | ||
| githubToken: "test-token", | ||
| }); | ||
|
|
||
| // WebSearch should NOT be in disallowed tools since user explicitly allowed it | ||
| // WebFetch should still be disallowed | ||
| expect(result.claudeArgs).toContain("--disallowedTools"); | ||
| expect(result.claudeArgs).not.toMatch(/--disallowedTools[^"]*WebSearch/); | ||
| expect(result.claudeArgs).toContain("WebFetch"); | ||
|
|
||
| // Clean up | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The regex Extended reasoning...What the bug isThe test "--allowedTools WebSearch removes WebSearch from disallowed list" uses a regex assertion to verify that WebSearch is not present in the expect(result.claudeArgs).not.toMatch(/--disallowedTools[^"]*WebSearch/);The regex claudeArgs = `${claudeArgs} --disallowedTools "${disallowedTools}"`;So the actual string looks like Step-by-step proofConsider the scenario where the code is broken and WebSearch is incorrectly included, producing:
The test passes even though the implementation is wrong. The assertion provides zero verification of the test's stated purpose. ImpactThe test cannot catch regressions where FixReplace the regex with one that looks inside the quoted value: expect(result.claudeArgs).not.toMatch(/--disallowedTools\s+"[^"]*WebSearch/);This matches |
||
| delete process.env.CLAUDE_ARGS; | ||
| if (originalHeadRef !== undefined) | ||
| process.env.GITHUB_HEAD_REF = originalHeadRef; | ||
| if (originalRefName !== undefined) | ||
| process.env.GITHUB_REF_NAME = originalRefName; | ||
| }); | ||
|
|
||
| test("--allowedTools WebSearch,WebFetch removes both from disallowed list", async () => { | ||
| const context = createMockAutomationContext({ | ||
| eventName: "workflow_dispatch", | ||
| }); | ||
|
|
||
| const originalHeadRef = process.env.GITHUB_HEAD_REF; | ||
| const originalRefName = process.env.GITHUB_REF_NAME; | ||
| delete process.env.GITHUB_HEAD_REF; | ||
| delete process.env.GITHUB_REF_NAME; | ||
|
|
||
| // Set CLAUDE_ARGS with both tools allowed | ||
| process.env.CLAUDE_ARGS = '--allowedTools "WebSearch,WebFetch"'; | ||
|
|
||
| const mockOctokit = { | ||
| rest: { | ||
| users: { | ||
| getAuthenticated: mock(() => | ||
| Promise.resolve({ | ||
| data: { login: "test-user", id: 12345, type: "User" }, | ||
| }), | ||
| ), | ||
| getByUsername: mock(() => | ||
| Promise.resolve({ | ||
| data: { login: "test-user", id: 12345, type: "User" }, | ||
| }), | ||
| ), | ||
| }, | ||
| }, | ||
| } as any; | ||
|
|
||
| const result = await prepareAgentMode({ | ||
| context, | ||
| octokit: mockOctokit, | ||
| githubToken: "test-token", | ||
| }); | ||
|
|
||
| // Neither WebSearch nor WebFetch should be in disallowed tools | ||
| // So --disallowedTools should not be present at all | ||
| expect(result.claudeArgs).not.toContain("--disallowedTools"); | ||
|
|
||
| // Clean up | ||
| delete process.env.CLAUDE_ARGS; | ||
| if (originalHeadRef !== undefined) | ||
| process.env.GITHUB_HEAD_REF = originalHeadRef; | ||
| if (originalRefName !== undefined) | ||
| process.env.GITHUB_REF_NAME = originalRefName; | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴
parseAllowedToolsonly captures the first value when users use space-separated syntax (--allowedTools "WebSearch" "WebFetch"), but the downstreamparseClaudeArgsToExtraArgscorrectly captures all values via its accumulating flag logic. This causes the missed tools to appear in bothallowedToolsanddisallowedToolssimultaneously, sincebuildDisallowedToolsStringuses the incomplete parse to decide what to disallow. The comma-separated and repeated-flag syntaxes work correctly; only the space-separated multi-value format triggers this conflict.Extended reasoning...
Bug Analysis
parseAllowedToolsinparse-tools.tsuses regex patterns that each require the--allowedTools(or--allowed-tools) prefix immediately before each value. For the input--allowedTools "WebSearch" "WebFetch", the double-quoted regex matches only--allowedTools "WebSearch"because"WebFetch"stands alone without a preceding--allowedToolsflag. The same issue affects unquoted space-separated values.Downstream Conflict
The downstream parser
parseClaudeArgsToExtraArgsinbase-action/src/parse-sdk-options.tshas explicit support for this syntax. Lines 107-114 show that forACCUMULATING_FLAGS(which includesallowedToolsanddisallowedTools), it consumes all consecutive non-flag values after the flag. So for the same input, it correctly captures bothWebSearchandWebFetch.Step-by-Step Proof
CLAUDE_ARGS='--allowedTools "WebSearch" "WebFetch"'parseAllowedToolsreturns["WebSearch"](missesWebFetch)buildDisallowedToolsString([], ["WebSearch"])returns"WebFetch"(WebFetch stays disallowed since it was not detected as allowed)claudeArgsbecomes:--disallowedTools "WebFetch" --allowedTools "WebSearch" "WebFetch"parseClaudeArgsToExtraArgscorrectly parses the accumulating flags and producesdisallowedTools: ["WebFetch"]ANDallowedTools: ["WebSearch", "WebFetch"]parse-sdk-options.ts, bothmergedAllowedToolsandmergedDisallowedToolsare returned as-is with no conflict resolution, soWebFetchends up in both arrays simultaneouslyImpact
The behavior when a tool is in both
allowedToolsanddisallowedToolsis undefined and depends on the SDK implementation. The user explicitly asked forWebFetchto be allowed, but the system silently adds it to the disallowed list too, potentially overriding user intent. Before this PR, agent mode had no--disallowedToolsat all, so this parsing discrepancy was harmless.Fix
parseAllowedToolsshould be updated to also consume consecutive non-flag quoted or unquoted values after--allowedTools, matching the accumulating behavior ofparseClaudeArgsToExtraArgs. Alternatively, the function could use the same shell-quote + iteration approach thatparseClaudeArgsToExtraArgsuses. The PR tests only cover comma-separated and single-value syntaxes, so a test for space-separated multi-value should be added as well.