D1 TS 1.5 · supports 3.1 · ~5 hrs · Skip to quiz →
▶ Watch: the hook that said no · 3 min
This whole module grows out of one distinction, so let's state it properly. When you write a rule in CLAUDE.md, you are writing a request in English to a system that reads English. "Never delete files without asking." Claude reads that rule at the start of every session, and it follows the rule almost every time. Almost. The rule competes for attention with everything else in a busy context window, and on a long afternoon, in a long session, "almost every time" is what you get.
For most rules, that is completely fine. Style preferences, naming conventions, tone: if one in fifty sessions drifts, nothing terrible happens and you correct it. But some rules are not preferences, and for those, a miss is not a style problem. It is an incident. The whole module rests on telling these two kinds of rules apart:
| A sign: a rule in CLAUDE.md | A lock: a hook | |
|---|---|---|
| What it is | A written request the model reads and follows | A script the software runs automatically |
| How reliably it holds | Almost every time | Every time, by mechanism |
| Right for | Preferences: style, naming, tone | Invariants: rules where a miss is an incident |
| Examples | "Prefer bullets," "spell out abbreviations" | "Never push to main," "never write an API key into a file" |
A hook is a small script that the Claude Code software itself runs at fixed moments, automatically, every time, whether the model is having a good day or not. The sign on the door works on almost everyone. The lock works on everyone. This module is about installing locks.
A hook attaches to a lifecycle event: a named moment in a session where Claude Code pauses and runs your script before continuing. Learn these as moments in a story, because that is what they are:
| Event | The moment it fires | What people use it for |
|---|---|---|
| PreToolUse | Claude has decided to do something (run a command, write a file) but has not done it yet | Blocking dangerous actions. The only event that can say no |
| PostToolUse | The action just finished | Auto-formatting a written file, running tests, logging what happened |
| UserPromptSubmit | You pressed enter on a prompt | Adding context, validating requests before Claude sees them |
| SessionStart | A session just opened | Loading extra context, environment checks |
| Stop | Claude finished its turn and is about to stop | Checking work is complete before allowing the stop |
| SubagentStop | A subagent just finished | Verifying work delegated to subagents |
| PreCompact | The context window is about to be summarized | Saving anything that must survive compaction |
The one to internalize first is PreToolUse, because it is where the power is. It fires in the gap between "Claude wants to do X" and "X happens." Your script gets told exactly what Claude is about to do, and it gets a veto.
A hook has two parts: a script, and a registration that tells Claude Code when to run it. People get this wrong constantly, so plant the flag early: hooks are registered in settings.json. There is no hooks folder. A script sitting in a folder that nothing points to is a doorbell nobody wired up.
Here is a complete, real registration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./scripts/check-command.sh" }
]
}
]
}
}
Read it from the outside in. PreToolUse is the moment. The matcher narrows it: this hook only cares about Bash tool calls, so file reads and edits skip it entirely. And the command is the script to run. That is the whole wiring.
This is the actual command blocker from this module's demo, and it is deliberately boring:
#!/bin/bash
# check-command.sh: refuse destructive shell commands
input=$(cat)
if echo "$input" | grep -qE "rm -rf|--force|push.*main"; then
echo "Blocked: destructive command. Archive instead of deleting." >&2
exit 2
fi
exit 0
Two things carry all the meaning:
That feedback channel is why the message says what to do instead. Claude reads "archive instead of deleting" and proposes exactly that on its next attempt. A good block message is not just a wall; it is a redirect. For richer control, a hook can print JSON to standard output instead (deciding to allow, deny, or ask, with a reason attached), but exit codes cover most real uses and are where to start.
These four patterns cover most of what teams actually deploy. The map first, then each one in turn:
| Hook | Event · matcher | What it protects against |
|---|---|---|
| Command blocker | PreToolUse · Bash | Destructive commands: rm -rf, force pushes, pushes to main |
| Secret scanner | PreToolUse · Write, Edit | Credentials landing in files |
| Audit log | PostToolUse · everything | "Nobody can say what the AI changed" |
| Auto checker | PostToolUse · Write, Edit | Forgotten formatting and unrun tests |
A PreToolUse hook on Bash, and about ten lines of script: read the proposed command, grep it for destructive patterns like rm -rf, a force push, or a push straight to main, and exit 2 with a message that points at the archive instead. (For anyone new to terminals: rm -rf is the delete command with every safety removed. It erases a folder and everything inside it, forced, no confirmation, no trash can, no undo. One wrong path and months of work vanish in under a second.) Every team's first hook, and rightly so, because it converts the scariest failure mode into a non-event.
Before any file gets written, scan the content for things that look like credentials: sk-ant, AKIA, BEGIN PRIVATE KEY, connection strings. Block on a match. This is the hook that makes "we never commit secrets" true instead of aspirational:
input=$(cat)
if echo "$input" | grep -qE "sk-ant-|AKIA[A-Z0-9]{16}|BEGIN.*PRIVATE KEY"; then
echo "Blocked: this write contains something that looks like a credential." >&2
exit 2
fi
exit 0
After every tool call, append one line to a log file: timestamp, tool, what it did. It never blocks anything; it just remembers. Unremarkable until the day someone asks "what exactly did the AI change last Tuesday?", at which point it is the most valuable file in the repo. Your compliance colleagues will receive this hook the way other people receive gifts.
After a file changes, run the formatter or the test suite and feed failures back. This one turns "please remember to run the tests" from a CLAUDE.md request into a thing that simply happens.
Hooks run synchronously. When one fires, everything stops until the script finishes: Claude proposes an action, your script runs, and only when it exits does the action proceed. One slow hook does not feel slow once. It feels slow four hundred times.
Do the arithmetic on a real afternoon. A busy session makes something like 40 file writes. A secret scanner that greps the content takes about a twentieth of a second, so all 40 checks together cost two seconds, spread so thin nobody ever notices them. Now replace that grep with a call to some external scanning service that takes 10 seconds per check. Same afternoon, same 40 writes, and the session now contains almost seven minutes of pure waiting, served ten seconds at a time to a person trying to work. That person will blame the assistant. Nobody ever blames the hook.
Commit settings.json to the shared repo and consider what one file now governs. Five consultants. Every session any of them opens. Every command any of those sessions proposes, all day, for the life of the engagement. That is a great deal of authority for one small file in a git repo. And it cuts in two directions, because the same commit that protects everyone also hands everyone the ability to edit the protections. Both edges matter, so take them one at a time.
Every teammate who pulls the repo inherits the same locks, automatically:
This is the first layer of the governance model the course assembles at the end: rules that enforce themselves, distributed through git like any other code.
But notice what you just built: a file that controls what five people's AI sessions can and cannot do, editable by anyone with push access. Play out how that goes wrong, without anyone meaning harm:
Hook changes get reviewed like code changes, because they are code changes, with higher blast radius than most. Concretely:
settings.json or anything in scripts/ gets a second set of eyes. Every time, no exceptions for small diffs.Back to the five consultant team at the regional health plan. By this point in their story, the second brain is humming: the skills are discoverable (the folder moved to the right address, the descriptions got rewritten to fire), sessions find the right files on the first search, and every consultant runs Claude Code against the shared repo all day. Five people, dozens of tool calls each per session, every one of those calls able to read, write, and run commands. And the number of hooks in the repo: zero. Everything that happens is governed by good manners and nothing else. For months, good manners held. Then one ordinary week showed the team what they had been trusting to luck.
A consultant wraps a workstream and asks their session to clean out the old draft folders. Claude reaches for rm -rf. In this repo, nothing stands between that proposal and the disk:
data/raw/ fall in exactly that category.Another consultant is building a small script that downloads monthly claims numbers from the client's reporting system. To log in, the script needs a credential: a long secret key the client issued, the digital equivalent of a password. The consultant asks Claude to save the connection settings in a file, and the file Claude writes has that credential pasted directly into it:
git push later, the credential is in the repo history on five machines, and scrubbing git history is an afternoon of work with a tool nobody enjoys.The engagement partner reviews the client materials, notices a reworded risk statement, and asks a fair question: what exactly did the AI change in here this week?
Now replay the same week with this chapter's three hooks in place:
.env, which sits in the project folder and is listed in .gitignore, so git never sees it and it never leaves the machine:
# .env (gitignored, stays on this machine only)
CLAIMS_API_KEY=sk-live-4471-xxxx-xxxx
# settings.py (committed, safe to share with anyone)
api_key = os.environ["CLAIMS_API_KEY"]
Now look at what that split buys. The committed file carries only a name, so the repo could be published tomorrow and the key stays private. Each machine supplies its own value, so the team's laptops and the client's server can hold different keys for the same code. And when a key must be replaced, one line changes in one local file, with no commit at all. The secret scanner and this pattern are partners: the scanner blocks credentials from entering files git tracks, and the .env pattern gives them the correct place to go instead.Here is the part that makes hooks an easy sell in any governance conversation: the price of all three protections, for the people being protected, is nothing. Nobody changes how they work. Nobody remembers anything. Nobody even needs to know the hooks exist. Three small scripts, one settings file, one code review habit, and the team's worst week becomes three non-events. That is enforcement by mechanism: governance that does not depend on training, memory, or mood, and the first concrete layer of the governance model the course assembles at the end.
▶ Watch the demo: building your guardrails · 4 min
The prompt pack: every demo prompt, cleaned and explained
The opening video showed one hook saying no. This demo builds all three, prompt by prompt: the command blocker (script, redirect message, and registration asked for in a single plain instruction), the secret scanner scoped to Write and Edit, and the audit log with no matcher at all. Then five tests run in a fresh session, and the last one is the keeper: the registration gets removed on purpose while the script stays put, and the destructive command sails straight through. Nothing teaches the wiring rule faster.
Then run the pack in your own repo. The prompt pack linked above holds every prompt from the demo, cleaned up and explained. Copied as is or rephrased in your own words, check what the demo checked as you go:
rm -rf request gets blocked, and Claude proposes a safer alternative on its own. The redirect is the block message doing its job.sk-ant-test123 gets blocked by the scanner.logs/audit.log shows every tool call with a timestamp, blocks included.Claude Code docs on Hooks, including the hooks reference for events and JSON output (code.claude.com/docs), and the Agent SDK documentation on hooks for the TS 1.5 framing.
Take the Module 03 quiz → then hit the Study App for Domain 1, lesson 1.5.