In the Context Files chapter you wrote a CLAUDE.md that tells the agent what to do and what not to do. That works most of the time. But instructions are suggestions — the agent follows them because it’s told to, not because it’s physically prevented from doing otherwise.
This chapter covers three tools that go further. Hooks can block an action before it happens, or run a check afterward and feed the result back to the agent. Custom commands package a workflow into a single slash command so the whole team runs it the same way. MCP servers let the agent reach tools it doesn’t have built in. Hooks get the most space because they’re the most important — they’re how you turn the guidelines from your CLAUDE.md and the hard rules from the Risks and Hard Rules chapter into something the agent can’t ignore.
A hook is a script that runs before or after the agent uses a tool. That’s it. Pre-tool hooks run before the action (and can block it). Post-tool hooks run after the action (and can give feedback). You write them in whatever language you want — Node, Python, Bash — as long as the script can read from stdin and exit with the right code.
When Claude Code is about to use a tool (read a file, edit a file, run a command), it checks if any hooks match that tool. If a pre-tool hook matches, the hook gets the tool’s input as JSON on stdin and decides: allow (exit 0) or block (exit 2). If a post-tool hook matches, it runs after the tool completes and can send feedback to Claude via stderr.
You give Claude a task
↓
Claude decides to use a tool (e.g., read a file)
↓
Pre-tool hook runs → exit 0 (allow) or exit 2 (block with message)
↓
Tool executes (if allowed)
↓
Post-tool hook runs → feedback sent to Claude via stderr
Hooks live in your Claude Code settings file. For project-wide hooks that the team shares, use .claude/settings.json (committed to the repo). For personal hooks, use .claude/settings.local.json (gitignored).
The matcher field takes tool names separated by |. The command field is the script to run. Two things that will bite you if you miss them:
Use absolute paths. Relative paths break when Claude Code changes working directories during a session. If you need portability across machines, use a setup script that substitutes $PWD into a template (more on this below).
Restart Claude Code after changing hooks. Hook configuration is read at startup. Changes won’t take effect until you restart the session.
This is the classic security hook from the Claude Code in Action course. It prevents the agent from reading sensitive files — your first line of defence for keeping secrets out of agent context.
The matcher covers both read and grep because either could access the file contents. When the agent tries to read .env, the hook blocks it and sends the error message to Claude, who adjusts accordingly.
This connects directly to the hard rules covered in the Risks and Hard Rules chapter — specifically the rule about scanning for secrets and never letting credentials into agent context. The hook makes that rule automatic rather than relying on the agent’s good intentions.
A post-tool hook that runs tsc --noEmit after every file edit. If there are type errors, the feedback goes straight to Claude, who can fix them in the same session — often before you even notice.
Notice the exit code is 0 even when there are errors. Post-tool hooks use stderr for feedback but exit 0 to let the session continue. The agent sees the type errors and fixes them on the next iteration. This creates a tight feedback loop — edit, check, fix — without you having to ask “did the types break?”
For .NET projects, the equivalent would be running dotnet build after edits. The compiler catches hallucinated methods, wrong types, and missing usings. Same principle: give the agent immediate feedback so it self-corrects.
Attach it as a pre-tool hook with "matcher": "*" to capture everything, then inspect hook-debug.log to understand the data shapes. Remove it when you’re done — you don’t want this running in normal sessions. If you keep the log file around temporarily, add hook-debug.log to your .gitignore so it doesn’t accidentally get committed.
The “use absolute paths” requirement creates a portability issue. Different team members have different directory structures. Here’s a pattern from the course that solves it:
Create a .claude/settings.template.json with $PWD as a placeholder:
{
"hooks": {
"preToolUse": [
{
"matcher": "read|grep",
"command": "node $PWD/hooks/block-env.js"
}
]
}
}
Then a small setup script (add it to your repo’s README or package.json scripts):
#!/bin/bash
# initclaude.sh — generate settings with correct absolute paths
Run the compiler, run the linter, check for test regressions, validate formatting
Pre-tool hooks are guardrails. Post-tool hooks are feedback loops. Both make the agent more reliable, but in different ways. Guardrails stop bad things from happening. Feedback loops help the agent fix its own mistakes before you even see them.
Guided creation: If you’d rather not write the JSON by hand, Claude Code has a built-in /hooks command that walks you through creating and configuring hooks interactively.
The Context Files chapter recapped the basics: a command is a markdown file in .claude/commands/ that becomes a slash command. Here’s where it gets useful for teams.
Commands in .claude/commands/ are committed to the repo and shared with the team. Every developer gets the same set of slash commands, producing consistent results regardless of who runs them.
If you want personal commands that only you use, put them in ~/.claude/commands/ (your global Claude directory). These work across all projects but aren’t shared.
The difference between a useful command and a useless one is specificity. Compare:
Too vague:
.claude/commands/review.md
Review the code in $arguments for issues.
Useful:
.claude/commands/review.md
Review $arguments against our project conventions:
1. Check that the code follows the patterns documented in CLAUDE.md
2. Verify all public methods have XML doc comments
3. Check that new dependencies (if any) are noted in the PR description
4. Flag any direct DbContext usage outside the Repository layer
5. Run dotnet build and dotnet test to verify nothing breaks
Report issues as a numbered list with file paths and line numbers.
The first version produces different results every time. The second produces a consistent, structured review that the whole team can rely on. The conventions from your CLAUDE.md are reinforced here — commands and context files work together.
You don’t need many. Start with two or three that match tasks your team repeats:
/write-tests [file] — generate tests following your project’s test patterns and naming conventions
/review [file or directory] — structured code review against your CLAUDE.md conventions
/spec [ticket description] — draft a technical spec from a ticket using your template from the Spec-First Workflow chapter
Each one should reference your project’s specific conventions, patterns, and constraints. Generic commands (“write tests”) produce generic results. Specific commands (“write xUnit tests using Moq, following the Arrange-Act-Assert pattern, in the corresponding tests/Unit/ directory”) produce useful ones.
MCP (Model Context Protocol) servers extend Claude Code’s capabilities beyond its built-in tools. An MCP server is a small program that exposes new tools — browse the web, interact with a database, control a browser, query an API — and Claude Code uses them the same way it uses its built-in file and terminal tools.
Claude Code can already read files, write files, run terminal commands, and search your codebase. MCP servers fill the gaps:
Playwright MCP — gives Claude a browser it can control. Navigate pages, take screenshots, interact with elements. Useful for checking that a UI change actually looks right, or for testing against a running dev server.
That’s the basic shape: claude mcp add [name] [start command]. After adding, restart Claude Code. The new tools will appear when you ask Claude “what tools do you have?”
The first time Claude tries to use a new MCP tool, it asks for permission. You can approve individual tools or auto-allow all tools from a server by adding it to your settings:
.claude/settings.local.json
{
"permissions": {
"allow": ["mcp__playwright"]
}
}
Be deliberate about this. Auto-allowing a trusted, well-maintained server like Playwright is reasonable. Auto-allowing something you found on a random GitHub repo is not.
MCP servers are code running on your machine with access to your development environment. Before installing one:
Check the source. Is it from a known publisher (Microsoft, Anthropic, etc.)? Is the repository actively maintained?
Understand what it can do. A Playwright server can navigate the web — including sites with your cookies. A database server can run queries. Know what you’re granting access to.
Scope to specific repos. If a server is only needed for one project, configure it in that project’s .claude/settings.local.json, not globally.
Don’t install MCP servers that pull content from untrusted sources. If the content could be attacker-controlled, it’s a prompt injection vector.
You’ve now got four ways to shape agent behaviour. Here’s how they fit together:
Mechanism
What it does
When to reach for it
CLAUDE.md
Tells the agent what to do and not do
Always — this is your baseline. Every project has one.
Hook
Enforces a rule automatically (block or give feedback)
When a CLAUDE.md instruction isn’t enough — the agent keeps violating a rule, or the stakes are high enough that you can’t rely on “please don’t.”
Custom command
Packages a workflow into a consistent, repeatable slash command
When the team repeats the same multi-step task and you want consistent results regardless of who runs it.
MCP server
Gives the agent new capabilities it doesn’t have built in
When the agent needs to interact with something outside the codebase — a browser, a database, an external API.
They layer. A typical project might have a CLAUDE.md with conventions, a hook that enforces the most critical ones, two or three commands for common workflows, and an MCP server for browser testing. You don’t need all of them from day one. Start with the CLAUDE.md (you already have one from the Context Files chapter), add hooks for the rules that matter most, and grow from there.
Time to put hooks into practice. You’ll build two: one that blocks something, and one that gives feedback.
Goal: Implement a pre-tool hook that blocks access to .env files and a post-tool hook that runs the TypeScript compiler after edits, then verify both work.
Repo: Rokkit200.Website (Next.js)
Steps:
Create a hook script at .claude/hooks/block-env.sh (or .js/.py — whichever you’re comfortable with). It should check whether Claude is trying to read or write a file matching .env* and exit with code 2 to block the action. The repo has a real .env file with Optimizely CMS credentials (OPTIMIZELY_CMS_CLIENT_SECRET, OPTIMIZELY_GRAPH_SECRET, etc.) — this is exactly the kind of file you want to protect.
Create a second hook script at .claude/hooks/typecheck.sh. It should run npx tsc --noEmit (the project’s tsconfig.json has strict: true and noEmit: true, so this is a pure type-check). Output any errors on stderr, and exit with code 0 so the agent sees the feedback without being blocked.
Configure both hooks in .claude/settings.local.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write",
"hook": ".claude/hooks/block-env.sh"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hook": ".claude/hooks/typecheck.sh"
}
]
}
}
Restart Claude Code (hooks are loaded at session start). Test the pre-tool hook: ask Claude to “read the .env file and tell me what environment variables are configured.” It should be blocked. Check that the blocking message is clear.
Test the post-tool hook: ask Claude to add a prop to a component but deliberately give it a wrong type (e.g. “add a count prop of type string to the Icon component, and pass it to a function that expects number”). After the edit, the type-check hook should catch the error and report it on stderr. Watch whether Claude sees the feedback and fixes the type error on the next iteration.
Verify the hooks work together: ask Claude to make a legitimate change (e.g. “add a title prop to the Icon component”). The .env hook shouldn’t trigger (not touching .env), and the type-check hook should report clean output.
Check your work:
Pre-tool hook blocks Claude from reading .env, .env.local, and similar files
The blocking message is clear (not a silent failure)
Post-tool hook runs tsc --noEmit after file edits
When a type error is introduced, Claude sees the feedback and attempts to fix it
When code is correct, the type-check hook reports clean output
Both hooks are configured in .claude/settings.local.json, not .claude/settings.json (local settings stay on your machine, they’re not committed to the repo)
Reflect: Think about your current projects. What’s the one rule that agents most commonly break? Could a hook prevent it? Would it be a pre-tool hook (blocking an action) or a post-tool hook (checking after the fact)?
You’ve now got the full toolkit for controlling how agents work in your codebase: context files set the expectations, hooks enforce the critical ones, commands standardise your workflows, and MCP servers extend what’s possible. The next question is: how do you know the output is good?
The Quality Gates chapter covers the review process — how to read agent-generated PRs, what to trust, what to check harder, and when to send it back.