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.