devAlice
← AI Agents

Claude Code Hooks — Auto-run Shell Commands on Session and Tool Events

settings.json hooks insert shell commands at session start / pre & post tool use / stop. Five examples: security block, auto-format, logging, external alerts.

Claude Code's hooks — defined under the hooks key in settings.json — let you auto-run shell commands on session start, before/after tool calls, and on session end. Intercept what the AI is about to do, fix things up afterward, clean up at the end. A powerful extension point.

This guide targets Claude Code 1.x / the official hooks spec. It tightens automation + security one notch beyond Claude Code setup.

TL;DR

Hook eventFiresTypical use
SessionStartRight after session beginsMemory sync notice, env validation
UserPromptSubmitRight after user submits a promptSlash command recognition, logging, post-processing
PreToolUseRight before a tool callBlock dangerous commands, protect secret files
PostToolUseRight after a tool callAuto-format (prettier), lint, side-effect logging
StopRight before session endChange summary, backup, notification

Key rules:

  • Exit code 2 blocks the tool call (PreToolUse).
  • JSON arrives on hook stdin — parse with jq.
  • Hooks are shell commands — mind OS differences (macOS/Linux/Windows WSL).

Prerequisites

  • Claude Code 1.x installed — Claude Code setup
  • jq installed (brew install jq / apt install jq / winget install jqlang.jq)
  • Edit access to settings.json (usually ~/.claude/settings.json)

Download the Baseline

The devAlice recommended five-hook baseline + permission deny/allow list — grab it and customize.

settings.json
# 1. Download
curl -fsSL https://devalice.jaceclub.com/assets/ai-agents/claude-code-hooks/settings.example.json -o settings.json
 
# 2. Verify SHA-256
shasum -a 256 settings.json
# Expected: 30c1d4117a935cac03b601c71125c11b71733c8c59517ab129fd8913cfb8a69e
 
# 3. Inspect (especially PreToolUse hooks)
less settings.json
 
# 4. Copy to its location (macOS/Linux)
mkdir -p ~/.claude
cp settings.json ~/.claude/settings.json

⚠️ Caution: hooks run arbitrary shell commands. Always inspect anyone else's hooks before applying. This sample is a safe five-hook baseline, but review and adapt for your environment.


1. Hooks Structure — 5 min

The hooks key in ~/.claude/settings.json:

{
  "hooks": {
    "EventName": [
      {
        "matcher": "regex or tool name",  // optional
        "hooks": [
          {
            "type": "command",
            "command": "/bin/bash -lc 'actual shell command'"
          }
        ]
      }
    ]
  }
}

Matching rules:

  • No matcher → fire on every event
  • matcher = tool name (Bash, Write, Edit, Read…) → only that tool
  • matcher = regex (^/(?:ej|euijinpro)\\b) → for UserPromptSubmit, matches the prompt text

JSON arriving on stdin:

{
  "session_id": "...",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/Users/...",
  "tool_name": "Bash",
  "tool_input": { "command": "..." },
  "tool_response": { /* PostToolUse only */ }
}

Extract with jq -r .tool_input.command.


2. SessionStart — Load Context

Simplest hook. Print a line when the session starts:

"SessionStart": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "/bin/bash -lc 'printf \"[session start] %s\\nmemory sync: latest (skip)\\n\" \"$(date \"+%Y-%m-%d %H:%M:%S\")\"'"
      }
    ]
  }
]

Stdout is exposed as a system message. Use it for:

  • Triggering memory file auto-load
  • Project-specific context output
  • Git status summary at start
# Richer example
command='/bin/bash -lc "echo [session start]; git -C \"$PWD\" status -s; git -C \"$PWD\" log --oneline -3"'

3. PreToolUse — Block Dangerous Commands (highest-value hook)

exit 2 blocks. Even if the AI tries a dangerous shell command, you stop it.

3.1 Dangerous Patterns

"PreToolUse": [
  {
    "matcher": "Bash",
    "hooks": [
      {
        "type": "command",
        "command": "/bin/bash -lc 'cmd=$(jq -r .tool_input.command); case \"$cmd\" in *\"rm -rf /\"*|*\"sudo rm\"*|*\":(){\"*) echo \"blocked dangerous command: $cmd\" >&2; exit 2;; esac'"
      }
    ]
  }
]

Blocked patterns:

  • rm -rf / — system destruction
  • sudo rm — broad delete (allow via whitelist if you need it)
  • :(){ — fork bomb (:(){ :|: & };:)

3.2 Block Edits to Secret Files

{
  "matcher": "Write|Edit",
  "hooks": [
    {
      "type": "command",
      "command": "/bin/bash -lc 'path=$(jq -r .tool_input.file_path); case \"$path\" in */.env|*/credentials.json|*/.aws/credentials) echo \"blocked secret-file edit: $path\" >&2; exit 2;; esac'"
    }
  ]
}

Prevents accidental edits to .env, credentials.json, ~/.aws/credentials, etc.

3.3 Protect Specific Directories

command='/bin/bash -lc "path=\$(jq -r .tool_input.file_path); case \"\$path\" in /etc/*|/System/*) echo \"blocked system path: \$path\" >&2; exit 2;; esac"'

4. PostToolUse — Auto-format / Lint / Log

Cleanup after the tool call.

4.1 Prettier Auto-format

"PostToolUse": [
  {
    "matcher": "Edit|Write|MultiEdit",
    "hooks": [
      {
        "type": "command",
        "command": "/bin/bash -lc 'path=$(jq -r .tool_input.file_path); case \"$path\" in *.ts|*.tsx|*.js|*.jsx) prettier --write \"$path\" 2>/dev/null || true ;; esac'"
      }
    ]
  }
]

After an AI Write/Edit, prettier auto-cleans. Adds ~0.5s per edit.

4.2 Log Changed Files

command='/bin/bash -lc "path=\$(jq -r .tool_input.file_path); echo \"[changed] \$(date +%T) \$path\" >> ~/.claude/change.log"'

Time-ordered record of every file change in the session.

4.3 Type-check / Lint (move slow work to Stop)

tsc --noEmit on every edit is expensive. Better in the Stop hook (§6).


5. UserPromptSubmit — Slash Detection / Post-processing Trigger

"UserPromptSubmit": [
  {
    "matcher": "^/(?:ej|euijinpro)\\b",
    "hooks": [
      {
        "type": "command",
        "command": "/bin/bash -lc 'echo \"[ej skill] prompt earmarked for euijin.pro logging\" >&2'"
      }
    ]
  }
]
  • Detect slash commands → trigger external systems (Slack, separate logging)
  • Auto-label specific keywords (urgent, bug)
  • Token budget threshold checks

6. Stop — Cleanup at Session End

"Stop": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "/bin/bash -lc 'printf \"\\n[session end] %s\\n\" \"$(date \"+%Y-%m-%d %H:%M:%S\")\"'"
      }
    ]
  }
]

Uses:

  • Changed-file git status summary
  • Backup (rsync or git stash)
  • External shutdown notification
  • One round of type-check / build
command='/bin/bash -lc "if git diff --quiet; then echo \"[clean]\"; else echo \"[changes]\"; git diff --stat; fi"'

7. permissions — Deny / Allow

Separate from hooks but the most important security key in settings.json.

"permissions": {
  "deny": [
    "Bash(rm -rf /:*)",
    "Bash(sudo rm:*)",
    "Read(.env)",
    "Read(.env.local)",
    "Read(.aws/credentials)",
    "Read(*/credentials.json)"
  ],
  "allow": [
    "Bash(git status:*)",
    "Bash(git log:*)",
    "Bash(git diff:*)",
    "Bash(pnpm:*)",
    "Bash(npm:*)",
    "Bash(node:*)"
  ]
}
  • deny wins unconditionally — complements hook blocks
  • allow auto-approves without user prompt (frequent safe commands)
  • Everything else asks the user every time

This guide's hooks overlap with deny — defense in depth.


8. settings.json Locations and Priority

LocationScopeNotes
~/.claude/settings.jsonUser-globalAll projects
<project>/.claude/settings.jsonProjectMerges with user on project entry
<project>/.claude/settings.local.jsonProject + user-localRecommended gitignored

Merge order: user → project → project.local. Later overrides earlier.

Hooks append rather than merge — 5 user hooks + 3 project hooks = all 8 run.


9. Debugging — When the Hook Doesn't Fire

9.1 Inspect stdin Payload

Temporary debug hook:

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "/bin/bash -lc 'cat > /tmp/claude-hook-debug.json'"
    }
  ]
}

cat /tmp/claude-hook-debug.json | jq .

9.2 Test Exit Codes

command='/bin/bash -lc "echo dbg >&2; exit 2"'

→ Verify blocking (PreToolUse) works.

9.3 Confirm Hook Fires

command='/bin/bash -lc "echo \"[hook fired $$ $(date +%T)]\" >> ~/.claude/hook-trace.log"'

Check the log after a session.

9.4 JSON Syntax Errors

settings.json is unforgiving — trailing commas and missed escapes are common. Validate via jq . ~/.claude/settings.json.


10. Troubleshooting

"command not found: jq"

brew install jq (macOS) / sudo apt install jq (Linux) / winget install jqlang.jq (Windows).

Hook Fires But Does Nothing

  • command must be single-line. For multiline, use \\n or call a separate script.
  • Watch shell escapes — \" is double-escaped inside JSON.

PreToolUse exit 2 Ignored

  • Confirm exit 2 actually reaches the last line of the shell command.
  • A set -e environment can let case ;; exit 0 — directly exit 2 inside the matched branch.

Prettier Doesn't Run

  • Confirm prettier is installed in the project root (pnpm prettier --version).
  • Hook PATH may not include node_modules/.bin → use npx prettier or an absolute path.

settings.json Changes Don't Apply

  • Session restart required. Or /config reload (if supported).
  • Could be silently ignored due to JSON syntax error. Validate with jq ..

Next Steps

References

Changelog

  • 2026-05-12 — Initial draft (devAlice M2 seed expansion)

Comments