[Bug] Auto mode refuses a plugin's $.tool.call unless an allow rule matches it
What happens
In auto mode, a plugin's $.tool.call({ tool: 'Bash', ... }) is refused unless a permission allow rule matches the command. Without one, it fails with:
The server-side auto mode classifier gave no verdict for Bash: the request that produced this action did not ask for one. Issue the action again once, as-is; if it is denied again, continue with other tasks that don't require it and tell the user that auto mode could not evaluate it.
The same command sent by Claude itself is decided by the classifier as usual. In default mode, the plugin's call shows the normal permission dialog, or runs under an allow rule such as Bash(npm test:*).
So a plugin tool that runs commands through $.tool.call, to keep them under the Bash permission check, works in auto mode only for commands the user has already allowed.
Expected
Auto mode's classifier decides a plugin's nested tool call when no rule does, as the dialog does in default mode. Or the plugin API gives the plugin a way to handle auto mode itself.
Repro
A plugin with one tool that runs npm test:
.claude-plugin/plugin.json
{ "name": "repro", "version": "0.1.0", "description": "A tool that runs npm test through $.tool.call" }
hooks/hooks.json
{ "modules": ["./register.ts"] }
hooks/register.ts
import type { Register } from 'claude-code'
export const register: Register = on => {
on('session.start', async ($, e, next) => {
const r = await next(e)
await $.tool.register({
name: 'run_tests',
description: 'Runs npm test and returns the result.',
inputSchema: { type: 'object', properties: {} },
})
return r
})
on('tool.call', { tool: 'mcp__repro__run_tests' as never }, async $ => {
const ran = await $.tool.call({ tool: 'Bash', command: 'npm test', description: 'Run tests' })
return { result: typeof ran.deny === 'string' ? `denied: ${ran.deny}` : `ran: ${ran.text}` }
})
}
In a project with an npm test script:
echo "Call the run_tests tool once, and quote exactly what it returned." | \
CLAUDE_CODE_PLUGIN_DIRS=/path/to/repro claude -p --permission-mode auto
The tool returns denied: The server-side auto mode classifier gave no verdict for Bash…, on both of Claude's tries.
With --allowedTools "Bash(npm test:*)" added, it returns ran: and the test output, in auto mode and in default mode.
What the plugin cannot do about it
- The plugin API does not expose the session's permission mode.
- A
tool.checkhook'snext(e)gives the same verdict for the nested call in auto and default mode:{ decision: 'ask', reason: 'This command requires approval' }. - A
tool.checkhook on the plugin's own registered tool did not fire in our tests, so the plugin cannot send its own tool call to the classifier instead. - In
claude -p, noauto_modeattachment reachesprompt.attachment, so the plugin cannot infer the mode from it.
Our workaround is to have Claude run the command itself in Bash when the plugin's call is refused, which loses what the plugin tool adds.
Related
#99214 reports the same refusal for a mod's $.mcp.call from a command.run hook. There, an allow rule did not help.
Possible fixes
- Classify a plugin's nested call in auto mode, with the plugin tool call that caused it as context.
- Or send the plugin's own tool call to the mode's decider, so the classifier sees its input, and let the nested calls it makes for that input inherit the verdict.
- Or expose the permission mode to plugins, so a plugin can choose another path in auto mode.
Environment
Claude Code 2.1.294 on macOS. The repro above uses claude -p. A larger plugin that does the same thing fails the same way in an interactive session.