Kill Switch Design für AI Agents (Writes sofort stoppen) + Code

Wenn dein Agent Schaden macht, brauchst du einen Kill Switch, der wirklich stoppt: global + per-tenant toggles, tool-disable, und saubere Stop-Semantik.
Auf dieser Seite
  1. Problem (aus der Praxis)
  2. Warum das in Production bricht
  3. 1) Teams bauen „Pause“-Buttons, die nichts pausieren
  4. 2) Kill Switches, die nicht im Tool Gateway gecheckt werden, leaken
  5. 3) „Stop the run“ reicht nicht
  6. 4) Scope: global vs per-tenant
  7. Implementierungsbeispiel (echter Code)
  8. Echter Incident (mit Zahlen)
  9. Abwägungen
  10. Wann du es NICHT nutzen solltest
  11. Checkliste (Copy/Paste)
  12. Sicheres Default-Config-Snippet (JSON/YAML)
  13. FAQ (3–5)
  14. Verwandte Seiten (3–6 Links)
Interaktiver Ablauf
Szenario:
Schritt 1/3: Execution

Action is proposed as structured data (tool + args).

Problem (aus der Praxis)

Dein Agent macht das Falsche.

Nicht „Antwort ist etwas off“. Falsch wie:

  • doppelte Emails senden
  • Tickets in Bulk erstellen
  • eine API hämmern, bis du rate-limited bist

Und jetzt der wichtige Teil: du hast keine Zeit, „Prompt fixen und redeployen“.

Du brauchst einen Kill Switch, der:

  • jetzt sofort wirkt
  • auditierbar ist (wer hat wann warum geschaltet)
  • Side Effects stoppt, nicht nur die UI

Wenn dein Kill Switch nur im Frontend lebt, ist er kein Kill Switch. Er ist Placebo. Wenn dein Kill Switch ein Env Var ist, ist er ein Deploy. Incidents warten nicht auf Deploys.

Warum das in Production bricht

1) Teams bauen „Pause“-Buttons, die nichts pausieren

Anti-Design:

  • UI versteckt den Button
  • API lässt die Agent-Loop weiterlaufen
  • Tool Gateway führt Writes weiter aus

Wenn Tool Calls noch durchgehen, hast du den Incident nicht gestoppt. Du hast ihn umbenannt.

2) Kill Switches, die nicht im Tool Gateway gecheckt werden, leaken

Wenn du den Kill Switch checkst:

  • in einer Route
  • aber nicht in Background Jobs
  • und nicht im Tool Gateway

…verpasst du einen Pfad.

3) „Stop the run“ reicht nicht

In-flight Tool Calls existieren:

  • lange HTTP Calls
  • Browser Sessions
  • Queue Worker, die schon arbeiten

Du brauchst Semantik:

  • stop new runs
  • stop new tool calls
  • optional force-cancel in-flight work (best-effort)

4) Scope: global vs per-tenant

Du willst nicht das ganze Produkt stoppen, weil ein Tenant in einer Loop hängt. Du willst:

  • global switch (nuklear)
  • per-tenant switch (chirurgisch)
  • per-tool disable list (z. B. „kein Browser heute“)

Implementierungsbeispiel (echter Code)

Dieses Pattern:

  • liest Kill-State aus einem shared store (pseudo)
  • checkt ihn an zwei Stellen: Loop + Tool Gateway
  • unterscheidet „stop all“ vs „disable writes“
PYTHON
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class KillState:
  stop_all: bool = False
  disable_writes: bool = True
  disabled_tools: set[str] = None


class Killed(RuntimeError):
  pass


def load_kill_state(*, tenant_id: str) -> KillState:
  # Pseudo: Redis/DB/feature-flag service. Must be fast + reliable.
  # Split global + per-tenant state.
  global_state = read_flag("agent_kill_global")  # (pseudo)
  tenant_state = read_flag(f"agent_kill_tenant:{tenant_id}")  # (pseudo)
  disabled_tools = set(read_list("agent_disabled_tools"))  # (pseudo)

  return KillState(
      stop_all=bool(global_state or tenant_state),
      disable_writes=True,
      disabled_tools=disabled_tools,
  )


WRITE_TOOLS = {"email.send", "db.write", "ticket.create", "ticket.close"}


def guard_tool_call(*, kill: KillState, tool: str) -> None:
  if kill.stop_all:
      raise Killed("killed: stop_all")
  if tool in (kill.disabled_tools or set()):
      raise Killed(f"killed: tool_disabled:{tool}")
  if kill.disable_writes and tool in WRITE_TOOLS:
      raise Killed(f"killed: writes_disabled:{tool}")


def run(task: str, *, tenant_id: str, tools) -> dict[str, Any]:
  kill = load_kill_state(tenant_id=tenant_id)
  for _ in range(1000):
      if kill.stop_all:
          return {"status": "stopped", "stop_reason": "killed"}

      action = llm_decide(task)  # (pseudo)
      if action.kind != "tool":
          return {"status": "ok", "answer": action.final_answer}

      guard_tool_call(kill=kill, tool=action.name)
      obs = tools.call(action.name, action.args)  # (pseudo)
      task = update(task, action, obs)  # (pseudo)

  return {"status": "stopped", "stop_reason": "max_steps"}
JAVASCRIPT
const WRITE_TOOLS = new Set(["email.send", "db.write", "ticket.create", "ticket.close"]);

export class Killed extends Error {}

export function loadKillState({ tenantId }) {
// Pseudo: feature-flag store. Must be fast + reliable.
const globalStop = readFlag("agent_kill_global"); // (pseudo)
const tenantStop = readFlag("agent_kill_tenant:" + tenantId); // (pseudo)
const disabledTools = new Set(readList("agent_disabled_tools")); // (pseudo)
return { stopAll: Boolean(globalStop || tenantStop), disableWrites: true, disabledTools };
}

export function guardToolCall({ kill, tool }) {
if (kill.stopAll) throw new Killed("killed: stop_all");
if (kill.disabledTools && kill.disabledTools.has(tool)) throw new Killed("killed: tool_disabled:" + tool);
if (kill.disableWrites && WRITE_TOOLS.has(tool)) throw new Killed("killed: writes_disabled:" + tool);
}

Echter Incident (mit Zahlen)

Wir hatten einen Agenten, der Follow-up Emails geschrieben und gesendet hat. Er war hinter einem „send_email“-Tool — und (oops) noch ohne Approval Gate.

Ein Prompt Change hat „follow up“ als „send now“ interpretiert.

Impact in 22 Minuten:

  • 117 Emails gesendet (einige Duplikate)
  • ~4 Stunden Customer Damage Control
  • das Modell war nicht „gehackt“ — es war einfach falsch, laut

Der Kill Switch, den wir dachten wir hätten, war ein UI Toggle. Background Worker haben ihn ignoriert.

Fix:

  1. Kill Switch enforced im Tool Gateway (writes disabled)
  2. per-tenant stop (damit ein Tenant nicht alle nuked)
  3. audit log entries wenn Kill State einen Tool Call blockt
  4. Incident Runbook: kill switch zuerst, Fragen später

Abwägungen

  • Kill Switches senken Availability während Incidents. Das ist besser als irreversible Writes.
  • Du musst den Kill Path testen. Untested kill switches failen zur schlechtesten Zeit.
  • Shared-State Reads adden Latenz; halte es schnell und cache kurz (Sekunden, nicht Minuten).

Wann du es NICHT nutzen solltest

  • Nutze „kill switch“ nicht als Ersatz für echte Governance (permissions, approvals, budgets).
  • Bau keinen Kill Switch, der nur client-side ist. Er wird dich anlügen.
  • Verlass dich nicht im Normalbetrieb auf Kill Switches. Sie sind fürs Stop-the-bleeding.

Checkliste (Copy/Paste)

  • [ ] Global kill switch (stop new runs)
  • [ ] Per-tenant kill switch (surgical stop)
  • [ ] Enforced in tool gateway (stops side effects)
  • [ ] Disable writes mode (read-only degrade)
  • [ ] Tool disable list (e.g., “no browser”)
  • [ ] Audit logs for kill blocks + operator actions
  • [ ] Tested runbook: flip, verify, drain, recover

Sicheres Default-Config-Snippet (JSON/YAML)

YAML
kill_switch:
  global_flag: "agent_kill_global"
  per_tenant_flag_prefix: "agent_kill_tenant:"
  mode_when_enabled: "disable_writes"
  disabled_tools_key: "agent_disabled_tools"
  cache_ttl_s: 2

FAQ (3–5)

Soll der Kill Switch alles stoppen oder nur Writes?
Default: Writes deaktivieren. ‚Stop everything‘ ist die nukleare Option, wenn du der Loop gar nicht vertraust.
Wo enforce ich den Kill Switch?
Im Tool Gateway und in der Run Loop. Wenn Tool Calls nicht geblockt werden, ist es nicht echt.
Kann ich Kill State cachen?
Ja, aber TTL in Sekunden. Incidents sind Sekunden, nicht Minuten.
Brauche ich per-tenant Kill Switches?
Wenn du multi-tenant bist: ja. Sonst wird ein Kunden-Incident zum Outage für alle.

Q: Soll der Kill Switch alles stoppen oder nur Writes?
A: Default: Writes deaktivieren. „Stop everything“ ist die nukleare Option, wenn du der Loop gar nicht vertraust.

Q: Wo enforce ich den Kill Switch?
A: Im Tool Gateway und in der Run Loop. Wenn Tool Calls nicht geblockt werden, ist es nicht echt.

Q: Kann ich Kill State cachen?
A: Ja, aber TTL in Sekunden. Incidents sind Sekunden, nicht Minuten.

Q: Brauche ich per-tenant Kill Switches?
A: Wenn du multi-tenant bist: ja. Sonst wird ein Kunden-Incident zum Outage für alle.

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱️ 6 Min. LesezeitAktualisiert Mär, 2026Schwierigkeit: ★★★
In OnceOnly umsetzen
Budgets + permissions you can enforce at the boundary.
In OnceOnly nutzen
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.
Autor

Diese Dokumentation wird von Engineers kuratiert und gepflegt, die AI-Agenten in der Produktion betreiben.

Die Inhalte sind KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Patterns und Empfehlungen basieren auf Post-Mortems, Failure-Modes und operativen Incidents in produktiven Systemen, auch bei der Entwicklung und dem Betrieb von Governance-Infrastruktur für Agenten bei OnceOnly.