[BUG] Registering a DirectoryAdded hook makes one scaffold Task subagent return no output even though the hook never fires
Summary
In our scaffold operation, registering a command hook on the DirectoryAdded event causes its Task subagent to complete and return no output. The parent session then waits on it, ends without a close, and the run is reported as failed — while the subagent's actual work completed correctly on disk. A different operation that also dispatches a subagent passes with the same registration, so this report deliberately does not generalize to every subagent.
The hook does not have to do anything. The minimal reproduction is a hook whose entire body is exit 0. It reads no stdin, writes nothing, and touches no files.
Removing the registration — changing nothing else — makes the same operation pass.
Environment
claude: 2.1.220 (claude --version)- macOS (Darwin 27.0.0), Apple Silicon
- Invoked through the Claude Agent SDK (Python host), model
claude-sonnet-5 - The dispatching session uses the
Tasktool to run a subagent that performs a multi-step scaffold and reports a JSON result
Reproduction
The hook — this is the whole file:
#!/usr/bin/env bash
exit 0
Registered in the plugin's hooks.json:
{"hooks": {"DirectoryAdded": [
{"hooks": [{"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/noop-hook.sh",
"args": [], "timeout": 5, "async": true}]}
]}}
Then run a session whose Task subagent creates a new project tree, runs its baseline, commits it, and returns a structured scaffold report. That is the operation shape reproduced below; a read-only shape operation is the explicit passing control.
Observed: the subagent runs (its TaskProgressMessage stream shows ~10 tool uses over ~40s, and its work lands correctly on disk), then the tool result is:
(Subagent completed but returned no output.
The parent resumes the subagent, waits, and the session terminates while further TaskProgressMessage entries are still arriving. The session's own ResultMessage reports subtype: "success", is_error: false, but no subagent report was ever delivered to the parent.
Expected: the subagent's final message is returned to the parent, as it is when the registration is absent.
Isolation
Every row is a separate run, same host, one variable changed.
| operation | DirectoryAdded registration | hook body | async | outcome |
|---|---|---|---|---|
| scaffold | absent | — | — | passes |
| scaffold | absent | — | — | passes |
| scaffold | present | full production script | true | fails |
| scaffold | present | full production script | true | fails |
| scaffold | present | full production script | true | fails |
| scaffold | present | full production script | true | fails |
| scaffold | present | exit 0 | true | fails |
| scaffold | present | exit 0 | omitted | fails |
| scaffold | present | logs every invocation, exit 0 | true | fails, and the hook is never invoked |
| shape | present | full production script | true | passes |
So it is:
- Not the hook's behaviour —
exit 0reproduces it. - Not stdout/hook-protocol corruption — the no-op writes nothing at all.
- Not
async— removing the key does not help. - Not the matcher-less registration shape per se — 7 of our 14 registered events carry no matcher and none of the others exhibit this.
- Not subagent dispatch in general. Our
shapeoperation also dispatches a subagent viaTask(its transcript showsTaskStartedMessageand sevenTaskProgressMessageentries) and passes with the same registration present. - Not the event firing. A hook that appends its payload to a file on every invocation, then exits 0, was registered for a full failing run. The log file was never created — the hook never ran. The same probe writes correctly when invoked directly, so this is a real negative rather than a broken instrument.
The passing scaffold rows and the failing exit 0 rows used an identical plugin directory copy at the same path, so the plugin location is not a variable either.
It is also not the hook count. Our revert changed two things at once — the event and the total number of registered hooks (31 → 30) — so every "absent" pass above was also a "30 entries" pass. Restoring the count to 31 by adding an identical exit 0 entry on PreCompact, with no DirectoryAdded anywhere, passes. So the count is excluded and the event name is the variable.
The residue is genuinely odd, and we would rather state it than guess past it: the presence of a DirectoryAdded entry in the hook registry breaks this operation without the event ever occurring.
The one lead we can offer
We cannot see the implementation, but one site in the 2.1.220 bundle looks worth checking. In the register_repo_root path, the DirectoryAdded hook chain is fire-and-forget inside a comma expression, and a value acquired immediately beforehand is released only in that chain's .finally():
Mo.refreshConfig();
let qi = bs();
if (a$t(oo, "register_repo_root")
.then(({results, systemMessages}) => { /* logging */ })
.catch(...)
.finally(qi),
an.reload_claude_md) { ... }
(a$t is executeDirectoryAddedHooks; bs() returns the function passed to .finally, which reads as an acquire/release pair.)
With no hook registered, the chain has nothing to run and settles immediately, so qi is released at once. With a hook registered, the chain must actually execute it before releasing. If executing a hook requires something the held value is gating, that deadlocks: the hook never runs, .finally(qi) never fires, and the hold is never released — which would match all three of our observations (hook never invoked, only-when-registered, and a subagent that completes with empty content while the parent waits).
We are explicitly not claiming this is the cause. It is a hypothesis consistent with the evidence, offered because it names a concrete site to look at. A maintainer can confirm or dismiss it far faster than we can from outside.
Why this is easy to miss
The failure presents as a model behaviour problem, not a hooks problem. The parent's own summary reads like a reasonable narration — "the scaffolder agent ran but returned no output on its first pass, so I've resumed it and asked it to report" — and the underlying work is genuinely complete and correct on disk. Nothing in the session output mentions hooks, and the session's ResultMessage says success. We spent one full session attributing this to model variance and to an unrelated recently-shipped setting before an A/B on the hook registration isolated it.
Suggested behaviour
A DirectoryAdded entry in the hook registry should not prevent this scaffold operation's Task subagent from returning its final output, especially while the event itself never fires. Since the hook is never invoked in the failing runs, the interaction appears to be in registry construction or validation rather than in event delivery — which is where we would start looking, though we cannot see the implementation.