Production-ready hooks for Claude Code with zero dependencies: safety guardrails, secret protection, format-on-save, an audit log, desktop notifications, and transcript backups. Every hook is plain bash or Python standard library, so there is nothing to install: no Node, no uv, no Bun.
Tested against Claude Code v2.1.276 (September 2026): a 48-assertion test suite runs in CI on every push to main and every pull request (Ubuntu + macOS), and the guardrails were verified in a live session (the force-push deny below is a real transcript, not a mock).
Hooks are the deterministic layer of Claude Code: a CLAUDE.md rule is a suggestion the model can forget, a hook is code that always runs. And the key property, straight from the official docs: a PreToolUse deny fires before every permission check and cannot be bypassed by any permission mode, including --dangerously-skip-permissions. These guardrails keep working exactly when you need them most: in YOLO mode.
As a plugin (one marketplace add, one install):
/plugin marketplace add agent37-platform/claude-code-hooks
/plugin install claude-code-hooks@agent37-hooks
Or into one project (copies the scripts, writes .claude/settings.json only if you don't have one):
git clone https://github.com/agent37-platform/claude-code-hooks.git
cd claude-code-hooks
./install.sh --preset safety --target ~/code/your-project # or --preset fullOr by hand: copy the scripts you want from hooks/ into your project's .claude/hooks/, chmod +x them, and add the matching snippet below to .claude/settings.json. Then run /hooks inside Claude Code to confirm they registered.
| Hook | Event | What it does |
|---|---|---|
bash-guard.py |
PreToolUse (Bash) | denies rm -rf /, force-pushes to main (incl. +refspec), dd to disk devices, fork bombs; asks on curl | sh, git reset --hard, reading or shell-writing .env |
file-guard.py |
PreToolUse (file tools) | denies writes to .env and credential files, denies reads of private keys, asks before reading .env |
secret-scan.py |
UserPromptSubmit | blocks prompts containing API keys, tokens, or private key material before they reach the model |
auto-format.py |
PostToolUse (Edit/Write) | formats the file Claude just touched with whatever formatter your project already has (ruff, black, prettier, gofmt, rustfmt, ...) |
audit-log.py |
PostToolUse + PostToolUseFailure | appends one JSONL line per tool call (and per failure) to .claude/tool-log.jsonl |
notify.sh |
Notification + Stop | desktop notification when Claude needs input or finishes (osascript / notify-send / terminal bell) |
git-context.sh |
SessionStart | injects branch, dirty-file count, and recent commits so sessions start oriented |
precompact-backup.sh |
PreCompact | snapshots the full transcript before lossy compaction, keeps the 20 newest |
Two presets wire them up: examples/settings.safety.json (the three guards) and examples/settings.full.json (everything).
This is a real v2.1.276 session with the full preset installed, asked to force-push and then echo:
> Run exactly this bash command: git push --force origin main
Then run exactly: echo e2e-ok
`git push --force origin main`: a safety hook (bash-guard) blocked it before
it ran, since it stops force-pushes to main, so nothing was pushed.
`echo e2e-ok`: succeeded and printed `e2e-ok`.
The audit log recorded exactly one entry: the echo that actually executed. Denied calls never run, so they never reach PostToolUse.
bash-guard.py and file-guard.py return a permission decision: deny cancels the call and tells Claude why, ask forces the human prompt even in auto mode, anything else falls through to your normal permission flow. Tune them by editing the DENY_PATTERNS / ASK_PATTERNS lists at the top of each script; every pattern carries its own explanation string.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/bash-guard.py\"" }
]
}
]
}
}Honest limits: these guards are seatbelts, not a sandbox. They match patterns, and a sufficiently creative command (or a prompt-injected agent trying on purpose) can evade a regex. Hooks also run with your full user permissions, so review any hook before installing it, including these. For a hard boundary, run the agent inside a container and let the guards handle the everyday accidents; our claude-code-docker is a pinned, egress-firewalled image built for exactly that.
bash tests/run.sh48 assertions pipe recorded event payloads through every hook and check exit codes and decisions: denies deny, asks ask, safe commands pass untouched, malformed payloads never break a session (every guard fails open on bad or wrong-shape JSON rather than bricking your tool calls). CI runs the suite on Ubuntu and macOS on every push to main and every pull request. The fixtures in tests/fixtures/ double as reference payloads for writing your own hooks.
Claude Code started 2025 with about ten hook events; v2.1.276 has 33, plus four handler types beyond shell commands (http, mcp_tool, prompt, agent). Most hook collections and tutorials still cover only the original set. docs/EVENTS.md is the full current list: every event, when it fires, matcher support, the version that added it, and the output contract essentials.
The classics, all verified against current behavior:
- Matchers are exact since v2.1.195.
mcp__brave-searchmatches nothing anymore; writemcp__brave-search__.*. Matchers are case-sensitive:bashnever matchesBash. - Exit 1 does not block. Only exit 2 (or a valid JSON decision) blocks. A policy hook that exits 1 is a no-op with a notice buried in the transcript.
- A mistyped script path silently disables the gate. Exit 127 is a non-blocking error and the action proceeds. After installing, run
/hooksto confirm registration and check the transcript (Ctrl+O) forhook errornotices on first run. - JSON fields at the wrong nesting level are silently ignored.
permissionDecisionandadditionalContextmust live insidehookSpecificOutput(withhookEventNameset). A top-leveladditionalContextparses fine and does nothing. - Shell profiles can poison stdout. If your
.bashrcechoes anything, hook stdout no longer starts with{and your JSON is treated as plain text. Guard profile output with[[ $- == *i* ]]. - Settings edits not showing up? Validate the JSON (no trailing commas), then check
/hooks; if it still isn't listed, restart the session. - Debug everything with
claude --debug-file /tmp/claude-debug.txt; the log shows which hooks matched, their exit codes, and full output.
Hooks like these matter most on agents that run unattended: a scheduled Claude Code session with no human watching is exactly where you want force-push protection, secret guards, and an audit log. Agent37 Cloud runs Claude Code as an always-on instance on your own Anthropic account, from $1.99/mo shared or $4.94/mo dedicated (2 vCPU / 4 GB), metered per minute with a $1 starter credit and no card required. Drop this repo's install.sh into the instance's workspace and every scheduled run gets the same guardrails.
- claude-code-docker: pinned, non-root Claude Code image with an egress firewall; the hard boundary these hooks complement
- agent37-skills-collection: Claude Code skills and commands from the same team
- anthropics/claude-code examples/hooks: the official example validator these guards generalize
MIT