Environment
- ZCode 3.14.4 (3.14.4.7912), macOS (arm64)
- Component: hook runner (
core.hooks)
- Hook type: configuration-file
command hooks (workspace .zcode/config.json)
Summary
A command hook whose script prints nothing and exits 0 is logged as a failure. The runner appears to parse hook stdout as JSON unconditionally; parsing the empty string fails, so every "nothing to report" run produces a warn-level hook.run.failed event. Exit codes are still honored and JSON output (e.g. additionalContext) is still injected correctly — only the outcome bookkeeping is wrong.
The docs state empty output is fine and exit code 0 passes ("Hook output: standard output is parsed as JSON … or you may use exit codes: 0 passes … empty output is fine"), so this looks like a regression/violation of the documented contract.
Reproduce (100%)
- Register a trivial hook (workspace
.zcode/config.json):
{
"hooks": {
"enabled": true,
"events": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash .zcode/hooks/noop.sh", "enabled": true, "timeout": 10 }
]
}
]
}
}
}
noop.sh — prints nothing on most paths, exits 0:
#!/usr/bin/env bash
set -u
CMD=$(jq -r '.tool_input.command // empty' 2>/dev/null)
case "$CMD" in
*trigger*) jq -nc '{additionalContext: "matched"}' ;;
esac
exit 0
-
Run any Bash tool call that does NOT hit the *trigger* branch (no stdout, exit 0).
-
Check the log (~/.zcode/cli/log/zcode-<date>.jsonl):
{"level":"warn","event":"hook.run.failed","module":"core.hooks","message":"Hook execution failed","durationMs":10,"context":{"hookEventName":"PreToolUse","hookIndex":0,"matcher":"Bash","source":"project.workspace-hook-0-PreToolUse-0-0"}}
No error field, ~10 ms duration, one event per no-op run. A *trigger* run injects its additionalContext correctly and is also logged as failed.
Expected
Exit 0 + empty stdout = success. No warn event (or debug at most), consistent with the documented exit-code semantics.
Actual
Every empty-stdout run logs hook.run.failed at warn with no error detail.
Impact
Pure log noise at scale — a normal working day here produced 78 false failure events across two always-on hooks, which drowns real hook failures in log-based monitoring and makes working hooks look broken during development.
Workaround
Print a bare {} on no-op paths (valid empty JSON object, injects nothing). Since applying this: zero failures across all runs. Suggested fix: treat empty stdout as success, and when stdout is non-empty but unparseable, log the parse error with the output preview at debug/info.
Environment
core.hooks)commandhooks (workspace.zcode/config.json)Summary
A
commandhook whose script prints nothing and exits 0 is logged as a failure. The runner appears to parse hook stdout as JSON unconditionally; parsing the empty string fails, so every "nothing to report" run produces a warn-levelhook.run.failedevent. Exit codes are still honored and JSON output (e.g.additionalContext) is still injected correctly — only the outcome bookkeeping is wrong.The docs state empty output is fine and exit code 0 passes ("Hook output: standard output is parsed as JSON … or you may use exit codes: 0 passes … empty output is fine"), so this looks like a regression/violation of the documented contract.
Reproduce (100%)
.zcode/config.json):{ "hooks": { "enabled": true, "events": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash .zcode/hooks/noop.sh", "enabled": true, "timeout": 10 } ] } ] } } }noop.sh— prints nothing on most paths, exits 0:Run any Bash tool call that does NOT hit the
*trigger*branch (no stdout, exit 0).Check the log (
~/.zcode/cli/log/zcode-<date>.jsonl):{"level":"warn","event":"hook.run.failed","module":"core.hooks","message":"Hook execution failed","durationMs":10,"context":{"hookEventName":"PreToolUse","hookIndex":0,"matcher":"Bash","source":"project.workspace-hook-0-PreToolUse-0-0"}}No error field, ~10 ms duration, one event per no-op run. A
*trigger*run injects itsadditionalContextcorrectly and is also logged as failed.Expected
Exit 0 + empty stdout = success. No warn event (or debug at most), consistent with the documented exit-code semantics.
Actual
Every empty-stdout run logs
hook.run.failedat warn with no error detail.Impact
Pure log noise at scale — a normal working day here produced 78 false failure events across two always-on hooks, which drowns real hook failures in log-based monitoring and makes working hooks look broken during development.
Workaround
Print a bare
{}on no-op paths (valid empty JSON object, injects nothing). Since applying this: zero failures across all runs. Suggested fix: treat empty stdout as success, and when stdout is non-empty but unparseable, log the parse error with the output preview at debug/info.