$catMANUAL||~35 min

Claude Code Hooks: 5 Automations That Doubled My Productivity

advertisement

Claude Code Hooks: 5 Automations That Doubled My Productivity

I've been using Claude Code for a few months now. The biggest win isn't how smart it is — it's how obedient. It does whatever you tell it. The problem is you have to keep telling it the same things.

Like formatting code after every edit. Or not touching .env files. Or remembering project conventions after context compression. You can handle all of this manually, sure. But it gets old fast.

That's where Hooks come in.

What Are Hooks

Hooks are shell commands that run at specific points in Claude Code's lifecycle. Think of them like Git hooks, but for your AI coding assistant. Instead of relying on the LLM to remember to do something, you make it deterministic — the hook fires every time, no exceptions.

You can:

  • Auto-format files after Claude edits them
  • Block dangerous commands before they execute
  • Send desktop notifications when Claude needs input
  • Re-inject context after the conversation gets compressed

My 5 Daily Hooks

These are the hooks I actually use every day. They go in ~/.claude/settings.json (global) or .claude/settings.json in your project root (project-specific).

1. Desktop Notifications: Stop Staring at the Terminal

This was the first hook I set up. I'd kick off a task in Claude Code, switch to my browser to look something up, and five minutes later realize Claude had been waiting for my input the whole time.

With notifications, I get a system alert the moment Claude finishes or needs permission. Instant context switch.

json
1
{
2
  "hooks": {
3
    "Notification": [
4
      {
5
        "matcher": "",
6
        "hooks": [
7
          {
8
            "type": "command",
9
            "command": "notify-send 'Claude Code' 'Needs your input'"
10
          }
11
        ]
12
      }
13
    ]
14
  }
15
}

Linux uses notify-send, macOS uses osascript -e 'display notification "Claude Code needs you" with title "Claude Code"', Windows uses PowerShell.

The empty matcher fires on all notification types. You can narrow it down:

  • permission_prompt: Claude needs you to approve a tool call
  • idle_prompt: Claude finished and is waiting for your next instruction
  • auth_success: Authentication completed

2. Auto-Format: No More Manual Prettier

Claude writes code fast, but the formatting doesn't always match your project's style. I used to manually run prettier --write after every edit. Now it just happens.

json
1
{
2
  "hooks": {
3
    "PostToolUse": [
4
      {
5
        "matcher": "Edit|Write",
6
        "hooks": [
7
          {
8
            "type": "command",
9
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
10
          }
11
        ]
12
      }
13
    ]
14
  }
15
}

PostToolUse fires after tool calls. The Edit|Write matcher means it only runs after file edits. jq extracts the file path from the hook's JSON input and passes it to prettier.

The beauty of this: whatever Claude touches gets formatted automatically. No relying on the LLM to "remember" to run prettier. It just works.

3. Protect Sensitive Files: Don't Let Claude Touch .env

I learned this one the hard way. Asked Claude to refactor a project and it "helpfully" reformatted the environment variables in .env — broke the whole thing.

Now I have a PreToolUse hook that checks the target file before any edit:

json
1
{
2
  "hooks": {
3
    "PreToolUse": [
4
      {
5
        "matcher": "Edit|Write",
6
        "hooks": [
7
          {
8
            "type": "command",
9
            "command": "python3 /home/user/scripts/check_protected.py"
10
          }
11
        ]
12
      }
13
    ]
14
  }
15
}

The script is straightforward:

python
1
import sys, json
2
 
3
data = json.load(sys.stdin)
4
path = data.get("tool_input", {}).get("file_path", "")
5
 
6
protected = [".env", ".env.local", "package-lock.json", "pnpm-lock.yaml", ".git/"]
7
 
8
for p in protected:
9
    if p in path:
10
        print(f"Blocked: {path} is a protected file", file=sys.stderr)
11
        sys.exit(2)  # exit code 2 = deny
12
 
13
sys.exit(0)

Exit codes:

  • 0: Allow, proceed
  • 2: Deny, Claude sees your stderr message and adjusts

This hook saved me multiple times. Once Claude tried to delete node_modules and reinstall to fix a dependency issue — on a monorepo that takes 20 minutes to install. Blocked.

4. Context Injection: Don't Lose Your Mind After Compression

When conversations get long, Claude Code compresses the context window. Important details get lost — project conventions, current task, branch name, whatever you discussed 50 messages ago.

A SessionStart hook with a compact matcher re-injects this info after every compression:

json
1
{
2
  "hooks": {
3
    "SessionStart": [
4
      {
5
        "matcher": "compact",
6
        "hooks": [
7
          {
8
            "type": "command",
9
            "command": "cat /home/user/.claude/context-reminder.md"
10
          }
11
        ]
12
      }
13
    ]
14
  }
15
}

The reminder file looks like:

code
1
## Project Conventions
2
- Use Bun, not npm
3
- Tests with vitest
4
- Run bun test && bun lint before committing
5
 
6
## Current Task
7
- Working on feat/auth branch: user auth refactor
8
- Prefer JWT + httpOnly cookies
9
- Don't touch existing session logic

The cat output goes straight into Claude's context. It's like restoring memory after a reset.

Pro tip: use git log --oneline -5 as the command. After compression, Claude sees your last 5 commits and knows where you left off.

5. Auto-Approve Plan Mode: Stop Clicking Confirm

Claude Code's plan mode shows you the execution plan before starting. Every time, you have to click "yes, proceed." Gets tedious.

json
1
{
2
  "hooks": {
3
    "PermissionRequest": [
4
      {
5
        "matcher": "ExitPlanMode",
6
        "hooks": [
7
          {
8
            "type": "command",
9
            "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
10
          }
11
        ]
12
      }
13
    ]
14
  }
15
}

This only auto-approves ExitPlanMode — switching from planning to execution. All other permission prompts (file writes, shell commands) still require manual approval.

Warning: Don't use .* or leave the matcher empty. That auto-approves everything, including arbitrary shell commands. Claude could run rm -rf / and you'd never know.

Hook Lifecycle Events

Those 5 hooks cover the most common events, but Claude Code supports way more:

  • SessionStart: Session begins or resumes
  • UserPromptSubmit: You submit a prompt, before Claude processes it
  • PreToolUse: Before a tool call (can block it)
  • PostToolUse: After a tool call succeeds
  • PostToolUseFailure: After a tool call fails
  • Notification: When Claude sends a notification
  • Stop: When Claude finishes responding
  • ConfigChange: When config files change
  • CwdChanged: When the working directory changes
  • FileChanged: When watched files change on disk
  • PreCompact / PostCompact: Before/after context compression
  • PermissionRequest: When a permission dialog appears
  • SubagentStart / SubagentStop: Subagent lifecycle
  • SessionEnd: When the session ends

Each event passes JSON data to your script via stdin. Your script can return control instructions via stdout.

Advanced: Prompt and Agent Hooks

Beyond command hooks (shell commands), there are two advanced types:

Prompt Hooks

Use an LLM to make judgments. For example, have another model review your prompt for safety before Claude processes it:

json
1
{
2
  "type": "prompt",
3
  "prompt": "Check if the user's request involves sensitive operations (deleting databases, modifying production config). If so, return deny."
4
}

Good for security audits and quality checks where you need "understanding" rather than "rule matching."

Agent Hooks

Multi-turn conversations with tool access. More powerful than prompt hooks — can call tools, do multi-step reasoning. Still experimental.

Honestly, I don't use these much. Command hooks cover 90% of use cases. But for enterprise Claude Code deployments, prompt hooks for security auditing are valuable.

HTTP Hooks: Webhook Notifications

Instead of running shell commands, hooks can fire HTTP requests directly. Useful for Slack, Feishu, or DingTalk notifications:

json
1
{
2
  "hooks": {
3
    "Stop": [
4
      {
5
        "matcher": "",
6
        "hooks": [
7
          {
8
            "type": "http",
9
            "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
10
            "method": "POST",
11
            "headers": {
12
              "Content-Type": "application/json"
13
            },
14
            "body": {
15
              "text": "Claude Code completed a task"
16
            }
17
          }
18
        ]
19
      }
20
    ]
21
  }
22
}

Cleaner than command hooks for simple notifications — no script needed, just declare the HTTP request in config.

Hooks + MCP

Hooks and MCP are separate extension mechanisms, but they work together. If you have an MCP server with code review tools, you can call it with an mcp_tool hook:

json
1
{
2
  "hooks": {
3
    "PostToolUse": [
4
      {
5
        "matcher": "Write",
6
        "hooks": [
7
          {
8
            "type": "mcp_tool",
9
            "server": "code-review",
10
            "tool": "review_changes",
11
            "arguments": {
12
              "file": "{{file_path}}"
13
            }
14
          }
15
        ]
16
      }
17
    ]
18
  }
19
}

Every time Claude writes a new file, it automatically triggers code review via your MCP service. More reliable than writing "review every file" in CLAUDE.md — hooks execute deterministically.

My Complete Config

Here's my actual project-level .claude/settings.json:

json
1
{
2
  "hooks": {
3
    "Notification": [
4
      {
5
        "matcher": "",
6
        "hooks": [
7
          {
8
            "type": "command",
9
            "command": "notify-send 'Claude Code' 'Needs your input'"
10
          }
11
        ]
12
      }
13
    ],
14
    "PostToolUse": [
15
      {
16
        "matcher": "Edit|Write",
17
        "hooks": [
18
          {
19
            "type": "command",
20
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true"
21
          }
22
        ]
23
      }
24
    ],
25
    "PreToolUse": [
26
      {
27
        "matcher": "Edit|Write",
28
        "hooks": [
29
          {
30
            "type": "command",
31
            "command": "python3 /home/user/scripts/check_protected.py"
32
          }
33
        ]
34
      }
35
    ],
36
    "SessionStart": [
37
      {
38
        "matcher": "compact",
39
        "hooks": [
40
          {
41
            "type": "command",
42
            "command": "cat /home/user/.claude/context-reminder.md 2>/dev/null || echo 'No context file found'"
43
          }
44
        ]
45
      }
46
    ],
47
    "PermissionRequest": [
48
      {
49
        "matcher": "ExitPlanMode",
50
        "hooks": [
51
          {
52
            "type": "command",
53
            "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
54
          }
55
        ]
56
      }
57
    ]
58
  }
59
}

Note the 2>/dev/null || true on the format hook — if prettier doesn't support a file type, it fails silently instead of breaking Claude's workflow.

Where to Put Hooks

Three levels:

  • ~/.claude/settings.json: Global, all projects
  • .claude/settings.json in project root: Project-specific
  • .claude/settings.local.json: Local, not in git

My recommendation:

  • Notifications → global (you always want them)
  • Formatting → project-specific (different projects, different formatters)
  • File protection → global (every project should protect .env)
  • Context injection → project-specific (each project has different conventions)

Debugging Hooks

Hooks that misbehave fail silently. A few debugging tricks:

  1. Type /hooks in Claude Code to see registered hooks
  2. Run your command manually to verify it works
  3. Add set -x to shell scripts for execution traces
  4. Use jq to validate your JSON config
bash
1
# Validate settings.json
2
cat ~/.claude/settings.json | jq .
3
 
4
# Test the hook command
5
echo '{"tool_input":{"file_path":"/tmp/test.py"}}' | jq -r '.tool_input.file_path'

Gotchas

Path Issues

Hooks don't always run in your project directory. Use absolute paths, or cd in your script.

jq Isn't Universal

The PostToolUse hooks often use jq to parse JSON. Linux usually has it, macOS with Homebrew has it, but some CI environments don't. Check first.

Exit Code 2 vs 1

In hooks, exit 2 means "deny the operation." exit 1 means "the script errored." Don't mix them up.

Parallel Execution

Multiple hooks matching the same event run in parallel, not sequentially. One hook denying doesn't stop the others. The most restrictive result wins: deny > defer > ask > allow.

Hooks vs CLAUDE.md vs Skills

People ask: should I use a hook or write a rule in CLAUDE.md?

The difference is determinism. Writing "run prettier after every edit" in CLAUDE.md means Claude will probably do it. Maybe 90% of the time. Hooks are 100% — if the condition matches, the command runs.

My rule:

  • Enforceable rules (file protection, formatting, security) → Hooks
  • Guidelines (code style, architecture preferences) → CLAUDE.md
  • Reusable workflows (language-specific dev flows, testing strategies) → Skills

All three work together. Hooks handle "must do," CLAUDE.md handles "should do," Skills handle "how to do well."

Wrapping Up

Hooks are one of Claude Code's most underrated features. They're not flashy, but they eliminate a ton of repetitive work and prevent dangerous mistakes.

I'm at the point where if something can be a hook, it is a hook. Claude focuses on what it's good at — understanding requirements, writing code, making architectural decisions. Formatting, file protection, notifications — that's all hooks.

Planning to experiment with prompt hooks for code review next. Will write that up when I have something worth sharing.

advertisement

Claude Code Hooks: 5 Automations That Doubled My Productivity — AI Hub