Back to Lab
RAXXO Studios 8 min read No time? Make it a 1 min read

The 9 Claude Code Hooks That Audit Every File I Write

AI Tools
8 min read
TLDR
×
  • 9 shell scripts run on every Edit and Write tool call in Claude Code
  • Hooks read JSON on stdin and return additionalContext that Claude self-corrects from
  • The brand check alone has blocked 400+ violations across 17 repos in six months
  • Writing a working hook takes under 20 lines of bash

I run 17 repos solo. There is no code reviewer, no design QA, no second pair of eyes before a commit lands on main. So I replaced all of that with 9 shell scripts. They hook into Claude Code on every Edit and Write, and they refuse to let a file pass if it violates brand, accessibility, or SEO rules. Claude Code hooks are the cheapest guardrail I have ever built, and they run whether I am paying attention or not.

This is how they work, what the 9 of them actually catch, and how to write your first one tonight.

How Claude Code hooks actually plug in

A hook is a shell command that Claude Code runs at a specific moment in the tool lifecycle. The two I use constantly are `PreToolUse` (runs before a tool executes, can block it) and `PostToolUse` (runs after, can warn and feed context back). The matcher is a regex against the tool name. For file-editing checks, the matcher is `Edit|Write`. For bash safety checks, it is `Bash`.

When the hook fires, Claude Code pipes a JSON payload to the script's stdin. That payload includes the file path, the tool name, and the tool input. Your script reads it, does whatever checking it wants, and prints JSON back to stdout. If you include an `additionalContext` field, Claude sees your message and self-corrects on the next turn. This is the part people miss. A hook is not just a linter yelling into the void, it is a feedback channel. I tell Claude "you used #fff on line 42, our brand color is #F5F5F7" and it fixes it without being asked twice.

Registration lives in `settings.json`:


{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "bash ~/.claude/hooks/brand-check.sh" }
        ]
      }
    ]
  }
}

That is the whole wiring. Drop the script at that path, make it executable, and every file Claude touches runs through it.

The 9 hooks that audit every file I write

Here is the actual stack, in the order I built them. Each one caught a mistake I made more than twice.

1. brand-check.sh blocks banned colors (#fff, #ffffff, #000000, #0d0d0d, #1a1a1e, any grayscale grays), em dashes, off-brand vocabulary (certain animation-studio names I refuse to evoke), my personal name in non-legal files, non-EUR currency references, and light text sitting on bright accent backgrounds. This one alone has stopped over 400 violations since October. Every time I commit, I can see in the hook logs what it caught, and the number only goes up.

2. spacing-check.sh enforces the spacing scale: 0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 48, 64 px. If I try to ship `padding: 17px`, it gets flagged. No exceptions, no "just this once".

3. font-check.sh allows only the Outfit font family, enforces a 10px minimum font size, and restricts border radius to 20, 10, or 9999 (pill). Anything else and I hear about it.

4. icon-check.sh requires Phosphor fill icons in Liquid templates. Lucide, Heroicons, and Feather are blocked on sight. The shop ships with one icon library, not four.

5. a11y-check.sh checks every `` for alt text, flags any `target="_blank"` without `rel="noopener"`, catches empty anchor tags, and warns if the html root is missing a lang attribute. This is the hook that quietly saved my Lighthouse score.

6. seo-check.sh requires an h1 inside hero sections and a meta description on page templates. Landing pages that shipped without meta used to be a weekly problem. Now they cannot.

7. affiliate-check.sh scans articles for mentions of Shopify, Buffer, ElevenLabs, and Freepik. If the name appears but the affiliate link does not, the hook flags it. That rule paid for its own setup within the first month. German affiliate law also requires proper disclosure, so the hook doubles as a compliance check.

8. verify-theme-id.sh is a PreToolUse hook on Bash. Before any `shopify theme push` runs, it prints the target theme ID and makes me look at it. I have pushed to the wrong theme exactly once in my life and I am not doing it again.

9. verify-blog-post.sh is a PostToolUse hook that runs after a publish command. It pings the live URL, checks the HTTP status, and confirms the article actually shipped. If Shopify quietly 500s, I know before the next article goes out.

Build your first hook in under 20 lines

Pick the smallest real problem you have. Mine was em dashes. I could not stop Claude using them no matter how many times I put it in CLAUDE.md. So I wrote this:


#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

[ -z "$file" ] || [ ! -f "$file" ] && exit 0

if grep -qP '\x{2014}' "$file"; then
  count=$(grep -oP '\x{2014}' "$file" | wc -l | tr -d ' ')
  jq -n --arg msg "Found $count em dash(es) in $file. Brand rule: use commas, hyphens, or periods instead. Please fix." \
    '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $msg}}'
fi

exit 0

That is it. 12 lines of real code. It reads the JSON payload, grabs the file path, greps for the em dash codepoint (U+2014), and if it finds any, prints a JSON response with `additionalContext`. Claude reads that message on its next turn and rewrites the line. I have never had to manually remove an em dash since.

The JSON output shape matters. The useful fields are `hookSpecificOutput.additionalContext` (what Claude sees and acts on) and, for PreToolUse hooks, a `permissionDecision` of `deny` with a `permissionDecisionReason` if you want to hard-block a tool call. Everything else is optional.

Once you have the first one working, the pattern repeats. Add a regex, add a message, register it in settings.json. My brand-check.sh is 180 lines now, but it started as the script above with one rule.

What Claude Code hooks cannot do

Hooks are single-file, single-moment checks. They see the file Claude just wrote and nothing else. They do not know that the CSS variable you just defined is referenced in three other files. They do not know that the blog article you are publishing duplicates one from three months ago. They do not know intent.

So I do not ask them to. Cross-file logic, systemic audits, and anything stateful lives in skills and slash commands. `/qa` runs before every push: build, tests, brand lint, security scan, full repo sweep. `/audit` is the big monthly one, 12 categories of checks across all 17 repos in parallel, designed to find the stuff hooks miss. `/stale-check` verifies counts (articles, products, years) across every repo so I never ship a homepage saying "100 articles" while the blog has 177.

Hooks catch the mistake at the moment of writing. Skills and commands catch the mistake at the moment of shipping. Both layers are necessary and neither replaces the other. I learned this the hard way when a brand-compliant file with a perfect alt tag still broke production because it referenced a variable that got renamed in a sibling file two days earlier. Hooks would never have caught that. `/qa` did.

The other thing hooks cannot do is override themselves. If a hook is wrong, or a rule genuinely does not apply (a legal page that has to contain my legal name, for example), the hook needs an explicit allowlist. I keep a small list of paths that skip brand-check: `impressum.html`, `privacy-policy.html`, a handful of internal tooling files. Everything else gets audited. Explicit allowlist, never implicit. If I can't name the file that should skip the check, the check runs.

Bottom Line

Write the first hook tomorrow. Pick the mistake you make most often, write 15 lines of bash, register it in settings.json, ship it. Write the ninth hook a month from now. By then the codebase is enforcing itself and you are free to think about actual product work instead of reviewing your own commits. 9 hooks took me six months of iteration and they run on every single file, forever, for free. The compounding part is what matters. Each new hook layers on top of the rest, and every rule you encode is a rule you never have to remember again. That is how a solo studio stays consistent across 17 repos without burning out on reviews.

Stay in the loop
New tools, drops, and AI experiments. No spam. Unsubscribe anytime.
Back to all articles