Most people use Claude Code as a chat tool. They type a request, Claude responds, and they copy the output manually.
That works. But it misses 80% of what Claude Code can actually do.
The feature almost nobody talks about is hooks. Hooks let you run shell commands automatically before or after Claude takes any action. No manual steps. No reminders. The system just does the thing.
Here are five hooks I run every day and why each one earns its place.
What Are Claude Code Hooks?
Hooks are shell commands that execute in response to Claude Code events. You configure them in .claude/settings.json under a hooks key.
The main hook types:
-
PreToolUse- runs before Claude uses any tool -
PostToolUse- runs after a tool completes -
Stop- runs when Claude finishes a response
Each hook receives the event context as JSON via stdin. You can use that context to trigger different behavior depending on which tool fired.
A basic hook config looks like this:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo 'File written'"
}
]
}
]
}
}
That is the structure. Now here is what you can actually build with it.
Hook 1: Auto-Format on Every Write
The problem: Claude writes a file. The formatting is slightly off. You notice it three PRs later when someone comments on it in review.
The hook:
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "prettier --write $(cat /dev/stdin | jq -r '.tool_input.file_path') 2>/dev/null || true"
}
]
}
Every time Claude writes a file, Prettier runs on it automatically. The output is always formatted before it ever hits your editor.
This sounds small. It isn't. I used to spend 15-20 minutes per session fixing whitespace, trailing commas, and quote styles. Now I spend zero.
What you actually need: prettier installed globally (npm install -g prettier) and a .prettierrc in your project.
If you want to skip the manual setup, I have a full hook configuration template that covers formatting, linting, and git operations. It's in the free toolkit below.
[Get the Claude Code automation toolkit - free]
Hook 2: Git Auto-Stage After Every Edit
The problem: You finish a long Claude session. You go to commit. You have 47 modified files across six different features because you never staged anything.
The hook:
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "cd $(cat /dev/stdin | jq -r '.tool_input.file_path | split(\"/\")[:-1] | join(\"/\")') && git add -p . 2>/dev/null || true"
}
]
}
A cleaner version that stages the specific file:
{
"type": "command",
"command": "FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path') && git add \"$FILE\" 2>/dev/null || true"
}
Every edited file goes straight to the staging area. Your working tree stays clean. Commits become specific and intentional instead of "everything Claude touched."
Hook 3: Run Tests After Any File Change
The problem: Claude edits a utility function. The tests pass in isolation. But three components downstream now fail and you don't find out until the build pipeline tells you 20 minutes later.
The hook:
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm test --passWithNoTests --watchAll=false 2>&1 | tail -20"
}
]
}
This runs your test suite every time Claude writes or edits a file. The last 20 lines of output surface immediately in Claude's context, so Claude can see failures and fix them before moving to the next step.
For large test suites, scope it to the affected module:
{
"type": "command",
"command": "FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path') && npm test --testPathPattern=\"$(dirname $FILE)\" --passWithNoTests --watchAll=false 2>&1 | tail -20"
}
This cut my debugging time in half. Claude catches its own mistakes mid-session instead of leaving them for me to find.
Three hooks in and you've already automated formatting, git staging, and test feedback. The next two are where it gets interesting.
Hook 4: Session Summary on Stop
The problem: Long Claude sessions generate a lot of changes. At the end, you have to mentally reconstruct what happened to write a useful commit message or a PR description.
The hook:
{
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "git diff --stat HEAD 2>/dev/null | head -30 && echo '---' && git diff --cached --stat 2>/dev/null | head -20"
}
]
}
]
}
When Claude finishes a response, you get a clean diff summary in your terminal. Files changed, lines added, lines removed. You always know where you stand.
For a richer summary, pipe it through Claude itself:
{
"type": "command",
"command": "git diff HEAD --name-only 2>/dev/null | head -20 | xargs echo 'Files modified:'"
}
This one hook eliminated the "what did we even do" moment at the end of every session.
Hook 5: Block Writes to Protected Files
The problem: Claude is refactoring and helpfully rewrites your .env file or your production config because it looks like it belongs in the change.
The hook:
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path') && case \"$FILE\" in *.env|*.env.*|*secret*|*credential*) echo 'BLOCKED: protected file' && exit 1 ;; esac"
}
]
}
]
}
If Claude tries to write to any .env file or any file with "secret" or "credential" in the name, the hook exits with code 1. Claude Code treats that as a block and stops before writing.
This is the safety hook. You set it once and forget it. It never fires on legitimate work, but when it does fire, it saves you from a bad day.
Putting It Together
The full configuration in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path // empty') && [ -z \"$FILE\" ] || case \"$FILE\" in *.env|*.env.*|*secret*|*credential*) echo 'BLOCKED: protected file' && exit 1 ;; esac"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && git add \"$FILE\" 2>/dev/null || true"
},
{
"type": "command",
"command": "npm test --passWithNoTests --watchAll=false 2>&1 | tail -10"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "git diff --stat HEAD 2>/dev/null | head -20"
}
]
}
]
}
}
Drop that in your project. Adjust the test command for your stack. You are done.
The Actual ROI
Before hooks: Claude writes, I review, I format, I stage, I run tests, I check what changed.
After hooks: Claude writes. Everything else happens.
The five hooks above save me roughly 3 hours a week on a medium-sized project. More on bigger projects because the test-on-write hook catches issues before they compound.
The deeper benefit is that Claude's sessions become more coherent. When Claude can see test output and diff summaries in real time, it makes better decisions mid-session instead of generating work that needs human cleanup.
Want the complete hooks starter pack? I put together a set of 12 hook configurations for different project types (Node, Python, monorepo, API-only). Free download below.
[Download the Claude Code hooks starter pack]
What to Read Next
If you are new to Claude Code and want to understand how skills and hooks fit together, start here:
- 5 Claude Code Skills That Run My Entire Ecommerce Operation
- 10 Claude Code Skills I Wish I Had When I Launched My First Product
If you are already running skills and want to push further, hooks are the next level. Set up one hook this week. The auto-stage hook is the easiest. You will never go back.
Top comments (0)