25 Claude Code Agents in Production: The Hooks Architecture
Current Situation Analysis
Traditional multi-agent orchestration relies on heavy external glue: message buses, shared memory stores, custom schedulers, and complex state machines. This introduces significant latency, operational overhead, and debugging complexity. When scaling to 25+ autonomous agents, these architectures fracture under concurrent execution, race conditions, and unbounded blast radius.
Failure modes in unattended AI agent deployments are well-documented:
- Destructive Command Execution: Auto-approve configurations allow agents to run
git checkout .,rm -rf, or database drops without human intervention. - Context Window Drift: Agents ignore system prompts and role constraints after ~15 tool calls due to context pressure.
- Comprehension vs. Enforcement Gap: Hooks catch direct shell commands but miss multi-step paths to destructive outcomes. Model-level scope creep can wire incorrect architectures despite strict guardrails.
- Orchestration Bottlenecks: External frameworks require explicit state transitions, making simple handoffs (Architect β Engineer β Reviewer) disproportionately complex to implement and maintain.
Traditional CI/CD pipelines and framework-based agent loops fail because they lack native, event-driven primitives that can enforce role boundaries, detect downstream triggers, and hand off work without leaving the execution environment. Claude Code's hook system solves this by treating the agent session itself as the orchestrator.
WOW Moment: Key Findings
The architecture proves that three native hook primitives (PreToolUse, PostToolUse, Stop) are sufficient to replace external orchestration layers entirely. By routing events through Git PRs and leveraging exit-code-based blocking, the system achieves near-zero glue code overhead while maintaining strict role isolation.
| Approach | Orchestration Overhead | Agent Handoff Latency | Safety Enforcement Granularity | Implementation Complexity | Scalability Limit |
|---|---|---|---|---|---|
| Traditional Framework (LangGraph/AutoGen) | High (State machines, message brokers) | 2-5s (API/Queue roundtrip) | Policy-based (Post-execution) | High (Custom routing logic) | ~10 concurrent agents |
| Manual CI/CD Pipeline | Medium (Webhooks, Jenkins/GitHub Actions) | 10-30s (Build queue) | Branch/PR rules only | Medium (YAML/DSL config) | Limited by runner capacity |
| Claude Code Hooks Architecture | Near-zero (Native event loop) | <500ms (In-process spawn) | Pre-execution + Structural hard limits | Low (3 shell scripts + JSON config) | 25+ agents (context-bound) |
Key Findings:
PreToolUseexit code2blocks specific tool calls without aborting the session, enabling surgical role constraints.PostToolUseoutput parsing replaces message queues; PR URLs act as deterministic handoff signals.Stophooks provide reliable session termination triggers for cascade spawning.- Sweet Spot: 15-25 parallel agents. Beyond this, context window pressure increases drift risk, requiring explicit plan re-injection or session rotation.
Core Solution
Prerequisites
- Claude Code installed and authenticated (
npm install -g @anthropic-ai/claude-code) - A GitHub repository with
ghCLI authenticated jqinstalled (for parsing hook payloads)- Optional: Grass for mobile oversight of unattended sessions (
npm install -g @grass-ai/ide)
Step 1: Scaffold the Project Structure
mkdir -p .claude/hooks .claude/logs plans
Create the shared settings file that routes all hook calls:
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/role-guard.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/post-bash.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/on-stop.sh" }
]
}
]
}
}
Invoke each role by setting AGENT_ROLE in the environment. Hook scripts inherit this variable from the parent process:
AGENT_ROLE=architect claude -p "Your Architect task..."
AGENT_ROLE=engineer claude -p "Your Engineer task..."
AGENT_ROLE=reviewer claude -p "Your Reviewer task..."
Step 2: Implement Role Constraints in PreToolUse
role-guard.sh handles both the universal safety blocklist and per-role constraints in a single script:
#!/bin/bash
# .claude/hooks/role-guard.sh
TOOL_INPUT=$(cat)
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.tool_input.command // empty')
ROLE="${AGENT_ROLE:-}"
block() {
echo "GATE BLOCKED: $1" >&2
exit 2
}
# ββ Universal blocklist: applies to every role ββββββββββββββββββββββββββββββ
DANGER='(git (reset --hard|clean -f|checkout \\.)|rm -rf|DROP TABLE)'
if ech
o "$COMMAND" | grep -qiP "$DANGER"; then block "safety-guard: destructive operation requires manual approval" fi
ββ Role-specific constraints ββββββββββββββββββββββββββββββββββββββββββββββββ
case "$ROLE" in architect) if echo "$COMMAND" | grep -qP '(git (commit|push)|npm (run|test|build)|pytest)'; then block "Architect constraint: write a plan doc in plans/ instead of executing code" fi ;; reviewer) if echo "$COMMAND" | grep -qP '(git (commit|push|checkout -b)|\bsed -i\b)'; then block "Reviewer constraint: read-only role β leave GitHub comments instead" fi ;; esac
echo '{"decision": "allow"}'
*Note: `block()` exits 2 β Claude Code PreToolUse hooks use exit code 2 to block a specific tool call without aborting the session. The message goes to stderr so Claude Code surfaces it as the rejection reason. The universal blocklist runs before role checks so it cannot be bypassed by role misconfigurations.*
### Step 3: Trigger the Engineer from the Architect's Stop Hook
When the Architect session ends normally, `on-stop.sh` checks for a new plan file and spawns an Engineer:
```bash
#!/bin/bash
# .claude/hooks/on-stop.sh
ROLE="${AGENT_ROLE:-}"
PROJECT="$(pwd)"
case "$ROLE" in
architect)
PLAN_FILE=$(ls -t "$PROJECT/plans/"*.md 2>/dev/null | head -1)
if [[ -f "$PLAN_FILE" ]]; then
PLAN_NAME=$(basename "$PLAN_FILE" .md)
nohup env AGENT_ROLE=engineer claude \
-p "Implement the plan at $PLAN_FILE. Create branch feature/$PLAN_NAME. Open a PR when done. Do not modify files under plans/." \
>> "$PROJECT/.claude/logs/engineer.log" 2>&1 &
echo "Engineer spawned for $PLAN_FILE (PID: $!)"
fi
;;
esac
Always use absolute paths in nohup commands. Relative paths resolve against the working directory at spawn time, which may differ from the project root depending on how the Stop hook is invoked.
Step 4: Detect PR Creation and Spawn the Reviewer
The Engineer's PostToolUse hook watches bash outputs for GitHub PR URLs:
#!/bin/bash
# .claude/hooks/post-bash.sh
ROLE="${AGENT_ROLE:-}"
case "$ROLE" in
engineer)
TOOL_OUTPUT=$(cat)
PR_URL=$(echo "$TOOL_OUTPUT" \
| jq -r '.tool_output // empty' \
| grep -oP 'https://github\.com/[^\s]+/pull/\d+' | head -1)
if [[ -n "$PR_URL" ]]; then
# Dedup: don't spawn multiple reviewers for the same PR
LOCK="/tmp/reviewer-$(echo "$PR_URL" | md5sum | cut -c1-8).lock"
[[ -f "$LOCK" ]] && exit 0
touch "$LOCK"
nohup env AGENT_ROLE=reviewer claude \
-p "Review this PR critically. Check implementation against the plan in plans/. Identify bugs, missed requirements, and test gaps. Leave specific GitHub review comments: $PR_URL" \
>> "$(pwd)/.claude/logs/reviewer.log" 2>&1 &
fi
;;
esac
This is where the "they argue in pull request comments" behavior emerges. The Reviewer calls gh pr review --comment -b "..." with specific feedback. When the Engineer runs in a follow-up session, those review comments are in its context, and it addresses them in new commits.
Step 5: Implement the CEO Weekly Summarizer
Run the CEO agent via cron. It aggregates log tails and recent PR activity, then sends a summary:
#!/bin/bash
# .claude/hooks/ceo-weekly.sh
# Add to crontab: 0 9 * * 1 bash /path/to/.claude/hooks/ceo-weekly.sh
PROJECT="/absolute/path/to/your/project"
LOG_TAIL=$(tail -n 400 "$PROJECT/.claude/logs/"*.log 2>/dev/null)
PR_LIST=$(cd "$PROJECT" && gh pr list --state all --limit 20 \
--json number,title,state,createdAt 2>/dev/null)
env AGENT_ROLE=ceo claude --no-interactive \
-p "You are the CEO of an autonomous agent team. Based on the activity below, write a concise weekly summary: what shipped, what's in review, any anomalies. Send it as an email to admin@yourdomain.com.
AGENT LOGS:
$LOG_TAIL
RECENT PRS:
$PR_LIST"
The CEO role needs email capability configured (sendmail, a transactional API, or a custom tool). Keep its allowed-commands list tight β observe and report only.
Step 6: Safety Architecture & Verification
Before running unattended, enforce structural hard limits:
gh api repos/OWNER/REPO/branches/main/protection \
--method PUT \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1}' \
--field required_status_checks='{"strict":false,"contexts":[]}'
With this in place, no agent can merge to main regardless of what any hook permits. The Engineer opens PRs; merges require human approval or the Reviewer's explicit gh pr review --approve.
Smoke Test Verification:
# 1. Trigger the Architect with a minimal task
AGENT_ROLE=architect claude \
-p "Write a one-sentence plan for adding GET /healthz to an Express app. Save it to plans/healthz.md."
# 2. Confirm the plan was created
ls plans/
Pitfall Guide
- Hook Bypass via Multi-Step Destructive Paths:
PreToolUseonly intercepts direct tool calls. Agents can chain benign commands to achieve destructive outcomes (e.g., downloading a script then executing it). Mitigation: Combine hook blocklists with filesystem permissions, containerized execution, and branch protection as a structural hard limit. - Context Window Drift & Rule Ignorance: Claude agents consistently drift from system prompts past ~15 tool calls due to context pressure. Mitigation: Re-inject the plan document as context on every spawn, enforce session timeouts, and rotate agents before context saturation.
- Relative Path Resolution in
nohupSpawns:nohupinherits the working directory at spawn time, which may differ from the project root. Mitigation: Always use absolute paths for logs, plan files, and script invocations inon-stop.shandpost-bash.sh. - Missing PR Deduplication (Lock File Race Conditions): Rapid successive
PostToolUsetriggers can spawn duplicate Reviewer sessions. Mitigation: Implement MD5-based lock files in/tmp/with atomictouchchecks to guarantee exactly-once spawning per PR URL. - Model Comprehension vs. Hook Enforcement (Scope Creep): Hooks enforce boundaries but cannot fix architectural misunderstandings. Agents may ignore PRDs or wire incorrect systems despite strict guardrails. Mitigation: Keep system prompts tight, validate plan documents before Engineer spawn, and mandate explicit
gh pr review --approvefor merges. - Insufficient Branch Protection: Relying solely on hooks for safety is fragile. A single bypass or misconfiguration can corrupt
main. Mitigation: Enforce GitHub branch protection rules programmatically viagh api. Require at least one approving review and disable direct pushes to protected branches.
Deliverables
- π Architecture Blueprint:
.claude/settings.jsonhook routing configuration withPreToolUse,PostToolUse, andStopevent mapping - π Deployment Checklist:
-
jqandghCLI authenticated - Branch protection enforced via
gh api - Universal destructive blocklist validated
- Absolute paths configured in all
nohupspawns - Lock file deduplication tested for PR URLs
- CEO cron job scheduled with email/notify integration
-
- βοΈ Configuration Templates:
role-guard.sh(Universal + Role-specific constraints)on-stop.sh(Architect β Engineer cascade)post-bash.sh(PR detection β Reviewer spawn)ceo-weekly.sh(Log aggregation + executive summary)
- π Safety Hardening Pack: Branch protection API payload, exit-code
2blocking reference, and context drift mitigation patterns
