If you read one line: A kill switch is a layer outside your model that every agent action must pass through, checks each action against rules the model can't argue with, and can halt the agent instantly — including from an external flag you flip while it's already running. It's about forty lines of Python, it wraps your tools rather than your framework, and you should ship it before you spend a dollar on a commercial control plane.
The runtime-control-plane category arrived this month — Microsoft's open-source toolkit, Netzilo, Alterion's Draco. All three sell the same primitive dressed in enterprise features. The primitive itself is small enough to build in an afternoon, and if you run one or two agents, that's exactly what you should do first.
What a kill switch actually is#
Three things, in order. An interception point: one function that every tool call passes through, so there's a single place to say no. A policy check: rules evaluated on the actual action — its name, its arguments, its running cost — not on the model's stated intentions. And an out-of-band stop: a way to halt the agent that does not depend on the model deciding to stop, because a looping or hijacked agent never will. Miss any one of the three and you don't have a kill switch; you have a suggestion. Everything below builds exactly these three.
Step 1: Route every action through one chokepoint#
The foundation is boring and non-negotiable: your agent must not call tools directly. It calls one function — guard() — that dispatches to the real tool only after policy passes. This is the interception point the whole thing hinges on, and it's why you wrap the tools instead of the framework: any agent loop, in any library, ultimately calls named functions with arguments, so a single dispatcher covers all of them. Register your real tools in a dict and force every call through the gate.
# killswitch.py — the chokepoint every action passes through
class Halt(Exception):
"""Raised to stop the agent immediately and unignorably."""
TOOLS = {} # name -> callable
def tool(fn):
TOOLS[fn.__name__] = fn
return fn
def guard(name, **args):
fn = TOOLS.get(name)
if fn is None:
raise Halt(f"unknown tool: {name}")
check_policy(name, args) # Step 2 — may raise Halt
check_tripwire_and_stop(name) # Step 3 — may raise Halt
return fn(**args)
Now the only way an action reaches the world is through guard(). That single fact is what makes the next two steps enforceable rather than advisory.
Step 2: Write policies the model can't talk you out of#
Policies operate on the concrete action, never on the model's explanation of it. That distinction is the entire security value: a prompt-injected agent will happily narrate good intentions while calling delete_prod_db, so you judge the call, not the story around it. Keep policies as plain, auditable Python predicates — an allowlist of tools, hard limits on arguments, and a running spend counter that trips before, not after, you overspend. Deny by default; a tool that isn't explicitly allowed doesn't run.
# policy.py — deny-by-default rules on the real action
STATE = {"spent": 0.0, "calls": 0}
ALLOWED = {"search_web", "read_file", "send_email"}
BUDGET_USD = 5.00
COST = {"send_email": 0.00, "search_web": 0.01, "read_file": 0.00}
def check_policy(name, args):
if name not in ALLOWED:
raise Halt(f"tool '{name}' not on allowlist")
if name == "read_file" and ".." in str(args.get("path", "")):
raise Halt("path traversal blocked")
STATE["spent"] += COST.get(name, 0.05)
if STATE["spent"] > BUDGET_USD:
raise Halt(f"budget exceeded: ${STATE['spent']:.2f}")
These rules are deterministic and testable — you can unit-test that delete_everything never runs and that the 501st cent trips the budget. That is the part a system prompt can never promise.
Step 3: Add the tripwire and the external stop#
The final piece is the halt that doesn't need the model's permission. Two halves. A tripwire you set from your own code — a hard cap on total actions so a loop can't run forever. And an external stop: a flag outside the process — the simplest reliable version is a file on disk — that you (or a webhook, or a teammate) can flip to kill a run that's already misbehaving. Checking a file every action costs microseconds and buys you a stop you can pull without redeploying or waiting for the agent to cooperate.
# stop.py — halts that don't depend on the model
import os
MAX_ACTIONS = 40
STOP_FILE = "/tmp/agent.stop"
def check_tripwire_and_stop(name):
STATE["calls"] += 1
if STATE["calls"] > MAX_ACTIONS:
raise Halt(f"tripwire: >{MAX_ACTIONS} actions")
if os.path.exists(STOP_FILE):
raise Halt("external stop flag set")
To kill any run instantly from anywhere: touch /tmp/agent.stop. The next action the agent attempts raises Halt and the loop is over — no redeploy, no waiting for the model to notice.
Step 4: Catch the halt at the top of your loop#
The guard raises Halt; something has to catch it and end the run cleanly. Put one try/except Halt around your agent's outer loop so that any blocked action — budget, allowlist, tripwire, or external flag — tears down the whole run instead of letting the agent retry the forbidden thing. Log the reason, release credentials if you can, and exit. This is the difference between a stopped agent and an agent that hits a wall and immediately tries again.
def run_agent(task):
try:
while not done(task):
name, args = model_decides_next_action(task)
result = guard(name, **args) # the ONLY path to a tool
task = advance(task, result)
except Halt as h:
log.warning("AGENT KILLED: %s", h)
revoke_credentials() # optional but recommended
return {"stopped": True, "reason": str(h)}
That's the whole primitive: interception, policy, out-of-band stop, clean teardown. Forty-ish lines, framework-agnostic, testable.
When to graduate to a commercial plane#
Build this first — but know its ceiling. The DIY switch protects one process you control. The moment you're running many agents across Bedrock, Vertex, LangGraph, and a couple of SaaS harnesses, you need a central place to write policy once, an audit trail auditors will accept, and kill switches you didn't have to write per-harness. That's what the runtime-control category sells: Microsoft's open-source toolkit for the reference shape, Netzilo for cross-harness governance, Alterion's Draco for turnkey compliance (SOC 2, ISO 42001, EU AI Act). Study Microsoft's MIT-licensed toolkit to see how a production interceptor is structured — then decide build-vs-buy with the primitive already running. The mistake isn't DIY-ing too long; it's shipping an autonomous agent with no stop at all.



