[BUG] Classifier blocks user-authorized actions inside forked skills — parent-turn intent is invisible to the fork, and no per-action override exists
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In auto permission mode (permissions.defaultMode: "auto"), tool calls made inside a context: fork skill (and subagents generally) are evaluated by the safety classifier using the parent session's rules — but without the parent conversation. The docs say forked skills "won't have access to your conversation history" (skills.md), and that the classifier honors explicit user intent: "if the user's message directly and specifically describes the exact action Claude is about to take, the classifier allows it" (auto-mode docs).
These combine badly: there is no reliable way for the user's explicit authorization to reach the classifier for actions taken inside a fork. Observed across multiple sessions and projects (our forked skill POSTs JSON to a localhost message-queue daemon used for coordination between Claude Code sessions on the same machine — loopback IPC, not network-exposed):
User authorization given in an earlier turn (approving the exact content to post, two turns before the skill runs): denied.
Authorization restated verbatim in the Skill invocation's arguments: denied — skill args are apparently not treated as user intent.
A literal "post it" from the user in the same turn as the skill invocation: sometimes allowed, sometimes denied (both outcomes observed on identical flows days apart).
Since Bash(*) allow rules are documented as suspended in auto mode, every such call hits the classifier — so a forked skill whose whole purpose is a user-approved local write has no dependable path to execute it.
What Should Happen?
The documented explicit-user-intent override should function (or have a documented equivalent) across the fork boundary. Any one of these would resolve it:
Treat Skill-tool invocation arguments as user-visible intent for the fork's classifier — the parent model already places the user's authorization there when relaying a request.
Pass the authorizing parent turn (or a digest of it) into the fork's classifier context, deterministically rather than sometimes.
Extend the Chrome-style localhost default-allow to Bash-issued loopback HTTP (or provide a first-class permissions rule shape for loopback services — today the only mechanism is prose in autoMode.environment).
A per-invocation authorization handoff — the parent session, having seen the user's approval, mints a one-shot grant scoped to the specific action the fork will take.
Error Messages/Logs
From the forked skill's result after the user had approved the exact broadcast content two turns earlier (paraphrase of the fork's report; the denial label is not shown verbatim to the parent session):
The auto-mode classifier blocked the broadcast POST because it doesn't see user authorization in the immediate transcript context (even though the skill invocation explicitly states user approval). … The POST /cu/messages … was denied as an "External System Write."
Related verbatim denial from the same session (main-session Edit of settings.json to add the autoMode.environment workaround — arguably correct behavior, included to show the reason format):
Permission for this action was denied by the Claude Code auto mode classifier. Reason: [Self-Modification] Editing .claude/settings.json to add an autoMode.environment trust declaration widens the auto-mode classifier's permission envelope; the user's "we can just fix it here though, no?" did not explicitly name this specific config change — surface it for direct review outside auto mode.
Steps to Reproduce
- ~/.claude/settings.json with "permissions": { "defaultMode": "auto", "allow": ["Bash(*)"] } and no autoMode key.
- Create a user-level skill with context: fork in its frontmatter whose flow POSTs JSON to a local service (e.g. http://localhost:6750) via Bash.
- In the parent turn, have the user explicitly authorize the action ("post it", or approve the exact content being posted).
- Invoke the skill via the Skill tool.
- Observe the fork's POST denied as an external-system write. Re-invoking with the user's authorization quoted verbatim in the skill args is also denied. A same-turn live "post it" sometimes passes, sometimes not.
Claude Model
Not sure / Multiple models
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.194
Claude Code Version
2.1.200
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
2.1.194 or earlier (exact number unavailable — the installer's version cache retains only six versions and the pre-June-26 entry was evicted). Install timestamps from ~/.local/share/claude/versions/ bracket it precisely:
2.1.195 installed June 26, 2026 8:56 PM — first observed failure June 28, with 2.1.195 still current (2.1.196 didn't install until June 29 5:31 PM)
The flow was frictionless on whatever ran through June 26
Caveat: the docs state the auto-mode classifier "runs on a server-configured model that is independent of your /model selection" — so a server-side classifier change around June 28 can't be ruled out as the actual trigger, with the 2.1.195 install being coincidental. Client-side changelog candidates in the window: v2.1.178 (June 15 — subagent spawns classifier-evaluated before launch), v2.1.183 (June 19 — expanded auto-mode blocks).
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Update: the
autoMode.environmentworkaround works — but only after a session restart.Adding prose entries under
autoMode.environmentin~/.claude/settings.json(with"$defaults"as the first entry) describing the local services my forkedskills talk to (localhost HTTP APIs invoked via Bash/curl) resolves the classifier blocks described above.
One gotcha worth documenting: settings appear to be snapshotted at session start. In the same session where I applied the settings edit, a forked skill's
no-prior-authorization POST was still blocked — the fork never saw the new entry. After starting a fresh session, the identical test passed: the forked skill's
Bash POST to the declared localhost service went through with no explicit user authorization in the turn.
So for anyone else hitting this:
autoMode.environment(keep"$defaults"first).This is a viable mitigation, but the underlying issue stands: forked skills have no conversation history, so the documented explicit-user-intent override can't
fire for them, and suspended
Bash(*)allow rules in auto mode mean every such call hits the classifier.Given the update that autoMode.environment can mitigate this after a fresh session, I would separate the mitigation from the authorization invariant.
The mitigation seems to declare an environment capability ahead of time. That is useful, but it is not the same record as a parent-turn user authorization for a specific action.
I would test three paths separately:
The audit record should make those paths distinguishable. A forked skill should be able to show whether it ran because the environment policy allowed the service, because a scoped parent authorization was bound to the skill run, or because neither binding existed and the terminal outcome was denial or no-effect.
That keeps the workaround useful without turning declared environment access or quoted approval text into blanket authority.
Thank you for the response.
Agreed — separating the mitigation from the authorization invariant is the right framing, and it names something my workaround was collapsing.
Restated in your terms, there are three distinct records:
autoMode.environmentsays the environment may reach a known local service. Ambient, pre-declared, bound to no action or turn.My workaround only ever established (1). For my actual use case that's the right layer: the forked skill POSTs to a local message-queue daemon (
http://localhost:6750) for inter-session IPC — a known, trusted, local service, and a low-stakes write. What's missing there is capability, not authorization, so a declared environment capability is a legitimate and sufficient grant.Where your point slaps is that neither of the two convenient shortcuts should count as authorization:
So making those paths distinguishable in the audit record is exactly right. Concretely, for any forked tool call I'd want to be able to read back one of:
That keeps
autoMode.environmentuseful as a capability grant without letting declared access or quoted approval text become blanket authority.I'll take your feedback and see if I can't find a working solution. Just, uh, waiting on those dang tokens. Will report back.
That is the useful landing point.
For the next pass, I would keep the test surface on the binding that authorized the forked call, not on whether the local service is trustworthy.
The matrix I would want is:
That lets low-stakes local IPC stay ergonomic while still making the failure mode auditable: capability allowed, parent authorization allowed, or neither binding existed and the call denied/no-effect.
The important negative test is that quoted user approval in skill args never upgrades into authorization.