P3.9 · D1 + D5 · Process42% of CCA-F26 min build

Claude for Operations.

An on-call ops agent. Alerts arrive structured (service, severity, root_cause_signal, 4-bucket isError: Transient · Permission · Data · Business); the agent matches a runbook from a small registry (3-5 safe playbooks: restart, rollback, drain, scale); a PreToolUse hook denies destructive Bash (rm -rf, sudo, drop database) before it executes; on ambiguity or denial, the harness emits a structured escalation block (incident_id, service, severity, root_cause, partial_status, recommended_action) to PagerDuty; every tool call writes a PostToolUse audit log entry with the full call context. The most-tested distractor: sentiment-based escalation. Sentiment is orthogonal to ops. Escalate on policy gaps and access failures, never on log-message tone.

Mental modelArchitecture chooses the flow. Deterministic controls enforce policy. Structured state preserves truth.
Loop mascot illustrating Claude for Operations.
Share
01 · System & parts

What this system is, and its 5 parts

Think of this as the on-call agent that picks up the 3 AM page so a human doesn't have to. But only for the boring, predictable incidents, and only when the action is reversible. The agent reads a structured alert, classifies it into one of four buckets (transient blip vs permission failure vs bad data vs business policy), matches the right runbook, and executes the safe steps. The dangerous steps (rm -rf prod, dropping a production database) are blocked by a deterministic hook before they ever run. The agent literally cannot execute them, no matter how cleverly the alert text is phrased. Anything ambiguous gets a structured escalation block (incident ID, service, severity, root cause, recommended next step) routed to a human in PagerDuty. The whole point is composure under pressure: hooks for safety, structured isError for clarity, audit logs for after-action review.

5Components
D1Primary domain
8Build steps
9Decision traps
8Concept links
Stack · Claude SDK · monitoring webhook · PagerDuty/Slack · durable audit logNeeds · Hooks (Pre/Post) · structured escalation · 4-bucket isError
Claude for Operations component architecture.
5 components. Each owns one concept.
01

Monitoring Alert Parser

structured isError, 4 buckets

Receives raw alert payloads (Datadog, Prometheus, Sentry, custom monitors) and projects them into a stable 4-bucket isError contract: {bucket: 'Transient'|'Permission'|'Data'|'Business', service, severity, root_cause_signal, retryable}. The agent reads bucket and retryable and routes; it never parses raw alert text. Without this contract, the agent retries permission errors forever and panics on transient blips.

ConfigurationWebhook receiver → schema validator → bucket classifier (rule-based or Haiku-classified) → enriched alert. Schema requires bucket + retryable; the alert ingestor rejects alerts missing the contract.
Concept: structured-outputs
02

Runbook Registry (3-5 Safe Playbooks)

small, audited, reversible

A tight registry of 3-5 named runbooks the agent can execute. Each runbook is a sequence of safe, reversible commands with explicit preconditions, timeouts, and rollback steps. Rare or unique incidents NEVER get a runbook. They escalate. The registry stays small on purpose; expanding it past 5 erodes the agent's routing accuracy and increases blast radius.

ConfigurationRegistry: ["restart_service", "drain_region", "rollback_release", "rotate_credentials", "scale_replicas"]. Each has {preconditions: [...], steps: [...], timeout_s, rollback: [...]}. Reviewed in PRs; production-deployed via the same CI/CD as application code.
Concept: tool-calling
03

PreToolUse Hook (Blocklist Gate)

deterministic destructive-command guard

Sits between the model's tool_use request and Bash/destructive tool execution. Reads the proposed command; matches against an explicit blocklist regex (rm -rf, sudo, drop database, kill -9, chmod 777, >:). On match, exits 2 with a model-readable reason. The agent observes the deny in the next turn as a tool_result with is_error: true and re-plans (typically by escalating). Deterministic. No prompt-injection bypass.

Configurationmatcher: "Bash". Blocklist regex (compiled once): r"\b(rm\s+-rf|sudo\s|drop\s+(database|table)|kill\s+-9|chmod\s+777|>:)\b". Exit 2 with stderr message. Allowlist for known-safe binaries (kubectl, docker, journalctl, curl, jq).
Concept: hooks
04

Structured Escalation Block

30-second human triage

When the agent can't (or shouldn't) act. Unknown runbook, hook denied, ambiguous alert, explicit request. The harness writes a STRUCTURED escalation block to PagerDuty / Slack. Six fields, every field required: incident_id, service, severity, root_cause_signal, partial_status (what the agent already did), recommended_action. Humans triage in ~30 seconds vs ~5 minutes reading a raw transcript.

ConfigurationSchema: {incident_id, service, severity: "P1|P2|P3", root_cause_signal: "permission|transient|data|business|unknown", partial_status: "what agent did before stopping", recommended_action: "single sentence"}. Posted to PagerDuty REST + Slack #ops-incidents.
Concept: escalation
05

PostToolUse Audit Log

the post-mortem replay tool

Fires AFTER every tool call (Bash, runbook, escalate, log query). Writes a canonical row: ts, tool_name, tool_input, tool_result_bucket, latency_ms, stop_reason_context, hook_decisions. Append-only JSONL on durable storage. Indispensable for the 4 AM 'what did the agent do?' post-mortem; without it, trust in the agent collapses on the first incident.

Configurationmatcher: '*'. Append to audit/{YYYY-MM-DD}.jsonl. Retain ≥ 90 days. Searchable by incident_id, service, tool_name. Includes both successful and denied calls.
Concept: evaluation
02 · Problem framing

The problem

What the user needs
  1. Auto-resolve boring incidents. Pod restarts, cache flushes, log rotation. Without paging a human.
  2. Block destructive commands deterministically. rm -rf /prod must NEVER execute, no matter what the alert text says.
  3. Hand off ambiguous incidents cleanly. When the agent can't safely act, the on-call human gets a structured block (not a raw transcript) and can decide in 30 seconds.
Why naive approaches fail
  1. Agent runs commands without a hook gate → rm -rf /prod executes because the alert text said 'clear stuck pods'.
  2. Permission-denied confused with empty result → agent retries forever thinking 'no data yet'; backoff never converges.
  3. Sentiment-trigger escalation → angry-but-correct alerts wake humans; calm-but-broken alerts get ignored.
  4. No audit log → post-mortem can't reconstruct what the agent did at 3 AM; trust evaporates.
Definition of done
  • Every monitoring alert classified into the 4-bucket isError contract (Transient · Permission · Data · Business)
  • 3-5 safe runbooks in the registry; rare/unique incidents always escalate (don't add a 6th runbook for an edge case)
  • PreToolUse hook denies destructive commands by regex; exit 2 returns a model-readable reason
  • Structured escalation block for every human handoff: incident_id, service, severity, root_cause, partial_status, recommended_action
  • PostToolUse audit log writes every tool call with input, output, latency, and the bucket the result fell into
  • Sentiment is logged but never gates routing. Escalation triggers are policy gaps, access failures, and explicit user request only
03 · Data flow

One run, traced end to end

Claude for Operations sequence diagram.
Claude for Operations end-to-end flow.
04 · Build

8 steps to production

01

Parse alerts into the 4-bucket isError contract

The webhook receiver projects raw monitoring payloads (Datadog/Prometheus/Sentry/custom) into a stable shape: {bucket, service, severity, root_cause_signal, retryable, raw}. The bucket is the single most important field. It's what the agent's routing logic branches on. Bucket assignment is rule-based for known patterns; ambiguous alerts get classified by Haiku (cheap, fast) and emit the bucket + a confidence score.

Concept: structured-outputs
Python
from enum import Enum
from typing import TypedDict, Literal

class Bucket(str, Enum):
    TRANSIENT  = "Transient"   # network blip, rate limit. Retry
    PERMISSION = "Permission"  # 403/401. Escalate, won't fix itself
    DATA       = "Data"        # malformed input. Surface to user
    BUSINESS   = "Business"    # policy violation. Block + log + escalate

class Alert(TypedDict):
    bucket: Bucket
    service: str
    severity: Literal["P1", "P2", "P3"]
    root_cause_signal: str
    retryable: bool
    raw: dict

def classify_alert(raw: dict) -> Alert:
    """Project arbitrary monitoring payload into the 4-bucket contract."""
    msg = (raw.get("message") or "").lower()
    if any(s in msg for s in ["503", "timeout", "rate limit", "circuit breaker"]):
        bucket, retryable = Bucket.TRANSIENT, True
    elif any(s in msg for s in ["403", "401", "forbidden", "unauthorized"]):
        bucket, retryable = Bucket.PERMISSION, False
    elif any(s in msg for s in ["malformed", "schema", "validation", "400"]):
        bucket, retryable = Bucket.DATA, False
    elif any(s in msg for s in ["policy", "compliance", "limit exceeded"]):
        bucket, retryable = Bucket.BUSINESS, False
    else:
        # Ambiguous. Classify with Haiku (cheap) and trust the bucket it returns
        bucket, retryable = haiku_classify_bucket(raw)
    return {
        "bucket": bucket,
        "service": raw.get("service", "unknown"),
        "severity": raw.get("severity", "P2"),
        "root_cause_signal": raw.get("root_cause") or msg[:100],
        "retryable": retryable,
        "raw": raw,
    }

def haiku_classify_bucket(raw: dict) -> tuple[Bucket, bool]:
    """Cheap fallback classifier for ambiguous alerts."""
    resp = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=64,
        messages=[{"role": "user", "content":
            f"Classify into one bucket (Transient|Permission|Data|Business) "
            f"+ retryable (true|false). Alert: {raw}\n"
            f"Output JSON only: {{\"bucket\": ..., \"retryable\": ...}}"}],
    )
    parsed = json.loads(resp.content[0].text)
    return Bucket(parsed["bucket"]), parsed["retryable"]
02

Define a small runbook registry (3-5 entries)

Every runbook is a named sequence of safe, reversible commands with explicit preconditions, timeouts, and rollback steps. The registry stays at 3-5 entries. The agent's routing accuracy degrades past 5, and a sixth runbook for an edge case is exactly the wrong move (escalate the edge case instead). Each runbook is reviewed in PRs and deployed through CI like application code.

Concept: tool-calling
03

Wire the PreToolUse hook with a destructive blocklist

The hook is the deterministic safety gate. It reads tool_name + tool_input.command from stdin JSON, applies a regex blocklist (compiled once), exits 2 with a stderr message on match. The model sees the deny as a tool_result with is_error: true and re-plans. No prompt-injection bypass; the blocklist is in code, not in the prompt.

Concept: hooks
04

Build the structured escalation block

When the agent can't act safely. Unknown runbook, hook denied, ambiguous alert, explicit escalation request. The harness emits a structured escalation block to PagerDuty / Slack. Six required fields. Humans triage in ~30 seconds vs ~5 minutes reading a raw transcript. The block is the single most important UX artifact in the whole system; tune the wording to be a one-screen read.

Concept: escalation
05

Wire the PostToolUse audit log

PostToolUse fires AFTER every tool call (Bash, runbook, escalate, log query, hook decisions). Writes a canonical row: ts, tool_name, tool_input, tool_result, latency_ms, hook_decisions, incident_id. Append-only JSONL on durable storage; retain ≥ 90 days. Indexed by incident_id, service, tool_name. The post-mortem replay tool depends on it; without it, trust collapses on the first incident.

Concept: evaluation
06

Run the agent loop with stop_reason branching

The on-call agent loop is a strict stop_reason FSM. end_turn: success, log + close incident. tool_use: extract the tool call, run hooks, execute, append result, continue. tool_use → hook denied: append the deny as a tool_result with is_error: true, continue (the agent re-plans, usually by escalating). max_tokens: persist partial state, escalate. The loop NEVER branches on response text. The structured stop_reason is the only contract.

Concept: agentic-loops
07

Pin incident metadata in CASE_FACTS

Multi-turn ops conversations need the incident metadata anchored. Pin incident_id, service, severity, bucket, partial_status, runbook_in_progress at the top of every system prompt iteration. This survives summarization and ensures the agent on turn 8 still knows it's working on PD-4711, not a generic incident. Without case-facts pinning, multi-turn ops conversations regress to single-turn quality.

Concept: case-facts-block
08

Test incident handling with adversarial scenarios

Build an eval set of 50 incidents across the 4 buckets + a 'sentiment trap' subset (alerts with angry phrasing but a clean runbook should run the runbook, not escalate). Run the agent over the set; measure: bucket-classification accuracy, correct-runbook rate, false-escalation rate, hook-deny-then-recover rate. Re-run on every change to the runbook registry, hooks, or system prompt.

Concept: evaluation
05 · Right call, wrong call

9 decisions the exam turns into distractors

01 · Destructive command in a runbook step
Looks right

Trust the system prompt to instruct the model never to run rm -rf

Actually right

PreToolUse hook denies via blocklist regex; agent escalates

DEC-01
02 · Alert classification ambiguous
Looks right

Pass the raw alert to the agent and ask it to figure out the bucket

Actually right

Haiku-classify into one of the 4 buckets + retryable; trust the bucket

DEC-02
03 · Runbook registry size
Looks right

Add a 6th, 7th, 8th runbook for edge cases as they appear

Actually right

3-5 safe, reversible runbooks; rare incidents always escalate

DEC-03
04 · Agent can't resolve. Escalate
Looks right

Forward the full transcript to the on-call human

Actually right

Structured 6-field escalation block to PagerDuty

DEC-04
05 · Runbook execution without hook enforcement
Looks right

Agent calls Bash with rm -rf /tmp/stale_pods. Typo'd as rm -rf / in production. The command runs because the only guard was a system-prompt warning. Service goes down; recovery takes hours.

Actually right

PreToolUse hook with a blocklist regex (rm -rf, sudo, drop database, kill -9, chmod 777, curl | sh). Exit 2 with stderr message on match. The agent observes the deny, escalates instead of retrying. Deterministic, no bypass.

AP-OP-01
06 · Prompt-only constraints
Looks right

System prompt says 'never run dangerous commands'. Production logs show the agent sometimes runs them anyway (3-5%) because the alert text was very persuasive or used unusual phrasing.

Actually right

Move the constraint to a deterministic PreToolUse hook. Exit 2 on the blocklist match. The system prompt becomes a soft suggestion that complements the hard hook, not a substitute for it.

AP-OP-02
07 · Ambiguous incident classification
Looks right

Agent reads raw alert text and infers severity / type. Same alert pattern produces different classifications on different days; routing drifts; on-call humans get inconsistent escalations.

Actually right

Project every alert into the 4-bucket isError contract at the webhook receiver (rule-based + Haiku fallback). The agent reads bucket and retryable; never parses raw alert text. Classification is consistent and auditable.

AP-OP-03
08 · Silent escalation, no audit trail
Looks right

Agent escalates ambiguous incidents but writes nothing to a durable log. Post-mortem at 9 AM can't reconstruct what the agent saw, what it tried, what it skipped. Trust collapses on the first incident.

Actually right

PostToolUse audit hook on every tool call: append to audit/{date}.jsonl with ts, tool_name, tool_input, tool_result, latency_ms, hook_decisions, incident_id. Retain ≥ 90 days. Replay tool reconstructs any incident in seconds.

AP-OP-04
09 · Access-failure as empty-result
Looks right

Monitoring API returns 403 forbidden; agent treats empty response as 'no alerts to handle' and goes back to sleep. Real incident festers undetected for hours; eventually a human finds the auth-token expired.

Actually right

Structured isError bucket (Permission). The monitoring tool wrapper returns {is_error: true, bucket: 'Permission', retryable: false, detail: 'auth token expired'}. Agent reads bucket: Permission and immediately escalates. Permission errors NEVER look like empty results.

AP-OP-05
Claude for Operations failure map.
06 · Budget

Cost & latency

~$0.005-0.015Per-incident cost (typical, with cache)

Haiku alert classification (~$0.0001) + Sonnet 4.5 ops loop ~3-5 turns × ~1500 cached input + ~300 output tokens. Total per resolved incident: ~$0.01. PagerDuty integration adds no token cost.

~5-15ms per tool call; ~0% token costHook overhead

PreToolUse + PostToolUse run as subprocesses reading stdin JSON. No LLM call. Pure local Python/TS. Latency below the noise floor of typical Bash / kubectl execution.

~5-10 MB/month at 1000 incidents/monthAudit log storage

Each row ~5 KB; 1000 incidents × ~5 tool calls each = 5000 rows/month ≈ 25 MB. At $0.023/GB/month object storage, <$0.001/month. Negligible.

~$0.30/runEval set run (50 incidents, weekly)

50 × $0.006 average per incident. Weekly = $1.20/month. Insurance against runbook registry / hook regression; gates deploys.

~$30-100 per false-escalation preventedFalse-escalation cost (avoided)

A false escalation wakes an on-call human at 3 AM (~30 min wasted at $60/hr fully-loaded ≈ $30) or worse, trains them to ignore alerts (compounding cost). The eval set + sentiment-trap test pays for itself many times over by preventing this.

07 · Ship checklist

Check every gate before release

0/11 checked
  • structured-outputs
  • tool-calling
  • hooks
  • evaluation
  • escalation
  • system-prompts
  • case-facts-block
  • agentic-loops
08 · Practice

5 exam-pattern questions

Work through one question at a time, check the architecture, then move through the set.

Question 1 of 5 · D1Choose the best answer

An ops agent processes alerts. In about 8 percent of cases it calls restart_pod when the alert was actually a 403 permission-denied from monitoring. What architectural change distinguishes access-failure from actual pod crash?

09 · FAQ

Frequently asked

How do I distinguish a permission-denied error from an actual service failure?

Both bucket projection AND structured isError on the monitoring tool. The webhook receiver classifies the alert into one of 4 buckets; the monitoring tool wrapper that fetches additional context returns {is_error: true, bucket: 'Permission', retryable: false, detail: '...'} instead of an empty result. The agent reads bucket and retryable and routes. Permission → escalate, Transient → retry. Don't parse error text.

Can the hook block dangerous commands?

Yes. That's its whole job. PreToolUse hook with a blocklist regex on Bash. Exit 2 with a stderr message on match; the agent sees the message as tool_result: {is_error: true, content: <stderr>} and re-plans. Tested against rm -rf, sudo, drop database, kill -9, chmod 777, curl | sh. Pair with an allowlist of safe binaries (kubectl, docker, journalctl, etc.) so unknown binaries also deny.

Should audit logging be a tool call or a hook?

A PostToolUse hook. Tool-based logging via the agent is easily skipped (the model decides not to call it on a busy turn); hooks run automatically by the SDK on every tool execution. The hook writes a canonical row to append-only JSONL; the replay tool reconstructs any incident later. This is the difference between 'we have logs sometimes' and 'we have logs always'.

What triggers an escalation?

Five structural triggers, in priority order: (1) explicit user request, (2) hook denial of a needed action, (3) unknown runbook (no match in the registry), (4) Permission bucket (access failure won't self-recover), (5) max_tokens or iteration cap. Sentiment is logged but NEVER triggers escalation. The 'sentiment trap' eval subset specifically tests that an angry-but-clean alert gets the runbook, not an escalation.

How does the agent know which runbook to execute?

Runbook registry has explicit preconditions; the agent's match_runbook(alert) function returns the first runbook whose preconditions all match. No match → escalate (don't bend a runbook to fit an edge case). The match logic is deterministic (rule evaluation), not LLM-judged. Adding a runbook means adding a precondition definition + the steps + the rollback; it's a code change, PR-reviewed, deployed through CI.

What if the operator approves an escalation later?

Round-trip via the escalation block + a separate approval flow. The structured block hits PagerDuty / Slack; the operator clicks approve in their tool; an approval webhook fires; the harness picks up the approval, loads the case-facts checkpoint, and resumes the agent loop with the approved action injected. The agent never bypasses the escalation. Humans authorize the next step explicitly.

Can the agent execute arbitrary bash?

No. Two layers: (1) PreToolUse hook with a destructive blocklist regex denies known-dangerous patterns; (2) allowlist of safe binaries (kubectl, docker, journalctl, curl, jq, etc.). Anything else is denied even if not on the blocklist. Both layers are deterministic. The agent's Bash access is narrow enough that a prompt-injection attack can't escalate to repo-wide damage.

Help someone build it

Share this scenario.

One share is one less team repeating the same architecture mistake.