Claude Code hooks
The UserPromptExpansion hook in Claude Code
UserPromptExpansion fires when a typed command expands into a prompt, before Claude sees it. It matches on the command name and can block the expansion.
expanded_prompt is the field worth reading closely: it carries the actual text about to reach Claude, which is not guaranteed to match the command's source file once arguments and any templating are resolved. A hook auditing what a command really sends should read this field rather than assume from command_name alone.
- When it fires
- When a command you typed expands into a prompt, before that prompt reaches Claude.
- Can it block?
- Yes. Exit 2 blocks the expansion.
- Matcher
- Yes, on
command_name - Matcher examples
post-weekreview|deploy
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
command_name | string | Which command was typed. Also what the matcher filters on. |
expanded_prompt | string | The full prompt the command expanded into. |
prompt_id | string | Identifies this turn. |
session_id | string | Stable for the session. |
cwd | string | Working directory. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Success. Anything on stdout is added to the context. |
2 | Blocks the expansion. stderr is shown to Claude. |
other | Non-blocking error. |
What it can return
Matching on command_name is what makes this useful. It lets one command carry rules the others do not, without those rules living inside the command itself where they can be edited away.
{
"decision": "block",
"reason": "string",
"additionalContext": "string",
"systemMessage": "string"
}
Stopping a publishing command from running on the wrong day
A command that queues a week of posts should not run mid-week and quietly overwrite the queue. Checking that here refuses it before Claude has read a single instruction, which is cheaper than having Claude work it out and stop.
In settings.json
{
"hooks": {
"UserPromptExpansion": [
{
"matcher": "post-week",
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/weekend-only.mjs" }
]
}
]
}
}
The script
const day = new Date().getDay();
if (day === 0 || day === 6) process.exit(0);
console.error("This one is a weekend job. Running it now would overwrite the queue.");
process.exit(2);
The prompt is blocked before Claude reads it, so nothing is spent on a run that was never going to be wanted.
The catch
This is a different event from UserPromptSubmit. Typing a message raises UserPromptSubmit; typing a command that expands raises this one as well. A rule you want on both has to be attached to both.