Skip to content

Claude Code Hooks: Enforce Rules and Automate Actions

Core Concept LearningAugust 18, 202610 min read

A developer tells Claude Code to commit changes to main, and Claude Code is about to execute git commit && git push. But your team has a rule: no direct commits to main, always go through a pull request. Without a hook, Claude Code would execute the command anyway, and you'd catch the mistake after the fact. With a hook, you can intercept that command before it runs, reject it with a message, and suggest the correct workflow instead. Hooks are how you enforce policy, automate side effects, and add guardrails without slowing down Claude Code's core productivity loop.

This article covers how hooks work, the difference between pre-tool and post-tool hooks, built-in security and lint hooks, and how to write your own in JavaScript or Python. We follow a running example through a payments-api repository where hooks prevent common mistakes (pushing to main, committing secrets, deploying without tests) and automate notifications (posting a Slack update when a test passes, triggering a rebuild when a file changes).

For how hooks fit into the broader Claude Code architecture, see How Claude Code Works. For environment setup and .claude/settings.json, see Claude Code Workflow.

What hooks are and when they fire

A hook is a function that Claude Code invokes at a specific point in the tool-execution lifecycle: either before a tool runs (pre-tool hook) or after it completes and returns a result (post-tool hook). The hook receives context about the request — what tool, what arguments, what working directory — and returns a decision:

  • Allow — tool executes as normal
  • Reject — tool is blocked, and Claude Code is told why it failed
  • Modify — hook changes the arguments (e.g., git push origin main becomes git push origin my-feature)
  • Augment (post-tool only) — hook adds data to the result (e.g., test stdout becomes test stdout + Slack notification sent)

Hooks are registered in .claude/settings.json under the hooks key, or loaded from .claude/hooks/ as individual files. They can be synchronous (return immediately) or asynchronous (wait for a network call to complete), and they can be JavaScript, Python, or shell scripts.

Running example: a payments-api repository has three hooks:

1. Pre-tool, git: block any commit to main 2. Pre-tool, bash: prevent commands that write to the database without a transaction 3. Post-tool, bash: post a Slack message when tests pass

Quick reference

  • Pre-tool hooks intercept before execution — fast to respond, and can prevent irreversible actions.
  • Post-tool hooks see the actual output — useful for logging, notifications, or conditional actions based on success/failure.
  • A hook receives the full context: tool name, arguments (with values), environment variables (masked for secrets), working directory, and session metadata.
  • If a hook rejects a tool call, Claude Code will retry with the next suggestion — you are not silently blocking the agent, you are redirecting it.
  • Hooks can be stateful (e.g., track how many times a command was rejected), and they can call external services (APIs, webhooks, databases).

Remember this

A hook is a checkpoint in the tool pipeline — before execution (to prevent mistakes) or after (to automate side effects).

Pre-tool hooks: Preventing mistakes before they happen

Pre-tool hooks fire right after Claude Code decides to call a tool but before the tool actually runs. They are the place to enforce policy: no commits to main, no deletes without confirmation, no database changes without explicit approval.

Example: your team rule is "no direct push to main." Add this hook:

1// .claude/hooks/block-main-push.js2module.exports = async (context) => {3  const { tool, args } = context;4  if (tool === "bash" && args.command.includes("git push") && args.command.includes("origin main")) {5    return {6      allowed: false,7      reason: "Direct pushes to main are blocked. Create a pull request instead: git push -u origin <branch>, then open a PR.",8      suggestion: `git push -u origin $(git rev-parse --abbrev-ref HEAD)`,9    };10  }11  return { allowed: true };12};

When Claude Code tries to execute git push origin main, this hook will reject it with a message and a suggested alternative. Claude Code will show the error to the user and ask what to do next — user can approve a retry with the suggested command, or give Claude Code a new instruction.

Another example: block database writes outside of transactions:

1// .claude/hooks/require-transaction.js2module.exports = async (context) => {3  const { tool, args } = context;4  if (tool === "bash" && args.command.includes("psql") && !args.command.includes("BEGIN")) {5    return {6      allowed: false,7      reason: "Database commands must be wrapped in a transaction (BEGIN...COMMIT). Add BEGIN; and COMMIT; to your command.",8    };9  }10  return { allowed: true };11};

Quick reference

  • Pre-tool hooks have sub-millisecond latency — keep them fast by avoiding network calls when possible.
  • Return allowed: false to reject; return allowed: true to allow; return modification to change the tool arguments.
  • Rejections include a reason (shown to the user) and an optional suggestion (code the user can approve).
  • A hook can call external APIs (OAuth token validation, audit logging, cost estimation) but should have a timeout (e.g., 5 seconds).
  • Hooks see masked secrets — environment variables marked as secrets are replaced with *** — use the unmasked copy if you need the actual value.

Remember this

Pre-tool hooks block unsafe actions at the moment of execution — faster and safer than catching mistakes after they happen.

Post-tool hooks: Automate side effects and notifications

Post-tool hooks fire after a tool completes and returns a result. They are useful for notifications, logging, conditional triggers, and adding context to the result.

Example: when tests pass, post a Slack notification:

1// .claude/hooks/notify-slack-on-test-pass.js2module.exports = async (context) => {3  const { tool, result } = context;4  if (tool === "bash" && result.command.includes("npm test") && result.exitCode === 0) {5    await fetch(process.env.SLACK_WEBHOOK_URL, {6      method: "POST",7      body: JSON.stringify({8        text: `✅ Tests passed in ${context.workingDirectory}`,9      }),10    });11    return {12      augment: {13        message: "Slack notification sent.",14      },15    };16  }17  return {};18};

When Claude Code runs npm test and it succeeds, this hook will automatically post to Slack and add a note to Claude Code's output.

Another example: log file writes to an audit trail:

1// .claude/hooks/audit-file-writes.js2module.exports = async (context) => {3  const { tool, args, result } = context;4  if (tool === "edit" && result.success) {5    console.log(`[AUDIT] File edited: ${args.path} by session ${context.sessionId}`);6  }7  return {};8};

This hook logs every file edit without changing Claude Code's behavior — useful for compliance, incident investigation, or understanding what the agent did.

Quick reference

  • Post-tool hooks have access to the tool result, so they can react to actual success/failure, not just intended commands.
  • Use post-tool hooks for side effects (notifications, logging, webhooks) and pre-tool for validation (security, policy).
  • The augment return field adds data to Claude Code's context — use it to tell Claude Code about side effects it triggered.
  • Post-tool hooks can also modify the displayed result (e.g., hide sensitive data, format output, or add clarifying notes).
  • Async post-tool hooks (waiting for an API call) run in the background; Claude Code will continue while waiting.

Remember this

Post-tool hooks automate everything that happens after a tool runs — notifications, logging, conditional actions, without blocking the agent.

Built-in hooks: Security, linting, and format validation

Claude Code ships with optional built-in hooks that cover common security and quality concerns. You enable them in .claude/settings.json:

1{2  "hooks": {3    "builtIn": {4      "preventSecretExposure": true,5      "preventDangerousCommands": true,6      "requireLintPass": false,7      "requireTypeCheck": false8    }9  }10}

preventSecretExposure (default: true) — blocks commands that would write secrets to files or logs. If Claude Code tries to commit .env to Git, this hook rejects it.

preventDangerousCommands (default: true) — blocks high-risk commands without confirmation: rm -rf /, database drops, production deployments without tags. You can still run them, but Claude Code will ask explicitly.

requireLintPass (default: false) — after any file edit, run your linter (ESLint, Clippy, etc.) and reject the change if it fails. Useful for teams that want style enforcement automatic.

requireTypeCheck (default: false) — after TypeScript/Kotlin/Rust files are edited, run the type checker and reject if it fails.

You can customize thresholds in .claude/settings.json. For example:

1{2  "hooks": {3    "builtIn": {4      "preventDangerousCommands": {5        "enabled": true,6        "whitelist": ["rm src/temp/**"],7        "requireConfirmation": ["deployment", "database-alter"]8      }9    }10  }11}

Quick reference

  • Built-in hooks are well-tested and updated with every Claude Code release — start with them before writing custom hooks.
  • preventSecretExposure checks against a list of common secret patterns (OPENAI_API_KEY, DATABASE_PASSWORD, etc.).
  • preventDangerousCommands maintains a curated list of risky patterns — git push --force without a branch name, DROP TABLE, etc.
  • Type-check and lint hooks run async, so they don't block Claude Code — but they do add a verification step before the result is final.
  • Disable any built-in hook if it's too restrictive for your team, or customize its behavior in settings.

Remember this

Built-in hooks handle security, linting, and type safety without custom code — enable them to add guardrails to your workflow.

Writing custom hooks: JavaScript, Python, or shell

You can write hooks in JavaScript (Node.js), Python, or shell (bash/zsh). Hooks are registered by path in .claude/settings.json:

1{2  "hooks": {3    "preTool": [4      { "path": ".claude/hooks/block-main.js" },5      { "path": ".claude/hooks/check-env.py", "timeout": 5000 },6      { "path": ".claude/hooks/audit.sh" }7    ],8    "postTool": [9      { "path": ".claude/hooks/notify.js" }10    ]11  }12}

JavaScript hooks run in Node.js and receive the context as a parameter. They must return a Promise resolving to a decision object:

1// .claude/hooks/my-hook.js2module.exports = async (context) => {3  // context = { tool, args, workingDirectory, sessionId, environment, result (post-tool only) }4  return {5    allowed: true,6    // or: { allowed: false, reason: "...", suggestion: "..." }7    // or: { modification: { args: { ... } } }  (pre-tool only)8    // or: { augment: { message: "..." } }  (post-tool only)9  };10};

Python hooks follow the same pattern but use standard input/output for context and response:

1#!/usr/bin/env python32import json3import sys4 5context = json.loads(sys.stdin.read())6if context['tool'] == 'bash':7    print(json.dumps({'allowed': True}))8else:9    print(json.dumps({'allowed': False, 'reason': 'tool not allowed'}))

Shell hooks receive context as environment variables and output JSON to stdout:

1#!/bin/bash2if [[ "$CLAUDE_TOOL" == "bash" ]] && [[ "$CLAUDE_COMMAND" == *"rm "* ]]; then3  echo '{"allowed": false, "reason": "Destructive commands require approval"}'4else5  echo '{"allowed": true}'6fi

Use whichever language your team is most comfortable with. JavaScript is easiest for I/O-heavy hooks (HTTP calls, database lookups), Python for data processing, shell for quick checks.

Quick reference

  • Hooks can read from disk (load a list of allowed paths, check a config file) or call network APIs (OAuth, Slack, databases).
  • Set a timeout in settings (default 5 seconds) — if a hook takes longer, it is killed and treated as rejected.
  • Errors in hooks are logged and reported to the user; a crashing hook rejects the tool call to be safe.
  • Hooks run in the working directory specified in context, so relative paths are available.
  • Secrets from environment variables are masked in context unless you explicitly read process.env or os.environ.

Remember this

Custom hooks are just functions that see the tool call and decide: allow it, reject it, modify it, or trigger a side effect.

Common patterns and real-world use cases

Most teams start with a few simple pre-tool hooks (block dangerous commands) and gradually add post-tool hooks for notifications and logging. Here are three real patterns:

Pattern 1: Guardrails for shared repositories — New teams often use hooks to prevent accidental commits to main, deployments without testing, or secrets in Git. One pre-tool hook per rule keeps the policy obvious and audit-able.

Pattern 2: Team notifications — Post-tool hooks automatically notify Slack, Teams, or Discord when important events happen: test pass, deployment complete, merge request created. This keeps the team in sync without Claude Code waiting for manual updates.

Pattern 3: Cost control in production — Pre-tool hooks that call a cloud provider API (AWS, GCP, Azure) and estimate the cost of a command before it runs. For expensive operations (spinning up infrastructure, running large batch jobs), the hook can ask for approval or suggest a cheaper alternative.

Hook use cases by team size and risk profile
Use CaseTypeBenefitComplexity
Block commits to mainPre-tool, gitEnforce branching strategyLow — one-line check
Prevent secret exposurePre-tool, all toolsSecurity guardrailMedium — pattern matching
Notify Slack on successPost-tool, bashTeam visibilityLow — one HTTP call
Require test pass before mergePre-tool, gitQuality gateMedium — run test suite
Log all file writes to audit trailPost-tool, editComplianceLow — append to file
Cost estimation before deploymentPre-tool, bashCost controlHigh — call cloud APIs
Enforce code review for mainPost-tool, gitProcess enforcementHigh — GitHub API check

Quick reference

  • Start with 2–3 security hooks (block dangerous commands, prevent secrets) before adding workflow automation.
  • Test your hooks in development with cclude test-hook <hook-file> before deploying to production.
  • Document each hook's purpose in .claude/hooks/README.md so teammates understand when and why they block.
  • Use hook version control: keep hooks in Git, review hook changes in pull requests, and use CI to validate hook syntax.
  • Monitor hook behavior with logging — if a hook rejects too often, it's a signal that the policy needs adjustment.

Remember this

Start simple (one security guardrail), test it, then add workflow automation (notifications) and cost controls as confidence grows.

Key takeaway

Hooks are how you keep Claude Code safe and productive at scale. A pre-tool hook blocks a mistake the moment Claude Code thinks of it, saving hours of incident response. A post-tool hook automates notifications and logging without slowing down the agent. Start by copying one of the common patterns from this article into .claude/hooks/, enable it in .claude/settings.json, and test it on a safe task. Then expand: add a Slack notification, prevent deployments without tests, or integrate with your audit logging.

Try this now: create .claude/hooks/block-main-push.js with the example from the "Pre-tool hooks" section, add it to your .claude/settings.json, and ask Claude Code to "commit and push to main." It will suggest the correct workflow instead. That's a hook in action — enforcing policy without blocking productivity.

Share:
PK

Polo Khan

Lead Author & Systems Architect

Software engineer and distributed systems architect specializing in backend scalability, cloud-native infrastructure, databases, and AI engineering workflows. Author and maintainer of Core Concept Learning.

Human-Engineered & Fact-CheckedOriginal Visual DiagramsEditorial Standards →Send Feedback

Related Articles

GitHub Actions lets you trigger Claude Code workflows directly from pull requests—enabling @claude mentions in PRs, auto

Read

Claude Code has a dense vocabulary because it is more than a chat box beside your editor. The important terms describe w

Read

Claude Code is a suite of AI-native coding interfaces — the same underlying agent loop works whether you're typing at th

Read

Keep learning

Follow a structured path or browse all courses to go deeper.