Claude Code hooks: block dangerous git and enforce your rules.
Instructions ask; hooks enforce. A Claude Code hook is a command that runs at a fixed point, such as before a tool call (PreToolUse), after one (PostToolUse), when you submit a prompt or when a session starts. The hook receives the event as JSON on stdin and can allow, deny or ask, or feed a message back to the agent. Use hooks for the handful of rules that must hold even when the model forgets them.
Block dangerous git before it runs
This PreToolUse hook reads the shell command Claude is about to run and denies force pushes, hard resets and forced cleans. A denial reaches the agent with the reason, so it can ask you instead.
// block-git.mjs: refuse force pushes, hard resets and forced cleans before they run
let raw = '';
process.stdin.on('data', d => { raw += d; }).on('end', () => {
const command = JSON.parse(raw).tool_input?.command || '';
const banned = [
/\bgit\b.*\bpush\b.*(--force(?!-with-lease)|\s-f\b)/,
/\bgit\b.*\breset\s+--hard\b/,
/\bgit\b.*\bclean\s+-[a-z]*f/,
];
if (banned.some(re => re.test(command))) {
console.log(JSON.stringify({ hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: 'Blocked by policy. Ask the owner to run this command.',
} }));
}
});
Register it for shell tool calls. Use an absolute path; the hook runs from whatever folder the session is in.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node /abs/path/hooks/block-git.mjs", "timeout": 3 }
]
}
]
}
}
To ask instead of refusing, return permissionDecision: 'ask': Claude Code then shows you the command and waits. Codex hooks have no ask decision, so the same policy can only deny there.
Enforce a writing rule after a write
A PostToolUse hook on Write|Edit|MultiEdit can read the file that was just written and send the agent back with a precise reason. This one checks for em and en dashes; swap in any rule a script can check.
// dash-scan.mjs: after a write, send the agent back to fix banned dashes
import { readFileSync } from 'node:fs';
let raw = '';
process.stdin.on('data', d => { raw += d; }).on('end', () => {
const file = JSON.parse(raw).tool_input?.file_path;
if (!file) return;
const lines = readFileSync(file, 'utf8').split('\n');
const hits = lines.flatMap((l, i) => /[\u2013\u2014]/.test(l) ? [i + 1] : []);
if (hits.length) console.log(JSON.stringify({
decision: 'block',
reason: `Em or en dash on lines ${hits.join(', ')} of ${file}. Use a plain hyphen.`,
}));
});
Feedback after a write cannot undo the write. It does reliably make the agent fix the problem in the next step, which beats a rule it read an hour ago.
Know the limits
- A regex is not a shell parser. The example misses commands hidden in variables, aliases or
bash -cstrings. CommonRig's shell guard splits command chains, treats quoted text and heredocs as data and checksgit -Ctargets; it is still a conservative checker, not a sandbox. - Fail open. If your hook crashes, it should allow and log, never wedge every tool call.
- Keep them fast. Hooks run on every matching call. Set a short timeout.
- Hooks do not replace permissions. For a hard read-only boundary, use operating-system permissions.
Roll out in log mode
A new guard that blocks too much stalls real work. Run it in log mode for a week: allow everything, record what would have been blocked, then read the log for false positives. Switch to enforce when the log matches what you meant, and keep a kill switch (an environment variable the hook checks) for the day it misfires. CommonRig's hooks ship this way, with log, enforce and off modes and a RIG_HOOKS=off kill switch.
Check your own files
Paste your CLAUDE.md or AGENTS.md into the free footprint scorer, or get the free sample and run rig doctor --lite locally. The hosted doctor check is A$0.50 a run and scores the whole setup, hooks included. The kit (A$10 once) has the rule source, generator, hooks and protocols these guides describe.