← Course home · Module 03 · Week 2

Hooks & Guardrails

D1 TS 1.5  ·  supports 3.1  ·  ~5 hrs  ·  Skip to quiz →

1 · Signs and locks

▶ 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 isA written request the model reads and followsA script the software runs automatically
How reliably it holdsAlmost every timeEvery time, by mechanism
Right forPreferences: style, naming, toneInvariants: 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.

2 · The moments a hook can fire

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:

EventThe moment it firesWhat people use it for
PreToolUseClaude has decided to do something (run a command, write a file) but has not done it yetBlocking dangerous actions. The only event that can say no
PostToolUseThe action just finishedAuto-formatting a written file, running tests, logging what happened
UserPromptSubmitYou pressed enter on a promptAdding context, validating requests before Claude sees them
SessionStartA session just openedLoading extra context, environment checks
StopClaude finished its turn and is about to stopChecking work is complete before allowing the stop
SubagentStopA subagent just finishedVerifying work delegated to subagents
PreCompactThe context window is about to be summarizedSaving 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.

3 · Anatomy of a hook

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.

Part one: the registration

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.

Part two: the script

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

The verdict: exit codes

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.

The exam's framing (TS 1.5): the exam describes hooks in Agent SDK language: functions that intercept a tool call while it is in flight. An intercepting hook can do two jobs. It can gate: allow the call or refuse it, which is exactly what the command blocker above does. And it can normalize: reshape the data passing through, like stripping member account numbers out of a database result before the model ever sees them. Same mechanism, two uses. A settings.json registration is simply how Claude Code lets you attach that mechanism without writing a whole program.

4 · Four hooks worth stealing

These four patterns cover most of what teams actually deploy. The map first, then each one in turn:

HookEvent · matcherWhat it protects against
Command blockerPreToolUse · BashDestructive commands: rm -rf, force pushes, pushes to main
Secret scannerPreToolUse · Write, EditCredentials landing in files
Audit logPostToolUse · everything"Nobody can say what the AI changed"
Auto checkerPostToolUse · Write, EditForgotten formatting and unrun tests

The command blocker

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.

The secret scanner

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

The audit log

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.

The auto checker

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.

One warning that applies to all four: hooks make Claude wait

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.

5 · Shared hooks are a trust boundary

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.

The payoff: protection that distributes itself

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.

The exposure: the locks are editable

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:

The rule that protects the rules

Hook changes get reviewed like code changes, because they are code changes, with higher blast radius than most. Concretely:

6 · Case study: the repo with no locks

The setup

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.

Monday: one tidy up away from data loss

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:

Wednesday: nothing reads what gets written

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:

Friday: no answer to "what changed?"

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?

Three scripts, three different sentences

Now replay the same week with this chapter's three hooks in place:

Side lesson: where secrets actually live. That phrase "the credential goes to an environment variable" deserves its own minute, because it is how working software keeps secrets everywhere, well beyond Claude. An environment variable is a named value the operating system hands to a program when it starts: the program asks for the value by its name, and the name is all that ever appears in code. The values themselves live in a small file conventionally called .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.

What it costs the team

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.

7 · Demo: watch the hooks get built, then build yours

▶ 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:

8 · Wrap-up: what you should be able to do cold

Readings

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.