UserPromptSubmit hook: blocked prompts are echoed to the UI and written to the session transcript
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When a UserPromptSubmit hook blocks a prompt, the prompt text is still:
- displayed back in the terminal under
Original prompt:, and - written to
~/.claude/projects/<slug>/<session>.jsonl— twice:
- a
type: "queue-operation",operation: "enqueue"record containing the raw prompt, written before the hook runs - a
type: "system"record whosecontentembeds the block notice including the raw prompt
The prompt is correctly never sent to the model. But for hooks whose purpose is to keep
certain data off the machine, the data the hook rejected is still persisted locally.
What Should Happen?
The hooks documentation's exit-code-2 behavior table says, for UserPromptSubmit:
Blocks prompt processing and erases the prompt
I expected "erases the prompt" to mean no verbatim copy remains. Instead the prompt is kept in
two transcript records and redisplayed in the terminal. Either the behavior or the wording
needs to change — organizations are building data-handling controls on that sentence.
Error Messages/Logs
Steps to Reproduce
- Create a hook that always blocks:
#!/usr/bin/env python3
import sys
sys.stdin.read()
if (sys.argv[1:] or [""])[0] == "prompt":
sys.stderr.write("blocked by probe\n")
sys.exit(2)
sys.exit(0)
- Wire it as a UserPromptSubmit hook.
- Submit a prompt containing a recognizable synthetic token, e.g. summarize the payment 4111111111111111 reference MARKER7731.
- Observe the terminal: the block notice is followed by Original prompt: and the full text.
- grep -l MARKER7731 ~/.claude/projects//.jsonl — the value is in the transcript.
Also tried
The JSON form, {"decision": "block", "reason": "..."}, behaves identically — same echo, same transcript records. So there is no hook-side mitigation.
Verified NOT affected
The blocked prompt does not reach the model. Blocking a prompt containing a unique marker and then asking in a later turn whether that marker had appeared returns "No".
Not a duplicate of the similar-looking issues
Checked before filing:
- #92353 (open) — "hook registered and valid but never invoked." There the hook does not run, so the prompt is sent to the model and lands in the transcript as a normal
"type":"user"message with token usage; suspected registration race. Here the hook does run, the prompt is verifiably not sent, and it is still retained — in aqueue-operationrecord written before the hook ran and inside the block notice. Fixing the race in #92353 would not change anything reported here. - #85669 (closed, stale) —
UserPromptSubmitskipped for attachment-bearing prompts in the VSCode extension. Also a not-invoked bug, and extension-specific; this report is about a hook that was invoked and succeeded. - #59822 (closed) — the block reason is not displayed in Claude Code Desktop. That is the opposite surface of this report: too little shown to the user, rather than the original prompt being retained and redisplayed.
The common thread in those three is "the hook didn't fire" or "the UI showed too little." This report is "the hook fired correctly and the data was retained anyway."
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude Code 2.1.281 (also reproduced on 2.1.266)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
Managed settings at /etc/claude-code/managed-settings.json, allowManagedHooksOnly: true
Why it matters
UserPromptSubmit hooks are the documented way to enforce an organizational data boundary. A hook can currently prevent transmission but cannot prevent local retention of the same value, and cleanupPeriodDays is the only lever — a retention window, not a redaction.
Possible remedies (any one would resolve it)
Don't write the enqueue record until the hook has allowed the prompt.
Omit the prompt body from the block notice, or truncate it to a length the hook controls.
Let the hook return a sanitized prompt for display/logging (a displayPrompt field, or reuse the updatedToolOutput pattern from PostToolUse).
If the current behavior is intended, state it explicitly in the hooks documentation so organizations don't build controls on the "erases it" wording.