Runtime (withruntime.com) gives every agent sandbox a real undo: a fork copies the running machine, memory and processes included, and a snapshot starts again as it was. Agents make mistakes the way junior engineers do, only faster and without hesitation. This post shows how to put a checkpoint before every step that can cause damage. It covers which kind of checkpoint each step needs, what each one costs, and which side effects no snapshot can undo.
Why does an agent need an undo button?
An agent learns what a command does by running it. A person reads
git reset --hard and hesitates. A model runs it because the last error message
suggested it. On a laptop, that mistake costs you an afternoon. In a sandbox, it
costs the agent's progress: the cloned repository, the installed dependencies,
the half-finished refactor, and the dev server that took twenty tool calls to
start.
Retrying from scratch is the usual answer. 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 sandbox to step 29, exactly as it was, keeps the work and lets the agent try something else.
An undo is worth having when it has three properties:
- 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. A bad step cannot reach outside the sandbox before you decide to keep it.
Which steps are risky?
A step is risky when it changes something you cannot rebuild from the transcript. Sort steps 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 change only files, and git can undo them in milliseconds. The next two need the whole machine saved. No snapshot can undo the last one, so it 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 sandbox-level checkpoints, plus 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 new boot from that disk | $0.08 per GB a month, stored bytes |
| Memory snapshot | Files, memory and running processes | Seconds | A new sandbox from it, running | $0.08 per GB a month, stored bytes |
| Fork | A second live copy; the original is untouched | Seconds | None: discard the copy | The copy's running time; its own snapshot is free |
The fork 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. You keep the copy only if the step passed. The original never saw the step, so there is nothing to restore. A copy keeps the source's size and is billed as a create of that size would be. The snapshot a fork takes for itself is deleted when the fork ends, and it is never billed (forks).
How do you run a risky step in a copy?
Fork the sandbox, run the step and its check in the copy, and keep whichever sandbox is right. This version also cuts the copy off from the internet while the step runs. 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, logAfter every call, pass the kept sandbox to the agent's tool layer. Give the
log back to the model either way. A failed step then reads to the model like
any failed command. The difference is that nothing it did remains on the sandbox.
Three details matter in practice:
verifymakes this an undo, 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 still breaks the build is a bad step.- Set
offlineto false 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. If that matters, give sandboxes names and look them up by name (find a sandbox by name).
How do you checkpoint files alone, in milliseconds?
Commit them. Most risky steps only touch the project folder. A git commit inside the sandbox saves that folder 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 returns 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 that your list misses. That is why the
sandbox itself, not the list, keeps the damage contained.
How do you go back more than one step?
Keep a chain of memory snapshots. A fork helps you decide whether to keep this step. But sometimes the agent learns five steps later that step 3 was the mistake. Take a snapshot after each step that passed, keep each one for a day, and start a new sandbox from whichever one you need:
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. Delete the chain when the task ends. The time grows with the memory the sandbox holds (how to snapshot a sandbox).
What does an undo cost?
The cost is very low. The expensive part of a sandbox is the time it runs, and both kinds of checkpoint keep that time short. Take a 2 vCPU, 4 GiB agent sandbox as the example.
A fork for a three-minute risky step that keeps one core busy. The copy runs for three minutes. Its own snapshot is not billed.
- CPU
- 180 CPU-seconds / 3,600 × $0.025
- $0.00125
- Memory
- 4 GiB × 3 min / 60 × $0.0075
- $0.00150
- Total
- $0.00275
A chain of ten memory snapshots, each storing 2 GB of its own and kept for one day:
- Storage
- 10 × 2 GB × $0.08 per GB-month / 30 days
- $0.0533
Both are negligible next to the model tokens that a lost session would cost to redo. CPU is billed on what the copy uses, at $0.025 per vCPU-hour. Memory is billed at $0.0075 per GiB-hour (pricing).
What can a snapshot never undo?
Anything that left the sandbox. A snapshot rolls back the sandbox. It cannot recall an email, unpush a branch, or reverse a payment. Plan for these cases 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) |
Every row follows the same pattern. Let the agent do anything inside the copy. Make every door out of the copy narrow and deliberate. Runtime enforces a sandbox's network rules on the host, outside the guest. 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. Keep work that you may want to undo on the sandbox's own disk. Use volumes for caches and for data that 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 sandbox. 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, or snapshotted and started again. Every
account gets 100 free hours of a 1 GB machine each month, with no card needed, at
withruntime.com/sign-in. Try tryInCopy on the next step your agent is
about to regret.