[FEATURE] Allow PreToolUse hook output to render before tool execution (non-blocking)

Resolved 💬 1 comment Opened Jun 1, 2026 by tpetrescu93 Closed Jul 4, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

A PreToolUse hook runs before its tool, but its systemMessage is only rendered to the user after the tool finishes. For fast tools that's invisible; for slow ones it's useless — a message meant to say what's about to run only appears once the tool is already done, by which point the user can no longer act on it (e.g. cancel a wrong or runaway command).

There is currently no non-blocking way to surface hook output before a tool executes. The only mechanism that renders before execution is blocking the call (exit 2), which has significant side effects (detailed under Alternatives).

Proposed Solution

Provide a non-blocking way for a PreToolUse hook to display output to the user before the tool runs. Any of these would solve it:

  1. A flag on the hook JSON output, e.g.:

``json
{ "systemMessage": "Running query: SELECT ...", "renderTiming": "before" }
``

  1. Or simply: render systemMessage from a PreToolUse hook before the tool executes (it already runs before — only the display is deferred), since for a pre-tool event "before" is the intuitive timing.
  2. Or a dedicated field, e.g. hookSpecificOutput.preExecutionMessage, rendered cleanly (no error styling) at hook time.

The key requirements: non-blocking (no extra model round-trip), rendered before execution, and not styled as an error.

Alternative Solutions

I've tried every documented path; all fail for "show before a slow command":

  1. systemMessage + exit 0 — renders after the tool completes. Confirmed empirically (a PreToolUse hook on sleep 10 shows its message only after the 10s elapse). Useless for long commands.
  2. hookSpecificOutput with permissionDecision: "deny" via stdout — ignored entirely in bypassPermissions mode; nothing reaches the user.
  3. exit 2 (the only thing that renders before execution) — requires a block-then-allow round-trip. exit 2 blocks the call outright, so to let the command actually run the hook must allow it on a retry (e.g. hash the command, block + display on the first call, allow on the second). This is the current workaround, and it's costly regardless of how the message is shown:
  • Burns a full extra model round-trip on every guarded command (block → retry → allow).
  • The displayed text can come from stderr (rendered as a red error line — looks like a failure) or from systemMessage (clean). On Bash tools the systemMessage path also shows the red stderr line; MCP tools show only the clean systemMessage — inconsistent.
  • Parallel tool calls get cancelled — blocking one cancels its siblings.
  • Leaks /tmp state files when a command is blocked but never retried (e.g. user interrupts).

Priority

Low - Nice to have

Feature Category

Configuration and settings

Use Case Example

  1. I have a PreToolUse hook that detects SQL commands (Snowflake / Postgres) and shows the query to me before it runs.
  2. The model issues a query that takes ~2 minutes against a large table.
  3. Today: I see a spinner for 2 minutes with no indication of what's running; the query text only appears after results return.
  4. With this feature: the query text renders immediately when the hook fires — so if it then runs for a long time, I can see exactly what's executing and cancel it if it's wrong, instead of waiting blindly on a spinner.

Additional Context

Minimal repro — attach to Bash, run sleep 10, and observe that the message appears only after the 10s, not before:

INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
jq -nc --arg m ">>> about to run: $CMD" '{"systemMessage":$m}'
exit 0

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗