mdashikjs/blog
All posts
Claude Code Hooks: How I Automated the Boring Parts of My Dev Loop
AI

Claude Code Hooks: How I Automated the Boring Parts of My Dev Loop

AI4 min

Claude Code Hooks: How I Automated the Boring Parts of My Dev Loop

Hooks turn Claude Code from a chat tool into a deterministic part of my workflow. Auto-format after edits, block secret commits, and run the right test suite automatically — configured once in settings.json.

Claude CodeAIDeveloper ExperienceHooks
Share:

The events I actually use

  • PreToolUse — fires before a tool runs. Can block the call.
  • PostToolUse — fires after a tool runs. Cannot block, but can lint, format, or notify.
  • Stop — fires when the assistant finishes a turn. Good for notifications.
  • UserPromptSubmit — fires when the user sends a message. Good for injecting context.

Hook one: auto-format on edit

Every time Claude edits a file, run the formatter for that file type:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "pnpm prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true"
      }]
    }]
  }
}

No more reminding the model to match code style. The formatter runs after every write. Zero friction.

Hook two: block accidental secret commits

{
  "PreToolUse": [{
    "matcher": "Bash",
    "hooks": [{
      "type": "command",
      "command": "if echo \"$CLAUDE_TOOL_COMMAND\" | grep -qE 'git (add|commit).*\\.env'; then echo 'Refusing to stage .env'; exit 1; fi"
    }]
  }]
}

If the model tries to stage a .env file, the hook exits non-zero and the tool call is blocked. Belt and suspenders against the "oops committed secrets" class of bug.

Hook three: run tests for the affected area

{
  "PostToolUse": [{
    "matcher": "Edit|Write",
    "hooks": [{
      "type": "command",
      "command": "./scripts/test-for-file.sh \"$CLAUDE_TOOL_FILE_PATH\""
    }]
  }]
}

test-for-file.sh maps a source file to its test file and runs only that test. Fast feedback; no waiting for a full suite.

Hook four: notify on stop

{
  "Stop": [{
    "hooks": [{
      "type": "command",
      "command": "notify-send 'Claude' 'Turn complete'"
    }]
  }]
}

Long-running tasks — migrations, refactors across many files — fire a desktop notification when done. I can switch to another window and come back when it pings.

Where hooks live

User-level: ~/.claude/settings.json. Project-level: .claude/settings.json. Per-project local (not checked in): .claude/settings.local.json. Project settings override user settings; local overrides everything.

I keep general hooks (format, notify) in user settings and project-specific hooks (test runners, lint configs) in project settings.

The gotcha

Hooks run synchronously for PreToolUse. A slow hook blocks the tool call until it finishes. For formatters and linters this is fine. For anything that could hang, fire it in the background and let PostToolUse be fire-and-forget.

What hooks are not for

Hooks execute deterministically — they are not AI behavior. If you want the model to decide whether to do something, that belongs in a skill or in your prompt. Hooks are for "always do this when X happens." Treat the distinction seriously; fighting the model via hooks works for a week and breaks on the novel case.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.