
Ship Team-Wide Policy in Shell: Claude Code Hooks for Guards, Cleanup, and Remote Enforcement
Chris Harper
3 min read
Jul 20, 2026 · 04:07 UTC
Claude Code hooks are shell scripts the harness runs at every tool call — Claude cannot skip them, making them the right layer for linters, secret guards, and auto-commit.
Prompts and instructions are suggestions. Hooks are contracts. When Claude edits a file, your PostToolUse hook runs ESLint before Claude even forms its next thought. When Claude tries to push to main, your PreToolUse hook can block it and return a message explaining why. The model sees the hook's feedback, not the raw shell exit code.
Four handler types
// .claude/settings.json (commit this — all teammates get the same rules)
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "scripts/secret-guard.sh" }]
}],
"PostToolUse": [{
"matcher": "Edit",
"hooks": [{ "type": "command", "command": "scripts/format.sh" }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "scripts/audit.sh" }]
}]
}
}
- PreToolUse — fires before the tool runs; exit 2 to block and return a message to Claude; exit 0 to allow
- PostToolUse — fires after a write; cannot undo the action, but perfect for auto-format, lint-on-save, and logging
- Stop — runs when Claude finishes a turn; ideal for audit trails, Slack pings, or auto-commit
- HTTP hooks (added Feb 2026) — POST the event JSON to your own endpoint instead of a local script; enables remote policy validation that works uniformly across every machine in your org
Copy-paste examples
Secret scanner (PreToolUse on Bash, blocks git push containing API keys):
#!/bin/bash
# scripts/secret-guard.sh — reads tool_input JSON from stdin
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# block pushes that would expose credentials
if echo "$COMMAND" | grep -q 'git push' 2>/dev/null; then
if git diff --cached | grep -E '(AKIA[0-9A-Z]{16}|sk-ant-|ghp_[a-zA-Z0-9]{36})' >/dev/null 2>&1; then
echo '{"decision":"block","reason":"API key detected in staged diff — remove it before pushing."}'
exit 2
fi
fi
Auto-format after every file edit (PostToolUse on Edit):
#!/bin/bash
# scripts/format.sh — input JSON via stdin
FILE=$(jq -r '.tool_input.path // ""' 2>/dev/null)
[ -z "$FILE" ] && exit 0
case "$FILE" in
*.ts|*.tsx) npx prettier --write "$FILE" 2>/dev/null ;;
*.py) ruff format "$FILE" 2>/dev/null ;;
*.go) gofmt -w "$FILE" 2>/dev/null ;;
esac
exit 0 # never block on format failure
Team vs personal hooks
Hooks in .claude/settings.json are project-scoped and checked into git — everyone on the team gets the same guards. Personal overrides (e.g. a dev's preferred linter flags) go in .claude/settings.local.json (gitignored).
Matchers
The matcher field is a substring match against the tool name. An empty string ("") matches every tool — useful for logging all activity or HTTP hooks that enforce broad policies:
{ "matcher": "", // all tools
"hooks": [{ "type": "http", "url": "https://policy.example.com/check" }] }
{ "matcher": "Bash", // only shell commands }
{ "matcher": "Edit", // only file writes }
{ "matcher": "Write", "hooks": [...] }
Sources: Automate actions with hooks — Claude Code Docs, 15 Claude Code hook examples, Claude Code Hooks: Complete Guide