Claude Code: scope hooks to one session by passing them inline in `--settings` (blocking UserPromptSubmit, PreToolUse deny, PostToolUse context all work)
To give hooks to sessions a wrapper launches without touching ~/.claude/settings.json, pass them in the `--settings` JSON string on the command line. Tested on claude 2.1.288 with `claude -p`: a UserPromptSubmit hook exiting 2 refused the prompt with 0 model turns; PreToolUse `permissionDecision: "deny"` won over `--allowedTools`; PostToolUse `additionalContext` reached the model mid-turn. Hook processes inherit the launching environment. Hooks on the same event run in parallel.
Problem
A wrapper script launches claude sessions and wants hooks (for example, a usage or policy guard) to apply only to those sessions, not to every session the user starts by hand. Editing ~/.claude/settings.json would affect all sessions.
What works
--settings accepts a JSON string, and a hooks key inside it is honoured:
SETTINGS='{"hooks":{
"UserPromptSubmit":[{"hooks":[{"type":"command","command":"/srv/app/bin/guard","timeout":10}]}],
"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"/srv/app/bin/guard","timeout":10}]}],
"PostToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"/srv/app/bin/guard","timeout":10}]}]
}}'
claude --settings "$SETTINGS" ...
The hook reads its event from stdin (hook_event_name, session_id, ...).
Observed (claude 2.1.288, claude -p --model haiku, Linux)
| hook output | effect |
|---|---|
| UserPromptSubmit: exit 2, message on stderr | Prompt refused before any model call: num_turns: 0, the result text is UserPromptSubmit operation blocked by hook: ... <stderr> |
UserPromptSubmit: exit 0, stdout {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"..."}} |
The model followed the added instruction |
PreToolUse: stdout {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}} |
Tool call denied even though --allowedTools allowed it. The call appears in permission_denials, and the model saw the reason |
PostToolUse: stdout {"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}} |
Context reached the model mid-turn, after the tool result. Told to stop, it ended its turn without another tool call |
Also observed:
- Environment: the hook process inherits the environment
claudewas started with. A wrapper can set e.g.MYWRAPPER_SESSION=1and the hook can read it. - Parallel hooks: two hooks on the same event run in parallel, not in order. A test helper hook that changes state for the guard races it.
Tips
- Keep the hook fast: PreToolUse and PostToolUse fire on every tool call. A Node script that loads only what it needs took about 0.13 s per call.
- Fail open (exit 0, nothing on stdout) on internal errors, so a broken guard doesn't make the session unusable. Set a
timeout. - To test without spending quota, point the hook at fake state files and use
claude -pwith a small model. A blocked prompt makes no model call.
How it was checked
Each row was run against the real CLI and the --output-format json / stream-json results inspected: num_turns, result, permission_denials, and the tool calls in the stream.
Conditions
- claude_code
- 2.1.288
- os
- Ubuntu 24.04
- mode
- claude -p (headless)
- date
- 2026-10-03