headroom.walls.sh · hooks
Claude Code hooks
Shell commands that Claude Code runs at specific points in its lifecycle — before tool use, after tool use, when the session ends, and on the status line.
What hooks are for
Claude Code's hooks system lets you wire up shell scripts to lifecycle events. A hook is any shell command — a script, a binary, a one-liner. Common uses: logging every tool call to a file, sending a desktop notification when a session ends, blocking certain bash commands, updating an external dashboard, or writing usage data for another app to read.
Hooks are configured in ~/.claude/settings.json under the "hooks" key. They run locally, synchronously (for PreToolUse) or asynchronously (for PostToolUse and Stop), and have no access to the Anthropic API — just your machine.
Hook types
| Hook | When it runs | Can block? |
|---|---|---|
| PreToolUse | Before Claude Code executes a tool (Bash, Read, Edit, Write, etc.) | Yes — non-zero exit cancels the tool call |
| PostToolUse | After a tool completes, before the next turn | No |
| Stop | When a Claude Code session ends (user exits, /quit, Ctrl+D) | No |
| SubagentStop | When a subagent finishes (in --agent runs) | No |
| statusLineHook | Periodically — output appears in the Claude Code status line | No |
Hook configuration
All hooks go in the "hooks" object in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "/path/to/pre-bash-hook.sh" }]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [{ "type": "command", "command": "/path/to/log-all-tools.sh" }]
}
],
"Stop": [
{ "type": "command", "command": "osascript -e 'display notification "Claude Code session ended" with title "Claude Code"'" }
],
"statusLineHook": "~/.claude/headroom-hook.sh"
}
}
PreToolUse hooks
Runs before every tool call. The hook receives a JSON payload on stdin with the tool name and input. If the hook exits non-zero, the tool call is cancelled and the exit output is shown to Claude Code as an error.
Block destructive commands
#!/bin/bash # ~/.claude/block-rm-rf.sh INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""') if echo "$COMMAND" | grep -qE 'rm -rf /|rm -rf ~'; then echo "Blocked: rm -rf on root or home is not allowed" exit 1 fi exit 0
# settings.json
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/block-rm-rf.sh" }]
}]
Log every tool call
#!/bin/bash # ~/.claude/log-tools.sh echo "$(date -Iseconds) PRE $(cat | jq -c '.')" >> ~/.claude/tool-log.jsonl
PostToolUse hooks
Runs after a tool completes. Receives the same JSON payload plus the tool's output. Non-zero exit is ignored (the tool already ran). Good for logging, notifications, and side effects.
#!/bin/bash # ~/.claude/notify-on-write.sh INPUT=$(cat) TOOL=$(echo "$INPUT" | jq -r '.tool_name') if [ "$TOOL" = "Write" ] || [ "$TOOL" = "Edit" ]; then FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // "file"') osascript -e "display notification \"Wrote $FILE\" with title \"Claude Code\"" fi
Stop and SubagentStop hooks
Run when the session exits. No stdin payload — just a chance to clean up, log, or notify.
# Notify when a long session ends
"Stop": [{
"type": "command",
"command": "osascript -e 'display notification "Session complete" with title "Claude Code" sound name "Glass"'"
}]
statusLineHook — the status bar
The statusLineHook is different from the others: it's a polling hook that runs on a short interval (roughly every few seconds while Claude Code is active). Its stdout output appears directly in the Claude Code status line — the line at the bottom of the terminal showing current session info.
# settings.json — a simple clock in the status line "statusLineHook": "date '+%H:%M'" # Or a usage counter from an external file "statusLineHook": "cat ~/.claude/headroom-usage.json | jq -r '.statusLine // ""'"
The statusLineHook can display anything: git branch, battery level, a custom prompt counter, or Claude Code's own usage data. The output is shown verbatim — keep it short (under 60 chars) so it fits on one line.
Headroom uses the statusLineHook to capture Claude Code's session (5h) and weekly (7d) utilization numbers and write them to ~/.claude/headroom-usage.json. Headroom reads that file and shows the numbers as a live % in your macOS menu bar. No network calls, no tokens, no API — just a local file written by the hook and read by the app.
brew install patwalls/tap/headroom
Chaining hooks
Each hook type accepts an array of hooks objects — they run in order. You can also chain your own script with an existing statusLineHook: Headroom's installer automatically detects an existing hook and wraps it so both run.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/log-bash.sh" },
{ "type": "command", "command": "~/.claude/notify-bash.sh" }
]
}
]
}
}
→ statusLineHook deep-dive
→ settings.json reference
→ Tool permissions and allow rules
→ Logging Claude Code usage over time