DEV Community

Mirza Iqbal
Mirza Iqbal

Posted on

I have 160 Claude Code hooks. The 5 that pay rent every day.

$ ls ~/.claude/hooks/ | wc -l
160
$ ls ~/.claude/rules/ | wc -l
95
$ ls ~/.claude/skills/ | wc -l
75
Enter fullscreen mode Exit fullscreen mode

That is the state of my Claude Code setup as of this morning.

Most people stop at one or two hooks.

Most people then complain that Claude "forgets" rules.

Claude does not forget. It never had the rule enforced in the first place. A rule in a markdown file is a suggestion. A rule in a hook is a wall.

Here are the five hook patterns that actually pay rent in my daily flow. Each one solves a class of problem you cannot fix with prompt engineering.

1. The cost-gate hook (PreToolUse on Bash)

Anthropic split Agent SDK billing from subscription on 15 June 2026. Headless claude -p calls now bill against the SDK, not the Pro subscription. If your agent accidentally fires a nightly cron with claude -p, your bill changes shape overnight.

The fix is a PreToolUse hook on Bash that blocks the pattern.

#!/usr/bin/env bash
# @event: PreToolUse
# @matcher: Bash
PAYLOAD=$(cat)
CMD=$(echo "$PAYLOAD" | jq -r '.tool_input.command // ""')

if echo "$CMD" | grep -qE '\bclaude[[:space:]]+(-p|--print)'; then
  echo "BLOCKED. claude -p bills outside subscription after 2026-06-15." >&2
  echo "Use interactive Agent subagents instead." >&2
  exit 2
fi
Enter fullscreen mode Exit fullscreen mode

This hook has fired three times on my machine in the last two weeks. Each fire was a script I would have committed without realizing.

2. The em-dash banishment hook (PreToolUse on Write)

AI-generated text leaks em dashes everywhere. Editors can spot the pattern in a single scroll. A hook that blocks em dashes on Write to content files keeps the output sounding human.

#!/usr/bin/env bash
# @event: PreToolUse
# @matcher: Write|Edit
PAYLOAD=$(cat)
CONTENT=$(echo "$PAYLOAD" | jq -r '.tool_input.content // .tool_input.new_string // ""')

if echo "$CONTENT" | grep -qF $'\xe2\x80\x94'; then
  echo "BLOCKED. Em dash detected. Use periods or commas." >&2
  exit 2
fi
Enter fullscreen mode Exit fullscreen mode

The agent never gets to ship the draft with the wrong punctuation. It rewrites in place because the write failed.

3. The secrets-leak preemptive blocker (PreToolUse on Bash)

Pre-push hooks catch secrets at push time. By then they are already in your local commit history. A PreToolUse on Bash that watches for git add .env* or any add of a credential-shaped file catches the leak two steps earlier.

#!/usr/bin/env bash
# @event: PreToolUse
# @matcher: Bash
PAYLOAD=$(cat)
CMD=$(echo "$PAYLOAD" | jq -r '.tool_input.command // ""')

SECRET_PATTERNS='\.env($|\.|/)|credentials\.json|\.pem$|\.key$|service-account.*\.json'

if echo "$CMD" | grep -qE "git[[:space:]]+add.*($SECRET_PATTERNS)"; then
  echo "BLOCKED. Refusing to git add a secrets file." >&2
  echo "Add the file to .gitignore first, or use --explicitly-allow-secret flag." >&2
  exit 2
fi
Enter fullscreen mode Exit fullscreen mode

Two of my repos have .env.local in their root. Both stay out of git because this hook fires before the add lands.

4. The session-start surface (SessionStart event)

A SessionStart hook can read state files and show you what changed between sessions. Mine surfaces pending docs, drift in my agent-os repo, security advisories that landed overnight, and any hook that crashed three or more times in the last 24 hours.

#!/usr/bin/env bash
# @event: SessionStart

PENDING_DOCS=$(ls ~/.claude/cache/docs-pending/ 2>/dev/null | wc -l)
SECURITY_FLAGS=$(ls ~/.claude/cache/security/ 2>/dev/null | wc -l)
DRIFT=$(cd ~/repositories/agent-os && git status --porcelain 2>/dev/null | wc -l)

[ "$PENDING_DOCS" -gt 0 ] && echo "[DOCS-PENDING] $PENDING_DOCS items"
[ "$SECURITY_FLAGS" -gt 0 ] && echo "[SECURITY] $SECURITY_FLAGS advisories"
[ "$DRIFT" -gt 0 ] && echo "[REPO-DRIFT] agent-os has $DRIFT uncommitted files"

exit 0
Enter fullscreen mode Exit fullscreen mode

The signal arrives passively. No need to remember to check. Every session opens with the state of the world already loaded into the agent's view.

5. The complexity-cap guard (PreToolUse on Write)

This one is unique to running an evolving setup. I have a hard limit on each asset class (95 rules, 160 hooks, 75 skills). When I try to write a new rule that would push the count past the cap, the hook blocks the write and forces me to remove or merge an existing rule first.

#!/usr/bin/env bash
# @event: PreToolUse
# @matcher: Write
PAYLOAD=$(cat)
PATH_OUT=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path // ""')

case "$PATH_OUT" in
  */.claude/rules/*.md)
    COUNT=$(ls ~/.claude/rules/*.md 2>/dev/null | wc -l)
    CAP=95
    if [ "$COUNT" -ge "$CAP" ] && [ ! -f "$PATH_OUT" ]; then
      echo "BLOCKED. rules at $COUNT/$CAP cap." >&2
      echo "Merge or remove an existing rule before adding a new one." >&2
      exit 2
    fi
    ;;
esac
exit 0
Enter fullscreen mode Exit fullscreen mode

Without this hook my setup would have grown to 200 rules in a month and none of them would carry weight. With it, every new rule earns its slot by replacing weaker content.

What hooks change in practice

Three things you stop doing once you have hooks at this scale.

You stop writing rules that the agent will silently ignore.

You stop catching mistakes at push time when the cost of fixing them is highest.

You stop telling Claude "remember to do X" because X is now enforced at the tool boundary regardless of whether the model remembers.

The mental model shift is the same shift that took DevOps from "remember to deploy correctly" to "the pipeline will not let you deploy incorrectly". Hooks are the pipeline for Claude Code.

If you have one hook that you cannot live without, drop it in the comments. I am especially curious about hooks for parts of the agent loop I have not touched yet, like PreCompact and notification hooks. The pattern library only grows when more people share the ones that actually fire.

Top comments (0)