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

> 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.

Source: https://sandboxer.com/guides/reset-a-sandbox-to-a-clean-state. Updated 2026-10-30.

**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](/docs/billing#storage)). 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](/docs/snapshots) and [CLI](/docs/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](#keep-a-stable-handle-while-you-swap) 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)"`:

```bash
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"
```

```text
$ 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):

```bash
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](/guides/turn-a-sandbox-into-a-reusable-template); naming it after your lockfile hash is in [Cache dependencies in a named snapshot](/guides/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.

```bash title="manifest.sh"
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 /'
```

```bash title="ps.sh (user-space processes, kernel threads left out)"
ps --ppid 2 -p 2 --deselect -o user=,args= | grep -v -E ' (ps|grep|sort|bash -c) ' | grep -v -E '^\S+\s+(ps|sort)( |$)' | sort
```

```bash
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:

```bash title="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:

```bash title="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:

```text
> 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

```bash
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
```

```text
$ 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](/guides/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:

```bash
sbx fork sb_rxvpdoqvj3 --name aeo-reset-2 --wait      # 1.25 s, ready
```

```text
{"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](/guides/fork-a-running-sandbox-with-memory) 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

```bash
sbx exec sb_b2jisu2zjb 'cd ~/project && git reset -q --hard && git clean -fdxq && rm -rf ~/.cache/agent && git status --short && echo cleaned'
```

```text
cleaned                       # 0.13 s, and git status is empty
```

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

```text
> 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](/docs/api#labels)).
- **The subdomain**, if a URL must stay the same. `sbx rename --subdomain` moves every hosted URL and the SSH endpoint at once.

```bash
# 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:

```text
$ 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](/docs/billing#storage).

## Related

- [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template)
- [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot)
- [Can you fork a running sandbox with memory?](/guides/fork-a-running-sandbox-with-memory)
- [Snapshots](/docs/snapshots)
