Guard · AI incident response
Decide a held agent run and request its containment
A detector armed to enforcing fires on a run, the desktop opens an incident on your team's Containment queue, an owner or admin approves or denies it with a signed decision, and asks for the run's token to be revoked. The queue records the request. The desktop that holds the token performs it and reports each leg.
For: the owner or admin on call for a team's agent runs, and the operator who arms the detectors on each machine
What it does, and why it helps
The queue is the Containment queue subtab of the hosted AI incidents page. An incident lands there when the PreToolUse enforcement hook on a bound machine sees the lethal-trifecta gate or the consensus-action list fire while that detector is set to enforcing. The record carries the detector, the reason, the session context, the three co-occurring legs, the held call as bounded labels, and the on-call route. Identical firings inside ten minutes fold into one record. The hook opens the record as telemetry after it has decided: in this release it calls the gate without an approver, so the verdict is the degraded allow, the call runs, and the incident is the on-call event, not a paused action.
On the row an owner or admin presses Approve or Deny. The decision is signed over a digest recomputed from its own fields, and the panel reports whether that signature verifies and whether the leaf sits in the team's transparency log. Then the same person presses Request containment, which sets the status to revoke requested and nothing more. The desktop containment consumer asks every 15 minutes whether a containment request is waiting, pulls the team's incidents when one is, revokes the run's token through the broker when it is armed or through the local auth token otherwise, tries the branch block, and posts the leg results back. The status becomes contained only when every attempted leg completed, else contain failed, and the row offers a retry.
On the desktop, a HOLD can also be decided on its own case, and that decision stays on the local case: it is not sent to the hosted queue. The case page's Security section shows Decide the HOLD while a HOLD is attached and undecided: name yourself, add a reason if you like, and press Approve the held call or Deny it. The decision is signed with the machine's install key, kept on the case with a timeline entry, and read back in the Security section as approved or denied by that person. A HOLD is decided once. A deny records the decision and runs nothing: the response playbook and the kill switch under it are how a run is contained on the desktop.
The pain. A detector fires at 2am on a run nobody is watching. The person who could stop it does not hold the token, the person who holds the token is asleep, and whatever gets written down afterwards is someone's memory of what they clicked.
The point of view. Deciding and containing are two acts by two parties, and the record should say which one happened. The website can prove a human decided. Only the machine that holds the token can say it was revoked, and it should say so leg by leg.
What gets easier. Reading the record before touching anything: the legs, the held call, who decided and whether the signature checks. Retrying a containment the daemon could not finish, from the same row, without a second incident.
When it helps. A team on a paid plan with at least one bound desktop, the enforcement hook registered on that machine, and trifecta or consensus-action promoted to enforcing. Without those three the queue stays at No agent incidents yet.
Its limits. It does not pause the call: the hook records the hold and lets the verdict stand. It does not revoke anything from the browser. The branch leg on the consumer path answers no branch-block hook wired, so a record that carries a repo and branch reports contain failed with that reason. A verified, anchored decision proves a human decided, not that the model obeyed.
Understand it in 30 seconds
Read the narration
- 0:00 A detector held an agent's call.
- 0:02 The record is on the queue, undecided.
- 0:06 Read the legs and the held call first.
- 0:08 Then approve or deny, as a second person.
- 0:13 Request containment records a request.
- 0:15 The desktop daemon revokes the token and reports each leg; one that did not finish reads contain failed.
- 0:23 Decide on the record. Anchor the decision.
- 0:26 The guide covers the flags.
Synthetic example. Read the guide
Where to find it
- Hosted:
repoops.ai/team/agent-incidents?view=containment, from AI Security in the sidebar, then AI incidents under All tools, in the Egress and containment group. - Desktop: hosted only.
When to use it
A trifecta hold on a run that read a secret and reached out
Situation. The trifecta detector is at enforcing on a bound machine. A run reads a credential file, has an untrusted web fetch in the same turn, and then calls an upload. The hook opens one incident and, if a sink is armed, sends the approval message.
What you do. Open the Containment queue. Expand Incident record, read the three legs and the held call, then press Deny. Press Request containment and confirm. Come back after the next consumer tick.
What you see. The row reads denied, then revoke requested in amber. The Containment section of the record reads Revocation requested, not yet confirmed contained, with the queue's own note that it does not revoke the token. After the report the Run token line reads revoked, the Branch line reads not blocked (no branch-block hook wired), and the status is contain failed.
What it establishes. The token leg is done and recorded from the machine that did it; the branch leg is recorded as not done, with the reason. Nothing on the page claims the run was stopped. Recurrence not assessable: no captured sessions in the window.
An approval a second person has to give
Situation. A consensus-action hold on a run you started yourself. The hook filed the incident under your account, and you are the admin looking at it.
What you do. Press Approve. Read the refusal. Ask another owner or admin to open the row and decide it; or press Deny yourself, which needs no second pair of eyes.
What you see. The route answers the approver filed this incident; a self-approval is not four-eyes. After the second person approves, the panel reads Signature verified: the digest recomputed from this decision's own fields matches, and offers Anchor in the log.
What it establishes. An approve is a signed record by a human who is not the run's reporter. A deny is always available. Anchoring appends the leaf to the team's log and reports its index and the tree size.
Before you start
- Supported versions
- RepoOps v0.3.1, the release this guide was read against. The hook and the consumer run from the desktop daemon; the queue is served by repoops.ai.
- Where it runs
- Hosted: AI Security, then Containment under All tools (/team/agent-incidents?view=containment; the bare URL renders the same queue). Desktop: the Containment row under On repoops.ai opens that page; the case dialog shows Decide the held call as its next step on a case with a HOLD attached, and its Security section holds the approve and deny controls; the hosted Today page lists a held run as decision pending on Containment.
- Permissions
- Approve, Deny and Request containment need an owner or admin of the incident's team; a member or read-only viewer sees awaiting an owner or admin. Approve also needs four-eyes: a human approver who is not the account that filed the incident, and a reporter the server can resolve. Deny is always allowed. The report route accepts a device token only; a browser session can never mark an incident contained.
- Connections
- A bound device with cloud sync on, for both the push and the consumer. Optional: one alert sink (REPOOPS_SIGNALS_ALERT_SLACK_WEBHOOK, REPOOPS_SIGNALS_ALERT_PAGERDUTY_ROUTING_KEY, or the two incident.io values) so a hold sends an approval message. Optional: the credential broker armed, so the revoke goes through a per-run token id.
- Plan
- The team must be on a paid plan (hosted, team or enterprise) and the member inside its seats; below that the routes answer with the entitlement response. No plan gates the desktop hook or the consumer.
Configure it
- Register the enforcement hook on each machine.
Add scripts/cc-pretooluse-enforce.mjs to the PreToolUse hooks in the repository's .claude/settings.json. Until it runs, no detector can fire on a live call and nothing opens.
- Promote trifecta or consensus-action to enforcing.
On the desktop, open Local settings, then Governance under Workspace utilities: in its Enforcement ramp each wired detector shows its mode (advisory, warn or enforcing) and a Promote button once its false-positive meter clears the bound. Every detector starts at advisory, which reports and never holds.
- Bind the device and keep cloud sync on.
The push (pushIncidentToHosted) and the consumer share one gate: a device binding with a team, and cloud sync enabled. Either absent, nothing leaves the machine and nothing is pulled.
- Arm an alert sink, if you want the hold to reach a channel.
With a sink set, a hold sends RepoOps approval needed: <detector> HOLD on session <id> with the legs and the held call as bounded labels, linking to the desktop AI incidents subtab. With none set it is a recorded no-op.
- Decide the incident on the queue.
Approve or Deny on the row. The decision is signed; the Incident record panel reports the verification and the log inclusion in three states each (verified, did not verify, not checked), and offers Anchor in the log on a verified, unanchored decision.
- Request containment, then read the report.
Request containment confirms with the sentence that the revoke and branch block execute on the desktop daemon that holds the real token. A second press while the request is fresh answers 409. After an hour with no report the row says so and offers Re-request containment; after a failed report it offers Retry containment.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
modes.trifecta, modes.consensus-action | the local policy bundle; Local settings, Workspace utilities, Governance, Enforcement ramp, Promote | advisory (the default) until the meter clears, then enforcing on the machines you watch | Only a detector at enforcing produces holdCandidates; advisory and warn open nothing on this queue. |
REPOOPS_CONTAIN_CONSUMER_INTERVAL_MS | the data directory's .env | 300000 (the default, 5 minutes); 0 disables the consumer | The shortest gap between two pulls of revoke requested incidents. The pull runs once an hour with the machine's other calls to repoops.ai, and on any quarter hour when repoops.ai says a containment request is waiting, so a value under 15 minutes changes nothing; a request older than an hour with no report is what the stale note means. |
REPOOPS_BROKER, REPOOPS_BROKER_SECRET | the data directory's .env | unset (the default) unless the agent already runs on minted broker tokens | Armed, the revoke targets the run's broker token id; unarmed, it marks the local auth token dead via lib/auth.mjs and the report says which. |
REPOOPS_SIGNALS_ALERT_SLACK_WEBHOOK, REPOOPS_SIGNALS_ALERT_PAGERDUTY_ROUTING_KEY, REPOOPS_SIGNALS_ALERT_INCIDENTIO_CONFIG_ID and _TOKEN | the data directory's .env | one sink your on-call reads | The approval message goes to every armed sink; with none armed the hold is recorded and nobody is paged. |
REREQUEST_STALE_MS | website/lib/agent-incidents/store.ts, no control | 1 hour (fixed) | Below it a second Request containment is the same 409; past it the row offers Re-request containment. |
DEDUP_TTL_MS | lib/enforcement-incident.mjs, no control | 10 minutes (fixed) | Identical firings (same session, detector and action) inside the window open one incident. |
LIST_LIMIT_DEFAULT, LIST_LIMIT_MAX | website/lib/agent-incidents/store.ts, no control | 50 rows a page, 200 at most | The page says Showing the N most recent incidents when a page is cut, and offers Older and newest links; the Status row and the detector badge filter it. |
Team role | the team's Settings, Admin console | owner or admin for whoever is on call | The three buttons and Anchor in the log render for owner and admin only; the routes re-check the role on every call. |
What you should see
The normal case
Configuration. Hook registered, trifecta at enforcing, device bound, cloud sync on, broker unarmed, no sink.
Expect. A trifecta firing opens one incident in status open with the detector badge and the reason. The call itself ran. An owner or admin decides it; Request containment moves it to revoke requested; within about 15 minutes the desktop reports.
Verify. The Incident record panel reads Signature verified, then after the report the Run token line reads revoked (broker not enabled; revoked the local token via lib/auth.mjs), the Branch line reads not blocked (no branch-block hook wired), and the status is contain failed. The hosted Today page listed the row as decision pending on Containment while it was open.
The broker path
Configuration. As above, with REPOOPS_BROKER=1 and a secret, the run started on a minted token.
Expect. The push carries the broker token id in the local linkage only. The consumer's token leg reads via broker and revokes that id; the branch leg still reports no branch-block hook wired, so the verdict is still contain failed unless the record carries no repo and branch.
Verify. The report on the panel names the leg reasons; a re-run of the consumer does not revoke twice, because the stored report is re-sent instead.
No consumer picked it up
Configuration. Request containment pressed while the desktop daemon is off, or the device is unlinked.
Expect. The row stays revoke requested. After an hour it reads No desktop consumer has reported on this request for over 1 hour. The daemon that performs the revoke may be off or unlinked, and the button reads Re-request containment.
Verify. The stored containment keeps firstRequestedAt across the re-request, so the record still shows how long the incident has waited.
Data and cost
- What is captured
- One hosted row per incident: team, session id, repo, branch, detector, reason and action as bounded labels (200 characters), the legs, the provenance tags, the held call's tool, command and path (300 characters, URL query and userinfo stripped), a hash of the run token, the initiator, the on-call route, the status, the signed decision, the containment legs and reasons. Severity has no writer yet and reads empty. Locally, the raw token and the broker token id sit in the kv linkage contain-link.<id> until the report lands.
- Who can see it
- Every member of the team can read the queue and the record; owner and admin can act. The hosted connector-dispatch cron forwards new incidents to any vault connector the team armed. Messages on the record are between the people who can already see it; a containment incident carries no developer identity, so there is no consent gate or transcript ask on this queue.
- How long it is kept
- No retention knob and no purge route. Rows stay until the team is deleted, when they cascade. The local linkage and the pending-report marker are dropped once a report is accepted or durably refused. The team's transparency log is rebuilt on read up to 5000 leaves; past that the page reports the log as truncated rather than a wrong root.
- What leaves the machine
- From the desktop: one POST per opened incident with the labels above and the raw run token in transit, which the server hashes to run_token_ref and discards; one GET of the team's incident list and one POST of leg booleans and bounded reasons per report, all to the configured hosted base, never to an address from an incident field. From the browser: the decision and the request, under the same-origin check. To a sink: the approval message, labels only.
- What it costs
- LLM-free on every path. No model call, no metered cost; the routes are tier-gated, not priced per incident.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| The queue reads No agent incidents yet. | No detector is at enforcing, the hook is not registered, or the device is unbound or has cloud sync off. | Promote the detector on the desktop Governance page (Local settings, Workspace utilities), add the hook to .claude/settings.json, and check the binding in the desktop app. |
| The row shows awaiting an owner or admin and no buttons. | Your role on the incident's team is member or read-only. | Ask an owner or admin, or have one change your role in the Admin console. |
| Approve answers a self-approval is not four-eyes, or cannot verify four-eyes. | You filed the incident, or the reporter account could not be resolved. | Have a different owner or admin approve, or press Deny, which is always allowed. |
| Request containment answers revocation already requested for this incident. | A fresh request already stands; the store answers 409 for an hour. | Wait for the report, or for the stale note and the Re-request containment label. |
| The status is contain failed and the Branch line reads not blocked (no branch-block hook wired). | The consumer runs containIncident with its default branch leg, which is inert by decision, and the verdict is fail-closed. | Read the token leg; if it reads revoked, the credential is dead. Block the branch by hand or through the local kill switch under Run forensics. Retry containment only re-sends the same legs. |
| Signature DID NOT verify on a decision. | A field of the record changed after signing, or the server key changed. | Treat the record as unattested, as the panel says. Anchor in the log is not offered on it. |
| The desktop log says hosted incident pull rejected (HTTP 402); paused ~1h. | The team is below the paid tier or the seat check failed; the consumer backs off for an hour. | Fix the plan or the seat, or rebind the device, which clears the backoff at once. |
- Disable
- Any of the levers under To stop or undo. Demoting a detector stops new incidents on that machine only; the hosted rows and the queue stay.
- Roll back
- Not provided. No route reverses an approve, a deny, or a report: recordDecisionRow writes the terminal status and reportContainmentRow accepts a report only from revoke requested, so a decided incident cannot be re-opened. A revoked token stays revoked; start a new run. The data sits in website/lib/agent-incidents/store.ts and the agent_incidents table.
- Revoke access
- Removing a member's owner or admin role in the Admin console removes the three buttons and the routes refuse them. Disconnect in the desktop app forgets the binding and revokes the device token, which stops the push, the pull and the report from that machine. A broker token is revoked with POST /api/broker/revoke on the loopback, by its token id, when the broker is armed.
- Delete
- Not provided. No route deletes an agent_incidents row; the row goes only when its team is deleted (the foreign key cascades, website/db/schema.ts). The local linkage in contain-link.<id> is dropped by the consumer after a report.
Related tasks
Maintenance evidence
- Feature id
incident-response(spine leafcontainment)- Owner
- AI Security Phase 2, Runtime Containment (WS2-A, WS2-B, A1, A5), and the incident-response tab map (LDG-0659, LDG-0660). Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (the hosted page, the decision controls and the anchor button), the routes, the store and the consumer read in full; the hook's decide() call and the daemon wiring of the consumer checked for the two limits stated above.
- Example fixtures
- No fixture file; the shapes are inline in lib/daemon/contain-consumer.test.mjs (linkage hit and miss, only revoke_requested, a blocked record never contained, the broker path, the 4xx backoff, the pending-report re-send), lib/agent-incident.test.mjs (the attested decision, the tampered record, both revoke paths), website/lib/agent-incidents/four-eyes.test.ts (self-approve refused, deny always allowed, the containable statuses), website/components/incident-decision-controls.test.ts (which buttons on which status) and website/app/api/agent-incidents/route.integration.test.ts.
- Source references
website/app/team/(home)/agent-incidents/page.tsx,website/components/incident-decision-controls.tsx,website/components/incident-anchor-button.tsx,website/app/api/agent-incidents/[id]/decision/route.ts,website/app/api/agent-incidents/[id]/contain/route.ts,website/app/api/agent-incidents/[id]/contained/route.ts,website/lib/agent-incidents/store.ts,website/lib/agent-incidents/attest.ts,website/lib/agent-incidents/transparency.ts,website/db/schema.ts,lib/daemon/contain-consumer.mjs,scripts/daemon.mjs,lib/agent-incident.mjs,lib/agent-incident-link.mjs,lib/enforcement-incident.mjs,lib/enforcement-gate.mjs,lib/policy-bundle.mjs,scripts/cc-pretooluse-enforce.mjs,lib/session-signals-alert.mjs,public/governance.html,lib/canonical-tabs.mjs- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Narrated story rendered and published 2026-09-26 (render afdd9aafeba8, LDG-1014) with the breadcrumb AI Security, which lists the feature under Moved here, checked against main at 7aab4cd82 with LDG-1014 part 1. Six frames, the captions and the transcript were reviewed by the authoring agent, not an independent reviewer; the audio was not listened to by a person. Narration is the provisional Windows voice until LDG-0721.
Last updated