Skip to main content
Guide contents
Beginner12 min

Hooks in Claude Code for Beginners: Automating Repetitive Actions

A complete hands-on guide to configuring Hooks in Claude Code: automated linting, formatting, pre-commit quality gates, desktop notifications, and headless execution.

Published:

1. What Are Hooks in Claude Code and Why You Need Them

When working with terminal AI agents, developers frequently find themselves typing the same repetitive instructions: "now run ESLint", "format the code with Prettier", "check TypeScript types before committing". Even when added to CLAUDE.md, large language models can occasionally overlook these instructions or skip them to conserve output tokens.

Hooks in Claude Code solve this problem at the infrastructure level: they are deterministic triggers that automatically run system commands or scripts at exact lifecycle moments during an agent's execution.

The core philosophy of hooks is straightforward:

Whenever Claude performs event X → the operating system deterministically runs action Y.

mermaid
flowchart LR subgraph Natural_Language["CLAUDE.md Instructions (Probabilistic)"] A["Prompt: 'Always run linter'"] --> B{"Does LLM remember?"} B -->|Sometimes| C["Runs check"] B -->|Missed| D["Errors accumulate"] end subgraph Native_Hooks["Hooks in settings.json (Deterministic)"] E["Event: File modified"] --> F["OS trigger fires"] F --> G["Guaranteed execution of npx eslint"] end

Comparing Automation Approaches

FeatureManual PromptsRules in CLAUDE.mdNative Claude Code Hooks
Execution ReliabilityLow (depends on human memory)Moderate (probabilistic LLM behavior)100% (deterministic event interception)
Token ConsumptionHigh (consumes prompt tokens each time)Moderate (loaded into every system prompt)Zero (runs locally via subshell)
Response SpeedSlow (requires manual typing)Requires extra model turnInstant (native process invocation)
Blocking CapabilitiesNoneNoneYes (non-zero exit code halts operation)
Note

Claude Code hooks are configured as JSON objects inside .claude/settings.json at the project level, or globally in ~/.claude/settings.json.


2. Event Lifecycle: PreToolUse, PostToolUse, Notification, and Stop

Claude Code exposes four foundational lifecycle events where you can attach automated commands.

mermaid
sequenceDiagram autonumber actor Dev as Developer participant Agent as Claude Code participant Hook as Hook Engine participant Tool as Tool (Bash/Edit/Write) Dev->>Agent: Code modification request Agent->>Hook: Tool invocation attempt Note over Hook: PreToolUse Hook Hook-->>Agent: Validation passed (Exit Code 0) Agent->>Tool: Execute file change Tool-->>Agent: Execution success Agent->>Hook: Tool completed event Note over Hook: PostToolUse Hook Hook-->>Agent: Linting / Formatting result Agent->>Hook: Model response completed Note over Hook: Stop Hook (Notification) Agent-->>Dev: Final answer returned

Primary Event Hooks

  1. PreToolUse (Before tool execution): fires prior to Claude executing an action. If the hook command fails (exits with non-zero code), the tool execution is aborted. This is the optimal place for pre-commit quality gates and safety blocks.
  2. PostToolUse (After tool execution): fires immediately after a tool succeeds. This is the most common hook for running auto-formatters (Prettier) and linters (ESLint) against touched files.
  3. Notification (System notice): fires whenever Claude needs to alert the user or request permissions.
  4. Stop (Response completion): fires when Claude Code has finished its current answer and is waiting for your next instruction. Ideal for desktop notifications and audio chimes.
Lifecycle execution moments for Claude Code hooks ЗбільшитиLifecycle execution moments for Claude Code hooksLifecycle execution moments for Claude Code hooks

3. Configuration Structure in settings.json and Matcher Syntax

Hooks are defined in .claude/settings.json. If this file does not exist in your project root yet, create it.

Basic Configuration Syntax

json
{ "hooks": { "PreToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": "echo 'About to edit file...'" } ] } ], "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo 'Bash command finished successfully'" } ] } ] } }

How the Matcher Works

The matcher field specifies which agent tool activates the hook:

  • "Edit" — targets existing file modifications.
  • "Write" — targets new file creations or full overwrites.
  • "Bash" — captures any shell command execution.
  • "Bash(git commit*)" — pattern matcher filtering for shell commands starting with git commit.
  • "*" — universal wildcard matching all available agent tools.
Tip

Matcher names are case-sensitive. Use Claude Code's standard tool names: Edit, Write, Bash, Glob, Grep.


4. Automated Linting and Formatting (ESLint + Prettier)

The most valuable daily workflow is delegating styling and syntax fixing to Prettier and ESLint. This ensures newly generated or edited code always adheres to your repository standards.

Configuring PostToolUse for ESLint and Prettier

json
{ "hooks": { "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": "npx eslint --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null || true" } ] }, { "matcher": "Write", "hooks": [ { "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true" } ] } ] } }

Safety Flags Explained

  • "$CLAUDE_FILE_PATH" — an environment variable automatically populated with the absolute path to the file Claude just edited or created.
  • 2>/dev/null — silences noisy stderr diagnostics so your terminal stays clean.
  • || true — an essential shell fallback. It guarantees that even if a linter finds an unfixable syntax issue (exiting with code 1), the hook returns 0 and does not crash Claude's conversational flow.
mermaid
flowchart TD A["Claude calls Edit"] --> B["File written to disk"] B --> C["PostToolUse trigger"] C --> D["npx eslint --fix $CLAUDE_FILE_PATH"] D --> E{"Success or || true"} E --> F["Agent continues solving user prompt"]

5. Pre-commit Quality Gates (TypeCheck + Tests)

While formatting hooks should be permissive (|| true), committing code to source control demands strict quality gates.

Using PreToolUse, you can block git commit commands whenever TypeScript compilation errors exist or unit tests fail.

Configuring a Blocking Pre-commit Hook

json
{ "hooks": { "PreToolUse": [ { "matcher": "Bash(git commit*)", "hooks": [ { "type": "command", "command": "npm run typecheck && npm run lint" } ] } ] } }

The Error Interception Loop

  1. Claude initiates a git commit -m "..." command.
  2. The PreToolUse hook intercepts the execution before Git runs.
  3. The validation scripts (npm run typecheck and npm run lint) execute in the background.
  4. If an error is detected: the command exits with code 1.
  5. Claude Code receives the raw compilation failure directly in its context.
  6. Rather than committing broken code, the agent diagnoses the issue, fixes the types, and re-attempts the commit cleanly.
Important

Never append || true to blocking PreToolUse checks. Doing so causes the hook to always evaluate as successful, neutralizing the quality gate.


6. Audio and Desktop Notifications on Task Completion (Stop Hook)

Long-running refactors or test suite executions can take several minutes. Instead of staying glued to your terminal, configure an alert on the Stop event.

Tip

The Stop event triggers only when Claude completes its entire multi-step reasoning response, not between internal tool invocations.


7. Context Injection via Environment Variables

To make hook scripts modular and context-aware, Claude Code exports real-time execution metadata into shell environment variables.

Claude Code environment variables for hook execution ЗбільшитиClaude Code environment variables for hook executionClaude Code environment variables for hook execution

Claude Code Environment Variable Reference

Variable NameTypeDescription and Sample Value
$CLAUDE_FILE_PATHAbsolute PathTarget file currently being created or edited (/Users/dev/project/src/index.ts)
$CLAUDE_TOOL_NAMEString identifierName of the tool triggering the event (Edit, Write, Bash)
$CLAUDE_PROJECT_DIRAbsolute PathRoot directory where the current Claude Code session was launched

Building a Context-Aware Validator Script

Create a script at .claude/hooks/smart-validator.sh:

bash
#!/usr/bin/env bash set -e # Verify the target file exists and branch by extension if [[ -f "$CLAUDE_FILE_PATH" ]]; then case "$CLAUDE_FILE_PATH" in *.ts|*.tsx) echo "⚡ Validating TypeScript file: $CLAUDE_FILE_PATH" npx eslint --fix "$CLAUDE_FILE_PATH" || true ;; *.json) echo "🔍 Validating JSON syntax..." jq empty "$CLAUDE_FILE_PATH" 2>/dev/null || echo "Invalid JSON syntax detected!" ;; *.md) echo "📝 Documentation updated: $(basename "$CLAUDE_FILE_PATH")" ;; esac fi

Reference the script inside .claude/settings.json:

json
{ "hooks": { "PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "bash .claude/hooks/smart-validator.sh" } ] } ] } }

8. The "CI-in-a-Loop" Pattern: Continuous Fast Feedback

In unassisted agent sessions, a common problem occurs: Claude modifies a dozen files, runs the project build, and gets hit with a wall of 40 type errors. Identifying which individual edit caused each issue is difficult and token-expensive.

CI-in-a-Loop introduces tight, per-edit validation:

mermaid
flowchart TD A["Claude edits a file"] --> B["Instant tsc run"] B --> C{"Any error?"} C -->|Yes| D["Claude receives 1 focused error"] D --> E["Instant 5-second fix"] E --> B C -->|No| F["Proceeds to next line/file"]

Implementing Tight TypeScript Feedback

json
{ "hooks": { "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": "npx tsc --noEmit 2>&1 | head -n 20 || true" } ] } ] } }

Why head -n 20 Is Critical

If your compiler generates hundreds of lines of error logs, they flood Claude's context window. Pipelining through head -n 20 presents the most critical early diagnostics, allowing the agent to resolve issues immediately without blowing through token limits.


9. Headless Automation: -p Mode, Cron, and Git Hooks

Hooks can be paired with Claude Code's headless print/prompt mode (-p). This allows complex developer workflows to run entirely unattended.

mermaid
flowchart LR A["Cron Trigger (02:00 AM)"] --> B["Script nightly-audit.sh"] B --> C["claude -p 'Run test suite, fix bugs, commit'"] C --> D["PostToolUse Hooks validate code"] D --> E["Automatic push of verified changes"]

Unattended Nightly Audit Script (nightly-audit.sh)

bash
#!/usr/bin/env bash cd /var/www/my-project # Run Claude Code headlessly with an explicit objective claude -p "Run full test suite. If any tests fail, inspect the stack trace and fix them. Ensure typecheck passes. Finally, commit all fixes with a conventional commit." >> /var/log/claude-audit.log 2>&1

Scheduling with Cron

Open your crontab editor (crontab -e) and schedule execution every night at 3:00 AM:

text
0 3 * * * /usr/local/bin/nightly-audit.sh

Automated Validation via Git Hook (.git/hooks/post-merge)

Automatically audit dependency changes whenever pulling fresh code:

bash
#!/usr/bin/env bash # .git/hooks/post-merge echo "🚀 Running automated Claude Code audit on new merge..." claude -p "Check if package.json was updated. If so, run npm install, verify build, and report summary."

Remember to grant executable permissions: chmod +x .git/hooks/post-merge.


10. Engineering Best Practices and Safety Guidelines

Improperly designed hooks can induce infinite execution loops or lock up your terminal. Follow these four core engineering rules.

Four Safety Rules for Hook Authors

  1. Sub-second Execution: PostToolUse hooks fire after every single file edit. If a hook takes longer than 1–2 seconds, interacting with Claude will feel sluggish. Reserve heavy end-to-end suites for PreToolUse on git commit, never on each Edit.

  2. Dedicated Log Redirection: Always stream diagnostic outputs into a log file:

    bash
    "command": "bash .claude/hooks/check.sh >> /tmp/claude-hooks.log 2>&1 || true"

    If an automated check fails silently, /tmp/claude-hooks.log will reveal why.

  3. Standalone Shell Verification: Before adding any command to settings.json, run it in your clean terminal. If it fails there, it will reliably break Claude's session.

  4. Preventing Recursion Loops: Never configure a PostToolUse hook that modifies project files without ignore filters. If an automated script triggers another Edit, Claude will become trapped in an infinite execution cycle.

Warning

Never configure hooks that require interactive user input (such as read -p confirmation prompts or sudo password requests). They will hang the background terminal process indefinitely.


11. Hands-on Workshop, Cheatsheet, and Final Checklist

To kickstart automation in your own projects, reference this decision cheatsheet and production-ready configuration.

Claude Code hooks decision cheatsheet ЗбільшитиClaude Code hooks decision cheatsheetClaude Code hooks decision cheatsheet

Production-Grade .claude/settings.json Template

Copy this template directly into your .claude directory:

json
{ "hooks": { "PreToolUse": [ { "matcher": "Bash(git commit*)", "hooks": [ { "type": "command", "command": "npm run typecheck && npm test" } ] } ], "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true" } ] }, { "matcher": "Write", "hooks": [ { "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "osascript -e 'display notification \"Work completed!\" with title \"Claude Code\"' 2>/dev/null || true" } ] } ] } }

Quick Knowledge Check

Tip

1. What is the fundamental operational difference between PreToolUse and PostToolUse?

Answer: PreToolUse fires before a tool executes and can abort the operation if it exits with a non-zero status. PostToolUse fires after successful execution and is used for non-blocking cleanup like formatting and linting.

Tip

2. Why do formatting hooks include || true in their shell command?

Answer: To prevent lint or formatting warnings from returning an error code that halts Claude's response generation.

Tip

3. Which environment variable holds the path to the file currently being edited?

Answer: The $CLAUDE_FILE_PATH environment variable.

Production Readiness Checklist

  • Created .claude/ directory and settings.json in the project root.
  • Configured automatic file formatting via PostToolUse + Prettier.
  • Enforced strict PreToolUse quality gates on Bash(git commit*) running typecheck.
  • Attached audio or desktop completion notifications to the Stop event.
  • Tested all shell commands independently in a standard terminal session.
  • Confirmed no commands prompt for interactive passwords or [y/N] confirmations.
This guide is completely free. If it saved you an evening, you can support the project's growth.
Support the author