[BUG] Classifier blocks user-authorized actions inside forked skills — parent-turn intent is invisible to the fork, and no per-action override exists

Status Fixed / completed
Reported on v2.1.200
Maintainer reply None cached
Activity 6 comments · opened Jul 4, 2026 · closed Aug 17, 2026

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

  1. ~/.claude/settings.json with "permissions": { "defaultMode": "auto", "allow": ["Bash(*)"] } and no autoMode key.
  2. 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.
  3. In the parent turn, have the user explicitly authorize the action ("post it", or approve the exact content being posted).
  4. Invoke the skill via the Skill tool.
  5. 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).

View original on GitHub ↗

4 Comments

cubicleDowns · 1 month ago

Update: the autoMode.environment workaround works — but only after a session restart.

Adding prose entries under autoMode.environment in ~/.claude/settings.json (with "$defaults" as the first entry) describing the local services my forked
skills 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:

  1. Add your local services to autoMode.environment (keep "$defaults" first).
  2. Restart the session before testing — verifying in the session that made the edit gives a false negative.

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.

rpelevin · 1 month ago

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:

  1. declared environment capability: the fork can use a known local service after restart
  2. parent-turn authorization: the user approves a specific forked-skill action in the current turn
  3. stale or replayed authority: old approval text, copied args, or a pre-restart setting snapshot cannot expand scope

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.

cubicleDowns · 1 month ago

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:

  1. Declared environment capabilityautoMode.environment says the environment may reach a known local service. Ambient, pre-declared, bound to no action or turn.
  2. Scoped parent-turn authorization — the user approved this specific forked-skill action in the current turn, bound to the run.
  3. Stale/replayed authority — prior approval text, copied args, or a pre-restart settings snapshot. Must not expand scope; the correct outcome is denial / no-effect.

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:

  • the environment declaration is a capability, not a standing approval for arbitrary actions, and
  • restating the user's approval verbatim in the skill's args is replayed text, it shouldn't carry scope-expanding weight.

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:

  • allowed via declared environment capability (and which capability),
  • allowed via scoped parent-turn authorization (bound to which skill run / which user message), or
  • neither binding present → denied / no-effect.

That keeps autoMode.environment useful 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.

rpelevin · 1 month ago

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:

  • declared environment capability allows only the named local service after the snapshot/restart boundary
  • scoped parent-turn authorization is bound to the concrete forked skill run and action
  • copied approval text or pre-restart settings do not become authority
  • every path emits the same small receipt fields: capability_ref or authorization_ref, skill_run_id, action/input digest, policy snapshot id, decision, and terminal outcome

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.

Showing cached comments. Read the full discussion on GitHub ↗