Skip to content

About

Zero-dependency hooks for Claude Code: safety guardrails, secret scanning, auto-format, audit log, notifications. Tested against v2.1.276.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Claude Code Hooks

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.

Quick start

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 full

Or 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.

The hooks

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).

The guardrails, in action

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.

How the guards decide

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.

Tested, actually

bash tests/run.sh

48 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.

All 33 hook events (2026 reference)

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.

Troubleshooting: my hook doesn't fire

The classics, all verified against current behavior:

  • Matchers are exact since v2.1.195. mcp__brave-search matches nothing anymore; write mcp__brave-search__.*. Matchers are case-sensitive: bash never matches Bash.
  • 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 /hooks to confirm registration and check the transcript (Ctrl+O) for hook error notices on first run.
  • JSON fields at the wrong nesting level are silently ignored. permissionDecision and additionalContext must live inside hookSpecificOutput (with hookEventName set). A top-level additionalContext parses fine and does nothing.
  • Shell profiles can poison stdout. If your .bashrc echoes 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.

Run it always-on

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.

Related

License

MIT

About

Zero-dependency hooks for Claude Code: safety guardrails, secret scanning, auto-format, audit log, notifications. Tested against v2.1.276.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages