Hooks cannot block quietly: `decision: block` always renders a user-visible notice in the CLI (`suppressOutput` has no effect)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Jul 28, 2026

Summary

A hook that blocks a prompt always renders a user-visible notice in the CLI, and there is no way to make the block quiet. suppressOutput: true does not affect it (tested below).

This matters for hooks used as policy rather than as guardrails: "skip this one, it's redundant" is a block, but it isn't an error and there is nothing for the user to act on. Today every block is presented as one.

Repro (Claude Code 2.1.220, macOS)

settings.json:

{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"bash ./h.sh","timeout":10}]}]}}

h.sh:

#!/usr/bin/env bash
grep -q '"prompt":"BLOCKME"' <<< "$(cat)" || exit 0
printf '{"decision":"block","reason":"quiet please","suppressOutput":true,"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","suppressOriginalPrompt":true}}\n'
$ claude -p --setting-sources project --settings ./settings.json 'BLOCKME'
UserPromptSubmit operation blocked by hook:
quiet please

Identical output with "suppressOutput": false and "suppressOutput": true.

Why the existing knobs don't cover this

  • suppressOutput is documented as "hides the hook's stdout from the transcript". The notice is not the hook's stdout — it is constructed client-side from the reason field — so suppressing or redirecting streams has no effect. Confirmed by the A/B above.
  • exit 2 instead of decision: block still renders; with stderr empty the message just degrades to [<command>]: No stderr output. Redirecting stderr to a file (suggested in #39499) makes the notice less informative, not absent.
  • continue: false also renders, and additionally pushes a message into the conversation.

Concrete use case

A prompt-cache keepalive plugin. A cron enqueues a tiny heartbeat prompt on a schedule so an idle session's prompt cache stays warm. When a real turn happened recently the heartbeat is redundant, so a UserPromptSubmit hook cancels it before it costs an API request — which works well and is exactly what decision: block is for. The cost is that a routine, successful no-op prints two lines several times an hour.

Request

An opt-in quiet block, e.g. silent: true alongside decision: "block" (or inside hookSpecificOutput). Semantics unchanged — the prompt is still erased, no API request is made, the model still never sees it — only the client-side notice is skipped.

Prior art

#39499 asked for the same capability for Stop hooks (feeding context back to the model without surfacing it as an error). It was closed by the inactivity bot with no maintainer response. A commenter there reported that the desktop app does not render hook output while the CLI does, which suggests this is a TUI rendering choice rather than a protocol constraint.

View original on GitHub ↗