An undo button for AI agents: snapshot before every risky step
Before an agent runs a step that could break its machine, save a copy; if the step fails, carry on from the copy.
Runtime (withruntime.com) gives every agent sandbox a real undo: a fork copies the running machine, memory and processes included, in 2.96 s on Runtime's servers, and a sandbox started from a snapshot is running 440 ms after the request. Agents make mistakes the way junior engineers do, only faster and without flinching. This post shows how to put a checkpoint in front of every step that can do damage, which kind of checkpoint each step needs, what it costs, and the side effects no snapshot can take back.
Why does an agent need an undo button?
Because an agent learns what a command does by running it. A person reads
git reset --hard and hesitates; a model emits it because the last error
message suggested it. On a laptop that costs you an afternoon. In a sandbox
it costs you the agent's progress: the cloned repository, the installed
dependencies, the half-finished refactor and the dev server it took twenty
tool calls to get running.
Retrying from scratch is the usual answer, and it is expensive in the wrong currency. The tokens the agent spent getting to step 30 are gone, and the model may not find the same path twice. An undo that returns the machine to step 29, exactly as it was, keeps the work and lets the agent try something else.
Three properties make an undo worth having:
- It is cheap enough to take before every risky step, not once a session.
- It restores everything the step could have touched: files, installed packages, a database running in the sandbox, and processes in memory.
- It contains the step while it runs, so a bad step cannot reach outside the machine before you have decided to keep it.
Which steps are risky?
A step is risky when it changes something you cannot rebuild by reading the transcript. Sort them by what they can break:
| What the step does | Examples | What it can break |
|---|---|---|
| Edits or deletes files in the project | sed -i, rm, git checkout ., a code generator |
The working tree |
| Changes dependencies | npm install, pip install -U, uv lock --upgrade |
The working tree and the lockfile |
| Changes the system | sudo apt-get install, editing /etc, systemctl |
The whole machine |
| Changes state in a running process or database | a migration against Postgres in the sandbox, redis-cli |
Data and memory |
| Reaches outside the sandbox | git push, an HTTP POST, sending mail |
Someone else's system |
The first two only touch files, and git can undo them in milliseconds. The next two need the whole machine saved. The last one cannot be undone by any snapshot, and needs a different defense, covered below.
Which checkpoint fits which step?
Use the lightest checkpoint that covers everything the step can touch. Runtime offers three machine-level ones beside plain git:
| Checkpoint | What it keeps | Time to take | Time to go back | Cost while kept |
|---|---|---|---|---|
| git commit in the repo | Tracked and untracked files in one folder | Milliseconds | Milliseconds | Nothing |
| Disk snapshot | The root filesystem, no processes | Seconds | A fresh boot from that disk | $0.08 per GB a month, stored bytes |
| Memory snapshot | Files, memory and running processes | 2.54 s | 440 ms to running | $0.08 per GB a month, stored bytes |
| Fork | A second live copy; the original is untouched | 2.96 s | None: discard the copy | The copy's running time; its own snapshot is free |
The fork is the one that changes how you write the agent. Instead of saving a checkpoint and restoring it after a failure, you run the risky step in a copy and keep the copy only if it passed. The original never saw the step, so there is nothing to restore. Copies keep the source's size and are billed as a create of that size would be, and the snapshot a fork takes for itself is deleted when the fork ends and never billed (forks).
How do you run a risky step in a copy?
Fork, run the step and its check in the copy, then keep whichever sandbox is right. This version also cuts the copy off from the internet while the step runs, so whatever the step does stays inside the copy:
TypeScriptimport type { Sandbox } from "withruntime";type Outcome = { kept: Sandbox; passed: boolean; log: string };/** Runs `step` in a copy of `sbx` and keeps the copy only if `verify` exits 0. * With `offline`, the copy cannot reach the internet until it is kept. */export async function tryInCopy( sbx: Sandbox, step: string, verify: string, { offline = true, cwd = "/workspace/repo" } = {},): Promise<Outcome> { const rules = await sbx.network.get(); const copy = await sbx.fork({ labels: { role: "trial-step" } }); if (offline) await copy.network.off(); const ran = await copy.exec(step, { cwd, timeoutMs: 600_000 }); const checked = ran.exitCode === 0 ? await copy.exec(verify, { cwd, timeoutMs: 600_000 }) : ran; const log = `${ran.stdout}${ran.stderr}${checked === ran ? "" : checked.stdout + checked.stderr}`; if (checked.exitCode !== 0) { await copy.delete(); // the original never saw the step return { kept: sbx, passed: false, log }; } const { internet, allow, deny, connect } = rules; // put the original's rules back await copy.network.set({ internet, allow, deny, ...(connect.length ? { connect } : {}) }); await sbx.delete(); // the copy is the agent's machine from now on return { kept: copy, passed: true, log };}Pythonfrom withruntime import Sandboxdef try_in_copy(sbx: Sandbox, step: str, verify: str, offline: bool = True, cwd: str = "/workspace/repo") -> tuple[Sandbox, bool, str]: """Runs `step` in a copy of `sbx` and keeps the copy only if `verify` exits 0.""" rules = sbx.network.get() copy = sbx.fork(labels={"role": "trial-step"}) if offline: copy.network.off() ran = copy.exec(step, cwd=cwd, timeout_ms=600_000) checked = copy.exec(verify, cwd=cwd, timeout_ms=600_000) if ran.exit_code == 0 else ran log = ran.stdout + ran.stderr + ("" if checked is ran else checked.stdout + checked.stderr) if checked.exit_code != 0: copy.delete() # the original never saw the step return sbx, False, log copy.network.set(internet=rules["internet"], allow=rules["allow"], deny=rules["deny"], connect=rules["connect"] or None) # put the original's rules back sbx.delete() # the copy is the agent's machine from now on return copy, True, logHand the agent's tool layer the kept sandbox after every call, and give the
log back to the model either way. A failed step then reads, to the model,
like any failed command, except that nothing it did is still on the machine.
Three details matter in practice:
verifyis what makes this an undo and not a gamble. Use the project's test command, a type check, orcurl -f localhost:3000/healthfor a dev server. A step that exits 0 and breaks the build is still a bad step.- Turn
offlineoff for steps that must download, such asnpm install. Narrow the copy's network to the registry instead withcopy.network.set({ internet: true, allow: ["registry.npmjs.org"] }). - The copy has its own id. Anything that stored the old id, such as a preview link you shared, needs the new one. Name sandboxes and look them up by name if that matters (find a sandbox by name).
How do you checkpoint files alone, in milliseconds?
Commit them. Most risky steps an agent takes only touch the project folder, and a git commit inside the sandbox saves that in milliseconds at no cost. This guard classifies each command, commits before the risky ones, and resets when one fails:
TypeScriptimport { Sandbox } from "withruntime";const REPO = "/workspace/repo";// Steps that change the machine need a fork; steps that change files need a commit.const MACHINE = [/\bapt(-get)?\b/, /\bdpkg\b/, /\bsystemctl\b/, /\bmigrate\b/, /\/etc\//];const FILES = [ /\brm\b/, /\bmv\b/, /\bsed -i\b/, /\bgit (reset|checkout|rebase|clean)\b/, /\b(npm|pnpm|pip|uv) (install|add|remove|update|upgrade)\b/,];function checkpointFor(command: string): "fork" | "git" | "none" { if (MACHINE.some((p) => p.test(command))) return "fork"; return FILES.some((p) => p.test(command)) ? "git" : "none";}await using sbx = await Sandbox.create();await sbx.exec( `git init -q ${REPO} && git -C ${REPO} config user.email agent@example.com && git -C ${REPO} config user.name agent`,);async function guarded(command: string) { const kind = checkpointFor(command); if (kind === "git") await sbx.exec(`git add -A && git commit -q --allow-empty -m "before: ${kind}"`, { cwd: REPO }); const result = await sbx.exec(command, { cwd: REPO, timeoutMs: 300_000 }); if (result.exitCode !== 0 && kind === "git") await sbx.exec("git reset -q --hard HEAD && git clean -fdq", { cwd: REPO }); console.log(kind, result.exitCode === 0 ? "kept" : "rolled back", command); return result;}await guarded("sed -i 's/var /let /g' index.js");await guarded("ls");Pythonfrom withruntime import Sandboximport reREPO = "/workspace/repo"MACHINE = [r"\bapt(-get)?\b", r"\bdpkg\b", r"\bsystemctl\b", r"\bmigrate\b", r"/etc/"]FILES = [r"\brm\b", r"\bmv\b", r"\bsed -i\b", r"\bgit (reset|checkout|rebase|clean)\b", r"\b(npm|pnpm|pip|uv) (install|add|remove|update|upgrade)\b"]def checkpoint_for(command: str) -> str: if any(re.search(p, command) for p in MACHINE): return "fork" return "git" if any(re.search(p, command) for p in FILES) else "none"with Sandbox.create() as sbx: sbx.exec(f"git init -q {REPO} && git -C {REPO} config user.email agent@example.com" f" && git -C {REPO} config user.name agent") def guarded(command: str): kind = checkpoint_for(command) if kind == "git": sbx.exec('git add -A && git commit -q --allow-empty -m "before step"', cwd=REPO) result = sbx.exec(command, cwd=REPO, timeout_ms=300_000) if result.exit_code != 0 and kind == "git": sbx.exec("git reset -q --hard HEAD && git clean -fdq", cwd=REPO) print(kind, "kept" if result.exit_code == 0 else "rolled back", command) return result guarded("sed -i 's/var /let /g' index.js") guarded("ls")When checkpointFor answers fork, call tryInCopy from the section above
instead. The regular expressions are a starting point, not a security
boundary: an agent can always find a command your list misses, which is why
the sandbox itself, not the list, is what keeps the damage contained.
How do you go back more than one step?
Keep a chain of memory snapshots. A fork answers "should I keep this step?", but sometimes the agent only learns five steps later that step 3 was the mistake. Snapshot after each step that passed, keep each for a day, and start a fresh sandbox from whichever one you want:
TypeScriptimport { Runtime, type Sandbox } from "withruntime";const runtime = new Runtime();const chain: { step: number; snapshotId: string }[] = [];export async function remember(sbx: Sandbox, step: number) { const snap = await sbx.snapshot({ name: `step-${step}`, retentionDays: 1 }); chain.push({ step, snapshotId: snap.id });}export async function goBackTo(step: number, current: Sandbox) { const saved = chain.find((c) => c.step === step); if (!saved) throw new Error(`no checkpoint for step ${step}`); const restored = await runtime.sandboxes.create({ snapshot: saved.snapshotId }); await current.delete(); return restored; // running, with the memory and processes of that moment}A snapshot pauses the source while it is captured and wakes it before the call returns. Take one after a step, not before every command, and delete the chain when the task ends. A snapshot of a fresh sandbox is ready in 2.54 s on Runtime's servers, and longer the more memory the machine holds (how to snapshot a sandbox).
What does an undo cost?
Very little, because the expensive part of a sandbox is time running, and both kinds of checkpoint keep that short. Take a 2 vCPU, 4 GiB agent sandbox.
A fork for a three-minute risky step that keeps one core busy: the copy runs for three minutes, and its own snapshot is not billed.
TextCPU: 180 CPU-seconds / 3,600 × $0.025 = $0.00125Memory: 4 GiB × 3 min / 60 × $0.0075 = $0.00150Total: $0.00275A chain of ten memory snapshots, each storing 2 GB of its own and kept for one day:
TextStorage: 10 × 2 GB × $0.08 per GB-month / 30 days = $0.0533Both are rounding errors next to the model tokens a lost session would cost to redo. CPU is billed on what the copy uses at $0.025 per vCPU-hour, and memory at $0.0075 per GiB-hour (pricing).
What can a snapshot never undo?
Anything that left the machine. A snapshot rolls back the sandbox; it cannot recall an email, unpush a branch or reverse a payment. Plan for these separately:
| Side effect | Defense |
|---|---|
git push to a shared remote |
Push only after verify passes, from the kept sandbox, and to a branch rather than main |
| Calls to a third-party API | Run the step offline in a copy; give the API key as a secret whose rules allow only GET |
| Writes to a database outside | Read-only credentials for exploration, and a separate step a person approves for writes |
| Spending money on your cloud | A daily spending limit on the agent's key (set one) |
The pattern is the same in every row: let the agent do anything inside the copy, and make every door out of it narrow and deliberate. Runtime enforces a sandbox's network rules on the host, outside the guest, so root inside the sandbox cannot turn them off (the sandbox environment).
One limit to plan around: a sandbox with a volume attached cannot be snapshotted, so keep work you may want to undo on the sandbox's own disk, and use volumes for caches and data you are happy to keep whatever happens.
In short
- Save before any step that changes the system, the dependencies or a running database; a git commit is enough for steps that only touch project files.
- Running the step in a fork is better than saving and restoring: the original never sees a bad step, and the copy's own snapshot costs nothing.
- Always pair a step with a check, such as tests or a health probe, and keep the result only when the check passes.
- No snapshot recalls what left the machine; run risky steps offline and give keys only the methods they need.
Run it on Runtime
Runtime costs 42% to 88% less than fourteen other sandbox providers
for an agent that mostly waits on a model (compare costs).
Every Runtime sandbox is a Firecracker microVM that can be forked with its
memory and processes in 2.96 s or snapshotted and started again in
440 ms, measured on Runtime's servers (speed). Every account gets 100 hours of a 2 vCPU, 4 GB sandbox included every month with no card at
withruntime.com/sign-in; try tryInCopy on the next step your agent
is about to regret.