Learn Claude Code: Hooks Deep Dive Advanced Hook Patterns

Advanced Hook Patterns

Advanced 🕐 13 min Lesson 10 of 12
What you'll learn
  • Implement asyncRewake to monitor background processes and wake Claude when they complete or fail
  • Use MessageDisplay hooks to redact sensitive content before it appears in the terminal output
  • Apply terminal sequences and Elicitation hooks to build rich, reactive hook interactions

Beyond Synchronous Blocking

Most hooks fire, run, and return before Claude Code proceeds. The decision is synchronous: the hook runs in the foreground, and Claude Code waits. This is the right model when the hook's decision is needed immediately — a security check before a destructive command, a validation before a file write.

But some automation patterns do not need an immediate decision. Starting a CI build, triggering a deployment, or running a slow test suite should not block Claude Code while they complete. And some patterns need the hook to react later — after a background process finishes, after an external event occurs, after a waiting period.

Advanced hook patterns extend the basic model with background execution, terminal integration, content transformation, and reactive automation.

asyncRewake: Background Monitoring with Delayed Wakeup

The asyncRewake: true flag on a command hook enables a two-phase execution model. In the first phase, the hook runs in the background immediately — Claude Code fires it and continues without waiting. In the second phase, if and when the background hook exits with code 2, Claude Code wakes up and receives the hook's stderr as context for a new turn.

This pattern is ideal for CI monitoring. When a Stop hook triggers at the end of a turn where code was committed, the asyncRewake hook starts watching the CI build in the background. Claude Code is free to continue other work or go idle. When the CI build finishes — passing or failing — the hook exits 2 with the result, waking Claude with a message like "CI build completed: 3 tests failed in auth module."

#!/bin/bash
# Run in background via asyncRewake: true
build_id=$(cat | jq -r .last_build_id)
# Poll until the build finishes
while true; do
  status=$(curl -s "https://ci.example.com/build/$build_id/status")
  result=$(echo "$status" | jq -r .result)
  if [ "$result" != "pending" ]; then
    if [ "$result" = "failed" ]; then
      echo "CI build failed: $(echo "$status" | jq -r .failure_summary)" >&2
      exit 2
    fi
    exit 0
  fi
  sleep 30
done

The asyncRewake pattern converts CI polling from an active task Claude must manage into a passive notification Claude receives. It does not block the conversation while the build runs, but Claude hears about failures automatically when they happen.

Terminal Sequences: Notifications and Display Control

Hook stdout can include terminal escape sequences that control the terminal without appearing as text output. Claude Code allows a specific allowlist of OSC (Operating System Command) sequences and the BEL character.

Permitted sequences include OSC 0 and OSC 2 for setting the terminal title, OSC 1 for the tab title, OSC 9 for desktop notifications (iTerm2-compatible), OSC 99 for a variant notification format, OSC 777 for another notification format, and BEL (the bell character) for a simple audio or visual alert.

A Stop hook that sends a desktop notification when Claude finishes a long task:

#!/bin/bash
# OSC 9 desktop notification (iTerm2 / compatible terminals)
printf '33]9;Claude Code finished07'
exit 0

Return the sequence in the terminalSequence field of the JSON output (on exit 0), or write it directly to stdout. Both approaches are recognized. Sequences outside the allowlist are stripped for security — hooks cannot use escape sequences to execute arbitrary terminal commands or exfiltrate data through terminal protocols.

MessageDisplay: Redacting Content at Display Time

The MessageDisplay event fires when an assistant message is about to be displayed in the terminal. A hook can intercept this and replace what the user sees — without altering the transcript or the context Claude uses for its reasoning.

The hook receives the message content and can return replacement text. The transcript records the original unmodified message. The user sees the modified version in their terminal. This separation is the key property: you can redact sensitive content from display without affecting Claude's ability to reason about it.

Common uses include credential redaction (replacing API keys with [REDACTED] before they appear on screen), PII filtering (masking email addresses or phone numbers in displayed output), and content sanitization for shared screens or pair programming sessions.

MessageDisplay hooks should be fast — they fire on every displayed message, and noticeable latency would make the terminal feel sluggish. Avoid heavy computation or network calls in MessageDisplay handlers.

Elicitation Hooks and Worktree Hooks

MCP servers can request user input through an elicitation mechanism — presenting a form with fields the user is expected to fill in. The Elicitation event fires when a server requests input, and the ElicitationResult event fires after the user responds.

The matcher for both events filters by MCP server name. An Elicitation hook can intercept the request and pre-populate fields, validate that the requested information is available, or cancel the elicitation entirely. The action field in the hook output accepts "accept" (with a content map of field values), "decline", or "cancel".

In automated workflows where MCP servers request the same standard inputs repeatedly — environment name, project identifier, confirmation codes — Elicitation hooks can supply the values automatically, removing the need for human input in otherwise autonomous pipelines.

The WorktreeCreate event fires when Claude Code is about to create a new git worktree. Unlike most events, WorktreeCreate can block creation by exiting with any non-zero code. A hook that prints a custom path to stdout on exit 0 can redirect the worktree to a different location. The WorktreeRemove event fires when a worktree is removed; it cannot block the removal but can trigger cleanup actions.

Output Limits and Debugging

Hook output strings are capped at 10,000 characters. If a hook produces output larger than this limit — a long lint report, a verbose test output, a large diff — the output is truncated and the remainder is saved to a temporary file. The path to that file is included in the truncated output so it can be inspected separately.

Debugging hooks is most effective with two tools. First, the transcript_path field in the hook input points to the full session transcript. Reading this file shows exactly what the hook received and how Claude Code interpreted its output. Second, testing hooks independently: pipe a sample JSON payload to the hook script and inspect the output and exit code before connecting it to Claude Code. A hook that works correctly in isolation will work correctly in production.

The PostToolBatch event fires after a group of tool calls that were executed as a batch (when Claude makes multiple tool calls in a single turn). It provides the complete batch of results rather than individual tool results. Use it when you need to validate the combined outcome of multiple tool calls rather than reacting to each one separately.

Key takeaways
  • asyncRewake runs the hook in the background and allows Claude to continue — the hook can exit 2 later to wake Claude with context, enabling CI polling and build monitoring without blocking the current conversation
  • Terminal sequences use a strict OSC allowlist — only OSC sequences 0, 1, 2, 9, 99, and 777 plus BEL are permitted; all others are stripped for security to prevent terminal exploitation
  • MessageDisplay replaces what appears in the terminal without touching the transcript — use it to redact credentials, PII, or sensitive output from display while preserving the content for Claude
  • Elicitation hooks can automate MCP form-filling — pre-populate fields with action: accept and a content map to supply values automatically in autonomous workflows without requiring user input
  • Output is capped at 10,000 characters with file spillover — test hooks independently by piping sample JSON payloads to the script and read transcript_path for debugging production hook behavior