The Scenario
Your .claude/settings.json can ask before git push — but that's where a permission's job ends. It's a gate, not a worker: it can't format a file the moment it's edited, catch a missing test before code ships, or keep a log of what actually happened in a session. None of that is "is this call allowed" — it's "let me run my own logic around this event, whatever that logic needs to do."
That's where hooks come in: they let you run your own code at specific points in Claude Code's workflow — for security checks, sure, but just as often for formatting, automation, or just keeping a record.
That's the gap a hook fills — not "does this call match a permission rule?" but "inspect what's actually happening, then decide what to do about it."
Hook, in One Sentence
A hook is a script that runs automatically at a specific moment in Claude's workflow — not a declarative pattern match, but code you write that checks whatever it needs to check, then acts on what it finds.
The Events Worth Knowing
Claude Code fires hooks at specific moments. Two matter most starting out:
-
PreToolUse— right before a tool runs. Can still block it. -
PostToolUse— right after. Can't undo it, but can react (format the file, log what happened).
One Real Example
One of the jobs a hook can take on — a PreToolUse hook on git push that checks whether the files being pushed have a matching test file changed alongside them. For this example, assume the project follows a simple convention: books.ts has a corresponding books.test.ts (same books endpoint from the earlier articles) — real projects vary (.spec.ts, BookServiceTests.cs, test_books.py), so this is a toy heuristic demonstrating what a hook can do, not a general-purpose test-verification system.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(git push *)",
"hooks": [
{ "type": "command", "command": "./scripts/check-tests-before-push.sh" }
]
}
]
}
}
check-tests-before-push.sh diffs the push against the base branch, lists the changed source files, and for each one checks whether a matching test file also changed. If a source file changed with no matching test file touched, the script exits non-zero — which causes the PreToolUse hook to prevent the push.
📄 Full script: https://github.com/Alimurrazi/claude-demo-book-api/blob/master/scripts/check-tests-before-push.sh
It's a nudge, not a guarantee — it only checks that a test file changed, not that it actually covers the new logic. A real coverage guarantee would mean running your suite with a coverage tool instead; skipped here for simplicity.
⚠️ This only governs Claude, not everyone else. A PreToolUse hook fires only on a push when Claude Code session runs itself — not a manual push, not one from a third-party Git client (GitHub Desktop, GitKraken etc.), and not from another agent. It's not a real git hook like .git/hooks/pre-push; for simplicity, this article skips setting that up, so anything outside Claude Code stays unchecked.
Lives in the same .claude/settings.json as your permission rules — hooks and permissions share the file, they just do different jobs inside it.
What Else Hooks Are Good For
The test-before-push check is one example, not the whole picture. Once a hook can run any script, at any lifecycle moment, a lot opens up:
-
Auto-format on every edit —
PostToolUserunning your linter/formatter (eslint, prettier) so style is never a manual step -
Build before it ships —
PreToolUseongit push, runningnpm run buildand blocking the push if it fails -
Catch accidental secrets —
PreToolUseonEdit, scanning new content for anything that looks like a real API key before it lands - Session logging — a hook on session end, writing a record of what Claude actually did, outside the chat transcript
Same mechanism throughout — a script, a lifecycle moment, and some custom logic. Only the script changes. And it's not only about blocking, either — a hook can just as easily run automation, format a file, or collect information, the way the bullets above show.
A Word of Caution
A hook runs unconditionally, every time it matches — there's no judgment call, which is exactly the point, but also the risk. A hook with a bug can block legitimate work just as easily as it blocks a real problem, and it'll do it silently unless you test it.
Run claude --debug while you're writing a new hook, before you trust it in a real session.
Takeaways
- A hook checks, then acts, at a lifecycle event.
-
PreToolUseruns before an action;PostToolUseruns afterward. - Unlike permission matching, hooks can perform custom checks and automation.
- Hooks can block actions, but they can also format, log, scan, or trigger other work.
- Test your hooks before trusting them — a bad hook can block legitimate work.
Want more — the full event list, hooks that modify a call instead of just blocking it, or chaining multiple hooks? Worth its own deep-dive later; this is what you need to get started.
📦 Full example: https://github.com/Alimurrazi/claude-demo-book-api/tree/article-3-hooks
Previously in this series:




Top comments (0)