# The kill switch

> Stop one agent or a whole fleet at once, from the console, a phone, Slack, a SOAR playbook or your identity provider. A stopped agent is refused on every path Immiscible enforces, including its model calls.

Source: https://immiscible.fly.dev/docs/guides/kill-switch

From the moment an agent is stopped (frozen), it is refused on every path Immiscible enforces, before anything goes upstream: its action requests, its model calls through the gateway, its tool calls through the MCP proxy, and its card authorisations. Requests already waiting for approval cannot be approved. Nothing an agent key can do starts it again.

## What a stop stops

Path by path, for a stopped agent:

| Path | What happens |
|---|---|
| Action requests: `POST /v1/actions/authorize`, and the MCP server at `/mcp` | a `deny` decision with the signal `agent_frozen`; requests waiting for approval are cancelled |
| Model calls through the gateway: `/v1/chat/completions`, `/anthropic/v1/messages` | `403 agent_stopped`, before anything is routed, reserved or sent |
| Token counts: `/anthropic/v1/messages/count_tokens` | `403 agent_stopped`; nothing reaches the provider |
| The MCP proxy, `/mcp/proxy/:id`, to an MCP server or a plain HTTP API | HTTP `403`, JSON-RPC error `-32003` with `error.data.type` `agent_stopped`, for every method: it lists no tools and calls none |
| OAuth connections (`aat_...`) | refused, and the connection revoked; after a restart, a person connects the app again |
| Receipts (`POST /v1/verify`) | every unspent receipt reads as revoked |
| Card authorisations | declined at the card; Ramp cards are locked |

A restart takes effect on the next request, in every process: the check is one indexed database read per call, not a cache, so there is nothing to wait for and nothing to restart.

### The refusal

Model calls and proxied tool calls are refused with the same error. It names the hold and who may lift it:

```json
{
  "error": {
    "type": "agent_stopped",
    "message": "this agent is stopped under a security hold, so nothing it sends goes upstream and nothing was sent; its kill owner, a security lead or a workspace admin can restart it in the console under Agents, with a reason",
    "hold": "security",
    "stopped": "agent",
    "agentId": "agt_4f2c91a7",
    "fix": "Stop: the kill switch is on (error.hold says which hold, error.message who may lift it). Do not retry; tell the person you act for.",
    "docs": "https://immiscible.fly.dev/docs/guides/kill-switch#what-a-stop-stops"
  }
}
```

On `/anthropic/v1/messages` the envelope is Anthropic's (`type` is `permission_error`) and `immiscibleType` is `agent_stopped`. Model calls carry `x-should-retry: false`, so SDKs do not retry. On the MCP proxy the same fields are in `error.data`. Each refusal is on the evidence ledger as a policy action, `agent_stopped_refused`, with the path, the hold and the key: the first for a key on a path in any minute is written, and the retries after it in that minute are counted into the next record (`refusedSinceLast`), so an agent retrying in a loop cannot flood the ledger.

### Keys that are not bound to one agent

A person's gateway key, a service key and an admin key are not tied to one agent. Stopping one agent, or a selection of agents, leaves them working.

**Stop every agent** covers them too. That is a fleet freeze with `selector: { "all": true }` and nothing narrowing it, from the console or `/v1/admin/freeze`, or the kill switch in a personal workspace. While it stands, model calls and token counts on every key in the workspace are refused with `agent_stopped` and `"stopped": "workspace"`. The reason: the gateway cannot tell a coding agent running on a person's key from the person typing, and "every agent is stopped" would be false if that agent kept going. The cost is that people's own model calls through the gateway stop too, until the stop is lifted. Routes on an admin key that reach no upstream (reports, evidence, budgets) still answer.

It stands until the stop is lifted, or every agent it stopped has been restarted, under the same hold rules: an incident hold is lifted by a second person. An agent registered after the stop is not stopped by it; stop it on its own.

## Who can stop an agent

| Who | How |
|---|---|
| The person it acts for, owners and admins | **Agents**, **Freeze** in the console, or the link in any approval |
| Its **kill owner** and security leads | the same, and their stop defaults to a security hold |
| Anyone allowed, from Slack | `/immiscible freeze <agent> <reason>` |
| A SOAR playbook or script | a [service token](https://immiscible.fly.dev/docs/api/authentication.md#service-tokens) with `agents:freeze` |
| Your identity provider | a [Shared Signals](https://immiscible.fly.dev/docs/guides/shared-signals.md) event about the person the agent acts for |
| The system itself | an over-capture at the card, a review verdict of `incident`, lapsed recertification |

Name a kill owner for every agent that matters (under its standing). It is the person who may always stop it, whatever their role.

## Freeze one agent

Console session:

```bash
curl -X POST "https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/agents/agt_4f2c91a7/freeze" \
  -H "cookie: __Host-sid=$IMMISCIBLE_SESSION" -H "x-immiscible-csrf: 1" \
  -H "content-type: application/json" \
  -d '{ "reason": "posting to an unknown domain", "hold": "security" }'
```

Service token:

```bash
curl -X POST "https://immiscible.fly.dev/v1/admin/agents/agt_4f2c91a7/freeze" \
  -H "authorization: Bearer $IMMISCIBLE_SERVICE_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "reason": "Splunk SOAR: exfiltration rule 12", "hold": "incident" }'
```

The `hold` says who may lift the freeze: see [holds and drills](https://immiscible.fly.dev/docs/guides/holds-and-drills.md). A token's freeze is always at least a security hold: a machine stops agents, a person decides when they start again.

## Freeze a fleet

A **selector** names agents by `all`, `agentIds`, `principalId`, `vendor`, `tier` or `upstreamId`. Fields combine with AND. Always look before you leap:

```bash
# 1. how many, and which?
curl -X POST "https://immiscible.fly.dev/v1/admin/freeze" \
  -H "authorization: Bearer $IMMISCIBLE_SERVICE_TOKEN" -H "content-type: application/json" \
  -d '{ "selector": { "upstreamId": "mcu_6c1d0e" }, "dryRun": true }'
# => { "count": 40, "digest": "..." }

# 2. freeze exactly those
curl -X POST "https://immiscible.fly.dev/v1/admin/freeze" \
  -H "authorization: Bearer $IMMISCIBLE_SERVICE_TOKEN" -H "content-type: application/json" \
  -d '{ "selector": { "upstreamId": "mcu_6c1d0e" }, "reason": "compromised tool server", "hold": "incident", "confirmCount": 40 }'
```

Large selections need `confirmCount` equal to the count, so a selector that matches more than you expected is refused rather than obeyed. A bulk freeze returns a `batchId`; lifting it with [`POST /api/w/:wid/freeze/:batchId/lift`](https://immiscible.fly.dev/docs/api/post-api-w-wid-freeze-batchid-lift.md) restarts only what that batch froze. Agents someone else has frozen since stay frozen.

## Ceilings on machines

Machines have two hourly freeze ceilings: `freezeCeiling` per token or SSF issuer (default 500) and `workspaceFreezeCeiling` for all machines together (default 1000). A freeze past either waits as **pending** for a person, so a looping playbook or a compromised token cannot stop the whole company.

A person confirms or dismisses a pending freeze under **Agents**. Confirming recomputes the selection: if the agents it names have changed, the confirm is refused with `409 selection_changed`, `oldCount` and `newCount`, until it is sent again with `confirmCount` equal to the new count.

## Starting again

Unfreezing is a person's decision, in the console: [`POST /api/w/:wid/agents/:aid/unfreeze`](https://immiscible.fly.dev/docs/api/post-api-w-wid-agents-aid-unfreeze.md) with a reason. The hold decides who may. Every freeze and lift, with who and why, is in the agent's history ([`GET .../freezes`](https://immiscible.fly.dev/docs/api/get-api-w-wid-agents-aid-freezes.md)) and in the evidence ledger.

> **Tip**
> Test it before you need it. A [drill](https://immiscible.fly.dev/docs/guides/holds-and-drills.md#drills) freezes for real, proves every path refuses, restores, and gives you a signed report with timings for the auditor who asks when you last tested the kill switch.
