1
0
Fork 0

feat: initial companion repo for CC-024 Hooks

This commit is contained in:
Kjell Tore Guttormsen 2026-03-30 10:38:57 +02:00
commit cd834a7476
7 changed files with 790 additions and 0 deletions

View file

@ -0,0 +1,158 @@
# Exercise 01: Block Dangerous Commands
**Concept:** Hooks (CC-024)
**Level:** Basic
**Time:** ~10 minutes
---
## Objective
Set up a pre-tool-use hook that blocks dangerous shell commands before
Claude executes them. You will configure the hook, test it by asking
Claude to run a blocked command, and confirm the block worked.
---
## Before You Start
Confirm you have:
- [ ] Claude Code installed (`claude --version` prints a version)
- [ ] This repo cloned and open in Claude Code
- [ ] Python 3 available (`python3 --version`)
---
## Instructions
**Step 1:** Read the hook script.
Open `hooks/pre-tool-use.sh` and read through it. Pay attention to:
- How it reads input from stdin using `cat`
- How it extracts `tool_name` and `command` from the JSON
- What the `BLOCKED` array contains
- What exit code 2 means vs exit code 0
You do not need to modify the script. Understanding it is the goal of this step.
**Step 2:** Make the script executable.
```bash
chmod +x hooks/pre-tool-use.sh
```
This is required before Claude Code can run it as a hook.
**Step 3:** Configure the hook in `.claude/settings.json`.
Create the file `.claude/settings.json` in this repo with this content:
```json
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(echo:*)",
"Bash(pwd)",
"Bash(date)",
"Read",
"Write",
"Edit",
"Glob",
"Grep"
],
"deny": [
"Bash(rm -rf *)",
"Bash(sudo *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash hooks/pre-tool-use.sh"
}
]
}
]
}
}
```
The `matcher` field is a regex pattern matched against the tool name.
`"Bash"` matches the Bash tool. `".*"` would match all tools.
The `command` field is the shell command Claude Code runs before
executing the tool. It must exit 0 to allow, 2 to block.
**Step 4:** Start Claude Code in this directory.
```bash
cd claude-code-hooks
claude
```
Claude Code reads `.claude/settings.json` on startup and registers the hooks.
**Step 5:** Test the block by asking Claude to run a dangerous command.
Paste this prompt into Claude Code:
```
Run this shell command: sudo ls /root
```
Watch what happens. Claude will attempt the Bash tool call. The
pre-tool-use hook will run first, match the `sudo` pattern, and exit 2.
**Step 6:** Verify the result.
After the hook blocks the command, paste this prompt:
```
Did the command succeed? Why or why not?
```
Claude should explain that the command was blocked by a hook and show
the reason returned by the script.
---
## Expected Output
After completing the steps, you should see:
- Claude attempting the `sudo ls /root` command
- An error message from the hook: `Commands with sudo are not allowed`
- Claude reporting that the command was blocked and could not proceed
- No output from `sudo ls /root` because it never ran
If Claude executes the command anyway, check that `pre-tool-use.sh` is
executable and that `.claude/settings.json` has the correct hook config.
---
## What You Learned
- **PreToolUse hooks intercept before execution.** The command never runs
if the hook exits 2. This is different from permission deny lists, which
are pattern-matched by Claude Code before the tool call. Hooks let you
run arbitrary logic.
- **Exit code 2 means block.** Exit 0 means allow. Any other exit code is
treated as an error and may allow or block depending on Claude Code's
configuration.
- **The reason field reaches Claude.** Whatever you put in the `reason`
field of the JSON output, Claude receives it as the error message. This
lets you explain why a command was blocked in human-readable terms.
---
## Next
Move to [Exercise 02: Build an Audit Trail](./02-audit-trail.md).

173
exercises/02-audit-trail.md Normal file
View file

@ -0,0 +1,173 @@
# Exercise 02: Build an Audit Trail
**Concept:** Hooks (CC-024)
**Level:** Intermediate
**Time:** ~15 minutes
---
## Objective
Add a post-tool-use hook that logs every tool call Claude makes to a
local file. You will give Claude a multi-step task, then read the audit
log to see everything it did.
This is the foundation for compliance logging, debugging, and
understanding what Claude actually does during a session.
---
## Before You Start
Confirm you have completed Exercise 01, or at minimum:
- [ ] `hooks/post-tool-use.sh` exists in this repo
- [ ] `.claude/settings.json` exists with the PreToolUse hook from Exercise 01
---
## Instructions
**Step 1:** Read the post-tool-use hook.
Open `hooks/post-tool-use.sh` and read through it. Notice:
- It always exits 0. Post-tool-use hooks cannot block.
- It extracts different keys depending on the tool (`command`, `file_path`, `pattern`)
- It appends to `hooks/audit.log` in the same directory as the script itself
- The timestamp format is UTC ISO 8601
**Step 2:** Make the script executable.
```bash
chmod +x hooks/post-tool-use.sh
```
**Step 3:** Add the PostToolUse hook to `.claude/settings.json`.
Update your `.claude/settings.json` to include both hooks:
```json
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(echo:*)",
"Bash(pwd)",
"Bash(date)",
"Read",
"Write",
"Edit",
"Glob",
"Grep"
],
"deny": [
"Bash(rm -rf *)",
"Bash(sudo *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash hooks/pre-tool-use.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "bash hooks/post-tool-use.sh"
}
]
}
]
}
}
```
The `matcher: ".*"` pattern matches every tool, not just Bash. This
means every Read, Write, Grep, and WebSearch call will be logged.
**Step 4:** Start or restart Claude Code.
```bash
cd claude-code-hooks
claude
```
If Claude Code is already running, restart it so it picks up the new
settings file.
**Step 5:** Give Claude a multi-step task.
Paste this prompt into Claude Code:
```
List the files in this directory, then read README.md, then tell me
how many lines it has.
```
This will trigger multiple tool calls: a Bash call for `ls`, a Read
call for README.md, and a Bash call for `wc -l`.
**Step 6:** Read the audit log.
After Claude finishes, paste this prompt:
```
Show me the contents of hooks/audit.log
```
Or read it directly from your terminal:
```bash
cat hooks/audit.log
```
---
## Expected Output
The audit log should contain entries like:
```
2026-03-30T04:12:33Z | Bash | ls -la
2026-03-30T04:12:34Z | Read | README.md
2026-03-30T04:12:35Z | Bash | wc -l README.md
```
Each line shows the UTC timestamp, the tool name, and the primary
argument (command, file path, or search pattern).
If `audit.log` does not exist after the task, check that `post-tool-use.sh`
is executable and that the `PostToolUse` hook is in `settings.json`.
---
## What You Learned
- **PostToolUse hooks run after every tool call.** They receive the same
JSON input as PreToolUse hooks but cannot influence the outcome. Their
job is observation, not control.
- **`matcher: ".*"` captures everything.** Narrowing the matcher to `"Bash"`
would log only shell commands. Using `".*"` logs all tool types.
- **Audit logs survive sessions.** The log file persists between Claude
Code sessions. This makes it useful for compliance and debugging:
you can always reconstruct what happened.
- **Logs are per-project.** The log path is relative to the script, so
each project has its own audit trail.
---
## Next
Move to [Exercise 03: Prompt-Based Approval](./03-prompt-hooks.md).

View file

@ -0,0 +1,215 @@
# Exercise 03: Prompt-Based Approval
**Concept:** Hooks (CC-024)
**Level:** Advanced
**Time:** ~20 minutes
---
## Objective
Create a hook where Claude itself evaluates whether to proceed with
a file edit. Instead of a static blocklist, the hook sends a prompt
to Claude asking "is this change risky?" and uses the answer to
allow or block the action.
This is AI-powered guardrails: one Claude instance watches another.
---
## Before You Start
Confirm you have completed Exercises 01 and 02, or at minimum:
- [ ] Claude Code installed with an API key configured
- [ ] `.claude/settings.json` exists in this repo
- [ ] You understand how exit codes control hook decisions
---
## Background: Prompt Hooks
Claude Code supports three hook types:
| Type | What it does |
|------|-------------|
| `command` | Runs a shell script. Exit 0 to allow, 2 to block. |
| `prompt` | Sends a prompt to Claude. Claude responds with `allow` or `block`. |
Prompt hooks let you express approval logic in plain language rather
than regex patterns or shell conditionals. The tradeoff: they cost
API tokens and add latency to every hook invocation.
The prompt hook configuration looks like this:
```json
{
"type": "prompt",
"prompt": "The user asked Claude to make the following file edit:\n\n{tool_input}\n\nDoes this edit look like it could break existing functionality? Respond with exactly 'allow' or 'block'."
}
```
The `{tool_input}` placeholder is replaced with the serialized tool
input at runtime.
---
## Instructions
**Step 1:** Understand what you are building.
You will add a prompt hook on the Write tool. Before Claude writes
any file, the hook will ask: "Is this edit likely to break existing
tests or functionality?"
If Claude answers `block`, the write is prevented. If it answers
`allow`, the write proceeds.
This is a demonstration of the concept, not a production guardrail.
Real prompt hooks would use a more precise prompt and probably
target specific file patterns (e.g., only `*.test.*` files).
**Step 2:** Update `.claude/settings.json` with the prompt hook.
Add a `PreToolUse` entry for the Write tool:
```json
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(echo:*)",
"Bash(pwd)",
"Bash(date)",
"Read",
"Write",
"Edit",
"Glob",
"Grep"
],
"deny": [
"Bash(rm -rf *)",
"Bash(sudo *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash hooks/pre-tool-use.sh"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "prompt",
"prompt": "Claude is about to write a file. The file path and content are:\n\n{tool_input}\n\nDoes this write look like it could overwrite important existing content or break existing functionality? If it is writing to a new file or making a clearly safe addition, respond 'allow'. If it is overwriting a file in a way that seems risky or destructive, respond 'block'. Respond with exactly one word: allow or block."
}
]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "bash hooks/post-tool-use.sh"
}
]
}
]
}
}
```
**Step 3:** Restart Claude Code to load the new settings.
```bash
claude
```
**Step 4:** Ask Claude to make a safe write.
Paste this prompt:
```
Write a file called scratch.txt with the content: "hello world"
```
This is a new file with harmless content. The prompt hook should
evaluate it as safe and respond `allow`. The write proceeds.
**Step 5:** Ask Claude to make a risky write.
Paste this prompt:
```
Overwrite README.md with a single line: "This file has been cleared."
```
This destroys existing content. The prompt hook should evaluate
it as risky and respond `block`. Claude will report that the write
was blocked.
**Step 6:** Review what happened.
Check `hooks/audit.log` to see the tool calls that were logged,
including the ones the prompt hook evaluated.
```bash
cat hooks/audit.log
```
Note that the blocked write does not appear in the log (it never
ran), but the audit log still captures the preceding tool calls.
---
## Expected Output
After Step 4: `scratch.txt` is created. Claude confirms the write succeeded.
After Step 5: The write is blocked. Claude reports that the hook
prevented it from overwriting `README.md`. The message from the
prompt hook will indicate the block decision.
If the risky write is allowed, the prompt hook may have evaluated
the edit differently. This is expected: prompt hooks are probabilistic.
Try refining the prompt to be more explicit about what counts as risky.
---
## What You Learned
- **Prompt hooks add AI judgment to the pipeline.** Instead of matching
patterns, you describe the decision in plain language. Claude reads
the context and decides.
- **The `{tool_input}` placeholder carries the full context.** The entire
tool input is available to the evaluating Claude, including file paths
and content. This gives the hook genuine information to reason from.
- **Prompt hooks are latency-sensitive.** Each invocation makes an API
call. Apply them selectively to high-risk operations, not every tool.
- **Prompt hooks are not deterministic.** The same input can produce
different outputs. For hard security requirements, use command hooks
with explicit patterns. Prompt hooks are best for nuanced judgment calls.
---
## Going Further
A few directions worth exploring once you have the basics working:
- Narrow the Write hook matcher to only trigger on specific paths:
`"matcher": "Write.*\\.py$"` (Python files only)
- Add a PostToolUse prompt hook that summarizes what changed each session
- Combine command hooks for hard blocks with prompt hooks for soft approvals
The full Claude Code hooks documentation is at:
https://docs.anthropic.com/en/docs/claude-code/hooks