215 lines
5.9 KiB
Markdown
215 lines
5.9 KiB
Markdown
# 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
|