Sandboxes & Workspaces
A sandbox is an isolated container on your Temps instance that you can run arbitrary commands in. It has its own filesystem, its own network identity, and optionally a copy of one of your git repositories.
Sandboxes are useful for anything you don't want running on your laptop or inside a deployment: running an untrusted PR's test suite, giving an AI agent somewhere to work, reproducing a bug on a clean machine, or handing a contractor an environment without handing over your repo credentials.
Two kinds of sandbox
The difference is what happens after you stop typing.
| Ephemeral | Workspace | |
|---|---|---|
| Created with | sandbox create | sandbox create --workspace |
| After the idle timeout | Container stops | Container stops |
| Next command | Fails with 409 | Wakes automatically and runs |
| Files after suspension | Kept until you delete it | Kept until you delete it |
| Good for | CI jobs, one-off commands, SDK-driven automation | Development you return to over days |
Both classes suspend when idle. That is deliberate: a suspended container costs nothing but disk, whereas a handful of always-on idle containers will happily starve the deployments on a small server. "Persistent" here means your files persist, not that the container runs forever.
The practical difference is the second row. An ephemeral sandbox is owned by whatever created it — a stopped one stays stopped, because a script that shut it down does not want it silently restarting. A workspace is owned by a person, and a person coming back the next morning should just find their work where they left it.
Create an ephemeral sandbox
# Empty container, default image, 1 hour idle timeout
bunx @temps-sdk/cli sandbox create --name test-run
# With code and a longer timeout
bunx @temps-sdk/cli sandbox create \
--git-url https://github.com/org/repo.git \
--branch main \
--timeout 7200
The response includes a sandbox ID like sbx_a1b2c3d4e5f6. Every other
command takes that ID.
Create a persistent workspace
From a directory that's already linked to a Temps project, you don't need any
flags — the project comes from .temps/config.json, and the repository and
branch from your git remote:
cd ~/code/my-repo
bunx @temps-sdk/cli sandbox create --workspace
Or say it explicitly, and start a new branch at the same time:
bunx @temps-sdk/cli sandbox create --workspace \
--project my-api \
--branch main \
--new-branch feat/checkout
Come back whenever. The workspace will have suspended in the meantime, and this wakes it:
bunx @temps-sdk/cli sandbox exec sbx_a1b2c3d4e5f6 -- git status
The default sandbox image ships git, gh, glab, Node, Bun, and the
claude, codex, and opencode CLIs, so an AI coding agent can work in a
workspace out of the box. Provide its API key at create time with
-e ANTHROPIC_API_KEY=...; automatic credential injection is not yet
implemented.
Choosing the code
sandbox create works out what to clone from the first of these that matches:
- An explicit URL —
--git-urlor--tarball-url. No lookups; use this for anything your Temps instance can reach over the network. --repo owner/name— a repository on one of your connected git providers that has no Temps project. Temps resolves the clone URL and uses the stored provider token, so private repositories work without you handling a credential.--project <slug>, or the project linked in.temps/config.json,$TEMPS_PROJECT, or your active context. The project's repository, default branch, and git connection are all read from the project.- The git remote of the current directory.
- Interactive pickers — connection, then repository, then branch.
Step 5 only runs on an interactive terminal. In CI, a sandbox that can't be resolved from flags fails with a message naming the flag to pass, rather than hanging on a prompt.
Whichever route you take, the create output tells you what was chosen and how it was worked out, so an inferred repository is never a silent guess.
Private repositories
Two ways, and they're mutually exclusive:
--git-connection <id>(or letting--repo/--projectresolve it) — Temps injects the stored provider token server-side. The token never appears in your shell history, the sandbox's environment, or the clone URL.--git-username+--git-password— passed throughGIT_ASKPASS. Use this only for credentials Temps doesn't already hold.
Suspend and wake
Every sandbox has an idle timeout (--timeout, default 1 hour, maximum 24
hours). The timeout is genuinely idle-based: every command you run and every
file you read or write pushes the deadline forward. A sandbox in continuous
use is never suspended.
When the timeout does elapse, Temps stops the container. Nothing is deleted — the working directory, the home directory, and any volumes all survive.
For a workspace, the next exec or filesystem call starts the container
again and then runs your command. You'll notice it as a slower first command,
not as an error. Waking is a container start, not a rebuild: nothing is
re-cloned and no state is lost.
For an ephemeral sandbox, the same call returns 409 Conflict. Restart it
deliberately with sandbox resume, or push the deadline out ahead of time:
bunx @temps-sdk/cli sandbox resume sbx_a1b2c3d4e5f6
bunx @temps-sdk/cli sandbox extend sbx_a1b2c3d4e5f6 --secs 7200
Preview URLs do not wake a suspended workspace. If you're serving something from a workspace and the URL stops responding, run any command against the sandbox to bring it back.
Running commands
# Run and wait. Use `--` before any flags meant for the inner command.
bunx @temps-sdk/cli sandbox exec sbx_a1b2c3d4e5f6 -- npm test
# Long jobs: detach and stream the logs
JOB=$(bunx @temps-sdk/cli sandbox exec sbx_a1b2c3d4e5f6 --detach -- npm run build)
bunx @temps-sdk/cli sandbox logs sbx_a1b2c3d4e5f6 "$JOB"
# Read and write files directly
bunx @temps-sdk/cli sandbox fs read sbx_a1b2c3d4e5f6 --path /workspace/package.json
bunx @temps-sdk/cli sandbox fs write sbx_a1b2c3d4e5f6 --path /workspace/.env --file ./.env
Each exec is a separate command — cd does not carry over between them.
Use --cwd to choose a working directory, or sandbox shell below for a
session that keeps its state.
Interactive terminal
For anything you'd rather do by hand — including running an AI coding CLI — attach a real terminal:
# A login shell in the sandbox, in your own terminal
bunx @temps-sdk/cli sandbox shell sbx_a1b2c3d4e5f6
# Start (or reattach to) a long-running claude session
bunx @temps-sdk/cli sandbox shell sbx_a1b2c3d4e5f6 --tab claude --cmd claude
Detaching does not kill what's running. The sandbox holds the terminal
session itself, so closing your laptop, losing Wi-Fi, or pressing Ctrl-D
leaves the program going. Reattach with the same --tab and you land back
in the same process, with recent scrollback replayed. That's what makes it
practical to leave claude working on something and check back later.
Tabs are named; --tab defaults to main. Use different names to keep, say,
an editor and a test-watcher side by side in one sandbox.
Suspension is not the same as detaching. The terminal lives in the container's memory, so if the sandbox is suspended or restarted, every tab is gone — reattaching gives you a fresh shell in the same filesystem. While you're attached the sandbox counts as active and won't be suspended, so this only applies if you disconnect and leave it idle past the timeout.
Terminals require the Temps PTY agent, which ships in the default sandbox
image. A sandbox created from a custom --image won't have it, and
sandbox shell will tell you so rather than hanging.
Preview URLs
Anything listening on a port inside the sandbox can be reached from outside:
bunx @temps-sdk/cli sandbox domain sbx_a1b2c3d4e5f6 --port 3000
The hostname embeds an unguessable sandbox label. That is the only thing protecting it by default, so put a password on anything sensitive:
# Prints the password once — it is stored only as a hash
bunx @temps-sdk/cli sandbox password sbx_a1b2c3d4e5f6 --rotate
bunx @temps-sdk/cli sandbox password sbx_a1b2c3d4e5f6 --clear
Resource limits and isolation
bunx @temps-sdk/cli sandbox create \
--cpu-limit 1.0 \
--memory-mb 2048 \
--image node:22
Sandboxes run as Docker containers by default, with capabilities dropped and
no-new-privileges set. Hosts provisioned for it can also run sandboxes as
Firecracker microVMs for a hardware-virtualised boundary — see
Firecracker Sandboxes for host requirements and
how to select the backend per sandbox. Asking for a backend the host can't
provide fails the request rather than quietly downgrading your isolation.
Cleaning up
Nothing deletes a sandbox for you — not the idle sweep, and not any background job. A suspended workspace keeps its disk until you say otherwise:
bunx @temps-sdk/cli sandbox list --workspace
bunx @temps-sdk/cli sandbox rm sbx_a1b2c3d4e5f6
On a small server, workspaces are the thing most likely to fill your disk
quietly. sandbox list is worth checking periodically.
API and SDK
Everything above is the /v1/sandbox REST API, documented in the OpenAPI
spec at /api/api-docs/openapi.json under the Sandboxes tag.
The Node SDK is shape-compatible with @vercel/sandbox, so existing code
against that API works with an import change:
import { Sandbox } from '@temps-sdk/sandbox'
const sandbox = await Sandbox.create({
source: { type: 'git', url: 'https://github.com/org/repo.git', revision: 'main' },
timeoutSecs: 3600,
})
const { stdout } = await sandbox.exec(['npm', 'test'])
const previewUrl = sandbox.domain(3000)
await sandbox.stop()
See the CLI reference for every flag.