Claude Code Hooks for Production Teams

Use Claude Code hooks as deterministic team guardrails for tests, protected files, command logging, permissions, and safe rollout.

Wednesday, June 3, 2026Omid Saffari
Claude Code Hooks for Production Teams

Claude Code hooks are worth rolling out when they become deterministic guardrails, not another prompt convention. Put tests, protected-file rules, command logging, and approval policy in hooks and settings; leave judgment-heavy review to humans or a separate reviewer.

The Verdict

Treat Claude Code hooks as the enforcement layer for rules that must always run. A CLAUDE.md can tell Claude how your team works, but a hook can block a risky tool call, format edited files, log configuration changes, or stop a turn until a check passes.

That distinction matters when Claude Code moves from one strong engineer's laptop to a team rollout. The failure mode is not that hooks are too weak. The failure mode is that teams put every preference into hooks, create a slow local CI clone, and then wonder why people bypass it. Use hooks for deterministic controls. Use Claude Code rollout policy for who gets the tool, which repos are in scope, and what review path is required.

The practical line:

  • Use PreToolUse to block protected files, destructive shell commands, unsafe dependency operations, and policy violations before they happen.
  • Use PostToolUse to format, lint, log, or queue checks after edits.
  • Use Stop for final turn gates that should send Claude back to work.
  • Use ConfigChange to audit or block settings and skill changes.
  • Use Notification to pull a human back only when attention is actually needed.
  • Use async hooks for background feedback, never for rules that must block.

The goal is not to make Claude Code impossible to use. The goal is to make the safe path the default path, with enough logging to debug what happened after a long agentic edit session.

The Production Hook Set

Start with five hooks. They cover the highest-risk moments without turning every tool call into a policy meeting.

ControlEventMatcherProduction purpose
Protected filesPreToolUse`EditWrite`
Dangerous shell commandsPreToolUseBashDeny commands that delete broadly, rewrite git history, or touch production services
FormattingPostToolUse`EditWrite`
Config auditConfigChange`project_settingslocal_settings
Human attentionNotificationnarrow notification typesAlert only when Claude is waiting for input or permission

That is the first-week set. Add more only when a real incident or repeated review comment justifies it.

  1. Start In One Repo

    Pick one active repo with a clear test command and a clear list of files Claude must not edit. Do not start with every repo in the company. The pilot should prove that hooks reduce review friction instead of hiding it.

  2. Ship Project Scope First

    Put team-shared hooks in .claude/settings.json and scripts in .claude/hooks/. Project settings are checked into source control, so every collaborator gets the same repo defaults.

  3. Keep Personal Overrides Local

    Put personal notification commands and workstation-specific paths in .claude/settings.local.json. Local settings are not checked in and are meant for experimentation or machine-specific preferences.

  4. Promote Non-Negotiables Later

    Move organization security controls to managed settings only after the pilot proves the rule. Managed settings cannot be overridden by user or project settings, so a bad managed rule blocks everyone.

Put Hooks In The Right Scope

Most teams should place repo behavior in project scope and enterprise policy in managed scope. That keeps the repo portable while reserving hard controls for rules that need central enforcement.

Claude Code's settings precedence is Managed, command line arguments, Local, Project, then User. That means managed settings sit at the top and cannot be overridden by user or project settings. Project settings are the right default for team-shared hooks, permissions, MCP servers, and plugins. Local settings are for personal overrides and experiments.

A sensible rollout uses these boundaries:

  • .claude/settings.json: repo formatters, protected file rules, approved MCP servers, project-specific notification behavior, and lightweight test hooks.
  • .claude/settings.local.json: personal desktop notifications, local binary paths, and temporary experiments.
  • Managed settings: organization deny lists, telemetry defaults, marketplace restrictions, and policy that security or platform engineering owns.

For larger organizations, Claude for Enterprise adds SSO, domain capture, role-based permissions, compliance API, and managed policy settings for organization-wide Claude Code configurations. Those controls matter when a hook policy is no longer "team convention" and becomes "company requirement."

The rollout mistake is committing a hook that assumes one person's machine. If a hook calls /opt/homebrew/bin/prettier, it will fail for someone on Linux. Use repo-local commands such as npx prettier, pnpm lint, uv run, or a checked-in script. Then make the script itself responsible for checking whether the required tool exists and returning a useful error.

The Guardrail That Blocks Before Damage

The highest-value hook is a narrow PreToolUse guardrail. It sees the tool request before execution, receives JSON on stdin, and can block the action with exit code 2 or a structured permissionDecision.

This is the pattern we use for team rollout: one script logs every Bash command to a local audit file, and another script blocks command shapes that should never run inside an agent session.

JSON
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh"
          },
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-bash.sh"
          }
        ]
      }
    ]
  }
}

The logging script should be boring and side-effect-light:

Bash
#!/usr/bin/env bash
set -euo pipefail

input="$(cat)"
mkdir -p "$HOME/.claude/audit"

jq -c '{
  ts: now | todate,
  session_id,
  cwd,
  event: .hook_event_name,
  tool: .tool_name,
  command: .tool_input.command
}' <<< "$input" >> "$HOME/.claude/audit/bash-commands.jsonl"

The blocking script should be narrow and explain the rule to Claude:

Bash
#!/usr/bin/env bash
set -euo pipefail

input="$(cat)"
command="$(jq -r '.tool_input.command // ""' <<< "$input")"

blocked_reason=""

case "$command" in
  *"rm -rf /"*|*"rm -rf ."*|*"rm -rf ~"*)
    blocked_reason="destructive recursive delete is not allowed from Claude Code"
    ;;
  *"git push --force"*|*"git push -f"*)
    blocked_reason="force pushes require a human-owned terminal session"
    ;;
  *"kubectl delete"*|*"terraform destroy"*|*"wrangler secret"*)
    blocked_reason="production or secret operations require explicit human approval outside Claude Code"
    ;;
esac

if [ -n "$blocked_reason" ]; then
  echo "Blocked: $blocked_reason" >&2
  exit 2
fi

exit 0

Exit code 0 reports no objection. For PreToolUse, it does not approve the call by itself; normal permission flow still applies. Exit code 2 blocks blockable events and sends stderr back to Claude as feedback. Any other exit code lets the action proceed and records a hook error notice, so do not use exit 1 for policy blocks.

Structured JSON gives finer control:

Bash
jq -nc '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: "Use rg instead of grep for repository search."
  }
}'

allow, deny, and ask are the key PreToolUse decisions. A deny cancels the tool call and sends the reason to Claude. An allow skips the interactive prompt, but it does not override deny or ask permission rules, including enterprise managed deny lists. That is the right priority: hooks can make approved paths smoother, but managed deny policy still wins.

One subtle production detail matters: when multiple hooks match the same event, every matching command runs to completion before Claude Code merges the results. A deny does not prevent sibling hooks from running. Put side effects after validation when possible, and keep sibling hooks safe even when another hook later blocks the action.

Use Permission Hooks Like A Scalpel

PermissionRequest hooks can auto-answer permission prompts, but broad approval is the fastest way to erase the safety boundary your rollout depends on. The matcher should name the specific tool or prompt you are approving.

A reasonable use is auto-approving ExitPlanMode, because it keeps the current conversation moving after Claude presents a plan. A risky use is an empty matcher that approves every file write and shell command. Anthropic's docs call out that leaving the matcher empty or using .* would auto-approve every permission prompt, including file writes and shell commands.

Use this rule:

  • Auto-approve narrow, low-risk workflow prompts that create noise.
  • Keep file writes, shell commands, secrets, deployment commands, and destructive operations behind explicit permission rules.
  • Do not persist bypass behavior as a default team mode.

Claude Code supports bypassPermissions only when the session was launched with bypass mode already available through flags or settings, and it is never persisted as defaultMode. That is a useful constraint. For a team rollout, the default should be "fast enough with narrow approvals," not "skip permissions and trust cleanup."

Do Not Turn Async Hooks Into Gates

Async hooks are for background work. They are not gates.

By default, hooks block Claude's execution until they complete. Setting async: true on a command hook runs it in the background while Claude continues. That is useful for long tests, indexing, Slack notifications, or telemetry exports. It is not useful for decisions that must stop a dangerous action, because async hooks cannot block tool calls or return decisions after the triggering action has already proceeded.

Use synchronous hooks for:

  • Protected file blocks.
  • Dangerous Bash command blocks.
  • Permission decisions.
  • Stop checks that must send Claude back to work before the turn ends.

Use async hooks for:

  • Long test suites that report back on the next turn.
  • Non-blocking log shipping.
  • Notifications that should not stall the editing loop.
  • Repository indexing or lightweight artifact generation.

If you run async hooks, set explicit timeouts. If no async timeout is specified, async hooks use the same 10-minute default as sync hooks. Each async hook execution also creates a separate background process with no deduplication across repeated firings, so a PostToolUse hook on every edit can start many test runs unless the script coalesces work.

A practical pattern is a synchronous PreToolUse deny list plus an async PostToolUse test reporter. The first prevents irreversible actions. The second gives Claude feedback without blocking every edit.

What To Log

Log enough to reconstruct a session without storing unnecessary source or secrets. The minimum useful hook log record is:

JSON
{
  "ts": "<iso-timestamp>",
  "session_id": "abc123",
  "cwd": "/repo",
  "event": "PreToolUse",
  "tool": "Bash",
  "matcher": "Bash",
  "decision": "deny",
  "reason": "force pushes require a human-owned terminal session"
}

For shell commands, log the command string only if your security posture allows it. For file edits, log the path and decision first. Do not casually log prompt text, tool content, raw API bodies, secrets, or full file diffs.

Claude Code can export telemetry through OpenTelemetry as metrics, logs/events, and optional distributed traces. The defaults matter: metrics export every 60 seconds and logs every 5 seconds. Tracing is off by default and requires CLAUDE_CODE_ENABLE_TELEMETRY=1, CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1, and OTEL_TRACES_EXPORTER.

When tracing is active, each user prompt starts a claude_code.interaction root span, and API calls, tool calls, and hook executions are recorded as children. User prompt text, tool input details, and tool content are redacted by default unless you enable the relevant OTEL_LOG_* gates.

One implementation detail prevents a common observability bug: Claude Code does not pass OTEL_* environment variables to subprocesses it spawns, including Bash, hooks, MCP servers, and language servers. If a hook or the command it runs needs to export its own telemetry, set the exporter variables directly in that command or wrapper script.

Rollout Checklist

Ship hooks like production code. They run with the user's full system permissions and can modify, delete, or access any file that user account can access.

The first rollout should be small and visible:

  • Pick one repo with active Claude Code usage and a known test command.
  • Add .claude/settings.json with only the baseline hooks.
  • Add .claude/hooks/ scripts with set -euo pipefail, explicit matchers, useful stderr, and no hidden network calls.
  • Review hook changes through normal code review.
  • Run a seeded exercise: edit a safe file, attempt a protected file edit, attempt a denied Bash command, trigger formatting, and change settings to confirm audit behavior.
  • Track blocked actions, hook failures, average hook runtime, and developer bypass requests for a week.
  • Promote only proven security controls to managed settings.

The decision rule is simple: a hook earns its place when it prevents a real class of production risk or removes a repeated review chore. Everything else belongs in docs, CLAUDE.md, CI, or human review.

What are Claude Code hooks?

Hooks are lifecycle controls that run commands, HTTP endpoints, MCP tool calls, prompts, or agent checks inside Claude Code. Use them when a rule must run deterministically instead of relying on Claude to remember an instruction.

Which Claude Code hook should block dangerous shell commands?

Use PreToolUse matched to Bash. Parse .tool_input.command from stdin, return permissionDecision: "deny" or exit 2 with a clear stderr reason, and keep the matcher narrow.

Should Claude Code tests run in Stop or PostToolUse?

Use Stop for a fast final gate that should send Claude back to work before completion. Use async PostToolUse for longer tests that can report results on the next turn without blocking every edit.

Can Claude Code hooks auto-approve permission prompts?

Yes, PermissionRequest can return an allow decision for a matched prompt. In production, only auto-approve narrow low-risk prompts; an empty matcher or .* can approve every permission prompt, including file writes and shell commands.

Are Claude Code hooks safe to share across a team?

They are safe only when reviewed like code. Put repo-shared hooks in project settings, keep machine-specific behavior local, promote hard policy to managed settings, and remember that command hooks run with the user's full system permissions.

Last Updated

Jun 3, 2026

CategoryCoding

More from Coding

View all Coding articles
Newsletter

One letter, every week. Working systems — not hot takes.

Build logs, agentic engineering decisions, agent failures, evals, and what survives real users. Sent weekly, never more.

Weekly. No spam. Unsubscribe anytime.