DEV Community

Dan Jeong
Dan Jeong

Posted on

5 Claude Code Hooks Every Developer Needs

Claude Code hooks let you run scripts before and after every tool call. Most people ignore them. Here are five that will save you from real problems.

Quick Setup

All hooks go in ~/.claude/settings.json. Each one runs a script at a specific lifecycle event. The structure:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "bun ~/.claude/hooks/your-hook.ts" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Four lifecycle events: PreToolUse, PostToolUse, SessionStart, SessionEnd.

1. Audit Logger (PostToolUse)

Track every command Claude runs. When something breaks at 2am, you want a log.

// ~/.claude/hooks/audit-logger.ts
import { readFileSync, appendFileSync, mkdirSync } from "fs";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));

const logDir = `${process.env.HOME}/.claude/logs`;
mkdirSync(logDir, { recursive: true });

const entry = {
  timestamp: new Date().toISOString(),
  tool: input.tool_name,
  input: input.tool_input,
};

appendFileSync(
  `${logDir}/audit-${new Date().toISOString().split("T")[0]}.jsonl`,
  JSON.stringify(entry) + "\n"
);

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

Hook config:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "bun ~/.claude/hooks/audit-logger.ts" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Empty matcher = runs for every tool, not just Bash. You get a complete audit trail.

2. Git Safety Net (PreToolUse)

Block force pushes to main, accidental hard resets, and history rewrites.

// ~/.claude/hooks/git-safety.ts
import { readFileSync } from "fs";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
if (input.tool_name !== "Bash") process.exit(0);

const cmd = input.tool_input?.command || "";

const blocked = [
  { pattern: /git\s+push.*--force\s+.*(main|master)/, msg: "Force push to main/master" },
  { pattern: /git\s+reset\s+--hard/, msg: "Hard reset discards uncommitted work" },
  { pattern: /git\s+clean\s+-[dfx]/, msg: "Git clean removes untracked files permanently" },
  { pattern: /git\s+checkout\s+\.\s*$/, msg: "Checkout . discards all changes" },
];

for (const rule of blocked) {
  if (rule.pattern.test(cmd)) {
    console.error(`[BLOCKED] ${rule.msg}`);
    process.exit(2);
  }
}

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

Exit code 2 tells Claude Code to block the command. Claude sees your error message and adjusts.

3. Credential Guard (PreToolUse)

Prevent Claude from reading, copying, or exfiltrating your secrets.

// ~/.claude/hooks/credential-guard.ts
import { readFileSync } from "fs";
import { resolve } from "path";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));

const protectedPaths = [
  "~/.ssh",
  "~/.aws",
  "~/.gnupg",
  "~/.env",
  "~/.config/credentials",
];

const home = process.env.HOME || "";
const expanded = protectedPaths.map(p => p.replace("~", home));

// Check both Bash commands and file operations
const targets: string[] = [];

if (input.tool_name === "Bash") {
  targets.push(input.tool_input?.command || "");
}
if (input.tool_name === "Read" || input.tool_name === "Write" || input.tool_name === "Edit") {
  targets.push(input.tool_input?.file_path || "");
  targets.push(input.tool_input?.path || "");
}

for (const target of targets) {
  const resolved = resolve(target);
  for (const protected_path of expanded) {
    if (resolved.startsWith(protected_path) || target.includes(protected_path)) {
      console.error(`[BLOCKED] Access to protected path: ${protected_path}`);
      process.exit(2);
    }
  }
}

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

This covers Read, Write, Edit, and Bash. Claude can't cat ~/.ssh/id_rsa or cp ~/.aws/credentials /tmp/.

4. Session Context Loader (SessionStart)

Auto-load project notes, TODOs, and context when Claude starts. No more re-explaining your project every session.

// ~/.claude/hooks/session-context.ts
import { readFileSync, existsSync } from "fs";

const contextFiles = [
  "CLAUDE.md",
  ".claude/context.md",
  "TODO.md",
  "ARCHITECTURE.md",
];

const cwd = process.cwd();
const loaded: string[] = [];

for (const file of contextFiles) {
  const fullPath = `${cwd}/${file}`;
  if (existsSync(fullPath)) {
    loaded.push(file);
  }
}

if (loaded.length > 0) {
  console.error(`[Context] Loaded: ${loaded.join(", ")}`);
} else {
  console.error("[Context] No project context files found. Consider adding a CLAUDE.md.");
}

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

Hook config for SessionStart:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "bun ~/.claude/hooks/session-context.ts" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Dangerous Pattern Detector (PreToolUse)

Catch novel dangerous patterns that simple string matching misses.

// ~/.claude/hooks/pattern-detector.ts
import { readFileSync } from "fs";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
if (input.tool_name !== "Bash") process.exit(0);

const cmd = input.tool_input?.command || "";

// Pipe chains with network tools = potential exfiltration
if (/\|.*\|.*\|/.test(cmd) && /(curl|wget|nc|netcat)/.test(cmd)) {
  console.error("[BLOCKED] Complex pipe chain with network access");
  process.exit(2);
}

// Loops with delete operations
if (/(for|while).*do.*rm/.test(cmd)) {
  console.error("[BLOCKED] Loop containing delete operations");
  process.exit(2);
}

// Eval with variable injection
if (/eval.*\$/.test(cmd)) {
  console.error("[BLOCKED] Eval with variable expansion");
  process.exit(2);
}

// Download and execute pattern
if (/(curl|wget).*\|\s*(bash|sh|zsh|python)/.test(cmd)) {
  console.error("[BLOCKED] Download and execute pattern");
  process.exit(2);
}

// Writing to system paths
if (/(>|tee)\s+\/(etc|usr|bin|sbin)\//.test(cmd)) {
  console.error("[BLOCKED] Writing to system directory");
  process.exit(2);
}

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

Combining All Five

Here's the complete settings.json with all hooks wired up:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "bun ~/.claude/hooks/credential-guard.ts" }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bun ~/.claude/hooks/git-safety.ts" },
          { "type": "command", "command": "bun ~/.claude/hooks/pattern-detector.ts" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "bun ~/.claude/hooks/audit-logger.ts" }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "bun ~/.claude/hooks/session-context.ts" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Each hook runs independently. If one blocks (exit code 2), the command stops. If one logs (exit code 0), execution continues.

Going Further

These five hooks cover the basics. For production use, you want:

  • YAML-based pattern configs (easier to update than code)
  • Path protection tiers (zero-access, read-only, no-delete)
  • Centralized logging with search
  • Cost tracking per session

I packaged all of this into two products:

Both run on Bun/TypeScript. Drop into ~/.claude/ and they work.

Top comments (0)