Cereal docs
Sandboxes

The sandbox API and the prepare hook

Acting on the machine a session owns from your own code, and preparing it before the agent's first command.

Use the sandbox API from your application to run commands or set runtime environment variables. A prepare hook can initialize a workspace or provide a short-lived credential before the agent's first command. Keep runtime credentials out of reusable image layers.

Two verbs

await client.sandboxes.exec({
  id: sandboxId,
  command: "cat > /workspace/.netrc",
  stdin: netrc,                     // or base64 with `encoding: "base64"`
  cwd: "/workspace",
  env: { GIT_TERMINAL_PROMPT: "0" },
  timeoutSeconds: 60,
  background: false
})
// → { output, isError }: byte for byte what the model's own `bash` would have seen

await client.sandboxes.env({
  id: sandboxId,
  set: { GITHUB_TOKEN: token },     // every later command is launched with these
  unset: ["NPM_TOKEN"],
  replace: false
})
// → the names, never the values

await client.sandboxes.get({ id: sandboxId })   // state, and the environment's names

exec runs a shell command. Pass file contents through stdin; use encoding: "base64" for binary input. env persists environment variables for later commands, including after a snapshot or resume. It supports up to 64 variables and 32 KiB of values; reads return names without values.

Both operations require data scope with an API key. Denying the sandbox's bash tool also denies exec.

Scoped access from a callback

A tool or hook callback for a live sandbox receives a short-lived sandbox token. It permits only sandboxes.get, sandboxes.exec, and sandboxes.env for that sandbox. Use this scoped access when your callback needs to prepare or inspect the workspace.

@trycereal/sdk turns it into ctx.sandbox, with the id already bound:

tool({
  name: "prepare_repo",
  handler: async (input, ctx) => {
    if (!ctx.sandbox) return "this session has no sandbox"
    await ctx.sandbox.env({ set: { GITHUB_TOKEN: await mintInstallationToken(ctx) } })
    await ctx.sandbox.exec("cat > /workspace/.gitconfig", {
      stdin: gitconfigFor(ctx.session?.identity)
    })
    const { output } = await ctx.sandbox.exec("git clone https://github.com/acme/api /workspace/api")
    return output
  }
})

Sandbox API calls can resume a snapshotted machine. Preview requests do not resume it.

The prepare hook

sandbox.prepare is the second hook Cereal waits for. It fires when a machine has come up, either provisioning or resumed from a snapshot, and before the agent has run a single command in it, so a handler can put a credential, a clone or a cache in place that every later tool call will find.

export const agent = defineAgent({
  agent: {
    slug: "repo-bot",
    sandbox: {
      provider: "modal",
      image: { steps: ["RUN apt-get update && apt-get install -y git"] },
      // `sync` fills this in from the connection it registers when
      // `onSandboxPrepare` is defined. `timeoutMs` defaults to 30 s, ceiling 5 min.
      prepare: { connection: "repo-bot", timeoutMs: 120_000, onFailure: "release" }
    }
  },
  hooks: {
    onSandboxPrepare: async (sandbox, ctx) => {
      // sandbox: { id, event: "provisioning" | "resumed", from?, provider, imageRef, envNames }
      if (sandbox.event === "resumed" && sandbox.from === "snapshot") return
      await ctx.sandbox.env({ set: { GITHUB_TOKEN: await mintInstallationToken(ctx) } })
      await ctx.sandbox.exec("git clone https://github.com/acme/api /workspace/api")
      // returning nothing is "ready"; `{ ok: false, reason }` is a failure
    }
  }
})

Use one registered connection for the prepare hook; inline URLs are not supported. Development channel connections work too. The hook is not retried automatically. Make preparation repeatable for a sandbox that may be resumed later.

The default deadline is 30 seconds. ctx.deadlineAt tells the handler when Cereal stops waiting. Failure or timeout releases the machine and fails the triggering tool call by default. Set onFailure: "continue" only for optional preparation. Failures are recorded as session.error.

A snapshot brings the filesystem back and not the processes or the environment a previous preparer set through exec; what it set through env survives on the row. That is the case the from field exists for.

On this page