feat: initial companion repo for CC-024 Hooks
This commit is contained in:
commit
cd834a7476
7 changed files with 790 additions and 0 deletions
51
CLAUDE.md
Normal file
51
CLAUDE.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Hooks: Run Code When Claude Code Acts
|
||||||
|
|
||||||
|
## What this project is
|
||||||
|
|
||||||
|
A companion repo for the From AI to Chitta article on **Hooks**
|
||||||
|
(concept ID: `CC-024`). It contains working hook scripts and exercises
|
||||||
|
that let you set up real guardrails and audit logging in Claude Code.
|
||||||
|
|
||||||
|
Article: https://fromaitochitta.com/cc-024
|
||||||
|
|
||||||
|
## How to use this
|
||||||
|
|
||||||
|
1. Open Claude Code in this directory: `cd claude-code-hooks && claude`
|
||||||
|
2. Work through exercises in order: `exercises/01-block-danger.md`, then 02, then 03
|
||||||
|
3. Each exercise has concrete steps with expected output
|
||||||
|
4. Compare your results to the expected output described in each exercise
|
||||||
|
|
||||||
|
## What is configured
|
||||||
|
|
||||||
|
- **Pre-tool-use hook** (`hooks/pre-tool-use.sh`): Blocks a set of dangerous
|
||||||
|
shell commands before Claude executes them. Reads JSON from stdin, matches
|
||||||
|
against blocked patterns, exits 2 to block or 0 to allow.
|
||||||
|
- **Post-tool-use hook** (`hooks/post-tool-use.sh`): Logs every tool call to
|
||||||
|
`hooks/audit.log` with a timestamp, tool name, and input summary.
|
||||||
|
- **Settings** (`.claude/settings.json`): Wires both hooks into Claude Code.
|
||||||
|
|
||||||
|
## Code quality expectations
|
||||||
|
|
||||||
|
- This is a learning repo. Clarity over cleverness.
|
||||||
|
- Every exercise must run without modification on a clean clone
|
||||||
|
- No external dependencies beyond Python 3 (standard on macOS)
|
||||||
|
- If something does not work as expected, that is useful information: note it
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
hooks/ pre-tool-use.sh, post-tool-use.sh, audit.log (generated)
|
||||||
|
exercises/ Three numbered exercises with steps and expected output
|
||||||
|
.claude/ settings.json with hook configuration
|
||||||
|
CLAUDE.md This file: project instructions
|
||||||
|
README.md Overview for anyone who clones this repo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
Claude Code Automation
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
|
||||||
|
Keep session notes in a local `MEMORY.md` (gitignored). This file
|
||||||
|
(CLAUDE.md) is for instructions. MEMORY.md is for state.
|
||||||
70
README.md
Normal file
70
README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Hooks: Run Code When Claude Code Acts
|
||||||
|
|
||||||
|
Control what Claude Code can and cannot do by running your own scripts
|
||||||
|
before and after every tool call. Hooks are the mechanism behind security
|
||||||
|
guardrails, audit trails, and AI-powered approval workflows.
|
||||||
|
|
||||||
|
Companion repo for the article
|
||||||
|
[Hooks: Run Code When Claude Code Acts](https://fromaitochitta.com/cc-024) on From AI to Chitta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You Will Learn
|
||||||
|
|
||||||
|
By working through the exercises in this repo, you will be able to:
|
||||||
|
|
||||||
|
- Block dangerous commands with pre-tool-use hooks before they ever execute
|
||||||
|
- Build automatic audit trails with post-tool-use hooks that log every action
|
||||||
|
- Create prompt-based hooks where Claude evaluates a decision before proceeding
|
||||||
|
|
||||||
|
These are not theoretical outcomes. Complete the exercises and you will
|
||||||
|
have working hooks you can configure in any Claude Code project.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Claude Code installed and running (`claude --version` prints a version)
|
||||||
|
- Basic shell scripting knowledge (read a bash script, understand exit codes)
|
||||||
|
- Python 3 (for JSON parsing in the hook scripts; comes with macOS)
|
||||||
|
|
||||||
|
No npm install, no Docker, no build step.
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
| # | Exercise | What It Covers |
|
||||||
|
|---|----------|---------------|
|
||||||
|
| 01 | [Block Dangerous Commands](exercises/01-block-danger.md) | PreToolUse hooks, exit codes, blocking behavior |
|
||||||
|
| 02 | [Build an Audit Trail](exercises/02-audit-trail.md) | PostToolUse hooks, append-only logging |
|
||||||
|
| 03 | [Prompt-Based Approval](exercises/03-prompt-hooks.md) | AI-powered hooks, risk evaluation before acting |
|
||||||
|
|
||||||
|
Start at `exercises/01-block-danger.md`. Each exercise links to the next.
|
||||||
|
|
||||||
|
## How to Use This Repo
|
||||||
|
|
||||||
|
**"Show me what it does":** Read through `hooks/pre-tool-use.sh` and
|
||||||
|
`hooks/post-tool-use.sh`. Then read `.claude/settings.json` to see how
|
||||||
|
they are wired in. The pattern becomes clear before you run anything.
|
||||||
|
|
||||||
|
**"Let me actually do it":** Open Claude Code in this directory and
|
||||||
|
follow the exercises in order. Each one has an expected output section
|
||||||
|
so you know when you have succeeded.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.fromaitochitta.com/fromaitochitta/claude-code-hooks.git
|
||||||
|
cd claude-code-hooks
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `exercises/01-block-danger.md` and follow the steps.
|
||||||
|
|
||||||
|
## Domain
|
||||||
|
|
||||||
|
**Claude Code Automation** - This repo is part of the Claude Code Automation series on
|
||||||
|
From AI to Chitta. See the full series for related concepts.
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
Built by [Kjell Tore Guttormsen](https://fromaitochitta.com) as
|
||||||
|
part of the From AI to Chitta project, exploring the intersection
|
||||||
|
of AI tools and inner development.
|
||||||
|
|
||||||
|
MIT License.
|
||||||
158
exercises/01-block-danger.md
Normal file
158
exercises/01-block-danger.md
Normal 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
173
exercises/02-audit-trail.md
Normal 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).
|
||||||
215
exercises/03-prompt-hooks.md
Normal file
215
exercises/03-prompt-hooks.md
Normal 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
|
||||||
51
hooks/post-tool-use.sh
Normal file
51
hooks/post-tool-use.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# post-tool-use.sh: Append an audit log entry after every tool call.
|
||||||
|
#
|
||||||
|
# Claude Code calls this script AFTER every tool execution.
|
||||||
|
# The script receives a JSON object on stdin with two fields:
|
||||||
|
# - tool_name: the name of the tool that ran (e.g., "Bash", "Read", "Write")
|
||||||
|
# - tool_input: the arguments passed to the tool
|
||||||
|
#
|
||||||
|
# This script writes one line to hooks/audit.log:
|
||||||
|
# 2026-03-30T04:12:33Z | Bash | ls -la /tmp
|
||||||
|
#
|
||||||
|
# The log is append-only. It grows during the session and persists between sessions.
|
||||||
|
# Read it any time with: cat hooks/audit.log
|
||||||
|
#
|
||||||
|
# Exit code:
|
||||||
|
# 0 Always. This hook does not block anything.
|
||||||
|
#
|
||||||
|
# Make this script executable: chmod +x hooks/post-tool-use.sh
|
||||||
|
|
||||||
|
# Read the full JSON payload from stdin
|
||||||
|
input=$(cat)
|
||||||
|
|
||||||
|
# Extract the tool name
|
||||||
|
tool_name=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name','unknown'))" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# Extract the most relevant part of the tool input.
|
||||||
|
# Different tools use different argument keys:
|
||||||
|
# Bash -> command
|
||||||
|
# Read -> file_path
|
||||||
|
# Write -> file_path
|
||||||
|
# Grep -> pattern
|
||||||
|
# Fall back to a truncated string representation of the entire input dict.
|
||||||
|
tool_input=$(echo "$input" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.load(sys.stdin).get('tool_input', {})
|
||||||
|
value = d.get('command') or d.get('file_path') or d.get('pattern') or str(d)[:120]
|
||||||
|
print(value)
|
||||||
|
" 2>/dev/null || echo "(parse error)")
|
||||||
|
|
||||||
|
# Timestamp in UTC ISO 8601
|
||||||
|
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
# Write the log directory relative to this script's location.
|
||||||
|
# This keeps the log inside the repo regardless of where Claude Code is invoked from.
|
||||||
|
log_dir="$(dirname "$0")"
|
||||||
|
log_file="$log_dir/audit.log"
|
||||||
|
|
||||||
|
# Append one line to the log file
|
||||||
|
echo "$timestamp | $tool_name | $tool_input" >> "$log_file"
|
||||||
|
|
||||||
|
exit 0
|
||||||
72
hooks/pre-tool-use.sh
Normal file
72
hooks/pre-tool-use.sh
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# pre-tool-use.sh: Block dangerous shell commands before Claude executes them.
|
||||||
|
#
|
||||||
|
# Claude Code calls this script BEFORE executing any Bash command.
|
||||||
|
# The script receives a JSON object on stdin with two fields:
|
||||||
|
# - tool_name: the name of the tool being called (e.g., "Bash")
|
||||||
|
# - tool_input: the arguments passed to the tool (e.g., {"command": "ls -la"})
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 Allow the command to proceed
|
||||||
|
# 2 Block the command (Claude receives the "reason" field as an error message)
|
||||||
|
#
|
||||||
|
# When blocking, write a JSON object to stdout:
|
||||||
|
# {"decision": "block", "reason": "Human-readable explanation"}
|
||||||
|
#
|
||||||
|
# Make this script executable: chmod +x hooks/pre-tool-use.sh
|
||||||
|
|
||||||
|
# Read the full JSON payload from stdin
|
||||||
|
input=$(cat)
|
||||||
|
|
||||||
|
# Extract tool_name and the command argument from the JSON
|
||||||
|
tool_name=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
|
||||||
|
command=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
||||||
|
|
||||||
|
# Only inspect Bash tool calls. Allow everything else immediately.
|
||||||
|
if [ "$tool_name" != "Bash" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Each blocked pattern is checked separately with a descriptive reason.
|
||||||
|
# Pattern matching uses grep -qiE (case-insensitive extended regex).
|
||||||
|
|
||||||
|
block_if_match() {
|
||||||
|
local pattern="$1"
|
||||||
|
local reason="$2"
|
||||||
|
if echo "$command" | grep -qiE "$pattern"; then
|
||||||
|
echo "{\"decision\": \"block\", \"reason\": \"$reason\"}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Recursive deletion of the root filesystem
|
||||||
|
block_if_match 'rm -rf /' 'Recursive deletion from root is not allowed'
|
||||||
|
|
||||||
|
# sudo: prevents privilege escalation
|
||||||
|
block_if_match 'sudo ' 'Commands with sudo are not allowed'
|
||||||
|
|
||||||
|
# World-writable permissions: a common misconfiguration that opens security holes
|
||||||
|
block_if_match 'chmod 777' 'chmod 777 is not allowed: use more restrictive permissions'
|
||||||
|
|
||||||
|
# Direct disk write: can wipe an entire disk in seconds
|
||||||
|
block_if_match 'dd if=' 'Direct disk writes with dd are not allowed'
|
||||||
|
|
||||||
|
# Filesystem formatting: permanently destroys data
|
||||||
|
block_if_match 'mkfs' 'Formatting filesystems is not allowed'
|
||||||
|
|
||||||
|
# Fork bomb: crashes the system by spawning processes until resources are exhausted
|
||||||
|
block_if_match ':\(\)\{' 'Fork bombs are not allowed'
|
||||||
|
|
||||||
|
# Piping curl or wget output directly into a shell:
|
||||||
|
# executes arbitrary remote code without review
|
||||||
|
block_if_match 'curl.+\|.+bash' 'Piping curl into bash is not allowed'
|
||||||
|
block_if_match 'curl.+\|.+sh' 'Piping curl output into a shell is not allowed'
|
||||||
|
block_if_match 'wget.+\|.+bash' 'Piping wget into bash is not allowed'
|
||||||
|
block_if_match 'wget.+\|.+sh' 'Piping wget output into a shell is not allowed'
|
||||||
|
|
||||||
|
# Shutdown and reboot: should never be triggered by an automated agent
|
||||||
|
block_if_match '^shutdown' 'Shutdown commands are not allowed'
|
||||||
|
block_if_match '^reboot' 'Reboot commands are not allowed'
|
||||||
|
|
||||||
|
# Nothing matched: allow the command
|
||||||
|
exit 0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue