[Bug] Plugin API: a plugin's own hooks are skipped for $.agent.spawn made after its hook returned (auto mode then refuses the spawn)

Status Open
Reported on v2.1.295
Maintainer reply None cached
Activity 0 comments · opened Oct 13, 2026

Summary

A plugin's own hooks are skipped for an $.agent.spawn it makes after the hook that started the work has returned. In auto mode the spawn is then refused, because the plugin's own tool.check approval never runs.

Environment

  • Claude Code 2.1.295 (2.1.296 installed alongside)
  • Windows 11, desktop app
  • Permission mode: auto

Steps to reproduce

  1. In a mod, register an agent type with $.agent.register(...).
  2. Add a tool.check hook on { tool: 'Agent' } that answers { decision: 'allow' } for the mod's own agent types, and an agent.spawn hook that rewrites cwd.
  3. From a tool.call or command.run hook, start a $.clock.after(0, ...) timer and return.
  4. In the timer callback, call $.agent.spawn({ subagentType: '<mod>:<type>', prompt, description }).

Actual

  • The engine raises tool.check for the Agent tool with next.origin.plugin === '<mod>'. A second plugin's tool.check hook sees it.
  • The mod's own tool.check hook does not run: not its body, and not its .catch handler.
  • The mod's own agent.spawn hook does not run either.
  • auto mode refuses the spawn: "The server-side auto mode classifier gave no verdict for Agent: the request that produced this action did not ask for one."

The same spawn made inside the live hook, before it returns, runs both hooks: the approval and the cwd rewrite take effect, and the agent starts.

Expected

The $.agent.spawn doc says it "runs every hook but the calling one". The calling hook has already returned, and the plugin's other hooks (tool.check, agent.spawn) are not the calling one, so they should run. reference.md says work that outlives a dispatch is kept going with $.clock.after and $.clock.every, which is what the mod does.

Evidence

A trace in the mod recorded which path each of its own hooks took for each spawn:

live hook: check=body spawnHook=body -> {"model":"claude-haiku-5-5","agentId":"..."}
timer: check=none spawnHook=none -> {"deny":"The server-side auto mode classifier gave no verdict ..."}

A second plugin's tool.check hook logged the timer spawns with origin set to the first mod and an engine verdict of ask, at the same timestamps. So the event was raised, and only the calling plugin's own hooks were left out.

Related, same build

  • The cwd passed to $.agent.spawn is dropped: an agent.spawn hook sees no cwd, and the agent runs in the session's folder. Rewriting cwd in an agent.spawn hook works.
  • An Agent(<plugin>:<type>) allow rule in settings does not match a plugin's spawn; the engine's verdict stays ask.

Workaround

Spawns are queued and made only inside a live hook of the mod: its pane and band renders, its agents' tool calls and turn ends, its commands. It works, but it needs a surface drawing or an agent running, and making a spawn from a render hook is a side effect where none belongs.

View original on GitHub ↗