Skip to content

Hook runs with empty stdout and exit 0 are logged as hook.run.failed (empty output fails strict JSON validation) #908

Description

@Omar-MD

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%)

  1. 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 }
          ]
        }
      ]
    }
  }
}
  1. 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
  1. Run any Bash tool call that does NOT hit the *trigger* branch (no stdout, exit 0).

  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions