Claude Code UserPromptSubmit Hook Examples
UserPromptSubmit hooks default to a 30-second timeout and run before every prompt. Two tested hooks: add git context, and block prompts that contain API keys.
- A UserPromptSubmit hook runs when a prompt is submitted, before Claude processes it, and can add context or block the prompt.
- It also fires for scheduled tasks, background subagent reports and messages from other sessions, so be careful what you block.
- Its default timeout is 30 seconds; on timeout its output is discarded and the prompt goes through unless onFailure is block.
A UserPromptSubmit hook runs when a prompt is submitted, before Claude processes it. It can add context, or stop the prompt from reaching Claude at all. Below are two hooks for a dummy todo app: one adds the current branch and the number of uncommitted files, the other blocks prompts that look like they contain an API key. We tested on macOS with Claude Code 2.1.304 and jq 1.7.1, on 2026-10-20, by piping dummy JSON into the scripts. We did not watch them fire in a live session. For tool-level guards, see Claude Code PreToolUse Hook Examples.
When it fires
UserPromptSubmit doesn’t fire only on prompts you type. Claude Code also runs it when a session-scoped scheduled task fires (including a /loop iteration), when a background subagent reports back, and when another session sends a message to your main conversation. Keep that in mind before you block anything (source: https://code.claude.com/docs/en/hooks, checked 2026-10-20).
The input carries the submitted text in prompt. For a skill or command you typed, such as /deploy staging, the hook receives the line as typed, not the expanded skill content (same source).
{
"hook_event_name": "UserPromptSubmit",
"prompt": "add a due date to todos"
}
Timeouts are short
The default timeout here is 30 seconds for command, HTTP and MCP tool hooks, against 600 seconds on most other events. The hook blocks the prompt until it finishes, so a stuck hook stalls the session. When a UserPromptSubmit hook times out, its output, including any additionalContext, is discarded and the prompt goes through without it. To block the prompt on timeout instead, set "onFailure": "block" (same source).
Our context hook below took 0.037 seconds of wall time on the dummy repo, measured with time. Anything that calls the network or runs a test suite belongs elsewhere.
Add context to every prompt
On exit 0, plain-text stdout is added to Claude’s context. So is additionalContext in JSON output. Neither shows up as a visible transcript entry; both arrive as a system reminder that starts with the hook’s name (same source).
#!/usr/bin/env bash
# .claude/hooks/prompt-context.sh (UserPromptSubmit)
cd "${CLAUDE_PROJECT_DIR:?}" || exit 0
branch=$(git branch --show-current 2>/dev/null)
changed=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
echo "Current branch: ${branch:-unknown}. Uncommitted files: ${changed}."
exit 0
$ echo '{"hook_event_name":"UserPromptSubmit","prompt":"add a due date to todos"}' | .claude/hooks/prompt-context.sh
Current branch: main. Uncommitted files: 4.
exit=0
This line is added before every prompt, for the whole session. Add only what Claude can’t find on its own and needs most of the time. A line that adds nothing still costs context on every turn.
Block prompts that contain an API key
To block, print JSON with decision: "block". The reason is shown to you and is not added to Claude’s context. By default the block message ends with the original prompt text; suppressOriginalPrompt: true inside hookSpecificOutput leaves it out (same source).
#!/usr/bin/env bash
# .claude/hooks/block-secrets-in-prompt.sh (UserPromptSubmit)
prompt=$(jq -r '.prompt // ""')
if printf '%s' "$prompt" | grep -Eq 'sk-ant-[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}'; then
jq -n '{decision: "block",
reason: "The prompt looks like it contains an API key. Remove it and use an environment variable instead.",
hookSpecificOutput: {hookEventName: "UserPromptSubmit", suppressOriginalPrompt: true}}'
fi
exit 0
With a dummy key in the prompt:
{
"decision": "block",
"reason": "The prompt looks like it contains an API key. Remove it and use an environment variable instead.",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"suppressOriginalPrompt": true
}
}
exit=0
With add a due date to todos, it printed nothing and returned exit 0.
This doesn’t keep the key off disk. The docs say the submitted text can still end up in the session transcript and prompt history, even with suppressOriginalPrompt (same source). The hook stops the key from reaching the model; rotate any key you pasted. More in Keep Secrets Away from Claude Code.
The settings.json
UserPromptSubmit has no matcher support, so leave matcher out.
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/prompt-context.sh" },
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-secrets-in-prompt.sh" }
]
}
]
}
}
claude doctor reported no invalid settings. To act on a specific skill or command rather than every prompt, the docs point to the UserPromptExpansion event instead (source: https://code.claude.com/docs/en/hooks, checked 2026-10-20). For why hooks beat prose rules in the first place, see Claude Code Hooks in Practice.
Summary
- UserPromptSubmit runs before every prompt, including scheduled tasks, subagent reports and messages from other sessions.
- The default timeout is 30 seconds, and a timed-out hook’s context is dropped. Keep these hooks fast.
- Plain stdout on exit 0 becomes context. Add only what Claude needs on most turns.
- Block with
decision: "block"; thereasongoes to you, not to Claude. - A blocked prompt can still be written to local files. Rotate any key that was pasted.