Sandboxes

Using Sandboxes

Sandboxes are available on Hatchet Cloud only.

Sandboxes are a Labs feature and may change in future releases. In the dashboard, open Settings, go to Labs Preview and turn on Sandboxes.

Durable tasks can use the Hatchet Sandbox SDK to create and manage the lifecycle of sandboxes and carry out operations inside them.

The sandbox SDK is available for TypeScript. Python, Go and Ruby are coming soon. Join our Discord to stay up to date.

Install @hatchet-dev/sandbox next to the Hatchet SDK and import it once. It adds hatchet.sandbox to your Hatchet client.

pnpm add @hatchet-dev/sandbox @hatchet-dev/typescript-sdk
import { HatchetClient } from '@hatchet-dev/typescript-sdk/v1';import '@hatchet-dev/sandbox';export const hatchet = HatchetClient.init();

Inside a durable task, create a sandbox, run a command, and stop it.

export const session = hatchet.durableTask({  name: 'agent-session',  executionTimeout: '1h',  fn: async (input: { conversationId: string }) => {    const sandbox = await hatchet.sandbox.create({      id: input.conversationId,      provider: 'e2b',      metadata: { conversation: input.conversationId },    });    await sandbox.files.write('main.py', 'print("hello from the sandbox")');    const result = await sandbox.exec('python3 main.py');    await sandbox.kill();    return { stdout: result.stdout, exitCode: result.exitCode };  },});

provider is required when the tenant has more than one provider.

Running commands

exec runs a shell command in the workspace and returns its output once it finishes.

export async function runTests(sandboxId: string) {  const sandbox = await hatchet.sandbox.connect(sandboxId);  const result = await sandbox.exec('npm test', { timeoutMs: 5 * 60 * 1000 });  if (result.exitCode === 124) {    throw new Error(`tests timed out:\n${result.stderr}`);  }  return { passed: result.exitCode === 0, output: result.stdout };}
  • The result has stdout, stderr and exitCode. A non-zero exit code is a result, not an error.
  • timeoutMs stops the command after that long. A timed-out command returns exit code 124.

Working with files

sandbox.files reads and writes the workspace. Paths are relative to it.

export async function prepareProject(sandboxId: string, files: Record<string, string>) {  const sandbox = await hatchet.sandbox.connect(sandboxId);  await sandbox.files.write(    Object.entries(files).map(([path, data]) => ({ path: `src/${path}`, data }))  );  await sandbox.files.write('config/app.json', JSON.stringify({ debug: true }));  const entries = await sandbox.files.list('src');  const config = await sandbox.files.read('config/app.json');  const hasLock = await sandbox.files.exists('src/package-lock.json');  await sandbox.files.remove('src/.DS_Store');  return { files: entries.map((e) => e.name), config: JSON.parse(config), hasLock };}
  • read, write, list, exists, getInfo, makeDir, rename, copy and remove are the file operations. A path outside the workspace throws a FilesystemError with code invalid_path.
  • read and write carry up to 2 MiB per file. Larger files are not supported on Hatchet Cloud providers yet. A larger read fails with code too_large and a larger write with code invalid. One workaround is to fetch the file inside the sandbox and operate on it there.

Resuming a paused sandbox

Every sandbox has an ID. Pass your own with create({ id }) or take sandbox.sandboxId. From anywhere with a Hatchet client, connect by that ID.

export async function gitStatus(sandboxId: string) {  const sandbox = await hatchet.sandbox.connect(sandboxId);  const result = await sandbox.exec('git status --short');  return result.stdout;}

If the sandbox was stopped, because it was idle or you called kill, connect starts it again from its latest snapshot.

Forks

Start a new sandbox from any snapshot with create({ from }). A snapshot ID comes from the snapshotId on a call's result, or from latestSnapshot in hatchet.sandbox.list() once the sandbox was killed. Create two from the same snapshot and you have a fork, which is how you try two approaches from the same starting point.

export async function fork(repo: string) {  const base = await hatchet.sandbox.create({ provider: 'e2b', metadata: { repo } });  await base.exec(`git clone ${repo} . && npm ci`);  await base.kill();  const [info] = await hatchet.sandbox.list({ sandboxId: base.sandboxId });  const from = info.latestSnapshot!;  const [a, b] = await Promise.all([    hatchet.sandbox.create({ from, inheritParentMetadata: true }),    hatchet.sandbox.create({ from, inheritParentMetadata: true }),  ]);  return [a.sandboxId, b.sandboxId];}

A fork always runs on the provider that holds its snapshot.

Finding and cleaning up

hatchet.sandbox.list({ metadata }) returns the sandboxes whose tags include every pair you pass. hatchet.sandbox.delete({ metadata }) deletes every match with all of its snapshots, and returns their IDs. Both filters are subset matches, so { conversation: "42" } matches a sandbox tagged { conversation: "42", user: "ada" }.

export async function cleanup(conversationId: string) {  const sandboxes = await hatchet.sandbox.list({ metadata: { conversation: conversationId } });  for (const info of sandboxes) {    console.log(info.sandboxId, info.live ? 'running' : 'stopped');  }  const deleted = await hatchet.sandbox.delete({ metadata: { conversation: conversationId } });  return deleted;}

kill() stops the machine and keeps the snapshots, so connect still works and the provider stops billing you for a running machine. delete() removes the sandbox and its snapshots from the provider.

Idle sandboxes stop on their own after the provider's max idle time, 3600 seconds by default. Stopped sandboxes still hold their snapshots at the provider until you delete them.

Last updated on November 9, 2026

On this page