---
title: "Snapshot after npm install, then fork 10 copies"
description: "Install dependencies once in a sandbox, take a snapshot of it while it runs, and start ten parallel attempts from that snapshot, in TypeScript, Python or the CLI."
url: "https://www.promigence.ai/docs/guides/snapshot-and-fork"
site: "Promigence"
---

# Snapshot after npm install, then fork 10 copies

Install dependencies once in a sandbox, take a snapshot of it while it runs, and start ten parallel attempts from that snapshot, in TypeScript, Python or the CLI.

`npm install` is the slow, identical part of every attempt an agent makes. Run it once, take a snapshot of the sandbox
as it is at that moment, and start every attempt from the snapshot: ten copies, each with `node_modules` already in
place, each free to change its own files without touching the others.

## The short answer

Install once in one sandbox, snapshot it while it runs, and start ten sandboxes from the snapshot. On Promigence that
is `client.sandboxes.snapshot(id)` followed by `Sandbox.fork(snapshotId, { count: 10 })` in TypeScript, the same two
calls in Python, or `promigence sandbox snapshot` and `promigence run --count 10` in a shell. Every copy starts with
the files the sandbox had when you took the snapshot.

## In TypeScript

It has to run as an ES module, for the top-level `await`. The base sandbox already has Node.js with npm, and git
(`promigence templates show base` lists everything in it).

```typescript
import { PromigenceClient, Sandbox } from "@promigence/sdk";

const client = new PromigenceClient(); // reads PROMIGENCE_API_KEY, or the key signup stored
const REPO = "https://github.com/your-org/your-app";

// 1. One sandbox, and the slow part, once
const setup = await Sandbox.create({ client, spendCap: 1, timeoutMs: 30 * 60_000 });
await setup.commands.run(`git clone --depth 1 ${REPO} /work/app && cd /work/app && npm install`, {
  timeoutMs: 15 * 60_000,
});

// 2. A snapshot of the sandbox as it is now
const snap = await client.sandboxes.snapshot(setup.sandboxId, { alias: "app-installed", moveAlias: true });
await setup.kill();

// 3. Ten copies, each with node_modules already installed
const copies = await Sandbox.fork(snap.snapshot_id, { client, count: 10, spendCap: 3, timeoutMs: 20 * 60_000 });

// 4. One attempt per copy, all at once
const results = await Promise.all(
  copies.map(async (sbx, i) => {
    const r = await sbx.commands.run("cd /work/app && node agent.js && npm test", {
      envs: { ATTEMPT: String(i) },
      timeoutMs: 15 * 60_000,
      throwOnExit: false,
    });
    return { sbx, attempt: i, passed: r.exitCode === 0 };
  }),
);

const winner = results.find((r) => r.passed);
console.log(winner ? `attempt ${winner.attempt} passed in ${winner.sbx.sandboxId}` : "no attempt passed");
await Promise.all(results.filter((r) => r !== winner).map((r) => r.sbx.kill()));
```

`node agent.js` stands for your agent's attempt; `ATTEMPT` tells each copy which one it is, so it can vary a seed or
a prompt. The winner keeps running until you kill it or it reaches its timeout, so you can read its diff with
`sbx.commands.run("git -C /work/app diff")` before you end it.

`spendCap: 3` caps what the ten copies may spend together: ten small sandboxes for up to 20 minutes cost at most
10 × 1,200 × $0.000046 = $0.55 at list, so the cap leaves room. A sandbox that reaches its cap is paused with its
files kept, and if your script dies halfway, every copy still ends at its 20-minute timeout.

## In Python

The same four steps, with a thread per attempt:

```python
from concurrent.futures import ThreadPoolExecutor
from typing import List

from promigence import Promigence, Sandbox

REPO = "https://github.com/your-org/your-app"

with Promigence() as client:  # reads PROMIGENCE_API_KEY
    setup = Sandbox.create(client=client, spend_cap=1, timeout_ms=30 * 60_000)
    setup.commands.run(f"git clone --depth 1 {REPO} /work/app && cd /work/app && npm install", timeout_ms=15 * 60_000)
    snap = client.sandboxes.snapshot(setup.sandbox_id, alias="app-installed", move_alias=True)
    setup.kill()

    copies: List[Sandbox] = Sandbox.fork(
        snap["snapshot_id"], client=client, count=10, spend_cap=3, timeout_ms=20 * 60_000,
    )

    def attempt(i: int) -> bool:
        result = copies[i].commands.run(
            "cd /work/app && node agent.js && npm test",
            envs={"ATTEMPT": str(i)}, timeout_ms=15 * 60_000, throw_on_exit=False,
        )
        return result.exit_code == 0

    with ThreadPoolExecutor(max_workers=10) as pool:
        passed = list(pool.map(attempt, range(10)))

    winner = passed.index(True) if True in passed else None
    print(f"attempt {winner} passed" if winner is not None else "no attempt passed")
    for i, sbx in enumerate(copies):
        if i != winner:
            sbx.kill()
```

## From a shell

`--json` makes every command print one JSON document, so a script can pick out the ids:

```bash
SID=$(promigence fork --count 1 --cap 1 --timeout 30m --fresh --json | jq -r '.sandboxes[0].sandbox_id')
promigence exec --sandbox "$SID" --timeout 15m -- \
  sh -c "git clone --depth 1 https://github.com/your-org/your-app /work/app && cd /work/app && npm install"
promigence sandbox snapshot "$SID" --alias app-installed --move-alias
promigence sandbox kill "$SID"

promigence run --snapshot app-installed --count 10 --cap 3 --timeout 20m \
  -- sh -c 'cd /work/app && ATTEMPT=$PROMIGENCE_INDEX node agent.js && npm test'
```

`promigence run` starts the ten copies, runs the attempt in each with `PROMIGENCE_INDEX` set from 0 to 9, and prints
a validity report that says which attempts passed and whether any failure was the environment's or the attempt's.

## What the snapshot keeps, and what to watch

The sandbox you snapshot keeps running. Its files go into the snapshot, `node_modules` included, and every copy
starts with them. Commands started in the background (`background: true`, or `exec --background`) are ended to take
the snapshot, so start long-lived processes again in the copies.

Three details save a debugging session:

1. `sbx.fork()` on a sandbox object never copies your install: after any command it asks for `fromOrigin: true` and
   then copies the snapshot the sandbox started from. To copy it as it is now, take a snapshot first and fork that.
2. A sandbox that was given a secret is refused a snapshot unless you force it, because the snapshot and every copy
   could then contain the value. So install first, and give the secret to the copies (`secrets` on `Sandbox.fork`).
3. An alias names one snapshot in your organization. `moveAlias` (`--move-alias`) takes it from the snapshot that has
   it, so the script can run again.

When every task installs the same dependencies, build the snapshot from a recipe instead and let a verify command
check it. It is stored only if the verify command exits 0, the build is billed per second while it runs, up to $0.17
at the default small size ([quickstart](https://www.promigence.ai/docs/quickstart)), and a connected GitHub
repository can rebuild it on every push:

```bash
promigence snapshot create --image node:22 \
  --setup "git clone https://github.com/your-org/your-app /work/app && git -C /work/app checkout <40-char sha>" \
  --setup "cd /work/app && npm ci" \
  --verify "cd /work/app && npm test" \
  --alias app --wait --wait-timeout 10m
```

A build that fails its verify command is billed for the time it ran, and the sandboxes started from the snapshot pay no further build charge.

## At a glance

| | Snapshot of a running sandbox | Snapshot built from a recipe |
|---|---|---|
| Command | `promigence sandbox snapshot SID`, `client.sandboxes.snapshot(id)` | `promigence snapshot create --image REF --setup CMD --verify CMD` |
| Starts from | Whatever the sandbox holds at that moment | An image or a Dockerfile, plus your setup steps |
| Checked by a verify command | No | Yes: stored only if the command exits 0 |
| Best for | Branching a session mid-task, retries from one point | An environment many runs share, rebuilt per commit |
| Background commands | Ended to take the snapshot | A `--start` command runs in every copy |
| Copies | `Sandbox.fork(id, { count })`, `promigence run --snapshot NAME --count N` | The same |

**Install once, try ten times**

Your agent framework installs dependencies once in one sandbox and takes a snapshot of it. Ten copies start from that snapshot with node_modules already installed, each runs one attempt and its tests, and the framework keeps the attempt that passed.

Steps:
1. Your agent framework
2. One sandbox: git clone and npm install
3. Snapshot: app-installed
4. Ten copies with node_modules in place
5. Ten attempts, then npm test
6. Keep the attempt that passed, kill the rest

Connections:
1. Your agent framework → One sandbox: git clone and npm install: Sandbox.create
2. One sandbox: git clone and npm install → Snapshot: app-installed: sandboxes.snapshot
3. Snapshot: app-installed → Ten copies with node_modules in place: Sandbox.fork, count 10
4. Ten copies with node_modules in place → Ten attempts, then npm test: one attempt each
5. Ten attempts, then npm test → Keep the attempt that passed, kill the rest: results
6. Keep the attempt that passed, kill the rest → Your agent framework: diff of the winner

_Your agent framework branches one prepared sandbox into ten attempts_

## FAQ

### How do you snapshot a sandbox after npm install and fork it for 10 parallel attempts?

In one sandbox, run `npm install`, take a snapshot with `client.sandboxes.snapshot(sandboxId)`, then start ten copies
with `Sandbox.fork(snapshotId, { count: 10 })`. The copies start with `node_modules` already installed, and each runs
its own attempt. The full program is above in TypeScript, in Python and as CLI commands.

### Which sandbox platforms let me snapshot and fork a running agent environment?

Promigence is one: `promigence sandbox snapshot` takes a snapshot of a running sandbox without stopping it, and any
number of sandboxes can start from the result. When you compare platforms, ask whether a snapshot can come from a
running sandbox or only from a template build, whether copies start with the files you captured, what happens to
running processes, and whether a secret in the sandbox can end up in the snapshot.

### Does the original sandbox stop when I take the snapshot?

No. It keeps running, and you get an ordinary snapshot id you can start sandboxes from. Commands started in the
background are ended to take the snapshot; the CLI and the SDK say how many.

### Do the ten copies share files?

Each copy starts with the snapshot's files, and what one attempt writes is its own. No sandbox can see, reach or
address another, so a failing attempt cannot damage the others.

### What does forking ten copies cost?

Every copy bills per second at the rate for its size, and snapshot storage is included in every plan. At list, ten
small sandboxes for up to 20 minutes cost at most $0.55, and `spendCap` holds the batch to the number you set.

## Sources

- [CLI reference](https://www.promigence.ai/docs/cli): `sandbox snapshot`, `fork`, `exec`, `run`, `snapshot create`, read 17 Oct 2026
- [TypeScript SDK](https://www.promigence.ai/docs/sdk): `Sandbox.create`, `commands.run`, `kill`, spend caps, read 17 Oct 2026
- [Pricing](https://www.promigence.ai/#pricing): rates per size, read 17 Oct 2026
- [Quickstart](https://www.promigence.ai/docs/quickstart): builds billed per second while they run, failed ones included, read 19 Oct 2026
- [Limits and notices](https://www.promigence.ai/docs/limits): what happens at a spend cap, read 17 Oct 2026
- [Security](https://www.promigence.ai/security): isolation between sandboxes, secrets kept out of snapshots, read 17 Oct 2026
- [What is snapshot fork?](https://www.promigence.ai/glossary/snapshot-fork), Promigence glossary, read 17 Oct 2026
- [@promigence/sdk on npm](https://www.npmjs.com/package/@promigence/sdk): version 0.1.0-beta.7, read 17 Oct 2026
- [promigence on PyPI](https://pypi.org/project/promigence/): the Python SDK, version 0.1.0b7, read 17 Oct 2026

Updated 2026-10-17.

