A skill's unresolvable `agent:` silently falls back to `general-purpose`, widening its toolset

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 4 comments · opened Aug 1, 2026

Version: Claude Code 2.1.220
Platform: macOS (Darwin 25.4.0)

Summary

When a plugin skill declares context: fork with agent: <name>, and <name>
does not resolve, Claude Code forks a general-purpose agent instead. There
is no error, no warning, and nothing in the transcript indicating a substitution
took place.

This matters beyond a typo class of bug, because the substituted agent has a
wider toolset than the one specified. A subagent defined with a restricted
tools: list — the only real toolset restriction available, since allowed-tools
on a skill does not confine anything — is replaced by one with full access.

In our case the named agent was a read-only auditor:

# agents/auditor.md
name: auditor
tools: Read, Grep, Glob, Bash, WebFetch   # deliberately no Edit, no Write

The whole point of that agent is that it cannot modify what it audits. The skill
that invoked it ran with Edit and Write available for months of authoring
without anyone noticing, because the output looked correct.

Reproduction

  1. In a plugin, define an agent at agents/auditor.md with a restricted tools:

list.

  1. In a skill in the same plugin, set frontmatter:

``yaml
context: fork
agent: auditor
``

  1. Invoke the skill: claude -p '/my-plugin:my-skill'
  2. Read the recorded agent type:

``sh
cat ~/.claude/projects/<project>/<sessionId>/subagents/agent-*.meta.json
``

Observed: {"agentType":"general-purpose"}

Then change the frontmatter to the plugin-scoped form:

agent: my-plugin:auditor

and repeat.

Observed: {"agentType":"my-plugin:auditor"}

Two consecutive runs in the same project directory, nothing else changed:

17:07:44  {"agentType":"general-purpose"}      agent-a1c9555b3a6a0b626.meta.json
17:10:21  {"agentType":"project-kit:auditor"}  agent-a0f66cd7fa31341dd.meta.json

Expected

One of:

  • Resolve the bare name within the declaring plugin's own agents, the way a

reader would expect a plugin-local reference to work; or

  • Fail loudly — refuse to launch, or emit a visible warning naming the

unresolved agent and the fallback being used.

Silently substituting an agent with broader tool access is the outcome that
should not be available. The failure is invisible at every layer a user would
check: the skill runs, the output is plausible, and the only trace is a
meta.json most people do not know exists.

Notes on scope of this report

  • Verified only for a plugin skill referencing a plugin agent. Whether a

project-level or user-level agent behaves the same is untested here.

  • sub-agents.md documents that hook matchers for plugin subagents use the

plugin-scoped identifier (my-plugin:db-agent), so scoping is clearly the
intended reference form. The defect is the silent fallback, not the
requirement.

  • Not tested against any version other than 2.1.220.
  • The substituted agent in our runs happened to use only Bash, so nothing was

damaged. That is luck, not a mitigation.

View original on GitHub ↗

3 Comments

ondrejtucny · 25 days ago

Ran into this problem, too. This is a serious issue — first, it leads to unwanted model escalation or degradation, second, it's a session limit eater, providing the lower-intelligence agents are silently replaced with a general-purpose agent that inherits e.g. the Fable model.

ondrejtucny · 17 days ago

Adding field evidence from a long multi-agent session that reproduces #83133 in production and quantifies a consequence I think the current title under-states: **when a context: fork skill's agent: fails to resolve, the fallback general-purpose agent inherits the main session model, not the target agent's pinned model — silently, with no warning.** For a skill whose whole purpose is to run cheap work on a small model, this is a correctness-and-cost defect, not only a toolset-widening one.

Setup

A local plugin (pipr) ships a fork skill:

# skills/pipr/SKILL.md   (pipr v1.0.0)
name: pipr
context: fork
agent: pipr-runner          # <-- BARE ref; the agent registers as pipr:pipr-runner
# agents/pipr-runner.md
name: pipr-runner
model: haiku                # <-- deliberately Haiku: this agent only shells out to a CLI and reports
tools: PowerShell, Read, Grep, Glob

Plugin agents register under a namespaced id (pipr:pipr-runner), so the bare agent: pipr-runner does not resolve — exactly the case in this issue. Sibling plugins in the same session used the namespaced form (agent: infra-ops:jira-operator, etc.).

What was observed (single session, 2026-08-12/13, CC 2.1.228)

Across one session I counted every fork marker against the model each spawned agent actually ran (from the per-agent transcripts). During a ~5-hour window, only the bare-ref skill fell back; every namespaced sibling resolved correctly the entire time:

| Fork skill | Declared agent | Resolved to | Model that ran | Result |
|---|---|---|---|---|
| pipr:pipr (bare agent: pipr-runner) | pipr:pipr-runner (model: haiku) | general-purpose | claude-fable-5 ×2, claude-opus-5 ×17 | ❌ 19 fallbacks |
| infra-ops:jira-ops (namespaced) | jira-operator (model: sonnet) | infra-ops:jira-operator | claude-sonnet-5 | ✅ 10/10 |
| infra-ops:plastic-ops (namespaced) | plastic-operator (model: sonnet) | infra-ops:plastic-operator | claude-sonnet-5 | ✅ |
| epic-worker:group-bugfix (namespaced) | bug-fixer | epic-worker:bug-fixer | (session model) | ✅ resolved |

The bare-ref failures and the namespaced successes were interleaved in the same minutes (e.g. 16:11 jira-ops→sonnet ✅ and pipr→general-purpose ❌ back to back), which rules out a transient/environment cause. After the plugin was updated mid-session to the namespaced ref (agent: pipr:pipr-runner), the same fork resolved correctly on every subsequent call and ran Haiku (8/8).

The consequence worth highlighting

The fallback did two things silently:

  1. Widened the toolset (this issue's current framing) — the runner is declared with 4 read/exec tools; general-purpose carries the full set.
  2. Inherited the session model — the 19 fallbacks ran on claude-opus-5 / claude-fable-5, the main session's model, instead of the intended haiku. A skill explicitly pinned to Haiku for cost ran ~19 CLI-wrapper operations on the frontier model, for ~5 hours, with no warning anywhere in the UI or logs. The only way we found it was post-hoc forensics on the per-agent transcripts.

So the silent fallback is not just a safety issue (tools); it quietly inverts the operator's cost decision. The pin is the whole reason the agent exists, and it is dropped without a trace.

Ask

The primary fix (error/warn on an unresolvable agent:) is already the subject of this issue — this comment is just to reinforce it with real numbers. If a hard error is too aggressive for back-compat, even a single stderr/telemetry line at fork time —

skill "pipr:pipr": agent "pipr-runner" did not resolve; running as general-purpose (model + tool restrictions not applied)

— would have turned a 5-hour invisible regression into a one-line diagnosis. The model-inheritance half of the fallback is the part that currently has zero observability.

Repro (minimal)

  1. Plugin skill with context: fork and a bare agent: <name> whose agent registers namespaced as <plugin>:<name>, with model: haiku on the agent.
  2. Invoke the skill (Skill tool or slash command) from a session whose main model is not Haiku.
  3. Inspect the spawned subagent: agentType is general-purpose and the dispatched model equals the session model, not haiku. No warning is emitted.
  4. Namespace the ref (agent: <plugin>:<name>) → resolves correctly and honors the Haiku pin.

_Note: distinct from #86058 (frontmatter model: opus running the parent model when the newest Opus family is gated). That is a model-stepping bug on a resolved agent; this is a resolution failure that happens to also drop the pin._

ondrejtucny · 17 days ago

@bcherny repro here, re threads convo.

Showing cached comments. Read the full discussion on GitHub ↗