Firing a Shell Command on PostToolUse with Hooks
A PostToolUse hook is a shell command that runs automatically right after a matched tool call finishes.
Search across all documentation pages
A PostToolUse hook is a shell command that runs automatically right after a matched tool call finishes.
The most common use is auto-formatting: the moment Claude edits a file, a formatter runs against it without anyone asking.
Hooks are configured in settings.json, under an event name like PostToolUse.
Each hook entry has a matcher, which limits it to specific tools such as Edit or Write, and one or more hooks commands to run when that matcher fires.
Unlike a slash command, nothing about a hook goes through the model. The harness runs the shell command directly and does not ask Claude whether it should.
A hook receives details about the completed tool call as JSON, typically on stdin, including which file was touched.
Because the hook is deterministic, it is the right place for anything that should happen every single time, with zero exceptions and zero judgment calls.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
}
]
}
]
}
}When to reach for this:
#!/usr/bin/env bash
# .claude/hooks/format-on-edit.sh
# Reads the PostToolUse event payload from stdin and formats the touched file.
set -euo pipefail
payload="$(cat)"
file_path="$(echo "$payload" | jq -r '.tool_input.file_path // empty')"
if [ -z "$file_path" ]; then
exit 0
fi
case "$file_path" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
npx prettier --write "$file_path"
echo "Formatted: $file_path"
;;
*)
exit 0
;;
esac{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/format-on-edit.sh"
}
]
}
]
}
}What this demonstrates:
matcher field limits the hook to Edit and Write tool calls, so it never fires on, say, a Bash command.jq, rather than guessing at an environment variable.case statement narrows formatting to file types Prettier actually understands, so the hook silently no-ops on files it shouldn't touch.set -euo pipefail makes the script fail loudly on unexpected errors instead of silently doing nothing.PostToolUse event immediately after a tool call completes successfully.PostToolUse hook's matcher against the tool that just ran; only matching hooks execute.command runs as a plain shell command, with the event payload (tool name, tool input, and result) available to it, typically via stdin as JSON.| Matcher | Matches |
|---|---|
Edit | Only the Edit tool. |
Write | Only the Write tool. |
Edit|Write | Either Edit or Write (regex alternation). |
* or omitted | Every tool call, regardless of which tool ran. |
# Prefer reading structured input over relying on environment variables
# that may or may not be set consistently across hook implementations.
payload="$(cat)"
file_path="$(echo "$payload" | jq -r '.tool_input.file_path // empty')"
# Guard against an empty path before doing any work.
[ -z "$file_path" ] && exit 0settings.json, since it is easier to test, diff, and reason about in isolation.*) fires even on unrelated tools like Bash or Read, wasting cycles and cluttering output. Fix: set a matcher that names exactly the tools the hook cares about, such as Edit|Write.case statement above) so the hook silently exits on files it isn't meant to touch.| Alternative | Use When | Don't Use When |
|---|---|---|
| Asking Claude to format the file in the prompt | A one-off request, not something that should happen on every edit going forward. | The behavior should be unconditional and automatic for the whole team, not dependent on remembering to ask. |
| A PreToolUse hook | The goal is to validate or block the edit before it happens, not react after it completes. | The action is a side effect that should run after a successful edit, like formatting or logging. |
| A CI pipeline step | The check is expensive (full test suite, full build) and does not need to run on every single local edit. | Fast feedback on every edit matters more than centralizing the check in CI. |
No. PostToolUse fires after the tool call has already completed, so the edit has already happened by the time the hook runs. Blocking belongs to a PreToolUse hook instead.
The event payload includes the tool call's input, typically delivered as JSON on stdin, which contains the file path for Edit and Write tool calls.
Edit or Write.Edit|Write) lets one hook entry cover multiple tools.*, makes the hook fire on every tool call.Yes. Multiple hook entries can be registered under PostToolUse, and every entry whose matcher matches the completed tool call runs.
The non-zero exit is typically surfaced back into the session as a failure signal, which is useful for hooks doing lightweight validation in addition to a side effect like formatting.
No. It should check the file's extension (or path) and only run the formatter against types it actually understands, silently exiting on anything else.
Generally no, since the hook runs on every matching edit and a slow hook adds that latency to every single edit. Slow checks are usually better left to CI or a manually triggered command.
In settings.json, under a hooks.PostToolUse array, where each entry has a matcher and one or more hooks commands to run.
No. The hook's command runs as an independent shell command, separate from the model's reasoning process, and only its output and exit code are reported back.
Yes. The event payload generally includes both the tool's input and its result, which is useful for hooks that want to react differently depending on what the edit actually produced.
Yes, and it is recommended, since a project-scoped hook script in version control means every teammate gets the same automatic formatting or logging behavior without individual setup.
A PostToolUse hook fires after each individual matched tool call completes, while a Stop hook fires once, when the session or turn ends, making it better suited for end-of-turn summaries or cleanup rather than per-edit reactions.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5. Model names, pricing, and product features move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 16, 2026