Working hook configs for Claude Code. Each example includes the settings.json entry and the script. Copy what you need, modify the paths.
How Hooks Work
Hooks are defined in settings.json under the hooks key. Two event types:
- PreToolUse: Runs before a tool executes. If the hook exits non-zero, the tool call is blocked.
- PostToolUse: Runs after a tool completes. Cannot block, but can trigger side effects.
Each hook has a matcher (regex matching tool names) and a command (shell command to execute). The command receives environment variables with context about the tool call.
Hook structure in settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"command": "echo 'File being modified: $CLAUDE_FILE_PATH'"
}
],
"PostToolUse": [
{
"matcher": "Bash",
"command": "echo 'Command finished: $CLAUDE_COMMAND'"
}
]
}
}Hook Execution
Hooks run synchronously. Claude waits for the hook to complete before proceeding. Keep hooks fast (under 5s). Slow hooks degrade the interactive experience.
Auto-Format on Write
Run Prettier (or any formatter) every time Claude writes or edits a file. Keeps code style consistent without Claude needing to think about formatting.
Prettier auto-format hook
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "npx prettier --write "$CLAUDE_FILE_PATH" 2>/dev/null || true"
}
]
}
}The 2>/dev/null || true suppresses errors for files Prettier does not handle (images, binaries). Without it, Prettier errors would show up in Claude's context and confuse subsequent actions.
Black (Python) auto-format hook
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "if [[ "$CLAUDE_FILE_PATH" == *.py ]]; then black "$CLAUDE_FILE_PATH" 2>/dev/null; fi"
}
]
}
}Run Tests After Edit
Automatically run related tests when Claude modifies a source file. Catches regressions immediately instead of discovering them at commit time.
Run tests after file changes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/run-related-tests.sh"
}
]
}
}.claude/hooks/run-related-tests.sh
#!/bin/bash
# Run tests related to the modified file
FILE="$CLAUDE_FILE_PATH"
# Skip non-source files
[[ "$FILE" != *.ts && "$FILE" != *.tsx && "$FILE" != *.js ]] && exit 0
# Skip test files themselves
[[ "$FILE" == *.test.* || "$FILE" == *.spec.* ]] && exit 0
# Find matching test file
TEST_FILE="${FILE%.ts}.test.ts"
[[ ! -f "$TEST_FILE" ]] && TEST_FILE="${FILE%.tsx}.test.tsx"
[[ ! -f "$TEST_FILE" ]] && exit 0
# Run only the related test
npx jest "$TEST_FILE" --no-coverage 2>&1 | tail -5Protect Sensitive Files
Block writes to production configs, migration files, or CI pipelines. The PreToolUse hook exits non-zero to cancel the operation.
Protected file guard
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"command": "bash .claude/hooks/check-protected.sh"
}
]
}
}.claude/hooks/check-protected.sh
#!/bin/bash
PROTECTED_PATTERNS=(
".env"
".env.*"
"docker-compose.prod.yml"
"*.migration.*"
".github/workflows/*"
"terraform/*"
)
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$CLAUDE_FILE_PATH" == $pattern ]]; then
echo "BLOCKED: $CLAUDE_FILE_PATH matches protected pattern '$pattern'" >&2
echo "Edit this file manually or remove the guard in .claude/hooks/check-protected.sh" >&2
exit 1
fi
done
exit 0When Claude tries to edit a protected file, the hook blocks it and Claude sees the stderr message. Claude will explain what it wanted to change and suggest you make the edit manually.
Lint on Save
ESLint after writes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "if [[ "$CLAUDE_FILE_PATH" == *.ts || "$CLAUDE_FILE_PATH" == *.tsx ]]; then npx eslint --fix "$CLAUDE_FILE_PATH" 2>/dev/null; fi"
}
]
}
}ESLint runs with --fix to auto-correct issues. Claude's output plus ESLint's fixes produce cleaner code than either alone.
Commit Message Validation
Enforce conventional commit format when Claude runs git commit:
Conventional commit hook
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash .claude/hooks/check-commit-msg.sh"
}
]
}
}.claude/hooks/check-commit-msg.sh
#!/bin/bash
# Only check git commit commands
[[ "$CLAUDE_COMMAND" != *"git commit"* ]] && exit 0
# Extract the commit message from -m flag
MSG=$(echo "$CLAUDE_COMMAND" | grep -oP '(?<=-m ")[^"]*' || echo "$CLAUDE_COMMAND" | grep -oP "(?<=-m ')[^']*")
[[ -z "$MSG" ]] && exit 0
# Check conventional commit format
if ! echo "$MSG" | grep -qP '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)((.+))?: .+'; then
echo "BLOCKED: Commit message does not follow conventional format" >&2
echo "Expected: type(scope): description" >&2
echo "Got: $MSG" >&2
exit 1
fiNotification on Completion
Send a macOS notification when Claude finishes a long-running command:
macOS notification hook
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"command": "osascript -e 'display notification "Claude finished: $CLAUDE_COMMAND" with title "Claude Code"' 2>/dev/null || true"
}
]
}
}On Linux, replace the osascript command with notify-send "Claude Code" "Finished: $CLAUDE_COMMAND".
Logging and Audit Trail
Log all tool calls to a file
{
"hooks": {
"PostToolUse": [
{
"matcher": ".*",
"command": "echo "$(date -Iseconds) | $CLAUDE_TOOL_NAME | $CLAUDE_FILE_PATH $CLAUDE_COMMAND" >> .claude/audit.log"
}
]
}
}The .* matcher catches every tool. The log captures timestamps, tool names, and the operation target. Useful for reviewing what Claude did in a session, especially in autonomous or CI workflows.
Block Dangerous Commands
Block destructive bash commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash .claude/hooks/block-dangerous.sh"
}
]
}
}.claude/hooks/block-dangerous.sh
#!/bin/bash
DANGEROUS_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"rm -rf *"
"> /dev/sda"
"mkfs."
"dd if="
":(){:|:&};:"
"chmod -R 777 /"
"git push --force origin main"
"git push -f origin main"
"DROP TABLE"
"DROP DATABASE"
)
CMD="$CLAUDE_COMMAND"
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if [[ "$CMD" == *"$pattern"* ]]; then
echo "BLOCKED: Command contains dangerous pattern '$pattern'" >&2
exit 1
fi
done
exit 0Type-Check Before Commit
TypeScript check before git commit
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "if [[ "$CLAUDE_COMMAND" == *'git commit'* ]]; then npx tsc --noEmit 2>&1 | head -20; fi"
}
]
}
}This runs tsc --noEmit before every commit. If type errors exist, they appear in Claude's context and it can fix them before retrying. The head -20 prevents long error lists from flooding the context.
Troubleshooting
Hook does not fire
Check that the matcher regex matches the tool name exactly. Tool names are case-sensitive: Write not write, Bash not bash. Test your regex at a site like regex101. Add echo "fired" >> /tmp/hook-debug.log to the command to verify execution.
Hook blocks everything
Your PreToolUse hook is exiting non-zero unexpectedly. Add set -x at the top of your shell script to see which line fails. Common cause: a command in the script that fails for non-target files (e.g., running eslint on a .md file).
Hook is slow
Hooks run synchronously. If your hook runs npm test on every file write, it blocks Claude for the duration of the test suite. Scope your hooks: check $CLAUDE_FILE_PATH extension before running language-specific tools, or move expensive checks to PostToolUse so they do not block.
FAQ
Can I chain multiple hooks for the same event?
Yes. Add multiple objects to the PreToolUse or PostToolUse array. They run in order. For PreToolUse, if any hook exits non-zero, the tool call is blocked, and remaining hooks do not run.
Do hooks work with --dangerously-skip-permissions?
Yes. Hooks and permissions are independent systems. Skip-permissions removes the interactive approval prompt. Hooks still fire on every tool call regardless of the permission mode.
Can hooks modify Claude's output?
Hooks cannot directly modify tool output. PostToolUse hooks see the result but cannot change it. If you need to transform output (e.g., redact secrets from logs), use the hook to post-process the file on disk rather than modifying Claude's in-memory output.
Run Claude Code Through Morph
All hooks work with Morph's API proxy. Get faster file edits with the fast apply model. Same hooks, same settings, lower cost.