ideastack·7 min read·
Claude Code hooks for UK indie hackers: the eight automation patterns that pay for themselves on the first ship
The vibe-coder crowd treat Claude Code hooks like dev-ops ceremony. They are not. Hooks are the cheapest insurance a UK indie hacker can buy: a few lines of shell in settings.json that catch the rogue rm -rf, the force-push to main at 23:30, the leaked Stripe key in a debug log. Eight patterns that pay back inside the first weekend.

The vibe-coder crowd treat Claude Code hooks like dev-ops ceremony. They are not. Hooks are the cheapest insurance policy a UK indie hacker can buy: a few lines of shell in settings.json that catch the mistakes Claude Code actually makes — the rogue rm -rf, the force-push to main at 23:30 on Sunday, the leaked Stripe key in a debug log, the unused import that ships to production.
This post is the UK builder's hooks primer. The four hook events worth knowing. The eight patterns that earn back their setup time inside the first weekend. The exact settings.json snippets. And the one anti-pattern that turns hooks into a productivity tax instead of a safety net.
The four hook events you need to know
Claude Code fires hooks at specific moments in the agent's loop. There are more than four total, but four do 90% of the useful work for indie hackers:
PreToolUse— runs before any tool call. The veto point. You can inspect what the agent is about to do and block it.PostToolUse— runs after a successful tool call. The reaction point. You can run a typecheck after every edit, or remind the agent to commit after every five edits.UserPromptSubmit— runs when you submit a new prompt. The context-injection point. You can append freshgit statusoutput so the agent always knows where the repo is.Stop— runs when the agent declares itself done. The verification point. You can run the test suite one last time before the agent says "shipped".
Each hook is just a shell command. It receives JSON on stdin and returns an exit code. Zero means proceed, non-zero on PreToolUse means block. That's the entire interface.
Where hooks live
In your project root (.claude/settings.json) or globally (~/.claude/settings.json). Project settings beat global ones. Here's the shape:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "scripts/hooks/check-bash.sh" }
]
}
]
}
}
The matcher field decides which tool the hook fires on (Bash, Edit, Write, Read, etc.). Leave it empty to match all. Multiple hooks on the same event run in order; any blocking exit code stops the chain.
The eight patterns that earn their keep
1. Auto-typecheck after every edit (PostToolUse)
The single biggest hook win for a UK micro-SaaS in TypeScript. Claude Code is great at writing code that compiles 95% of the time. The other 5% is a wasted run-and-fail loop. A typecheck hook catches the type error before the agent moves on.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "pnpm exec tsc --noEmit 2>&1 | tail -20" }
]
}
]
}
}
The agent sees the tsc output, fixes the error in the next turn, and you never had to mention it.
2. Deny-list dangerous bash (PreToolUse)
Block the small set of commands that turn a tired Sunday-evening session into a recovery situation. rm -rf outside node_modules. git push --force to main or master. DROP TABLE. Curl with sudo.
#!/bin/bash
# scripts/hooks/check-bash.sh
cmd=$(jq -r '.tool_input.command' <<< "$(cat)")
if echo "$cmd" | grep -qE '(rm -rf [^n]|push --force.*(main|master)|DROP TABLE|curl.*\| *sudo)'; then
echo "Blocked dangerous command: $cmd" >&2
exit 1
fi
exit 0
Wire it as a PreToolUse hook on the Bash matcher. The agent sees the block, picks a safer path, and your repo's commit history stays clean.
3. Secret-leak guard (PreToolUse)
Stripe live keys (sk_live_…), Supabase service-role JWTs, SendGrid keys, Anthropic API keys. The agent occasionally tries to inline one of these into a debug log or a test fixture. A pre-write hook stops that before the file touches disk.
#!/bin/bash
# scripts/hooks/check-secrets.sh
content=$(jq -r '.tool_input.content // .tool_input.new_string' <<< "$(cat)")
if echo "$content" | grep -qE '(sk_live_[a-zA-Z0-9]{20,}|eyJ[a-zA-Z0-9._-]{40,}\.[a-zA-Z0-9._-]{20,}|ANTHROPIC_API_KEY=sk-)'; then
echo "Blocked: suspected secret in file content" >&2
exit 1
fi
exit 0
The hook is conservative on purpose. False positives are fine; false negatives are how Twitter learns your Stripe rotation policy the hard way.
4. Git-status reminder (UserPromptSubmit)
The agent's most common mistake on a long session: forgetting which branch it's on. A UserPromptSubmit hook can prepend a one-line summary to every prompt so the agent never loses orientation.
#!/bin/bash
# scripts/hooks/git-status-reminder.sh
branch=$(git branch --show-current 2>/dev/null || echo "no-git")
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
echo "[branch: $branch | uncommitted files: $dirty]"
exit 0
The hook's stdout is appended to the prompt the agent sees. After two days you forget it's there; the agent never does.
5. Pre-commit lint and format (PreToolUse)
Block commits that haven't been linted and formatted. Trivial to write, big payoff on team repos (and your future self, on a solo repo).
#!/bin/bash
# scripts/hooks/pre-commit.sh
cmd=$(jq -r '.tool_input.command' <<< "$(cat)")
if echo "$cmd" | grep -q '^git commit'; then
pnpm exec eslint . --max-warnings 0 || { echo "Lint failed, fix before commit" >&2; exit 1; }
pnpm exec prettier --check . || { echo "Format mismatch, run prettier --write first" >&2; exit 1; }
fi
exit 0
The agent sees the rejection, runs the formatter, retries. No half-formatted PRs.
6. Post-test summary (Stop)
When the agent declares itself done, run the test suite and report the result. If the suite is red, the Stop hook can block the agent from claiming the work is finished.
#!/bin/bash
# scripts/hooks/post-test.sh
pnpm test --run 2>&1 | tail -30
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "Tests failed - work is not done" >&2
exit 1
fi
exit 0
For a UK micro-SaaS, this is the single highest-impact hook. The agent does not get to claim "I'm done" until vitest agrees with it.
7. Env-pull on cd (PostToolUse)
When the agent switches projects mid-session, the environment in .env.local changes. A PostToolUse hook on Bash that detects a cd into a Vercel-linked project and pulls fresh env vars saves the mysterious "why is my Supabase key wrong" debug session.
#!/bin/bash
# scripts/hooks/env-pull.sh
cmd=$(jq -r '.tool_input.command' <<< "$(cat)")
if echo "$cmd" | grep -qE '^cd [^&]+sg-storefront|^cd [^&]+pixelshed'; then
vercel env pull .env.local --yes 2>/dev/null || true
echo "[env pulled for new project]"
fi
exit 0
Pin it to the projects where env drift bites. Skip it for repos that don't use Vercel-managed env.
8. Post-deploy smoke check (Stop)
After every vercel --prod or railway up in the agent's session, hit the live URL and verify HTTP 200. The Stop hook is the right place because it runs once per agent turn, not after every shell command.
#!/bin/bash
# scripts/hooks/smoke-check.sh
if git log -1 --pretty=%B | grep -qE '(deploy|release|prod)'; then
url=$(cat .deploy-url 2>/dev/null || echo "")
if [ -n "$url" ]; then
status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
echo "Smoke check $url -> $status"
[ "$status" = "200" ] || { echo "Deploy did not return 200" >&2; exit 1; }
fi
fi
exit 0
Pair it with a one-line .deploy-url file written by your deploy script. The agent now physically cannot mark a deploy "shipped" if the live URL is 500.
The one anti-pattern
Hooks that do more than they need to. A PostToolUse hook that runs the full test suite after every single Edit will turn a 30-second turn into a three-minute turn, and the agent will start avoiding edits to dodge the cost. Hooks should be fast (sub-second) and surgical (run the right thing at the right time). The typecheck hook (pattern 1) is fast; the full test hook is slow — put the full test in Stop, not PostToolUse.
The simple version of the rule: every hook should answer "is this still safe?", not "is this still good?". Goodness goes in CI. Safety goes in hooks.
A starting settings.json for a UK weekend project
If you set up exactly these three hooks on day one, you have caught the four most common failure modes — type drift, dangerous bash, leaked keys, red tests — at roughly 20 minutes of setup time. Three hours back over the first month.
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "bash scripts/hooks/check-bash.sh" }] },
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "bash scripts/hooks/check-secrets.sh" }] }
],
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "pnpm exec tsc --noEmit 2>&1 | tail -20" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "bash scripts/hooks/post-test.sh" }] }
]
}
}
Drop the four scripts into scripts/hooks/, commit them, and your CLAUDE.md picks up a free safety net.
Hooks vs CLAUDE.md rules — what goes where
A CLAUDE.md rule says "please don't do X". A hook physically blocks "X". Use CLAUDE.md for guidance the agent should internalise; use hooks for the guardrails that genuinely must hold.
| Constraint | CLAUDE.md | Hook |
|---|---|---|
| "Use server components by default" | Yes | No |
| "Never force-push to main" | Yes (rule) | Yes (PreToolUse Bash) |
| "Run tests before claiming done" | Yes | Yes (Stop) |
| "Match the existing import style" | Yes | No |
| "No secrets in source files" | Yes | Yes (PreToolUse Edit) |
| "Use GBP not USD" | Yes | No |
The hooks column is short on purpose. Most of CLAUDE.md is taste; hooks are for the small set of rules that are non-negotiable.
Want a data-backed UK business idea every week? Free reports drop every Thursday — keyword volumes, SERP analysis, builder prompts. Browse the latest free report on IdeaStack.
Frequently asked
Do hooks slow Claude Code down?
Only if you write slow hooks. A typecheck on a small project finishes in 200-400 ms. A regex check on bash commands finishes in single-digit ms. The Stop-time test suite is the slow one — and that's the point; you want the suite running before the agent claims it's done. If a `PostToolUse` hook starts taking over a second, move the work to `Stop`.
Can I share hooks across projects?
Yes. Global hooks live in `~/.claude/settings.json` and apply to every project. The pattern that works well for a UK indie hacker: put the safety hooks (deny-list, secret-leak) globally, put the project-specific hooks (typecheck command, deploy URL) in each project's `.claude/settings.json`. Project settings inherit and override the global ones.
What if a hook accidentally blocks the agent from doing something legitimate?
Two escape hatches. Tighten the hook (most false positives are over-broad regex). Or use an environment-variable bypass: have the hook check for `HOOK_BYPASS=1` and skip its logic when present. The bypass should log loudly to stderr so you notice when you're skipping a guardrail.
Do hooks work the same way in Cursor or OpenCode?
No. Hooks are a Claude-Code-specific feature. Cursor uses `.cursorrules` for the same job but it's purely prompt-level guidance — no real blocking. OpenCode has tool-permission rules but no equivalent of `PostToolUse` hooks running shell commands. If you live in Claude Code, hooks are one of the genuine moats over other AI coding tools.
Can a hook write to the file system or commit on its own?
Technically yes — a hook is a shell command, so it can do anything you give it permission to do. In practice, keep hooks read-only or strictly additive (writing to a log file is fine). A hook that auto-commits or auto-pushes is the kind of thing that turns "I'll just try this hook" into "where did my Sunday work go".
Filed under





