sandboxReuse: true in the Experiment, and several Attempts share the same Sandbox in turn. Each Sandbox is created only once, and between evals NiceEval restores the working directory to its starting state. Each Attempt still really executes, and still has its own .before() / .after() preparation and cleanup. Unchanged Docker preparation steps are reused directly instead of executing again.
Incus and OpenSandbox use a separate instance for each Attempt and do not support
sandboxReuse or Sandbox reuse within eval groups.
This page covers Experiment-level sandboxReuse. When only a few groups of compatible evals each need their own reuse boundary, use
eval groups: reuse runs serially within a group, while different groups keep running in parallel. An
Experiment that selects eval groups cannot also declare sandboxReuse: true.- Reuse of last results still applies. When an eval’s configuration is the same as last time and it already has
passedorfailed, the old result is reused directly and no Sandbox is created for it. Only Attempts that really need to run this time enter the shared Sandbox. - NiceEval cannot see changes to callback function bodies. When a Sandbox Plugin’s name, instance,
behaviorRevision, declaredidentity, or preparation steps change, old results are invalidated and rerun. When you only changed code inside a callback, updatebehaviorRevisionoridentityas well, or use--rerun all. - Interruption does not roll back external state. Resuming is equivalent to running straight through only when state shared across Attempts can be restored to just after the last completed Attempt. When that is not possible, start over with a clean copy of the state.
- State outside the working directory remains.
$HOME,/tmp, global installs, and background processes all carry over to the next eval. Preparation code must accept this; see “Write preparation code correctly” below. - It cannot be used together with
--keep-sandbox.
--rerun.
Turn it on: three fields in the Experiment
timeoutMs and lifetimeMs are two clocks: the former limits how long one Attempt runs, and the latter limits how long one Sandbox lives. To let a Sandbox live longer, raise lifetimeMs. Do not raise timeoutMs; that also loosens the protection against a stuck Agent.
The upper limit of lifetimeMs depends on your Provider account tier; for example, the E2B free tier is 1 hour. Beyond the limit, creating the Sandbox reports the Provider’s raw error.
Run with pnpm exec niceeval exp <experiment>. After the run, view results in the terminal with pnpm exec niceeval show, or in the browser with pnpm exec niceeval view.
Sandboxes are not shared across terminals
Sandboxes are reused only within oneniceeval exp command. When two terminals run the same experiment at the same time, each has its own Run and Sandboxes, and NiceEval never hands a Sandbox running in one process to another process. Both sides can write results into the same project’s .niceeval/record.sqlite; you do not need to split the result file for this.
To copy or archive .niceeval/record.sqlite, wait until the command finishes successfully. A SQLite file in use by a run cannot be treated as a portable snapshot.
If each Sandbox keeps only its own temporary state, the two sides can run different evals at the same time. When a dynamic .before() callback restores and saves the same external checkpoint, declare a stable key without secrets on the Experiment:
before and Sandbox creation, and releases it only after Sandbox cleanup, Provider finalization, and Experiment after all finish. A run with the same key in another terminal queues and does not create a Sandbox early. After it gets the lease, it continues with its own plan and does not read or reuse the other side’s results.
The key is also part of the result configuration. A different key means a different copy of the state, so old results do not mix in. The lease only protects the external checkpoint and has nothing to do with the result file; runs that do not use sharedState can still write results concurrently. Coordination across machines or different working copies still needs an external distributed lock.
Lifecycle: what runs how many times
After each Attempt, NiceEval runs
git reset --hard and git clean on workdir to restore its starting state, then starts the next. This step only handles workdir; /opt, $HOME, /tmp, global installs, package caches, and background processes do not disappear. Either the eval’s after cleans them up itself, or they are state you deliberately leave for the next eval.
Large persistent build outputs or caches must not grow unchecked. Give them a capacity limit and an early-warning threshold: record normal size and hit rates with facts, warn with diagnostic when nearing the risk threshold, and have a way ready to clean up, rotate, or replace this Sandbox.
Write preparation code correctly: layer by “what it changes with”
- Heavy dependencies every experiment needs (Agent CLIs, language runtimes): bake them into the Provider’s image / template / snapshot, or write them as the lowest-frequency
.before()preparation steps. - Preparation shared by the whole batch (installing toolchains, cloning shared repositories, warming build caches): write them as low-frequency
.before()preparation steps. On Docker, unchanged preparation steps can be reused directly on the next run. These steps are still passed through once per Attempt; you cannot ask for them to run only once per Sandbox. - Materials only this eval needs (its own repository, data, dependencies): put them in the eval’s own
beforeortest(t). They run again after each reset, so they must be code that is correct when repeated. - Background processes started or ports taken: the eval’s
aftercleans them up itself; resettingworkdirdoes not kill processes.
.niceeval-task-clone, and add it to diff.ignore. Before cloning, only run rm -rf .niceeval-task-clone to clear the previous eval’s leftovers. Never delete the .git at the root of workdir: the reset between evals depends on it, and it stays for the whole run.
Idempotence is a hard requirement: one bad, one good
Preparation code at both the Sandbox level and the eval level may hit half-finished state left by a previous run. Compare two ways of writing it:Agent-native Plugins: installation is handled by the Adapter
codexAgent({ plugins: [...] }) and claudeCodeAgent({ plugins: [...] }) work with sandboxReuse. Agent-native Plugins are installed in $HOME and carry over to the next eval, but you do not need to handle this: before each Attempt starts, the Adapter reinstalls them according to the source and ref you declared. Same-named marketplace registrations and plugins left by the previous eval are replaced. When a plugin’s own script rewrites the marketplace registration (for example switching to a hosted source), it is restored the same way.
This refers to native Plugins in the Agent factory. The top-level plugins field of evals, experiments, and eval groups is NiceEval’s Plugin; for how the two divide the work, see Reuse Complete Eval Conditions with Plugins.
Two things are still yours:
beforescripts must be safe to repeat. They run again on the carried-over$HOMEevery Attempt. A script that registers a hook in global configuration must produce the same configuration no matter how many times it runs. Before turning on reuse, run two or three evals through one Sandbox lane first; problems only show up from the second eval onward.- Where plugin data lives. The plugin installation directory is reinstalled and overwritten every Attempt. Data that should carry over to the next eval can only be written outside the installation directory. Whether carried-over data affects the next eval is a question your experiment design has to answer.
.before() preparation steps can also be reused on Docker. When marketplace fetching is too slow, use sparse to fetch only the plugin path, or bake the plugin into the template and remove its declaration from plugins. The cost is that the installation manifest no longer records the plugin and its resolved version.
A batch with mixed Sandbox configurations
When the selected evals each declare differenttemplates, you do not need to split them into several commands. NiceEval checks each combination of eval and Experiment. Attempts with different Sandbox configurations never share a Sandbox, and never share a running Sandbox. The experiment’s maxConcurrency limits the total number of selected evals running at once. Identical preparation steps can still each reuse their preparation results on Docker.
A typical rhythm
--keep-sandbox cannot be used with sandboxReuse, and a shared Sandbox has been used by the whole batch, so the scene it leaves is not accurate for any single eval. Do the same when something “fails when run together but passes alone”; do not debug inside a batch that shares Sandboxes. For the full process, see Keep the Sandbox to Debug Problems.
When you cannot make it idempotent, use concurrency instead of reuse
When preparation code cannot be made safe to repeat, or the eval depends on a fresh$HOME (for example the system under test has its own memory or stateful services), do not declare sandboxReuse. Concurrency alone is enough for speed:
Related reading
- Eval Groups: list group members explicitly; a group shares a Sandbox while groups run in parallel as usual.
- Rerun and Reuse: how last results are reused with Sandbox reuse, and when to use
--rerun. - Concurrency and Execution Order: how to get the same speedup with concurrency without reuse.
- Sandbox Provider Configuration: how to make each Provider’s template / image / snapshot.