Drop-in automation hooks for Claude Code. Battle-tested across 200+ sessions.
Built by Edgeless Labs -- the hooks we wish Claude Code shipped with.
Claude Code hooks are scripts that run automatically in response to events -- before/after tool calls, on session start/end, and more. They enforce conventions, prevent mistakes, and automate workflows without manual intervention.
Every hook receives a JSON payload on stdin and exits with a code that tells Claude Code what to do:
| Exit code | Meaning |
|---|---|
0 |
Allow / continue |
2 |
Block the operation (stderr message is shown to Claude) |
| Hook | Event | Purpose |
|---|---|---|
damage-control |
PreToolUse | Blocks dangerous commands and writes to protected paths |
no-force-push |
PreToolUse | Prevents force pushes to main, master, production |
security-scan |
PreToolUse | Detects API keys and secrets before they reach disk |
branch-naming |
PreToolUse | Enforces type/description branch naming convention |
verify-completion |
PostToolUse | Warns when completion is claimed without test evidence |
auto-commit-message |
PostToolUse | Validates Conventional Commits format after each commit |
lint-on-save |
PostToolUse | Runs linter on the file that was just written |
test-on-change |
PostToolUse | Runs the corresponding test file after source changes |
cost-tracker |
Stop | Logs token usage and estimated cost per session to SQLite |
session-logger |
Stop | Appends a summary line to a session CSV log |
git clone https://github.com/edgeless-ai/claude-code-hooks.git
cd your-project
../claude-code-hooks/install.shgit clone https://github.com/edgeless-ai/claude-code-hooks.git
cd your-project
../claude-code-hooks/install.sh damage-controlgit clone https://github.com/edgeless-ai/claude-code-hooks.git
# Copy the hook config
cp claude-code-hooks/hooks/damage-control.json .claude/hooks/
# Copy the script
cp claude-code-hooks/scripts/damage-control.py .claude/hooks/scripts/
cp claude-code-hooks/scripts/damage-control-patterns.yaml .claude/hooks/scripts/
chmod +x .claude/hooks/scripts/*.pyAfter copying the files, register each hook in your .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|MultiEdit|Read",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/scripts/damage-control.py"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/scripts/no-force-push.py"
},
{
"type": "command",
"command": "python3 .claude/hooks/scripts/branch-naming.py"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/scripts/security-scan.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/scripts/verify-completion.py"
},
{
"type": "command",
"command": "python3 .claude/hooks/scripts/auto-commit-message.py"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/scripts/lint-on-save.py"
},
{
"type": "command",
"command": "python3 .claude/hooks/scripts/test-on-change.py"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/scripts/cost-tracker.py"
},
{
"type": "command",
"command": "python3 .claude/hooks/scripts/session-logger.py"
}
]
}
]
}
}Blocks dangerous operations before they execute. Configure rules in damage-control-patterns.yaml:
dangerous_commands.bash.block_patterns-- regex patterns matched against every Bash command. Catchesrm -rf /,git push --force,DROP TABLE,curl | bash, and more.paths.zero_access-- paths Claude can never read or write (credential files, SSH keys).paths.read_only-- paths that can be read but never modified (node_modules, vendor, dist).paths.no_delete-- paths that can be modified but never deleted (src, package.json).allowlist-- path substrings that bypass all protections (e.g. /tmp/).
Blocks git push --force and git push -f to protected branches. Protected by default: main, master, production, prod, and any branch matching release/* or hotfix/*.
Configure PROTECTED_BRANCHES in the script.
Scans file content before it is written. Detects:
- OpenAI, Anthropic, AWS, Google API keys
- GitHub PATs (classic and fine-grained)
- Slack tokens, Telegram bot tokens
- JWTs and private key blocks
- Generic
password=,secret=,api_key=assignments
Suppress a false positive with an inline comment: # noqa: secrets
Enforces <type>/<description> branch naming when Claude creates branches. Default allowed types: feat, fix, chore, docs, refactor, test, ci, perf, hotfix, release, experiment.
Configure ALLOWED_PREFIXES and NAME_PATTERN in the script.
Two modes:
Hook mode (PostToolUse/Bash): Detects when Claude claims a task is complete ("task complete", "all done", etc.) and warns if no verifiable evidence is present in the output (test counts, build success, coverage %).
CLI mode (standalone): Runs project-specific checks and exits 0 (PASS) or 1 (FAIL).
# CLI mode
python3 .claude/hooks/scripts/verify-completion.py --cli --type python
python3 .claude/hooks/scripts/verify-completion.py --cli --type typescript
python3 .claude/hooks/scripts/verify-completion.py --cli --type go --verboseAfter each git commit, validates the commit subject against Conventional Commits format. If invalid, prints a suggested message Claude can use to amend. Does not block -- a bad message is better than no commit.
After Write/Edit/MultiEdit, runs the appropriate linter on the changed file:
| Extension | Linter |
|---|---|
.py |
ruff (preferred), flake8 |
.ts/.tsx/.js/.jsx |
eslint |
.go |
golangci-lint, go vet |
.rb |
rubocop |
.sh/.bash |
shellcheck |
Disable per-session: LINT_ON_SAVE_ENABLED=0
After Write/Edit/MultiEdit, finds the corresponding test file and runs it:
src/auth.pytriggerstests/test_auth.pycomponents/Button.tsxtriggerscomponents/Button.test.tsx
If the modified file is itself a test file, runs it directly.
Disable per-session: TEST_ON_CHANGE_ENABLED=0
On Stop, logs token usage and estimated USD cost to .claude/hooks/cost-tracker.db (SQLite). Pricing table is embedded in the script -- update it when Anthropic prices change.
# View cost report
python3 .claude/hooks/scripts/cost-tracker.py --report
python3 .claude/hooks/scripts/cost-tracker.py --report --days 30Override the database path: COST_TRACKER_DB=/path/to/db
On Stop, appends one line to .claude/session-log.csv:
timestamp,session_id,duration_secs,tool_count,project,notes
# View recent sessions
column -t -s, .claude/session-log.csv | tail -20Every hook follows the same pattern:
#!/usr/bin/env python3
import json, sys
def main() -> None:
# Read the tool use event from stdin
try:
hook_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0) # Always fail open on parse errors
tool_name: str = hook_data.get("tool_name", "")
tool_input: dict = hook_data.get("tool_input", {})
# PreToolUse: exit 2 to block, exit 0 to allow
if should_block(tool_input):
print("BLOCKED: reason here", file=sys.stderr)
sys.exit(2)
sys.exit(0)
# PostToolUse / Stop: print JSON response
# print(json.dumps({"continue": True}))
if __name__ == "__main__":
main()Key rules:
- Always fail open: on parse errors or unexpected exceptions, exit 0 (allow). Hooks should never break the agent.
- Print blocks to stderr: Claude Code displays stderr content to Claude.
- Keep hooks fast: they run synchronously before/after every tool call.
- Scrub secrets from logs: if you log tool inputs, redact credential patterns.
claude-code-hooks/
hooks/ # JSON configs (one per hook, declares event + matcher)
scripts/ # Python implementation scripts
damage-control.py
damage-control-patterns.yaml
no-force-push.py
security-scan.py
branch-naming.py
verify-completion.py
auto-commit-message.py
lint-on-save.py
test-on-change.py
cost-tracker.py
session-logger.py
install.sh # Installer
README.md
LICENSE
- Python 3.10+ (uses
str | Nonetype union syntax) - No third-party Python packages required -- stdlib only
- Linter hooks require the relevant linter to be installed (optional)
- edgelesslab.com -- AI agents, MCP servers, generative art
- Prompt Engineering OS -- Complete prompt engineering system
MIT -- see LICENSE