Six scripts that hook into Claude Code to enforce safety, track sessions, send notifications, and display context information.
| Script | Required | Optional | Setup Notes |
|---|---|---|---|
guard-bash.sh |
jq |
— | PreToolUse hook; no setup needed |
on-permission.sh |
jq, alerter |
— | PreToolUse (all tools) hook; brew install vjeantet/tap/alerter |
on-stop.sh |
jq, alerter |
— | Stop hook; brew install vjeantet/tap/alerter; requires System Settings → Notifications setup |
on-file-change.sh |
jq |
— | PostToolUse hook; no setup needed |
on-session-end.sh |
jq, curl |
Anthropic API token in Keychain | SessionEnd hook; runs in background to avoid 60s timeout |
status-line.sh |
jq, git, curl |
Anthropic API token in Keychain | Custom status line; 10-min cache at /tmp/claude-usage-last-good.json |
Type: PreToolUse hook Purpose: Blocks dangerous bash commands before execution
Reads stdin JSON containing the bash command (tool_input.command), validates it against five categories of dangerous patterns, and exits with code 2 to block unsafe execution. Exit code 2 feeds the block reason back to Claude.
-
Secret / credential files
- Blocks read, write, copy, move, or delete of sensitive files
- Examples:
.env,.env.*,.envrc,secrets.json,.netrc,.pgpass,id_rsa,id_ed25519,id_ecdsa,*.pem,*.key,*.p12,*.pfx
-
Dangerous SQL operations
DROP TABLE|INDEX|SCHEMA|DATABASE|COLUMN|VIEW|FUNCTION|TRIGGER|SEQUENCETRUNCATE TABLEDELETE FROM ... ;without a WHERE clauseUPDATE ...without a WHERE clauseALTER TABLE ... DROP COLUMN
-
Destructive filesystem operations
rm -rf(any flag ordering:-rf,-fr,-r -f,-f -r,--force --recursive, etc.)find -deleteorfind -exec rm(equivalent to recursive deletion on matched files)- Output redirection (
>) to source/config files (.ts,.tsx,.js,.jsx,.mjs,.cjs,.json,.yaml,.yml,.toml,.sh,.bash,.zsh,.sql,.env,.conf,.config,.ini,.prisma,.py,.rb,.go,.rs,.vue,.svelte,.scss,.css,.lock) chmod 777(security risk)
-
Irreversible Git operations
git push --forceorgit push -fgit reset --hard
-
Accidental publishing
npm publish,pnpm publish,yarn publish
- Exit code
0: command passes all checks - Exit code
2: command blocked; stderr message explains why - Patterns are normalized (whitespace collapsed, uppercase → lowercase) for matching
- Pattern matching handles varying flag order and spacing
Every blocked command is appended as a JSONL entry to ~/.claude/guard-blocked.log with fields: timestamp, command, reason.
Read the last 5 blocks:
tail -5 ~/.claude/guard-blocked.log | jq '.'Type: PreToolUse hook (all tools) Purpose: Sends a macOS desktop notification when Claude is about to use a tool that requires permission
Fires before every tool invocation. Skips read-only tools and auto-approved permission modes to avoid noise. For actionable tools, sends a notification so you know a permission prompt is coming.
- Auto-approved modes:
acceptEdits,dontAsk,bypassPermissions— exits 0 silently - Read-only tools:
Read,Glob,Grep,LS— exits 0 silently
| Tool | Message |
|---|---|
Bash |
Run: <first 60 chars of command> |
Edit, MultiEdit |
Edit: <filename> |
Write |
Write: <filename> |
| Any other tool | <ToolName> |
Title: Latuconsinafr x Claude Code
Subtitle: 🔐 Permission needed [project]
Sound: Basso
Icon: ~/.claude/claude-icon.png
The PreToolUse hook fires for all tool uses, including those already auto-approved. There is no field in the payload that reliably distinguishes "will prompt user" from "already approved". The script uses permission_mode to filter known auto-approve modes, but in default mode it cannot tell — so some notifications may fire even when no Y/N prompt appears.
brew install vjeantet/tap/alerter
# Then enable alerter in macOS System Settings → Notifications (set to Alerts, not Banners)If alerter is not installed, falls back to osascript.
Type: Stop hook Purpose: Sends a macOS desktop notification when Claude finishes a task
Reads the Stop hook payload (cwd, transcript_path, last_assistant_message), extracts task stats from the transcript, infers the stop reason, formats a notification, and sends it via alerter or osascript.
cwd: working directory (used to derive$PROJECT = basename)transcript_path: path to JSONL session transcriptlast_assistant_message: Claude's final message in the session
Title: Latuconsinafr x Claude Code
The subtitle is inferred from the last_assistant_message content:
- Ends with
?→🧠 Input needed [project] - Otherwise →
✅ Task complete [project]
Project name is appended in brackets (derived from basename $cwd).
Two-line format:
- Line 1:
{turns} turns · {tool_calls} tool calls(omitted if no tool uses) - Line 2: First 80 characters of last assistant message with markdown stripped, truncated with
…(omitted if empty)
Markdown removal strips:
- Code blocks (triple backticks)
- Inline code (backticks)
- Bold (
**text**) - Emphasis (
*text*) - Headings (
# text) - Bullet list markers
- Numbered list markers
Sound: Glass (macOS notification sound)
Icon: ~/.claude/claude-icon.png
Counts JSON lines in JSONL transcript:
- Tool calls:
grep '"type":"tool_use"' - User turns:
grep '"role":"user"'
alerter provides native macOS notification UI with better styling than osascript:
brew install vjeantet/tap/alerterThen in macOS System Settings:
- Go to Notifications
- Find "alerter" in the list
- Set "Alert Style" to "Alerts" (not "Banners")
If alerter is not installed, uses osascript (built-in macOS notification, simpler UI). Note: fallback also uses Latuconsinafr x Claude Code as title.
echo '{"cwd":"/Users/you/project","transcript_path":"/dev/null","last_assistant_message":"Updated README and fixed import?"}' | bash on-stop.shType: SessionEnd hook
Purpose: Logs session metadata + AI-generated summary to ~/.claude/session-log.jsonl
Runs entirely in a background subshell to avoid the 60-second hook timeout. Extracts user turns from the transcript, calls the Anthropic API (Haiku model) to generate a summary, and writes a single JSON line to the session log.
{
"timestamp": "2026-03-24T15:30:42Z",
"session_id": "uuid",
"project": "project-name",
"cwd": "/path/to/project",
"reason": "stop|user_stop|error|...",
"messages": 42,
"compactions": 1,
"summary": "Fixed bug in auth middleware and added unit tests.",
"modified_files": ["/path/to/file1.ts", "/path/to/file2.js"]
}modified_files is a deduplicated array of file paths modified during the session, populated by on-file-change.sh via /tmp/claude-files-{session_id}.txt.
- Counts total messages and compactions in transcript JSONL
- Extracts up to 60 user turns from transcript (capped to limit API token usage)
- Retrieves OAuth token from macOS Keychain:
security find-generic-password -s "Claude Code-credentials" -w - POSTs user turns to Anthropic API (claude-haiku-4-5-20251001) with a prompt requesting a 2–3 sentence summary
- Parses response text and writes to log
jq(parsing JSON)curl(API calls; 30s timeout, fails gracefully)- Anthropic API OAuth token in macOS Keychain (reads automatically; logs gracefully if missing)
Each line contains a message or annotation. Fields read:
role("user", "human", or "assistant")content(string or array of text objects)subtype("compact_boundary" to count compactions)
If transcript is missing or token unavailable, summary is set to "(no transcript)" or "(no token available for summarization)".
tail -1 ~/.claude/session-log.jsonl | jq '.'The entire logging logic runs in a background subshell: ( ... ) &>/dev/null &
This prevents the hook from hitting the 60-second timeout. The hook exits immediately (exit code 0) without waiting for the background process.
Type: Custom Claude Code status line Purpose: Displays working context, model, and quota information in a single line
Reads stdin JSON from Claude Code (workspace.current_dir, model.display_name, context_window.used_percentage, transcript_path), fetches quota data from the Anthropic API, and renders a formatted status line with ANSI colors and Nerd Font icons.
👤 farista in 📁 project on 💜 main* [ Sonnet · 45% ctx left · 2] ( 12% 5h (2h 30m) · 45% 7d (3d 2h))
Breakdown:
- User:
whoami - Directory:
basename $cwd - Git branch: current branch (shown as
branch*if dirty/uncommitted changes) - Model context:
[ ModelName · X% ctx left · N]- Context percentage = 100% minus
context_window.used_percentage - Compaction count = count of
"subtype":"compact_boundary"in transcript JSONL (only shown if > 0)
- Context percentage = 100% minus
- Quota:
( X% 5h (Yh Zm) · X% 7d (Yd Zh))- 5-hour rolling quota with time until reset
- 7-day rolling quota with time until reset
| Metric | <50% | 50–79% | ≥80% |
|---|---|---|---|
| Context usage | Green | Yellow | Red |
| 5h quota | Green | Yellow | Red |
| 7d quota | Green | Yellow | Red |
| Compactions | Dim | Yellow (≥1) | Red (≥3) |
Fetches from Anthropic API endpoint: https://api.anthropic.com/api/oauth/usage
Header: Authorization: Bearer $TOKEN
Header: anthropic-beta: oauth-2025-04-20
Cached: 10 minutes at /tmp/claude-usage-last-good.json
Token is extracted from macOS Keychain:
security find-generic-password -s "Claude Code-credentials" -w | jq -r '.claudeAiOauth.accessToken'10-minute cache at /tmp/claude-usage-last-good.json prevents excessive API calls. Cache is:
- Reused if modification time is < 600 seconds old
- Overwritten if fresh API data is retrieved
- Shared with other tools (e.g.,
on-session-end.sh)
If cache is stale or empty, the script fetches new data from the API. If the API call times out (3s max) or token is unavailable, quota display is omitted.
From CLAUDE.md, the echo pipe command:
echo '{"workspace":{"current_dir":"'"$PWD"'"},"model":{"display_name":"Sonnet"},"context_window":{"used_percentage":42}}' | bash status-line.shThis renders a minimal status line without quota (since no token is in the test payload).
jq(JSON parsing)git(branch info; returns empty if not in a repo)curl(API calls, 3s timeout; gracefully omits quota if unavailable)date(BSD on macOS; also supports GNUdatefallback for Linux)- Anthropic API OAuth token in macOS Keychain (optional; quota omitted if unavailable)
Type: PostToolUse hook Purpose: Tracks files modified by Claude per session
Fires on Edit, Write, and MultiEdit tools only (exits 0 silently for all others). For each matching tool invocation, appends the file path to /tmp/claude-files-{session_id}.txt.
- Fires on: Edit, Write, MultiEdit only
- Reads:
session_idandfile_pathfrom PostToolUse payload - Writes: Each modified file path to
/tmp/claude-files-{session_id}.txt - Exit: Always exits 0 (never blocks)
When the session ends, on-session-end.sh reads this temp file, deduplicates the paths (via sort -u), converts to a JSON array, and includes it as modified_files in the session log JSONL entry. The temp file is then deleted.
jq(reading JSON payload)
This hook runs automatically with no configuration or installation required.