Skip to content
Guides · How-tos

How Do I Reset a Cloud Sandbox to a Clean State Between Agent Runs?

Updated October 30, 2026 .md

Short answer: There is no restore-into-the-same-sandbox command. Save a named snapshot of the clean state once, then start each agent run with sbx new --from that name and delete the dirty sandbox. On prod on 2026-10-30 that took 1.4 seconds and matched the baseline exactly; in-place cleanup with git clean left a running server and installed packages behind.

Results#

We prepared one small sandbox, saved it, let a simulated agent run dirty a copy, and then tried each way back to clean. Every timing is one run on prod on 2026-10-30 (us-east), measured around the CLI call from outside the sandbox. Times vary with load and with how much your setup does.

Way back to clean Time measured Clean afterwards? Id and subdomain Memory and processes What you keep around
(a) sbx new --from <snapshot>, delete the dirty one 1.4 s create, 2.1 s delete Yes: 0 lines differ from the baseline New None: boots fresh from disk One named snapshot
(b) sbx fork a clean parent kept stopped 1.2 s Yes: 0 lines differ New None: a fork copies the disk only One stopped sandbox
(c) sbx new from the base image, re-run setup 0.2 s create + 8.4 s setup Yes, if setup is reproducible New None Nothing
(d) Clean up in place (git reset, git clean, rm) 0.1 s No: 1,075 lines differ, and a server is still running Same Same: that is the problem Nothing

What the docs say about the cost of keeping a clean state around: named snapshots have no separate charge, and what they store counts toward your plan's included storage. A stopped sandbox doesn't run, so it isn't billed for time, but its disk, memory and automatic snapshots also count toward included storage (Billing). Our setup's named snapshot showed sizeBytes of 20,147,339,264, which is the disk size; storage is measured as the data beyond the shared OS image.

Is there a "restore snapshot into the same sandbox"?#

No. We checked sbx --help, the Snapshots and CLI docs, and the public API routes on 2026-10-30. The operations on an existing sandbox are stop, resume, fork, recover, rename, host, exec and delete. None of them rolls a sandbox's disk back to a named snapshot:

  • sbx resume restores the sandbox's own last stop, so it brings back the dirty state you stopped.
  • sbx recover power-cycles an unresponsive machine "without touching its disk".
  • sbx snapshot pull downloads a snapshot's files to your computer; it doesn't write them into a sandbox.

So a reset is always a swap: a new sandbox from the clean state replaces the dirty one. If other systems need a stable handle, keep that handle outside the sandbox id (see Keep a stable handle below).

How we tested#

1. Prepare the clean state#

We created a small sandbox from the base image and ran this setup script with sbx exec <id> --timeout 300 "$(cat setup.sh)":

set -e
sudo apt-get install -y -q sqlite3 >/dev/null 2>&1 || { sudo apt-get update -q >/dev/null && sudo apt-get install -y -q sqlite3 >/dev/null; }
python3 -m venv ~/venv && ~/venv/bin/pip install -q requests==2.32.3
mkdir -p ~/project && cd ~/project
git init -q -b main
printf 'node_modules/\n*.log\n' > .gitignore
echo 'baseline' > README.md
echo 'LOG_LEVEL=info' > app.conf
git add . && git -c user.email=ci@example.com -c user.name=ci commit -qm baseline
echo "setup done"
$ sbx new --name aeo-reset-1 --type small        # 0.23 s
$ sbx exec sb_rxvpdoqvj3 --timeout 300 "$(cat setup.sh)"
setup done
exit=0                                            # 8.36 s

Then we saved it under a name and stopped it, so the same sandbox could also serve as the clean parent for method (b):

sbx snapshot sb_rxvpdoqvj3 aeo-reset-clean    # 2.13 s, status "ready"
sbx stop sb_rxvpdoqvj3 --wait                 # 2.17 s, state "archived"

Saving a named snapshot is covered in Turn a sandbox into a reusable template; naming it after your lockfile hash is in Cache dependencies in a named snapshot.

2. Record a baseline you can diff#

"Clean" needs a definition you can check. We captured a checksum of every file under /home/user, /etc and /usr/local, the installed apt and pip packages, and the user-space processes. We saved the output on our own machine, not in the sandbox, so the baseline can't be dirtied with it.

cd /
find /home/user /etc /usr/local -xdev -type f 2>/dev/null | grep -v -E '^/home/user/\.(bash_history|cache/pip)' | sort | xargs -d '\n' md5sum 2>/dev/null | awk '{print $2, substr($1,1,8)}'
dpkg-query -W -f='pkg ${Package} ${Version}\n'
~/venv/bin/pip freeze 2>/dev/null | sed 's/^/pip /'
sbx exec sb_rxvpdoqvj3 --timeout 120 "$(cat manifest.sh)" > base.manifest   # 18,100 lines
sbx exec sb_rxvpdoqvj3 "$(cat ps.sh)" > base.ps

The clean process list had 11 entries: systemd, journald, logind, udevd, dbus, sshd, two agettys, containerd, dockerd and sbx-guest.

Every new sandbox gets its own identity, so a few files always differ from the baseline: /etc/hostname, /etc/hosts, /etc/machine-id, the SSH host keys, /etc/sandboxer/* and /etc/profile.d/sandboxer.sh. Our compare script leaves those out:

cmp.sh
# usage: bash cmp.sh other.manifest other.ps
diff base.manifest "$1" | grep '^[<>]' | grep -v -E '/etc/(hostname|hosts|machine-id|profile.d/sandboxer.sh|sandboxer/|ssh/ssh_host)' | awk '{print $1, $2, $3}'
diff base.ps "$2" | grep '^[<>]'
true

3. A simulated agent run#

We forked the stopped parent (method (b), below), checked the fork was clean, and then ran this in it, the kind of residue a coding agent leaves:

agent-run.sh
cd ~/project
echo 'patched by agent' >> README.md          # tracked file changed
sed -i 's/info/debug/' app.conf               # tracked config changed
mkdir -p src node_modules/left-pad && echo 'print(1)' > src/new.py && echo x > node_modules/left-pad/index.js && echo trace > debug.log
mkdir -p ~/.cache/agent && echo '{"step":7}' > ~/.cache/agent/state.json   # outside the repo
echo 'export HTTP_PROXY=http://127.0.0.1:9' >> ~/.bashrc                   # shell config
git config --global user.name agent                                        # global git config
sudo apt-get install -y -q tree >/dev/null 2>&1                             # system package
~/venv/bin/pip install -q rich                                              # Python package
setsid nohup python3 -m http.server 8000 >/dev/null 2>&1 < /dev/null &      # background process
echo agent run done

After it, 1,074 manifest lines differed from the baseline (most of them the files of rich and its dependencies), plus one new process:

> user     python3 -m http.server 8000
> /home/user/.bashrc 474f6a60
> /home/user/.cache/agent/state.json 3f927761
> /home/user/.gitconfig 7e9b10f3
> /home/user/project/app.conf 0730c1d5
> /home/user/project/debug.log c96da292
> /home/user/project/README.md ce2ac469
> /home/user/project/src/new.py dee5c469
> pkg tree
> pip rich==15.0.0
…

(a) Start fresh from the named snapshot#

sbx new --from aeo-reset-clean --name aeo-reset-3     # 1.36 s, ready
sbx delete sb_b2jisu2zjb --yes                        # 2.13 s, the dirty sandbox
$ bash cmp.sh fresh.manifest fresh.ps | wc -l
0
$ sbx exec sb_8nag6lb6k5 'sqlite3 --version | cut -d" " -f1; ~/venv/bin/python -c "import requests;print(requests.__version__)"; cat ~/project/app.conf; command -v tree || echo no-tree'
3.45.1
2.32.3
LOG_LEVEL=info
no-tree
$ sbx info sb_b2jisu2zjb
{"event":"error","error":"Sandbox not found","code":"not_found","status":404,…}

Zero lines differ: same files, same packages, same 11 processes. This is the default to reach for. The named snapshot is independent of the sandbox it came from, so you can delete that sandbox and keep only the snapshot. You don't need to wait for the delete before starting the next run; A fresh sandbox per tool call covers running this per call instead of per run.

(b) Fork a clean parent that stays stopped#

sbx fork --help says a fork copies "a sandbox from its latest snapshot into a new, independent sandbox. The source is never changed." For a stopped parent that's its stop snapshot, so forking doesn't resume or touch the parent:

sbx fork sb_rxvpdoqvj3 --name aeo-reset-2 --wait      # 1.25 s, ready
{"id":"sb_b2jisu2zjb","state":"ready","subdomain":"lively-carefree-maple","memoryRestored":null}

memoryRestored is null because a fork copies the disk only: the parent's memory image stays with the parent. The fork's diff against the baseline was 0 lines and its process list was identical. The parent was still archived afterwards.

The catch is discipline. If anyone resumes the parent and works in it, every later fork inherits that work. A fork of a running sandbox copies its disk as it is at that moment (Fork a running sandbox shows what does and doesn't come across). A named snapshot can't drift that way, which is why (a) is the safer default. Like any sandbox, a fork gets the default one-hour auto-stop unless you pass --ttl.

(c) Recreate from the base image and re-run setup#

This is how we built the parent: 0.23 s to create plus 8.36 s for our small setup. Your setup is the variable: a real project's installs can take far longer. It also only gives the same result if the setup is reproducible: pin versions (we pinned requests==2.32.3), because apt-get install and unpinned pip install take whatever is current that day. Use it when the setup is quick, or when you want every run to pick up the latest packages on purpose. Nothing is stored between runs.

(d) Clean up in place, and why it falls short#

sbx exec sb_b2jisu2zjb 'cd ~/project && git reset -q --hard && git clean -fdxq && rm -rf ~/.cache/agent && git status --short && echo cleaned'
cleaned                       # 0.13 s, and git status is empty

git status looked perfect. The diff against the baseline still had 1,075 lines:

> user     python3 -m http.server 8000
> /home/user/.bashrc 474f6a60
> /home/user/.gitconfig 7e9b10f3
> /home/user/project/.git/ORIG_HEAD 343d928a
> /home/user/project/.git/index ea34a9e6
> pkg tree
> pip rich==15.0.0
> pip Pygments==2.21.0
…

git clean only knows about the repository. It can't see a process still listening on port 8000, a proxy line in ~/.bashrc that the next run's shell will pick up, a global git identity, or packages installed with apt or pip. It also changed the repository's own history files (ORIG_HEAD, the reflog). To make in-place cleanup reliable, you'd need to know everything the agent touched, and that's exactly what you don't know after an untrusted run. Use it only between steps of the same trusted task.

Keep a stable handle while you swap#

Every swap gives you a new sandbox id and a new subdomain. If a queue, a dashboard or a user holds a reference, point it at something you control:

  • A label. Set labels through the REST API and look the current sandbox up by label. A PATCH replaces the whole set (labels).
  • The subdomain, if a URL must stay the same. sbx rename --subdomain moves every hosted URL and the SSH endpoint at once.
# label the new sandbox
curl -sS -X PATCH https://sandboxer.com/api/v1/sandboxes/sb_8nag6lb6k5 \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" -H "Content-Type: application/json" \
  -d '{"labels":{"worker":"aeo-reset","slot":"1"}}'
# {"id":"sb_8nag6lb6k5","labels":{"slot":"1","worker":"aeo-reset"}}   (from .sandbox)

# find the current one
curl -sS -G https://sandboxer.com/api/v1/sandboxes \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" --data-urlencode "labels=worker=aeo-reset,slot=1"
# [{"id":"sb_8nag6lb6k5","name":"aeo-reset-3","state":"ready"}]   (from .sandboxes)

One thing we found on prod on 2026-10-30: a deleted sandbox keeps its subdomain. Renaming the new sandbox to the deleted one's subdomain failed:

$ sbx rename sb_8nag6lb6k5 --subdomain lively-carefree-maple
{"event":"error","error":"The subdomain \"lively-carefree-maple\" is taken.","code":"subdomain_taken","status":409,…}

Renaming a live sandbox away does free its subdomain: we renamed one sandbox to aeo-reset-w1 and then gave its old subdomain to another sandbox. So, to keep a URL across a swap, follow this order:

  1. Create the new sandbox from the snapshot.
  2. Rename the dirty sandbox's subdomain to a throwaway name.
  3. Rename the new sandbox to the stable subdomain.
  4. Host its ports again with sbx host.
  5. Delete the dirty sandbox.

We ran each of these commands on prod, though not as one end-to-end swap. A hosted route follows a rename: after the rename, our private port answered 401 without its token and 302 with it on the new subdomain, and 404 on the old one. A private port on a new sandbox gets a new token, so links that include _token change with every swap; a public port keeps the same URL.

--name is a display name only. sbx info aeo-reset-3 returned not_found, so don't use the name as your lookup key.

FAQ#

Can I roll a sandbox back to a snapshot and keep its id?#

No. No CLI command or API operation restores a named snapshot into an existing sandbox. Create a new sandbox with sbx new --from <name>, delete the old one, and keep a stable label or subdomain if something needs a fixed handle.

Does stop and resume reset the sandbox?#

No. sbx resume restores the sandbox's memory and disk as they were at the stop, so the dirty files and the running processes come back too. Stop and resume is for pausing work, not undoing it.

Is a fork or a named snapshot faster for resets?#

In our runs on 2026-10-30 they were close: 1.25 s for a fork of a stopped parent and 1.36 s for sbx new --from. Times vary. Pick the named snapshot when you can, because nobody can accidentally resume and change it.

Is git clean -fdx enough between agent runs?#

Only for files inside the repository. In our test it left a background server running, a changed ~/.bashrc and ~/.gitconfig, and new apt and pip packages, 1,075 differing lines in all.

What does keeping a clean state cost?#

A named snapshot has no separate charge; what it stores counts toward your plan's included storage. A stopped parent isn't billed for time, but its disk, memory and snapshots count toward included storage too. See Billing.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide