anthropics / anthropics/claude-code
[BUG] UserPromptSubmit hook registered and valid but never invoked on real prompt submission (Git Bash CLI, Windows, v2.1.261) — possible regression of #17277
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Title
`UserPromptSubmit` hook registered and valid, but never invoked on real interactive prompt submission (Git Bash CLI, Windows)
## Environment
- Claude Code version: v2.1.261
- OS: Windows 10, running Claude Code inside Git Bash (MINGW64)
- Model: Sonnet 5, medium effort
- Permission mode: `auto`
- Project: local project with `.claude/settings.json` defining a `UserPromptSubmit` hook and a `PreToolUse` hook (matcher: `Bash`)
## Summary
A `UserPromptSubmit` hook configured in the project's `.claude/settings.json` is correctly listed by `/hooks`, and the hook script itself works perfectly when invoked manually with the exact same JSON payload Claude Code would send it — but it is **never actually invoked** when a real prompt is typed and submitted interactively in the CLI. The prompt reaches the model unmodified every time, across multiple fresh session restarts.
## `.claude/settings.json` (relevant part)
```json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.js\"",
"shell": "bash"
}
]
}
]
}
}
```
## Steps to reproduce
1. Create a `UserPromptSubmit` hook as above, pointing to a Node.js script that:
- reads stdin JSON (`{"prompt_text": "..."}`),
- detects a hardcoded-looking secret in `prompt_text`,
- if found, writes a message to stderr and calls `process.exit(2)`.
2. Confirm the hook is registered: run `/hooks` inside the CLI session — it correctly lists the `UserPromptSubmit` entry pointing at the script, under "Project Settings".
3. Confirm the script works standalone, run directly in the same shell:
```bash
node ".claude/hooks/block-secrets.js" <<< '{"prompt_text":"const apiKey = \"sk-ant-api03-abcdefghijklmnop1234567890\";"}'
echo "exit: $?"
```
Output: the expected "Blocked: detected potential credential(s)..." message, exit code `2`.
4. Fully exit the `claude` process (`/exit`), start a brand-new session (`claude`) in the same project directory.
5. Type the exact same string directly into the prompt: `const apiKey = "sk-ant-api03-abcdefghijklmnop1234567890";`
## Expected
Per `/hooks`' own documented semantics for `UserPromptSubmit`:
> Exit code 2 - block processing, erase original prompt, and show stderr to user only
The prompt should never reach the model; only the hook's stderr message should be shown to the user.
## Actual
The model receives and responds to the raw prompt normally (a real assistant turn, with real token usage recorded), as if no hook were configured at all. No hook-block message is ever shown. This was verified directly by inspecting the session's `.jsonl` transcript in `~/.claude/projects//.jsonl`: the raw secret string appears as a plain `"type":"user"` message, immediately followed by a genuine `"type":"assistant"` message with non-zero `usage.input_tokens`/`output_tokens` — proving the model actually processed it. There is no hook-related entry anywhere near it.
This reproduced consistently across at least 3 separate fresh sessions (confirmed via distinct session transcript files), including after:
- adding `"shell": "bash"` to the hook definition,
- fully exiting and restarting the `claude` process each time,
- confirming no `.claude/settings.local.json` override exists,
- confirming the hook appears correctly in `/hooks`.
## Additional notes
- A separate `PreToolUse` hook (matcher: `Bash`), defined the same way in the same `settings.json`, *did* fire correctly and block a matching Bash tool call — but only when observed from a different client context (Claude Code running inside the Claude desktop app's "Code" tab), not yet independently confirmed to auto-fire from this same Git Bash CLI session.
- `permissions.deny` rules in the same `settings.json` (blocking `Read`/`Bash cat` on `.env`, `~/.aws/**`, etc.) *do* work reliably and show a clear, non-silent denial message — so the settings file is being loaded and at least partially honored; it specifically appears to be `UserPromptSubmit` hook invocation that silently doesn't happen.
- `/doctor` separately flagged "hook execution error" telemetry for a `PreToolUse` Bash-hook run in this same project, suggesting hook execution may be broadly unreliable in this environment, not specific to `UserPromptSubmit`.
## Impact
This defeats any credential-leak-prevention hook design relying on `UserPromptSubmit` to intercept typed prompts before they reach the model — the core guarantee the hook is documented to provide.
## Possible regression of a previously-closed issue
This looks like a regression of #17277 ("UserPromptSubmit hook not triggering consistently"), which described the exact same symptom — the hook failing specifically on prompts submitted **early in a session** — and hypothesized a race condition where hook registration happens asynchronously after session init, so the first prompt(s) arrive before the handler is registered. That issue was closed in January 2026 after the reporter said it "appears to work more consistently with 2.1.3." This report reproduces the identical symptom on v2.1.261, consistently on the first prompt of freshly-restarted sessions, suggesting the underlying race condition has resurfaced. Related closed reports with similar titles: #7873, #31114.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the project .claude/settings.json UserPromptSubmit hook and reproduce the behavior in a fresh Git Bash session using the documented secret prompt. Inspect the session JSONL transcript and compare it with /hooks and /doctor output. Done means the hook runs on interactive submission and exit code 2 prevents the prompt from reaching the model while showing the stderr message.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bash, node.js
- Domain
- cli, developer-experience, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100