← 所有文章
claudeClaude Code

How to Make Claude Follow Your Rules Every Single Time

I kept telling Claude "run the linter before committing" and it kept forgetting. Not sometimes — like every third commit. So I stopped asking and made it automatic. One JSON block in my settings, and now Claude literally cannot commit without linting first.
That's what hooks are. Not a suggestion to Claude. A rule it can't break.

What You'll Learn


Why Not Just Write It in CLAUDE.md?

You can tell Claude "always run tests before committing" in your CLAUDE.md. Claude will try. But it's an LLM — it interprets, it forgets, it sometimes decides the tests aren't necessary for "this small change."
Hooks are deterministic. They're shell commands that execute at specific points in Claude's workflow. Claude doesn't choose whether to run them. They just run. CLAUDE.md is advice. Hooks are enforcement.

The Four Hook Types

PreToolUse — Runs before Claude uses a tool. You can block the action or let it through. Perfect for catching dangerous commands.
PostToolUse — Runs after a tool finishes. Use for formatting, validation, or notifications.
Notification — Fires when Claude needs your attention. Great for desktop alerts so you don't watch the terminal.
PermissionDenied — New in auto mode. Fires when the classifier blocks an action. Return retry: true to let Claude try a different approach.

Your First Hook: Never Commit Without Linting

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [{
      "if": "Bash(git commit *)",
      "command": "npm run lint",
      "failAction": "block"
    }]
  }
}

Every time Claude tries to run git commit, the linter runs first. If it fails, the commit is blocked. The if field uses permission rule syntax so this only fires on commits, not every bash command.

✅ You'll know it's working when you see lint output in the transcript before each commit.


Three Hooks I Use Every Day

Desktop notification when Claude finishes:

{"hooks": {"Notification": [{"command": "osascript -e 'display notification \"$EVENT_TITLE\" with title \"Claude\"'"}]}}

On Linux, use notify-send instead of osascript.
Block force-push entirely:

{"hooks": {"PreToolUse": [{"if": "Bash(git push *--force*)", "command": "echo 'Blocked' && exit 1", "failAction": "block"}]}}

No exceptions. If I really need to force-push, I do it myself outside Claude.
Auto-format after file edits:

{"hooks": {"PostToolUse": [{"if": "Edit(*) Write(*)", "command": "prettier --write $TOOL_INPUT_FILE_PATH"}]}}

What You Probably Didn't Know: Prompt-Based Hooks

Hooks aren't limited to shell commands. You can use a Claude model to evaluate conditions:

{"hooks": {"PreToolUse": [{"if": "Bash(git commit *)", "type": "prompt", "prompt": "Review this commit for hardcoded secrets or API keys. Block if found."}]}}

This runs a separate Claude instance to evaluate the commit. Like a security reviewer that never takes a day off.

⚠️ Prompt-based hooks add 2-5 seconds per invocation. Use them for high-stakes checks (security, compliance), not every file edit. 🔬 Tested.


What Hooks Can't Do

❌ Hooks can't modify Claude's thinking or reasoning. They run alongside actions, not inside the model's head.
❌ Hooks don't persist state between runs. Each invocation is a fresh shell.


Setting Up

Hooks go in your settings file: ~/.claude/settings.json for personal, .claude/settings.json for project-wide, or .claude/settings.local.json for project + just you. Use /settings in Claude Code to open it directly.

The best automation is the kind you set up once and never think about again. Hooks won't make Claude smarter, but they'll make sure it follows the rules even when it's feeling creative.

← 所有文章OctoDock 首頁 →