Subagent tool calls: PreToolUse denials don't surface to the parent, and hook payload carries the parent's session_id
What happened
I use PreToolUse deny hooks to enforce a worktree-per-session policy. Building that enforcement, I hit two ways it silently fails for subagent (Task/Agent) tool calls. Both fail silent, not safe — the guard is off and nothing tells you.
Bug 1 — a subagent's permission denials don't surface to the parent.
With a PreToolUse deny in place, subagent tool calls were correctly blocked by the hook — but the parent session's result came back with permission_denials: [] (empty). A fan-out of subagents appears to succeed while having written nothing. There is no signal to the parent (or the user) that the guard fired inside the subagents.
Bug 2 — a subagent's hook payload carries the PARENT's session_id.
The JSON payload delivered to a hook invoked for a subagent's tool call reports the parent session's session_id, not a distinct one for the subagent. Any session-keyed lock or claim registry is therefore silently broken for subagents: N parallel subagents all present the same session_id and all pass the same mutex.
These compound: you can't build a reliable session-scoped guard (Bug 2), and even when a guard does fire in a subagent, the parent can't tell (Bug 1).
Steps to reproduce
- Configure a PreToolUse hook that returns a
denydecision for some tool (e.g. Edit/Write to a given path). - From a parent session, spawn one or more subagents (Task/Agent) that attempt that denied tool call.
- Observe the subagents are blocked, then inspect the parent's result payload.
Expected
- Bug 1: the parent's result reflects that subagent tool calls were denied —
permission_denialsis populated (or there is some equivalent signal), so a fan-out that wrote nothing does not read as success. - Bug 2: the hook payload for a subagent's tool call carries an identifier that distinguishes the subagent from its parent, so session-keyed coordination works.
Actual
- Bug 1: parent result shows
permission_denials: []; the fan-out looks successful despite writing nothing. - Bug 2: the subagent hook payload's
session_idequals the parent's.
Impact
Any hook-based enforcement (worktree isolation, path guards, claim/lock registries) that needs to cover subagent activity has a hole in it, and the hole is invisible at runtime. This is the single most time-consuming class of problem I hit building cross-session guards.
Related
- Same "silent no-op" class as #4669 / #37210 (malformed hook output does nothing, no warning; closed as not planned).
- Underlying docs gap: the documentation doesn't state whether hooks fire for subagent tool calls or what
session_idthey carry — I had to determine both empirically. Broader coordination context and the full write-up are in the companion feature request #76727.
Environment
- Solo dev, Windows 11, VS Code extension + Desktop app.
- Observed while running many concurrent sessions against one repo (~20 git worktrees, 15–20 sessions at a time).
- Caveat: n=1 — one machine, one repo. Reproduced consistently in my setup; offered as evidence, not a general claim.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
I've run into this same class of problem while building a multi-agent layer around Claude Code.
My approach has been to treat each team member as an independently managed Claude Code session rather than relying on a parent session + nested subagents for isolation.
Each member gets an explicit capability/permission configuration, and tool calls are checked against that member's capabilities before execution. The orchestrator also keeps the member identity outside of the Claude conversation itself, so the workflow doesn't depend on the parent session's identity being propagated correctly.
That separation became important for exactly the kind of case you're describing: if a tool call is denied, the orchestration layer needs to know that the specific member was denied rather than interpreting the overall task as successful.
The project is open source if useful:
hacker room
The architecture is roughly:
orchestrator
→ Planner / Coder / Reviewer / Tester / Auditor
→ independent Claude Code sessions
→ per-member capabilities + tool enforcement
I'm curious whether Claude Code eventually intends to expose a first-class, stable agent/session identity to hooks, since that seems necessary for reliable session-scoped policies when multiple agents are running concurrently.
Still relevant. Both halves are still the kind of failure that is silent rather than safe.
A subagent's
PreToolUsedenials still do not surface to the parent, which comes back withpermission_denials: [], so a fan-out reports clean while its children were being blocked. And the hook payload still carries the parent'ssession_id, so a hook cannot attribute a call to the subagent that made it.Together with #82323, this is the recurring shape: the guard is off, or attributing to the wrong session, and every observable signal reads green.
Still open. The part I would ask a maintainer to look at first is bug 2, the
session_idin the hook payload, because it is the one that breaks a guard by design rather than by omission. N parallel subagents all present the parent's session id, so any session-keyed lock or claim registry admits all of them and reports no conflict.Bug 1 compounds it rather than duplicating it. When a guard does fire inside a subagent, the parent's result comes back with
permission_denials: [], so a fan-out reads as successful while having written nothing.This sits in the same family as #82323 and #83952: a hook control that is off while every visible signal says it is configured. Those two cover a hook that never runs and a hook whose identity cannot be confirmed. This one covers a hook that runs, decides correctly, and reports nothing upward.