UserPromptSubmit hook: blocked prompts are echoed to the UI and written to the session transcript

Status Open
Reported on v2.1.281
Maintainer reply None cached
Activity 0 comments · opened Sep 24, 2026

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:

  1. displayed back in the terminal under Original prompt:, and
  2. 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 whose content embeds 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

  1. 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)
  1. Wire it as a UserPromptSubmit hook.
  2. Submit a prompt containing a recognizable synthetic token, e.g. summarize the payment 4111111111111111 reference MARKER7731.
  3. Observe the terminal: the block notice is followed by Original prompt: and the full text.
  4. 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 a queue-operation record written before the hook ran and inside the block notice. Fixing the race in #92353 would not change anything reported here.
  • #85669 (closed, stale) — UserPromptSubmit skipped 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.

View original on GitHub ↗