Repository navigation
fix: respect --allowedTools when building disallowed tools list - #1033
MaxwellCalkin wants to merge 1 commit into
Conversation
In agent mode, WebSearch and WebFetch are now properly disabled by default via --disallowedTools, consistent with tag mode's security posture. When users explicitly allow these tools via --allowedTools in claude_args, they are removed from the disallowed list. Fixes anthropics#690 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
It would be great to move this PR forward. |
|
@claude review |
| // Disable WebSearch and WebFetch by default for security, but respect | ||
| // user's --allowedTools from claude_args (e.g., --allowedTools WebSearch | ||
| // removes WebSearch from the disallowed list) | ||
| const disallowedTools = buildDisallowedToolsString([], allowedTools); |
There was a problem hiding this comment.
🔴 parseAllowedTools only captures the first value when users use space-separated syntax (--allowedTools "WebSearch" "WebFetch"), but the downstream parseClaudeArgsToExtraArgs correctly captures all values via its accumulating flag logic. This causes the missed tools to appear in both allowedTools and disallowedTools simultaneously, since buildDisallowedToolsString uses 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
parseAllowedTools in parse-tools.ts uses 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 --allowedTools flag. The same issue affects unquoted space-separated values.
Downstream Conflict
The downstream parser parseClaudeArgsToExtraArgs in base-action/src/parse-sdk-options.ts has explicit support for this syntax. Lines 107-114 show that for ACCUMULATING_FLAGS (which includes allowedTools and disallowedTools), it consumes all consecutive non-flag values after the flag. So for the same input, it correctly captures both WebSearch and WebFetch.
Step-by-Step Proof
- User sets
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"]- At lines 241-244 of
parse-sdk-options.ts, bothmergedAllowedToolsandmergedDisallowedToolsare returned as-is with no conflict resolution, soWebFetchends up in both arrays simultaneously
Impact
The behavior when a tool is in both allowedTools and disallowedTools is undefined and depends on the SDK implementation. The user explicitly asked for WebFetch to be allowed, but the system silently adds it to the disallowed list too, potentially overriding user intent. Before this PR, agent mode had no --disallowedTools at all, so this parsing discrepancy was harmless.
Fix
parseAllowedTools should be updated to also consume consecutive non-flag quoted or unquoted values after --allowedTools, matching the accumulating behavior of parseClaudeArgsToExtraArgs. Alternatively, the function could use the same shell-quote + iteration approach that parseClaudeArgsToExtraArgs uses. The PR tests only cover comma-separated and single-value syntaxes, so a test for space-separated multi-value should be added as well.
| expect(result.claudeArgs).not.toMatch(/--disallowedTools[^"]*WebSearch/); | ||
| expect(result.claudeArgs).toContain("WebFetch"); | ||
|
|
||
| // Clean up |
There was a problem hiding this comment.
🟡 The regex /--disallowedTools[^"]*WebSearch/ on line 253 is vacuous — [^"]* stops at the opening quote in --disallowedTools "WebFetch" and can never reach the tool name inside, so not.toMatch(...) always passes regardless of whether WebSearch was actually removed. Fix: use /--disallowedTools\s+"[^"]*WebSearch/ to match inside the quoted value.
Extended reasoning...
What the bug is
The test "--allowedTools WebSearch removes WebSearch from disallowed list" uses a regex assertion to verify that WebSearch is not present in the --disallowedTools value:
expect(result.claudeArgs).not.toMatch(/--disallowedTools[^"]*WebSearch/);The regex /--disallowedTools[^"]*WebSearch/ matches the literal --disallowedTools, then [^"]* matches zero or more non-quote characters, then looks for WebSearch. However, the production code on line 127 of src/modes/agent/index.ts constructs the string as:
claudeArgs = `${claudeArgs} --disallowedTools "${disallowedTools}"`;So the actual string looks like --disallowedTools "WebFetch". The [^"]* character class matches only the space between --disallowedTools and the opening ", then stops because " is excluded from the character class. It can never reach inside the quoted value.
Step-by-step proof
Consider the scenario where the code is broken and WebSearch is incorrectly included, producing: --disallowedTools "WebSearch,WebFetch".
- The regex engine matches
--disallowedToolsat the start. [^"]*starts matching — it matches the space(not a quote), then encounters"— stops.- The engine now expects
WebSearchbut finds"— no match. not.toMatch(...)passes because the regex didn't match.
The test passes even though the implementation is wrong. The assertion provides zero verification of the test's stated purpose.
Impact
The test cannot catch regressions where buildDisallowedToolsString fails to remove WebSearch from the disallowed list. The other assertions in the test (toContain("--disallowedTools") and toContain("WebFetch")) provide partial coverage of the output format but do not verify the core behavior being tested — that WebSearch was excluded from the disallowed tools value.
Fix
Replace the regex with one that looks inside the quoted value:
expect(result.claudeArgs).not.toMatch(/--disallowedTools\s+"[^"]*WebSearch/);This matches --disallowedTools, then whitespace, then the opening quote, then scans inside the quoted value for WebSearch.
Summary
Fixes #690
WebSearchandWebFetchwere not being explicitly disabled via--disallowedTools, inconsistent with tag mode's security behavior--allowedTools WebSearchinclaude_args, it had no effect on the disallowed tools list becausebuildDisallowedToolsString()was never called in agent modebuildDisallowedToolsString([], allowedTools)inprepareAgentMode()so that WebSearch/WebFetch are disabled by default, but respected when the user explicitly allows themChanges
src/modes/agent/index.ts: Import and callbuildDisallowedToolsString()with the parsedallowedToolsfromclaude_args, adding--disallowedToolstoclaudeArgswhen tools need to be disabledtest/modes/agent.test.ts: Updated existing test and added two new tests:--allowedTools WebSearchremoves only WebSearch from the disallowed list (WebFetch remains)--allowedTools WebSearch,WebFetchremoves both, resulting in no--disallowedToolsflagTest plan
--allowedTools WebSearchremoves WebSearch from disallowed list--allowedTools WebSearch,WebFetchremoves both from disallowed listclaude_args: '--allowedTools "WebSearch"'and confirm WebSearch is available🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com