Hook stderr output shows raw command path as prefix, leaking implementation details

Resolved 💬 4 comments Opened Mar 31, 2026 by iroha924 Closed May 4, 2026

Description

When a hook writes to stderr (e.g., exit 2 for DENY/block), Claude Code displays the message with the full command as a prefix:

Stop hook error: [node /path/to/hook.mjs stop]: Custom message here.

The [node /path/to/hook.mjs stop] prefix is an implementation detail that provides no value to users. For plugin hooks, it exposes internal paths like ${CLAUDE_PLUGIN_ROOT}/dist/hook.mjs.

Expected behavior

Show only the hook's stderr message, optionally with a human-readable hook identifier:

Stop hook: Custom message here.

Or use the plugin name if available:

my-plugin (Stop): Custom message here.

Reproduction

  1. Create a minimal hook that exits with code 2:
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Please fix errors before finishing.' >&2 && exit 2",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
  1. Start a session and let Claude attempt to stop
  2. Observe the output:
Stop hook error: [echo 'Please fix errors before finishing.' >&2 && exit 2]: Please fix errors before finishing.

The full command is shown as a prefix, and the intentional block is labeled "error".

Additional context

  • The word "error" is misleading for intentional blocking behavior (exit 2). Related: #12671, #17088
  • When both plugin hooks and settings hooks define hooks for the same event, the message appears multiple times with different command paths, making it look like separate errors
  • Plugin authors have no control over this formatting

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗