--- title: Policies and escalation description: Guard policies turn a detector incident into an enforcement decision — matching fields, five actions, escalation ladders, the durable latch and the three locks. keywords: agent enforcement policy, agent escalation ladder, automatic agent kill, policy dry run, agent governance rules eyebrow: Guard & governance --- # Policies and escalation A policy turns a detector incident into at most one enforcement decision per session per tick. The evaluator is **pure**: it does no I/O, opens no store and sends no signals. It takes incidents, policies and facts, and returns decisions. The daemon does the reading, the dispatching and the auditing. That split means the whole matching surface is testable without a daemon, a database or a live agent. ## A policy ```json { "policy_id": "stop-codex-loops", "enabled": true, "scope_runtime": "codex", "scope_agent_id": "", "trigger_kind": "stuck_loop", "min_severity": "warning", "min_repeat": 2, "min_duration_s": 120, "min_spend_usd": 0, "min_spend_at_risk_usd": 1.50, "action": "pause" } ``` | Field | Meaning | |---|---| | `enabled` | Off policies are not evaluated | | `scope_runtime` | Empty string means every runtime | | `scope_agent_id` | Empty string means every agent | | `trigger_kind` | Empty string means any detector kind | | `min_severity` | `info` or `warning` | | `min_repeat` | The incident must have been seen at least this many times | | `min_duration_s` | The session must have been bad for at least this long | | `min_spend_usd` | The session's total cost must be at least this | | `min_spend_at_risk_usd` | The **flagged stretch** must be worth at least this. `0` or missing means unused. | | `action` | `monitor`, `alert`, `pause`, `stop` or `kill` | | `steps` | Optional escalation ladder | `min_spend_at_risk_usd` is usually the right filter. It is the difference between "this session has cost $80 in total" and "the part that is currently going nowhere is worth $6". ## The five actions | Action | Effect | |---|---| | `monitor` | Record what it *would* have done. Changes nothing. **The default for a new policy.** | | `alert` | Fire an alert. Does not touch the process. | | `pause` | Suspend the process. Resumable. | | `stop` | Ask it to stop cleanly. | | `kill` | Terminate the process tree. | ## Escalation ladders A single action fired once is not how anyone actually responds to a stuck agent. The real shape is *pause it, tell me, give it five minutes, then kill it if it is still stuck.* ```json "steps": [ {"action": "pause", "after_secs": 0}, {"action": "alert", "after_secs": 0}, {"action": "kill", "after_secs": 300} ] ``` The semantics are chosen so a ladder can never act faster than a plain policy: - **Step 0 fires when the policy first matches.** Its `after_secs` is ignored — use `min_duration_s` for a delay before the first action. - **Step *n* becomes due `after_secs` after step *n-1* actually fired**, not after the incident started. A ladder measures the time the agent was *given to recover*. - **A due step only fires if the session is still matching this tick.** That is what makes "kill if still stuck" mean *still stuck*. If the detector stops reporting, the ladder simply stops. - **Every step passes the same three locks.** A `kill` step on a node with enforcement off is a recorded dry run, exactly like a `kill` policy. Ladders are capped at a maximum number of steps, so a policy cannot become an open-ended scheduler. ## The three locks Every autonomous action requires **all three**: 1. **The policy's own action** must be `pause`, `stop` or `kill`. A new policy defaults to `monitor`. 2. **`CLAWMETRY_POLICY_ENFORCE=1`** on the node. Default `0`. One environment variable disables every policy on that machine. 3. **An entitlement check that fails closed.** If the check cannot be answered, the answer is no. ```bash # The node-wide switch. Nothing autonomous happens without this. export CLAWMETRY_POLICY_ENFORCE=1 # Skip the evaluation pass entirely. export CLAWMETRY_GUARD_POLICIES=0 ``` ## The durable latch Each rung fires **at most once per session**, latched on `(session_id, policy_id, step_index)` as a primary key in the store. That is durable across a daemon restart. A daemon that dies mid-ladder resumes at the right rung instead of replaying it — which, for a ladder ending in `kill`, is the difference between a considered escalation and a restart loop that kills every session it sees. ## Managing policies ```bash # list curl -s localhost:8900/api/guard/policies | jq # create curl -s localhost:8900/api/guard/policies -X POST \ -H 'content-type: application/json' \ -d '{"scope_runtime":"codex","trigger_kind":"stuck_loop", "min_severity":"warning","min_repeat":2, "min_spend_at_risk_usd":1.5,"action":"monitor"}' # delete curl -s -X DELETE localhost:8900/api/guard/policies/ # what has fired, including dry runs curl -s localhost:8900/api/guard/actions | jq '.actions[:20]' ``` ## Rolling one out The order that avoids surprises: 1. **Create it as `monitor`.** That is the default; do not change it yet. 2. **Leave enforcement off.** Also the default. 3. **Read the decision log for a week.** `GET /api/guard/actions` shows exactly what would have happened. 4. **Check the false positives.** Would you have been happy for it to pause those sessions? If not, tune the policy, not the detector. 5. **Change the action to `pause`.** Still nothing happens — enforcement is off. Verify the log now shows the intended action. 6. **Turn on `CLAWMETRY_POLICY_ENFORCE=1`.** 7. **Only then consider a ladder ending in `kill`.** Skipping to step 6 is how you find out that a policy you thought matched three sessions a week actually matches thirty. ## Naming `/api/guard/policies` is deliberately distinct from `/api/tool-policy`. This one is **mid-run enforcement**; the other is the **pre-tool** permission surface. Different axis, different table, no shared state. → [Tool permissions](/docs/guard/tool-policy/)