[FEATURE] Allow PreToolUse hook output to render before tool execution (non-blocking)
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:
- A flag on the hook JSON output, e.g.:
``json``
{ "systemMessage": "Running query: SELECT ...", "renderTiming": "before" }
- Or simply: render
systemMessagefrom aPreToolUsehook before the tool executes (it already runs before — only the display is deferred), since for a pre-tool event "before" is the intuitive timing. - 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":
systemMessage+ exit 0 — renders after the tool completes. Confirmed empirically (aPreToolUsehook onsleep 10shows its message only after the 10s elapse). Useless for long commands.hookSpecificOutputwithpermissionDecision: "deny"via stdout — ignored entirely inbypassPermissionsmode; nothing reaches the user.exit 2(the only thing that renders before execution) — requires a block-then-allow round-trip.exit 2blocks 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). OnBashtools thesystemMessagepath also shows the red stderr line; MCP tools show only the cleansystemMessage— inconsistent. - Parallel tool calls get cancelled — blocking one cancels its siblings.
- Leaks
/tmpstate 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
- I have a
PreToolUsehook that detects SQL commands (Snowflake / Postgres) and shows the query to me before it runs. - The model issues a query that takes ~2 minutes against a large table.
- Today: I see a spinner for 2 minutes with no indication of what's running; the query text only appears after results return.
- 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 0This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗