Reliability

Kill Switches for AI Agents: Design Patterns That Actually Stop a Runaway Process

Akshay Sarode 8 min read
Direct answer

SIGTERM only asks a process to stop; it doesn't force it, and a kill switch that just sends SIGTERM and calls it done isn't really a kill switch. A working one needs a short grace period where SIGTERM gets a real chance, a hard SIGKILL fallback once that window closes, coverage for any subprocess the agent spawned, and a master switch that still works if the web dashboard is unreachable. None of it rolls back a file the agent already flushed to disk.

I've watched a dashboard say "Stopping..." for forty seconds while the process underneath kept writing to a file on disk. The agent had shelled out to run a build step, the step was blocked on a slow write, and the SIGTERM meant to end the whole thing landed on a process with no handler for it and a scheduler that hadn't gotten around to it yet. Nothing was actually broken. That's just what SIGTERM is: a queued request a healthy process usually honors within milliseconds and an unhealthy one can sit on for a while. If your kill switch's entire model of "stopped" is "we sent SIGTERM," you don't have a kill switch. You have a suggestion.

SIGTERM is a request, not a command

Unix gives you two signals worth knowing here. SIGTERM (signal 15) is what a default kill call sends: it lands in the process's signal queue, and the process decides what to do, ignore it, run a handler that closes files and shuts down children first, or take the default action and exit. SIGKILL (signal 9) works differently. The kernel terminates the process directly and no user-space code runs in response, not a handler, not a finally block. There's no negotiation with SIGKILL, and a lot of negotiation with SIGTERM, which is what a coding agent needs when it's mid-write on a file. It's also what lets a wedged agent sit there indefinitely if that's the only tool you built.

A stop button that fires the equivalent of kill <pid> and immediately flips the UI to "stopped" is lying twice: about whether the process is actually gone, since it might still be running or ignoring the signal, and about how much time passed between clicking stop and the machine actually stopping. Both lies matter more for an agent than a script you started by hand, because it might be a few tool calls into a plan that includes writing config or calling an API under a capability grant you gave it ten minutes ago and no longer feel great about.

The four states between running and stopped

Treat "kill this agent" as a small state machine instead of a boolean. There's the state where it's running normally, the state where it's been asked to stop, the state where you're waiting to see if it listens, and the terminal state where it's actually gone. Collapsing all of that into "running" and "not running" is what produces a dashboard stuck on "Stopping..." with nothing underneath it changing.

Running Warned (SIGTERM sent) Grace Period Killed (SIGKILL) stop requested timer starts timeout hit emergency stop: SIGKILL immediately, no grace period
Clean stop walks left to right through all four states. Emergency stop takes the dashed path straight from running to killed.

The side branch matters as much as the main path. A clean stop is the default: send SIGTERM, wait, escalate only if the agent doesn't exit on its own. An emergency stop skips the negotiation and goes straight to SIGKILL, for cases where you want the agent gone right now, for example because it's about to make a network call under a capability grant you're no longer comfortable with.

Walk through the states yourself

Each of the four states looks identical from a distance (a process either shows up in the list or it doesn't) but they carry different guarantees and different ways to go wrong. Click through them.

Try it

What's happening: The agent process is alive and doing whatever it was doing: writing files under its workspace_rw grant, maybe running a subprocess like a test suite or a package install if it holds the subprocess capability. Nothing has been requested yet.

What can still go wrong: Nothing related to killing it, but this is the state where the capability grant matters most. An agent holding network and subprocess capabilities has a wider blast radius to account for later than one scoped to workspace_rw alone, and that scope is set now, not at kill time.

What's happening: SIGTERM has been sent to the agent's process. If its runtime has a signal handler installed, it gets a window to close open file handles, tell any subprocess to wind down, and write a final line to the PTY before exiting on its own terms.

What can still go wrong: SIGTERM only reaches the process it's addressed to. If the agent forked a subprocess and the kill signal only targets the parent, the child keeps running after the parent has already heard the request. A process blocked deep in a syscall, waiting on a slow disk or a hung network call, won't process the signal until it's next scheduled, which from the dashboard looks identical to "cleaning up nicely."

What's happening: A countdown starts, not an indefinite wait. This is what a clean stop actually promises: a short, timeboxed window during which the agent can finish flushing the file it's mid-write on, let its subprocess exit, and call exit on its own.

What can still go wrong: A subprocess that detached from the agent, the way a daemon detaches from the thing that launched it, can get reparented to init and outlive the grace period entirely, invisible to the timer because it's no longer a child of anything the supervisor is watching.

What's happening: The grace period expired without a clean exit, or someone triggered emergency stop directly, so SIGKILL goes to the process, ideally the whole process group. The kernel tears it down unconditionally: no handler runs, no cleanup code executes, it's simply gone.

What can still go wrong: Nothing about the kill itself, SIGKILL is as deterministic as it gets. What's left over is whatever state existed at that exact instant: a half-written file, a lock file nobody released, a request the OS had already sent but the agent never logged as sent.

What happens to the thing it spawned

Capability tokens decide the blast radius before you ever reach the kill switch. An agent can only spawn a subprocess if it holds the subprocess capability, enforced at the OS sandbox level rather than by convention, so an agent running under the macOS Seatbelt profile without that grant simply can't fork a child no matter what it tries. That's the first line of defense: no subprocess capability, no orphan problem to solve later.

Once an agent does hold subprocess rights, the kill has to account for a tree, not a single PID. The pattern that works is putting the agent in its own process group at launch and signaling the group, not just the leader, so both SIGTERM and SIGKILL reach the build tool or linter it shelled out to. Skip that step and killing the parent leaves the child running, unattached to anything the supervisor is watching, still holding a file handle or socket open under a grant that was supposed to end when you clicked stop.

There's a second wrinkle worth naming. A killed agent is a different event from a crashed one. Bernato's restart-on-crash design uses a ceiling with backoff so a persistently failing agent doesn't respawn forever, but that logic exists to handle failures, not to second-guess a person. A user-requested kill needs to be its own outcome in the activity log, distinct from a crash, so the restart supervisor doesn't bring back the exact thing you just told it to stop.

The master switch has to work when the dashboard doesn't

There are two different things people mean by "kill switch," and conflating them is a mistake. One is "stop this specific agent," scoped to a single run. The other is "turn off the sandbox," a system-wide toggle that blocks new runs from starting and stops whatever's in flight, across every machine you own. Bernato logs that second one as its own event type, sandbox-toggled, because the two are different enough decisions to need separate audit trails.

The harder requirement is that the master switch has to work when the normal path to it doesn't. The dashboard and the mobile app both talk to bernatod, the local daemon, over a WebSocket broker the daemon dials out to. That's the right shape for watching a live PTY from your phone, but it makes the dashboard a client, not the authority. If your connection drops, a tab crashes, or your phone has no signal, the daemon is still sitting right there on the machine you own, and the correct move is a command that talks to it directly: a local CLI command or a tray toggle, not a button routed through a broker and a browser first.

This is a local-first argument wearing a kill switch's clothes. If the only way to stop an agent is a cloud service rendering a web page correctly, you've built a system that fails right when you need it most: the moment something's gone wrong enough that you're reaching for the kill switch at all.

Clean stop versus emergency stop

Most of the time you want a clean stop. It gives the agent a real chance to leave the workspace in a state you'd recognize: the current file write finished, the subprocess wound down, a final PTY line explaining itself. Reach for emergency stop when time matters more than tidiness: an agent that's clearly wedged, one acting under a capability grant you regret, or the master sandbox toggle, which can't afford to wait on individual agents to cooperate.

AspectClean stopEmergency stop
Signal sequenceSIGTERM, then SIGKILL only if the grace period expiresSIGKILL immediately, no SIGTERM step
Grace periodYes, a short timeboxed windowNone
Subprocess handlingWhole process group signaled, given time to exit on its ownWhole process group signaled, torn down immediately
Good forRoutine cancellation, the stop button in the live PTY viewA wedged or misbehaving agent, revoking a capability grant right now, the master sandbox toggle
Risk if you pick wrongAn agent ignoring SIGTERM just runs a few extra seconds until the grace period times out anywayWhatever file it was mid-write on is left exactly as it was, no cleanup

FAQ

What's the actual difference between SIGTERM and SIGKILL?

SIGTERM (signal 15) asks a process to exit and lets it run its own cleanup first: close files, shut down a subprocess, log a final message, then exit on its own terms. SIGKILL (signal 9) is enforced by the kernel directly, and the process is terminated with no chance to run any code in response, not a signal handler, not a destructor, nothing. SIGTERM is a conversation. SIGKILL is a fact.

What happens to a subprocess the agent spawned when you kill the parent?

It depends on whether the kill targets the process group or just the parent's PID. If the agent's subprocess capability let it fork a child and the kill only signals the parent, the child keeps running as an orphan, reparented to init, invisible to whatever was tracking the original agent. If the kill signals the whole group instead, the child gets the same SIGTERM or SIGKILL the parent does. An agent with no subprocess capability can't fork anything in the first place, so there's nothing to orphan.

Is an "immediate" kill actually instant?

No, and it's worth being straight about that. SIGKILL delivery at the kernel level is fast, typically low milliseconds, but the process still has to be scheduled off the CPU to actually die. Whatever's showing the status update, a dashboard or a phone screen, adds its own round trip: request to the daemon, daemon signals the process, daemon reports back over the broker, UI updates. Expect well under a second locally, more if you're triggering it remotely.

Do a killed agent's partial file writes get rolled back?

No. This isn't a database transaction with a rollback log, it's a process getting killed. Whatever the agent had already flushed to disk stays exactly as it was the instant the kill landed. The real safety net isn't the kill switch, it's whatever you're using to track the workspace, git being the obvious answer, so a bad partial write is something you diff and revert instead of something the kill switch was going to undo for you.

Should I default to emergency stop just to be safe?

No, default to clean stop. It costs a few seconds and buys the agent a real chance to leave the workspace in a state you'd recognize. Save emergency stop for when you actually need it: an agent that's stopped responding to SIGTERM, one whose current action you want interrupted regardless of consequences, or the master sandbox toggle, which exists for the moment you don't want to wait on anything to cooperate.