Advanced Feature

Hooks: Build Your Custom Workflow

Insert custom logic at every stage of Claude Code's tool calls — auto-format, test, review, and secure — all automated

#Hooks #Automation #ClaudeCode #CI/CD

What are Claude Code Hooks?

Hooks are Claude Code's event system that lets you run custom shell scripts before and after tool calls. Automatically run linters after file saves, execute tests before commits, or trigger alerts on sensitive file access — all without manual intervention.

Configuration nests three levels: the event, a matcher that filters tools, and your script inside the inner hooks array. The script reads this event's JSON from stdin and answers with exit codes: 0 means no objection, 2 blocks the call on events that allow it. For the four events below, a command hook is canceled at its 600 second default; those four are the common ones, not the full official list (checked 2026-09-22).

User
Sends a prompt
Hook checkpoint
Checks before the tool runs, can block
Tool run
Only then the tool executes

Hook Types Explained

Claude Code hooks cover tool, session and compaction lifecycle points, with command, HTTP and agent handlers

PreToolUse: before the call

Triggers before tool execution — validate params, check permissions, auto-approve or reject

"PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command" }] }]

PostToolUse: after the call

Triggers after tool execution — auto-format, run tests, send notifications

"PostToolUse": [{ "matcher": "Edit|Write", "hooks": [{ "type": "command" }] }]

Notification: on alerts

Triggers on Claude Code notifications — integrate with Slack, email, or other channels

"Notification": [{ "matcher": "permission_prompt", "hooks": [{ "type": "command" }] }]

Stop: when Claude wraps up

Triggers when Claude Code finishes responding — log results or run cleanup

"Stop": [{ "hooks": [{ "type": "command", "command": "~/scripts/on-stop.sh" }] }]

Three practical automation cases

The three cases below share one structure: pick the event, narrow it with matcher, and put the script in the inner hooks array. The snippets show intent only, so treat the official reference as the source for field names and nesting, and read whatever your script must check from that JSON on stdin.

1

Format before the commit

When Claude is about to run git commit, PreToolUse hands the arguments to your script first. The script detects the commit command, runs your formatter, and answers with an exit code: 0 lets the call through, 2 blocks it. All of this happens before execution, so a blocked commit leaves the working tree untouched.

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "command": "if echo \"$TOOL_INPUT\" | grep -q 'git commit'; then npm run format; fi"
    }]
  }
}
2

Notify when tests finish

PostToolUse fires only after a command returns successfully, so your script can spot pytest or npm test and raise a desktop notification. It cannot undo a call that already ran and stays silent on failures, so the notice means results are ready, not that the tests passed.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Bash",
      "command": "if echo \"$TOOL_INPUT\" | grep -q 'pytest\\|npm test'; then notify-send 'Tests completed'; fi"
    }]
  }
}
3

Refuse edits to secret files

Put a PreToolUse hook on Edit and Write: the script takes the target path from stdin and exits with code 2 when the name matches .env, credentials or similar. The write is refused before it touches disk, so there is nothing to undo. A matcher of Edit|Write covers both tools at once.

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "command": "if echo \"$TOOL_INPUT\" | grep -qE '\\.env|credentials|secrets'; then exit 2; fi"
    }]
  }
}

Headless Mode & CI/CD

Run Claude Code in CI/CD pipelines with the -p flag for unattended automation tasks

-p for non-interactive runs

Add the -p (or --print) flag to any claude command to run it non-interactively and print the result when done.

Pipe it into CI

Non-interactive mode reads stdin and writes the result to stdout; a 0 exit code means success, so build scripts can branch on it.

Structured output with JSON

Use --output-format to pick the response shape; json returns the result plus session ID and metadata, ready for scripts to parse.

Non-interactive call in CI
# Run a review non-interactively: JSON output, max 5 turns
claude -p "Review this PR and suggest improvements" \
  --output-format json \
  --max-turns 5

settings.json Configuration

Define your Hooks in .claude/settings.json

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "~/scripts/pre-bash-hook.sh"
      },
      {
        "matcher": "Edit|Write",
        "command": "~/scripts/protect-sensitive-files.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "command": "~/scripts/post-bash-hook.sh"
      }
    ],
    "Notification": [
      {
        "command": "~/scripts/send-notification.sh"
      }
    ],
    "Stop": [
      {
        "command": "~/scripts/on-stop.sh"
      }
    ]
  }
}

Hooks Best Practices

These four points follow the Claude Code Hooks reference, including its security notes, checked 2026-09-22; they cover how to write the scripts, while the configuration shape sits in the settings.json section above.

Block with exit code 2

The reference says that for most hook events exit code 2 is the only code that blocks on its own; any other exit code is a non-blocking error and the action proceeds. A script meant to enforce policy has to exit 2.

Narrow triggers with matcher

The matcher field filters when hooks fire. A plain tool name matches exactly, while a value with other characters is evaluated as an unanchored JavaScript regular expression: Edit.* matches both Edit and NotebookEdit, so add anchors for a whole-string match.

A timeout is not a block

Claude Code cancels a hook that reaches its timeout and discards the output, so on most events a timed-out hook renders no decision. The docs warn that on PreToolUse such a command hook does not block the call, while on PreModelSwitch it blocks the switch. Debug logs hold the execution details.

Treat the input as untrusted

The security notes list a few habits: never trust input data blindly, always quote shell variables, check for path traversal such as ../ and skip sensitive files like .env or .git. A hook reads a JSON payload whose fields can hold anything from outside.

Frequently Asked Questions

How does a hook script get the input JSON?

A command hook receives a JSON object on stdin. Common fields include session_id, transcript_path, cwd and hook_event_name; each event then adds its own, so tool events carry tool_name and tool_input.

How do I make a hook block a tool call?

Attach the script to PreToolUse and return exit code 2 for calls that must not run. The event table marks PreToolUse as blockable, so exit 2 stops that call; for a conditional decision print JSON with permissionDecision instead. Exit code 1 blocks nothing on most events.

What happens when a hook times out?

A hook that reaches its timeout is cancelled and its output discarded, which on most events means no decision at all; PreModelSwitch is the case where running out of time blocks the switch. A mistyped script path is a non-blocking error too, and the docs tell you to watch for that notice on a policy hook's first run.

Which file holds the hooks configuration?

Scope follows where you define it: ~/.claude/settings.json covers all your projects and stays on your machine; .claude/settings.json is per project and can be committed; .claude/settings.local.json is per project and gets gitignored when Claude Code saves a setting to it.

Enterprise Automation with QCode.cc

Combine QCode.cc developer platform with Hooks for enterprise-grade AI coding pipelines

Gateway access via ANTHROPIC_BASE_URL

Claude Code's official env-vars reference says ANTHROPIC_BASE_URL can point the API endpoint at a proxy or gateway; QCode publishes a setup guide for this route on-site (verified 2026-09-22).

Brakes before automation

Put brakes on before automating: the official example shows a script checking the target path against protected patterns and exiting with code 2 to block edits to sensitive files.

One config for the whole team

The official hook-locations table says .claude/settings.json can be committed to the repo; pair it with one team-shared QCode key and everyone’s Hooks behave the same.

One Plan, Three Platforms

QCode also powers OpenAI Codex / GPT-5.6

Your QCode quota works across Claude Code and OpenAI Codex CLI — one shared balance, zero duplicate spend.

Automate Your Claude Code Workflow

Unlock AI coding's full potential with Hooks + QCode.cc

Updated 2026-09-22

Try first, then decide

Not sure which tier? Start with Starter ($8.57/mo) and upgrade when you're happy — the unused value of the old plan goes back to your balance.