Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.aidonow.com/llms.txt

Use this file to discover all available pages before exploring further.

Executive Summary

Claude Code’s hook system provides two distinct instrumentation points: PreToolUse, which fires before a tool executes and can block it, and PostToolUse, which fires after a tool executes and cannot. This distinction is not a minor implementation detail — it defines two categorically different use cases. PreToolUse is an enforcement layer; PostToolUse is an observability and side-effect layer. This analysis examines PostToolUse hooks as a mechanism for capturing lifecycle events from the tool-use layer without modifying application code, business logic, or agent prompting. The central finding: the tool-use layer is a complete instrumentation surface for autonomous agent activity — every API call, every file write, every git operation passes through it — and PostToolUse hooks enable that surface to be observed and acted upon in ways that are invisible to the agent and never block its execution.

Key Findings

  • PostToolUse hooks are categorically non-blocking. They fire after the primary tool operation has completed. A hook that fails, hangs, or errors does not affect the operation that preceded it.
  • The tool-use layer captures lifecycle events that the application layer cannot. When an autonomous agent calls an external API via a Bash command, the application does not know the agent made the call — the PostToolUse hook does, and can fire a structured event without any application-layer changes.
  • Fire-and-forget execution is the correct pattern for PostToolUse side effects. Any synchronous work in a PostToolUse hook that takes non-trivial time will accumulate latency across a session. Daemon threads or background subprocesses must be used for all network operations.
  • Pattern matching on command text enables semantic routing of lifecycle events — the hook can detect that a specific API endpoint was called and map that to a domain event type without the agent needing to emit the event explicitly.
  • Multiple hooks on the same PostToolUse[Bash] event compose correctly, each observing the same tool invocation from independent perspectives — activity feed, correction log, notification system — without interference.
  • The correction-logging pattern converts session boundary amnesia from a complete information loss into a reviewable record, enabling post-session retrospectives that drive constitutional amendments to CLAUDE.md.

1. PostToolUse vs PreToolUse: Observability After vs. Enforcement Before

The Claude Code hooks documentation describes two hook types. The behavioral distinction between them is the central architectural decision for any hook implementation. PreToolUse hooks fire before a tool executes. They receive the tool name and the tool input. If the hook exits with a non-zero status, the tool execution is blocked and the agent receives the error output as feedback. This blocking property makes PreToolUse the correct choice for constraint enforcement: financial type safety, tenant scoping requirements, architectural boundary violations. The hook prevents the operation; the agent cannot proceed until the constraint is satisfied. PostToolUse hooks fire after a tool executes. They receive the tool name, the tool input, and the tool output. Their exit code does not affect the primary operation — the operation has already completed. This non-blocking property makes PostToolUse the correct choice for observability, side effects, and notifications: anything where the goal is to record, forward, or respond to what happened, not to prevent it.
DimensionPreToolUsePostToolUse
Execution timingBefore tool runsAfter tool completes
Can block tool executionYes — non-zero exit blocksNo — exit code is ignored
Receives tool outputNo — tool hasn’t run yetYes — complete output available
Primary use caseConstraint enforcementObservability and side effects
Failure consequenceTool is blocked (intended)Hook fails silently (acceptable)
Appropriate forSecurity gates, type safety, isolationLogging, notifications, event emission
The inability to block is a feature of PostToolUse, not a limitation. A side-effect hook that could block agent execution would introduce a new category of failure: an activity feed that is temporarily unavailable would stall the agent. The fire-and-forget constraint ensures that observability infrastructure failure never propagates to agent execution failure.

2. The Tool-Use Layer as an Instrumentation Surface

Every significant action an autonomous agent takes passes through the tool-use layer. File writes are Write tool calls. Terminal commands are Bash tool calls. Web requests are WebFetch tool calls. Git operations are Bash tool calls wrapping git commands. In aggregate, the tool-use layer is a complete trace of agent activity — more complete, in many cases, than application-layer logs, because it captures what the agent did regardless of whether the application recorded it. This completeness makes PostToolUse hooks a powerful instrumentation surface. An activity feed that must be populated by application-layer events requires that every relevant application be instrumented. The same feed populated from PostToolUse hooks requires instrumentation in exactly one place — the hook configuration — and captures activity from all agents and all applications regardless of their internal logging architecture. The instrumentation model is fundamentally different from traditional observability approaches:
Traditional: Application → Logger → Activity Feed
             (requires instrumentation in every application)

PostToolUse: Agent → Tool Call → Hook → Activity Feed
             (instrumentation in the hook layer, not the application)
The practical implication is significant for organizations running autonomous agents against existing systems that were not designed for agent observability. The agents can be fully instrumented through hooks without any changes to the existing systems.

3. The Lifecycle Event Bridge: Wiring Activity Feeds Without Business Logic Changes

The lifecycle event bridge pattern demonstrates the full capability of PostToolUse instrumentation. The pattern: when an agent executes a Bash command that calls an external API to update a task status, a hook detects the command, extracts the relevant identifiers, maps the status to a domain event type, and fires a structured event to an activity feed — all without any changes to the agent’s prompting, the task management system, or any application code. The hook fires on PostToolUse[Bash] and examines the command that was executed. Pattern matching on the command text identifies status update operations. The status identifier extracted from the command is mapped to a semantic event type.
#!/usr/bin/env python3
"""
PostToolUse lifecycle bridge.

Fires after Claude Code executes a Bash command. If the command is a 
task status update, emits a structured lifecycle event to the activity feed.
Runs in a daemon thread — never blocks agent execution.
"""

import json
import os
import re
import sys
import threading

def extract_lifecycle_event(command: str) -> dict | None:
    """
    Parse a task status update command and return a structured lifecycle event.
    Returns None if the command does not match the status update pattern.
    """
    # Match commands that update task status via the project tracker API
    pattern = r'issues/(\d+).*status_id[=\s:]+(\d+)'
    match = re.search(pattern, command)
    if not match:
        return None

    issue_id = match.group(1)
    status_id = int(match.group(2))

    # Map status identifiers to semantic event types
    event_map = {
        10: ("task.approved", "positive"),
        3:  ("task.pickup",   "neutral"),
        5:  ("task.done",     "positive"),
        6:  ("task.rejected", "negative"),
    }

    if status_id not in event_map:
        return None

    event_type, mood = event_map[status_id]
    return {
        "event_type": event_type,
        "entity_id":  issue_id,
        "mood":       mood,
        "tags":       ["autonomous", "agent-activity"],
    }


def emit_event(event: dict) -> None:
    """Post the lifecycle event to the activity feed. Fire-and-forget."""
    import urllib.request
    feed_url = os.environ.get("ACTIVITY_FEED_URL")
    if not feed_url:
        return
    try:
        payload = json.dumps(event).encode()
        req = urllib.request.Request(
            feed_url,
            data=payload,
            headers={"Content-Type": "application/json"},
        )
        urllib.request.urlopen(req, timeout=5)
    except Exception:
        # Swallow all errors — hook failure must not surface to agent
        pass


def main() -> None:
    hook_data = json.load(sys.stdin)
    tool_name = hook_data.get("tool_name", "")

    if tool_name != "Bash":
        return

    command = hook_data.get("tool_input", {}).get("command", "")
    event = extract_lifecycle_event(command)
    if not event:
        return

    # Daemon thread: process exits when main thread exits, never blocks
    thread = threading.Thread(target=emit_event, args=(event,), daemon=True)
    thread.start()
    thread.join(timeout=3)  # Allow up to 3 seconds, then exit regardless


if __name__ == "__main__":
    main()
The hook is registered in the Claude Code settings:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/lifecycle-bridge.py"
          }
        ]
      }
    ]
  }
}
The resulting activity feed receives structured lifecycle events — task.approved, task.pickup, task.done — reflecting agent activity in real time. The agent is not aware of the hook. The task management system received a standard API call. No business logic was modified. The activity feed is populated entirely from the tool-use instrumentation layer.

4. Fire-and-Forget: Why Hooks Must Never Block Agent Execution

The non-blocking property of PostToolUse hooks is guaranteed by the hook system’s exit code semantics — the exit code is ignored. But a hook that performs synchronous network I/O in the foreground, even though it cannot block the primary operation from having completed, will add latency to the agent’s session by delaying the hook return. In a session where the agent executes hundreds of Bash commands, each triggering a PostToolUse hook that performs a 200ms synchronous HTTP request, the accumulated latency is tens of seconds per session. Over a working day of autonomous agent sessions, this latency is non-trivial. The fire-and-forget pattern addresses this through daemon threads or background processes. The hook spawns the I/O work in a daemon thread with a short join timeout, then exits. The agent session proceeds at full speed. The I/O work completes asynchronously.
# Correct: fire-and-forget with bounded timeout
thread = threading.Thread(target=emit_event, args=(event,), daemon=True)
thread.start()
thread.join(timeout=3)  # Wait at most 3 seconds, then exit

# Incorrect: synchronous execution blocks hook return until complete
emit_event(event)  # If this takes 500ms, every matching Bash call adds 500ms
The timeout value warrants deliberate selection. A timeout of zero means the hook exits immediately after starting the thread, with no guarantee the event is delivered. A timeout of thirty seconds means a slow activity feed can hold up hook return for half a minute on every matching command. Three to five seconds represents a practical balance: sufficient time for a healthy network request, bounded enough to prevent runaway accumulation.
The daemon thread pattern means that if the hook process exits before the thread completes its work, the event is silently dropped. This is acceptable for activity feed events and notifications. It is not acceptable for operations with durability requirements — financial records, audit logs with compliance implications, or writes to the primary data store. PostToolUse hooks with fire-and-forget semantics are correct for observability side effects. They are not correct for durable transactional operations.

5. Four PostToolUse Patterns: Correction Logging, Notifications, Status Sync, Session Notes

Beyond the lifecycle event bridge, four additional PostToolUse patterns address common autonomous agent operational needs.

5.1 Correction Logging

When a human corrects an agent’s behavior — redirecting its approach, stopping an action, clarifying a constraint — that correction is a piece of operational knowledge that should be captured and later encoded as a constitutional amendment to the CLAUDE.md. The correction logging pattern captures these events automatically. The trigger for this hook is the user’s message text rather than the tool that ran. When the agent processes a user message containing a correction signal, a PostToolUse hook on the message-handling tool can detect the correction and append it to a session log.
#!/bin/bash
# PostToolUse correction logger
# Fires after user message processing; logs corrections for post-session review

HOOK_DATA=$(cat)
USER_MESSAGE=$(echo "$HOOK_DATA" | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('message', ''))
")

# Detection heuristics for correction signals
if echo "$USER_MESSAGE" | grep -qiE \
   "(don't|do not|stop|wrong|incorrect|that's not|instead|actually)"; then
    LOGFILE="/tmp/corrections-$(date +%Y%m%d).log"
    TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%MZ')
    echo "[$TIMESTAMP] CORRECTION: $USER_MESSAGE" >> "$LOGFILE"
fi
The session log accumulates throughout the day. A post-session review converts each logged correction into a candidate CLAUDE.md amendment.

5.2 Long-Running Task Notifications

Autonomous agents execute tasks that take minutes to complete — running test suites, building container images, executing database migrations. A PostToolUse hook on the Bash tool can detect the completion of long-running operations and deliver a notification, allowing the human operator to re-engage at the right moment rather than polling.
#!/bin/bash
# PostToolUse notification hook
# Fires after Bash commands that match long-running operation patterns

COMMAND=$(cat | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('command', ''))
")

# Match completion of operations worth notifying on
if echo "$COMMAND" | grep -qE "(cargo test|docker build|kubectl apply|pytest)"; then
    # Send notification via configured channel (Telegram, Slack, etc.)
    NOTIFICATION_URL="${NOTIFICATION_WEBHOOK_URL}"
    if [[ -n "$NOTIFICATION_URL" ]]; then
        MSG="Agent completed: $(echo "$COMMAND" | cut -c1-80)"
        curl -s -X POST "$NOTIFICATION_URL" \
             -H "Content-Type: application/json" \
             -d "{\"text\": \"$MSG\"}" \
             --max-time 5 &
    fi
fi
The notification hook does not wait for the notification to deliver before exiting — curl is backgrounded with &. The hook exits immediately; the notification sends asynchronously.

5.3 CI-Triggered Status Sync

When an agent executes a git push, a PostToolUse hook can update the associated task status to reflect that the change is in review. This synchronizes the project tracker with the repository state without requiring the agent to explicitly manage status updates as a separate workflow step.
#!/bin/bash
# PostToolUse CI status sync
# Updates task status when a branch is pushed

COMMAND=$(cat | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('command', ''))
")

if echo "$COMMAND" | grep -q "git push"; then
    # Extract branch name from the push command
    BRANCH=$(echo "$COMMAND" | grep -oP 'origin \K\S+' | head -1)
    if [[ -n "$BRANCH" && "$BRANCH" != "main" && "$BRANCH" != "master" ]]; then
        # Update associated task to "In Review" status — fire-and-forget
        python3 /path/to/status-sync.py \
                --branch "$BRANCH" \
                --status "in-review" &
    fi
fi

5.4 Session Notes on Decision Events

When an agent makes a significant architectural decision — selecting one implementation approach over alternatives, choosing a library, establishing a pattern — a PostToolUse hook on file writes to architectural decision record paths can capture a timestamped entry in the session notes automatically. This pattern is the observability complement to the CLAUDE.md self-improvement loop: where correction logging captures what went wrong, session note capture records what decisions were made and why.

6. Hook Composition: Multiple Hooks on the Same Tool Event

PostToolUse hooks compose correctly: multiple hooks can be registered for the same tool event, each observing the same invocation from an independent perspective. The hooks execute sequentially but independently — the failure of one hook does not affect the execution of subsequent hooks.
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /hooks/lifecycle-bridge.py"
          },
          {
            "type": "command",
            "command": "bash /hooks/correction-logger.sh"
          },
          {
            "type": "command",
            "command": "bash /hooks/notification-hook.sh"
          }
        ]
      }
    ]
  }
}
Each hook receives the same input: the tool name, tool input, and tool output from the single Bash invocation. Each hook processes that input independently according to its own pattern matching logic. The lifecycle bridge may fire an activity event while the correction logger finds no correction signal and exits silently, and the notification hook detects a test run completion and sends an alert — three independent observations of the same tool call. This composition model has an important operational property: hooks can be added and removed without affecting existing hooks. Adding a new observability concern — a new activity feed, a new logging target, a new notification channel — requires only adding a new hook entry to the configuration. Existing hooks are unaffected.
Hook composition should be governed by the same routing discipline documented in the CLAUDE.md tool routing section. Each hook should have a single, clear responsibility. A hook that combines correction logging, notification sending, and status syncing in a single script is harder to maintain, harder to debug, and harder to disable selectively. The composition model encourages separation of concerns; the implementation should honor it.

7. Implementation Constraints

Organizations implementing PostToolUse hooks should understand three constraints before relying on them for operational observability. Pattern matching brittleness. Hooks that detect lifecycle events through command text pattern matching are brittle to changes in how the agent constructs commands. An agent that switches from a REST API call via curl to a call via an MCP tool will stop matching the hook’s pattern. Pattern matching on command text should be treated as a heuristic that requires maintenance as agent behavior evolves, not as a stable parsing mechanism. Observability gap on non-Bash tool events. The PostToolUse[Bash] pattern captures events that the agent executes through terminal commands. Events that occur through other tool invocations — direct MCP tool calls that do not go through Bash, file writes that the agent performs through the Write tool — require separate hook registrations on those tools. An activity feed built entirely on PostToolUse[Bash] will have gaps where agent activity occurs through other tool paths. No transactional guarantees. Fire-and-forget hooks provide at-most-once delivery. If the hook process crashes, if the network is unavailable, or if the timeout expires before the event is delivered, the event is silently dropped. For activity feeds where occasional data loss is acceptable, this is not a problem. For audit logs with compliance requirements, PostToolUse hooks are not the correct mechanism — durable event delivery requires infrastructure with explicit retry and durability semantics.
Correction logs accumulated in /tmp/ by session logging hooks are not persisted across system restarts. If the operational review process relies on correction logs to drive CLAUDE.md amendments, those logs must be flushed to durable storage before the session ends. A session-end hook that copies the day’s correction log to a committed location in the repository provides durability without requiring external infrastructure.

8. Recommendations

  1. Implement PostToolUse hooks for every observability concern that currently requires manual tracking. If you find yourself checking an activity feed manually to verify that an agent completed a task, or reviewing session transcripts to find corrections, those are PostToolUse hook candidates. The tool-use layer has the information; extract it automatically.
  2. Always use daemon threads or backgrounded processes for network operations in PostToolUse hooks. Never perform synchronous HTTP calls in the hook foreground. Verify the daemon thread pattern with a timing test: run the agent on a task with many Bash operations and measure whether hook presence adds observable latency.
  3. Register separate hooks for separate concerns. Resist the impulse to consolidate all observability logic into a single hook script. Separate hooks are independently maintainable, independently disableable, and independently testable. The composition model is an asset; use it.
  4. Maintain a hook pattern registry that documents what each hook matches and why. As the hook library grows, the pattern strings become operational knowledge that must be maintained. Document the expected command patterns, the events they map to, and the last time the pattern was verified against actual agent command output.
  5. Review hook fire rates in session logs weekly. A hook that never fires may have a pattern that no longer matches current agent behavior. A hook that fires on every command may be matching too broadly and generating noise. Fire rate is the primary signal for hook health.
  6. Pair correction-logging hooks with a mandatory post-session review step. The correction log has no operational value unless it is reviewed and converted into CLAUDE.md amendments. Build the review into the session close ritual: before ending the day’s agent sessions, read the correction log and create amendment candidates. Unreviewed correction logs are evidence of a stalled self-improvement loop.

Conclusion

The PostToolUse hook is the observability primitive that autonomous agent workflows have lacked. Traditional application observability requires instrumentation at the application layer — every relevant service must emit the events that feed monitoring and activity systems. The tool-use instrumentation model inverts this: every action the agent takes, regardless of what application or service it touches, passes through a single observable layer. A single hook configuration captures lifecycle events across all systems the agent interacts with, without changes to any of them. As autonomous development organizations scale — more agents, more parallel workflows, more external system integrations — the observability gap between what agents do and what human operators can see will widen if addressed only through application-layer instrumentation. The organizations that have instrumented the tool-use layer will have complete visibility into agent activity at the point of action. Those that have not will be reconstructing agent behavior from fragmented application logs, session transcripts, and the memory of human operators who were present. The patterns documented here represent current practice in early autonomous development organizations; as the tooling matures, tool-use layer observability will become a baseline operational requirement rather than an advanced configuration.
All content represents personal learning from personal projects. Code examples are sanitized and generalized. No proprietary information is shared. Opinions are my own and do not reflect my employer’s views.