Manage Sandboxes

View as Markdown

A sandbox is the OpenShell data plane: a safe, private execution environment where an AI agent runs. Each sandbox combines runtime isolation with OpenShell policy controls that prevent unauthorized data access, credential exposure, and network exfiltration.

You need an active gateway before creating a sandbox.

Create a Sandbox

Create a sandbox with a single command. For example, to create a sandbox with Claude, run:

openshell sandbox create -- claude

The trailing command is the sandbox’s canonical main process. OpenShell starts it once, streams its output, and returns its exit status. Exit code 0 leaves a retained sandbox in Completed; a nonzero exit leaves it in Error with a MainProcessFailed condition. With no trailing command, OpenShell starts a login shell in a retained pseudo-terminal: /bin/bash -l when the image provides bash, otherwise a shell detected in the image such as /bin/sh on minimal bases like Alpine. Add --detach to create the sandbox without attaching:

openshell sandbox create --name worker --detach -- ./worker

Detached commands have no attachment grace period. When the command exits, OpenShell records its terminal phase immediately. For a foreground command, the create request declares one expected SSH attachment. OpenShell retains the terminal transport until that connection drains and closes naturally, then finalizes ephemeral cleanup.

Use --no-keep for an ephemeral command. OpenShell drains stdout and stderr, captures the command result, and deletes the sandbox after the command exits:

openshell sandbox create --no-keep -- sh -c 'echo done; exit 0'

--upload cannot yet be combined with a trailing main command because uploads finish after the canonical process starts. Create a scratch sandbox, upload the files, then launch the workload with sandbox exec, or build the files into the sandbox image.

For automation, use --output json or --output yaml to get machine-readable sandbox metadata after creation:

openshell sandbox create --output json

Every sandbox requires a gateway. Register or select one before running sandbox commands:

openshell gateway add http://127.0.0.1:18080 --local --name local
openshell gateway select local

CPU and Memory

Set per-sandbox CPU and memory amounts with --cpu and --memory:

openshell sandbox create --cpu 2 --memory 4Gi -- claude

CPU values use Kubernetes-style quantities such as 500m, 1, or 2.5. Memory values use byte quantities such as 512Mi, 4Gi, or 8G.

Docker and Podman apply these values as runtime limits. Kubernetes applies each value as both the request and the limit so the scheduler reserves the same amount the sandbox can use. The VM driver currently accepts these flags but does not change VM allocation.

Driver-Specific Configuration

Pass experimental driver-owned settings with --driver-config-json. The value must be a JSON object keyed by driver name. The gateway forwards only the block for its configured compute driver:

Nested keys inside each driver block use snake_case. The top-level envelope keys are driver names, such as kubernetes, and are not part of the nested schema.

openshell sandbox create \
--driver-config-json '{"kubernetes":{"pod":{"runtime_class_name":"kata-containers","node_selector":{"pool":"gpu"}}}}' \
-- claude

Use this only for driver-specific fields that do not have a stable CLI flag. Prefer stable flags such as --cpu, --memory, and --gpu when they cover the same behavior.

GPU Resources

To request GPU resources, add --gpu:

openshell sandbox create --gpu -- claude

Request a specific number of GPUs by passing a count to --gpu:

openshell sandbox create --gpu 2 -- claude

When you omit the count, OpenShell treats the request as --gpu 1. Kubernetes honors counted --gpu requests by setting the nvidia.com/gpu limit. Docker and Podman select the requested number of default NVIDIA CDI devices in round-robin order. VM gateways accept only one GPU, either through --gpu or --gpu 1; a single gpu_device_ids entry works with either form.

For Docker-backed sandboxes, GPU injection uses Docker CDI. If you enable Docker CDI after the gateway starts, restart the gateway so OpenShell can detect the updated Docker daemon capability.

Docker and Podman refresh the CDI inventory before validating or creating a default GPU request, so CDI devices added or removed after the driver starts can be reflected in later sandbox creates. On WSL2 all-only runtimes, the default can fall back to nvidia.com/gpu=all; that fallback counts as one selectable device.

Exact GPU device selection is driver-specific and still requires --gpu. For Docker or Podman, pass CDI IDs through cdi_devices. The top-level key must match the active driver; replace docker with podman when using Podman. CDI IDs are treated as opaque strings. The list must not contain duplicate IDs, and its length must match the effective GPU count:

openshell sandbox create \
--gpu \
--driver-config-json '{"docker":{"cdi_devices":["nvidia.com/gpu=0"]}}' \
-- claude

Custom Containers

Use --from to create a sandbox from the base image, another pre-built sandbox name, a rootfs tar archive, or a container image:

openshell sandbox create --from base
openshell sandbox create --from ollama
openshell sandbox create --from ./rootfs.tar
openshell sandbox create --from my-registry.example.com/my-image:latest

Bare names such as base and ollama resolve to images under ghcr.io/nvidia/openshell-community/sandboxes. Set OPENSHELL_COMMUNITY_REGISTRY when you need to use an internal mirror.

--from does not build local Dockerfiles or directories. Build and tag the image with the container engine used by your local gateway, then pass the resulting image reference:

# Docker gateway
docker build -t my-image:latest .
openshell sandbox create --from my-image:latest
# Podman gateway
podman build -t localhost/my-image:latest .
openshell sandbox create --from localhost/my-image:latest

For a remote gateway, push the image to a registry that the gateway can pull from and use that registry image reference.

Pre-0.1.0 breaking change: openshell sandbox create --from ./Dockerfile and directory sources no longer build images. Build and tag the image before you create the sandbox.

Rootfs Tar Archives

A rootfs tar archive (.tar, .tar.gz, .tgz) is a flat filesystem produced by docker export, podman export, or buildah mount plus tar. It lets you create a sandbox without a registry or a running image daemon:

docker create --name export-me my-image:latest
docker export -o rootfs.tar export-me
docker rm export-me
openshell sandbox create --from ./rootfs.tar

Rootfs tar sources require a local gateway running the VM compute driver. The CLI asks the gateway for a staging slot, writes the archive to the location the gateway allocates, and passes back a single-use token; the gateway resolves that token to a path for the driver. Because the CLI writes the archive directly to the gateway host’s filesystem, the two must share a filesystem and run as the same user. Gateways using the Docker, Podman, or Kubernetes drivers reject rootfs tar sources.

Gzip-compressed archives (.tar.gz, .tgz) are decompressed while the gateway stages them, so the sandbox sees the same filesystem either way. Compression is detected from the archive contents, not the file name.

The gateway caps archive size (10 GiB by default, configurable with the VM driver’s rootfs_tar_max_bytes), and reclaims an unused staging slot after 30 minutes. The cap applies to the expanded archive too: a compressed source that decompresses past the limit is rejected.

Reuse Workload Templates

Sandbox workload templates let workspace admins define reusable runtime shapes for a workspace. A template stores the image, environment, resource requests, and driver-specific configuration that sandboxes should inherit. When you create a sandbox from a template, the create request can still attach providers, labels, and policy, but the workload comes from the named template.

Create a template:

openshell sandbox template create gpu-kata \
--image registry.example.com/agent:latest \
--cpu 2 \
--memory 4Gi \
--gpu 1 \
--label team=runtime \
--env FEATURE_FLAG=on

Use --gpu without a count when the template should request the active driver’s default GPU assignment. Use --gpu COUNT when the template needs a specific number of GPUs.

Add driver-specific settings when the active compute driver needs them:

openshell sandbox template create gpu-kata \
--image registry.example.com/agent:latest \
--driver-config-json '{"kubernetes":{"pod":{"runtime_class_name":"kata-containers","node_selector":{"pool":"gpu"}}}}'

If you omit --image, the gateway applies its default sandbox image when a sandbox is created from the template. Use this when the template should only define resource, environment, or driver settings.

Create a sandbox from a template:

openshell sandbox create --template gpu-kata --provider github -- claude

The --template flag cannot be combined with inline workload flags such as --from, --cpu, --memory, --gpu, --env, or --driver-config-json. Put those values on the template instead. Create-time policy and provider attachments remain part of the sandbox request, so each sandbox can keep its own access boundary.

Inspect and manage templates:

openshell sandbox template list
openshell sandbox template list --label-selector team=runtime
openshell sandbox template get gpu-kata
openshell sandbox template delete gpu-kata

Use --all-workspaces with sandbox template list when you need an admin view across workspaces:

openshell sandbox template list --all-workspaces

For JSON or YAML, template list output contains templates and next_page_token fields. Pass the returned token to --page-token to continue.

Base Sandbox Container

The base sandbox container is the default runtime image for standard OpenShell sandboxes unless the gateway overrides its default sandbox image. It is published as ghcr.io/nvidia/openshell-community/sandboxes/base:latest and maintained in the OpenShell Community repository.

The base container includes common development tooling, supported agent CLIs, and the default sandbox policy. Use it when you want a general-purpose agent environment without a workflow-specific image:

openshell sandbox create --from base

For default policy coverage by agent, refer to Default Policy. For the supported agent list, refer to Supported Agents.

Connect to a Sandbox

Attach to the canonical main process in a running sandbox:

openshell sandbox connect my-sandbox

Disconnecting does not stop the process or close its stdin. A later connect attaches to the same process instance and replays up to 1 MiB of recent output. One attachment owns stdin at a time. Use sandbox exec --tty -- /bin/bash -l when you want a new independent shell instead.

Press Ctrl-P, then Ctrl-Q to disconnect without terminating the main process. Ctrl-C retains its normal terminal behavior and interrupts the foreground process.

Launch VS Code or Cursor directly into the sandbox workspace:

openshell sandbox create --editor vscode --name my-sandbox
openshell sandbox connect my-sandbox --editor cursor

When --editor is used, OpenShell keeps the sandbox alive and installs an OpenShell-managed SSH include file instead of cluttering your main ~/.ssh/config with generated host blocks.

Execute a Command in a Sandbox

Run a one-shot command inside a running sandbox without opening an interactive shell:

openshell sandbox exec -n my-sandbox -- ls -la /workspace

Pipe stdin into the command:

echo "hello" | openshell sandbox exec -n my-sandbox -- cat

The command’s exit code is propagated to the CLI, so exec works in scripts that check return codes.

Run an interactive shell with a TTY:

openshell sandbox exec -n my-sandbox --tty -- /bin/bash

OpenShell allocates a TTY automatically when both stdin and stdout are terminals. Force the behavior with --tty or disable it with --no-tty.

FlagPurpose
-n, --nameSandbox to target.
--workdirWorking directory for the command inside the sandbox.
--timeoutCommand timeout in seconds. 0 disables the timeout.
--ttyForce TTY allocation.
--no-ttyDisable TTY allocation even when attached to a terminal.
--no-login-shellRun the command without sourcing shell login startup files.
--envSet an environment variable for the command (KEY=VALUE, repeatable).

Skip shell startup files

By default sandbox exec runs the command through a login shell (bash -lc), so the sandbox user’s first available .bash_profile, .bash_login, or .profile is sourced first (and .bashrc only if that login file sources it). This makes tool-specific environment configuration available automatically, which suits interactive and tool-discovery use.

For automation and managed checks that need predictable output and side effects, pass --no-login-shell so those startup files are not sourced before the command runs:

openshell sandbox exec -n my-sandbox --no-login-shell -- /usr/local/bin/managed-probe

In this mode a sandbox user’s login startup files cannot write to the command’s output, create files, or otherwise affect the requested command before it starts. The command still runs under bash -c, which reads BASH_ENV if it is set in the command’s environment. The default (login-shell) behavior is unchanged when the flag is omitted.

Set Environment Variables

Inject environment variables into the sandbox at creation time:

openshell sandbox create --env API_KEY=sk-test --env DEBUG=1 -- my-agent

Variables set with --env are available to all processes in the sandbox, including interactive shells and exec commands.

When an --env key looks like a credential — a known provider variable, or a name whose underscore-separated segments include a credential word such as TOKEN, SECRET, PASSWORD, CREDENTIAL, API_KEY, ACCESS_KEY, or SECRET_KEY (for example DB_TOKEN or MY_ACCESS_KEY) — sandbox create prints a non-blocking warning. Matching is on whole segments, so unrelated names like TOKENIZERS_PARALLELISM or PASSWORDLESS_LOGIN do not warn. The agent inside the sandbox can read plain environment values directly, so to hide a secret from the agent, attach it through a profile-backed provider with --provider instead. Suppress the warning with --no-credential-warnings. Detection uses the key name only; values are never inspected or printed.

You can also set per-command environment variables with sandbox exec:

openshell sandbox exec -n my-sandbox --env MY_VAR=hello -- printenv MY_VAR

Per-command variables override the sandbox-level environment for that command only.

Environment variable names starting with OPENSHELL_ are reserved. Keys must match [A-Za-z_][A-Za-z0-9_]*.

Label a Sandbox

Attach labels when you create a sandbox to track ownership, environment, or workflow grouping:

openshell sandbox create --label env=dev --label team=platform -- claude

List only the sandboxes that match a label selector:

openshell sandbox list --selector env=dev
openshell sandbox list --selector env=dev,team=platform

The Python SDK accepts the same gateway labels and selectors. Labels passed to create are stored on the gateway sandbox object and are returned on SandboxRef.labels, so selector-based listing finds Python-created sandboxes:

from openshell import SandboxClient
with SandboxClient.from_active_cluster() as client:
sandbox = client.create(workspace="default", name="deep-research-1", labels={"env": "dev", "team": "platform"})
assert sandbox.labels["team"] == "platform"
matches = client.list_all(workspace="default", label_selector="env=dev,team=platform")
assert sandbox.id in {s.id for s in matches}

Python SDK list methods return a lazy Pager whose iteration yields one Page per gateway request. Use list_all only when you want to exhaust the collection; pass page_token to resume from a token saved from an earlier page.

Create reusable sandbox templates through the Python SDK when several runs should share the same workload shape:

from openshell import SandboxClient
with SandboxClient.from_active_cluster() as client:
templates = client.sandbox_templates()
templates.create(
workspace="default",
name="python",
image="ghcr.io/nvidia/openshell-community/sandboxes/python:latest",
)
sandbox = client.create_from_template(workspace="default", template_name="python")

For non-interactive automation, pass a renewable client-credentials provider. Omitted issuer, client ID, audience, and scopes are read from the active gateway’s metadata. The client requires TLS for non-loopback gateways:

from openshell import ClientCredentialsAuth, SandboxClient
auth = ClientCredentialsAuth(client_secret=lambda: load_secret())
with SandboxClient.from_active_cluster(client_credentials=auth) as client:
sandboxes = client.list_all(workspace="default")

Expose Long Running Services

Service forwarding makes a long-running process inside a sandbox reachable through a gateway-managed URL. Use it for development servers, notebooks, dashboards, or other services that keep listening after the sandbox starts. Run the service on loopback inside the sandbox, expose its port, then open the URL printed by OpenShell.

Expose a service that listens on loopback inside the sandbox:

openshell service expose my-sandbox 8080

Pass an optional service name to create a named service URL:

openshell service expose my-sandbox 8080 web

List exposed endpoints:

openshell service list

List endpoints for one sandbox:

openshell service list my-sandbox

Use structured output for automation:

openshell service list --output json
openshell service list my-sandbox --output yaml

Structured list output contains services and next_page_token fields. Each record contains workspace, sandbox, service, target_port, and url. The unnamed service uses an empty service string. Pass the returned token to --page-token to continue. An empty result has an empty services collection.

Show or delete one endpoint:

openshell service get my-sandbox web
openshell service delete my-sandbox web

Omit the service name to manage the unnamed endpoint:

openshell service get my-sandbox
openshell service delete my-sandbox

Loopback gateways return local openshell.localhost URLs. Remote gateways return HTTPS URLs that require normal gateway authentication. For gateway service-domain configuration, refer to Manage Gateways.

Monitor and Debug

List all sandboxes:

openshell sandbox list

Filter the list by labels when you want a narrower view:

openshell sandbox list --selector team=platform

Use -o json or -o yaml for machine-readable output:

openshell sandbox list -o json
openshell sandbox list -o yaml

Structured list output contains sandboxes and next_page_token fields. Pass the returned token to --page-token to continue.

Get detailed information about a specific sandbox. The output lists Policy source (sandbox or global), Revision (the active policy’s row version for that source), and the formatted active policy YAML:

openshell sandbox get my-sandbox

For automation, use --output json or --output yaml to get machine-readable sandbox details:

openshell sandbox get my-sandbox --output json

Print only that policy YAML for scripting (same effective policy, no metadata):

openshell sandbox get my-sandbox --policy-only

Stream sandbox logs to monitor agent activity and diagnose policy decisions:

openshell logs my-sandbox
FlagPurposeExample
--tailStream logs in real timeopenshell logs my-sandbox --tail
--sourceFilter by log source--source sandbox
--levelFilter by severity--level warn
--sinceShow logs from a time window--since 5m

OpenShell Terminal combines sandbox status and live logs in a single real-time dashboard:

openshell term

Use the terminal to spot blocked connections marked action=deny and provider-related proxy activity. If a connection is blocked unexpectedly, add the host to your network policy or update the attached provider profile. Refer to Policies for the workflow.

The dashboard has three panels stacked vertically: Gateways, Providers (or Global Settings), and Sandboxes. Navigate within a panel with Up/Down or j/k. At a list boundary the cursor overflows into the adjacent panel, skipping empty panels. Use Tab/Shift+Tab to cycle panels directly. Press h/l or Left/Right in the middle panel to switch between the Providers and Global Settings tabs.

Port Forwarding

Forward a local port to a running sandbox to access services inside it, such as a web server or database:

openshell forward start 8000 my-sandbox
openshell forward start 8000 my-sandbox -d # run in background

OpenShell prints the local URL only after the forward listener is reachable. Background forwards must be tracked locally so openshell forward list and openshell forward stop can manage them.

List and stop active forwards:

openshell forward list
openshell forward stop 8000 my-sandbox

Use JSON or YAML output when inspecting tracked forwards from automation:

openshell forward list --output json
openshell forward list --output yaml

Structured output includes sandbox, bind_address, port, pid, and alive. The alive boolean reports whether the tracked PID still matches the expected OpenShell SSH forward; it does not probe the forwarded socket. When no forwards are tracked, structured output returns an empty collection.

The default table colorizes the STATUS column only when both standard output and standard error are capable ANSI terminals; piping or redirecting either stream, or running under TERM=dumb, gives a plain-text table. Other styled output—including -v log lines, progress spinners, prompts, and error messages—is decided per stream, so redirecting one stream leaves the other styled. Prefer --output json for automation rather than matching on the table. Set NO_COLOR to any non-empty value or pass --color never to suppress ANSI formatting, and --color always to force it when piping into a pager. --color applies to every openshell command.

You can also forward a port at creation time with --forward:

openshell sandbox create --forward 8000 -- claude

SSH Config

Generate an SSH config entry for a sandbox so tools like VS Code Remote-SSH can connect directly:

openshell sandbox ssh-config my-sandbox

Append the output to ~/.ssh/config or use --editor on sandbox create/sandbox connect for automatic setup.

Transfer Files

Upload files from your host into the sandbox:

openshell sandbox upload my-sandbox ./src

When you omit the destination, OpenShell discovers the sandbox’s working directory and uploads there. For a named local directory, OpenShell preserves the basename, matching scp -r and cp -r. If that directory already exists, the upload merges into it and overwrites matching entries without deleting unrelated entries.

OpenShell preserves symlinks during upload. A symlink arrives in the sandbox as a symlink with the same target path instead of an expanded copy of the target file or directory. Dangling symlinks are also preserved.

Download files from the sandbox to your host:

openshell sandbox download my-sandbox output ./local

When the sandbox-side source is a single file, the destination follows cp-style placement: if the destination already exists as a directory or ends with /, the file lands inside it as <dest>/<basename>; otherwise the file is written at the exact destination path.

The CLI discovers the sandbox’s canonical working directory and only allows sandbox-side sources that resolve inside it. Paths that escape lexically, such as /etc/passwd or /sandbox/../etc/passwd, and paths that escape through a symlink are refused before any data is transferred. Relative sources are resolved from the working directory; absolute sources within the same canonical directory are also accepted.

You can also upload files at creation time with the --upload flag on openshell sandbox create. Pass --upload multiple times to upload several paths in a single command:

openshell sandbox create --upload ./src:/workspace/src --upload ./config:/workspace/config -- claude

By default, uploads inside a Git repository respect .gitignore rules so that build artifacts, dependency caches, and other ignored files are not transferred. If .gitignore filtering excludes every file in the upload path, the CLI falls back to an unfiltered upload and prints a warning. Pass --no-git-ignore to opt into unfiltered uploads explicitly, upload a path outside the Git work tree, or force-add the intended files if they should remain Git-aware.

Stop and Start Sandboxes

Stop compute when you want to retain a sandbox and its persistent workspace without keeping its container, pod, or VM running:

openshell sandbox stop my-sandbox
openshell sandbox start my-sandbox

The name is optional and defaults to the last-used sandbox. Stop stops local background forwards and waits for the Stopped phase. Start waits until the same sandbox returns to Ready. While stopped, you cannot connect, execute commands, transfer files, forward ports, or reach exposed services. Policies, provider attachments, settings, service definitions, and persistent workspace data remain associated with the sandbox.

Stop and start are idempotent. You can also start a retained Completed or Error/MainProcessFailed sandbox to launch a fresh instance of its canonical command. Starting a fresh instance invalidates SSH sessions issued for the previous runtime generation. Delete an inactive sandbox normally when you no longer need its retained state.

Delete Sandboxes

Deleting a sandbox stops all processes, releases resources, and purges injected credentials.

openshell sandbox delete my-sandbox

Delete multiple sandboxes by listing their names in one command:

openshell sandbox delete sandbox-a sandbox-b

When a multi-sandbox delete fails for one entry, the CLI reports that sandbox’s failure and continues with the remaining names. The command exits with an error after it attempts every requested deletion if any entry failed.

Sandbox Lifecycle

Every sandbox moves through a defined set of phases:

PhaseDescription
ProvisioningThe runtime is setting up the sandbox environment, or the gateway is waiting for the sandbox supervisor to establish its authenticated control session.
ReadyThe sandbox is running and its supervisor control session is connected. You can connect, execute commands, sync files, and view logs.
StoppingThe gateway accepted a stop request and is stopping compute while retaining persistent state.
StoppedCompute was stopped explicitly and access is unavailable.
StartingCompute is starting. The sandbox becomes usable only after a fresh supervisor session connects.
CompletedThe canonical main process exited with code 0. Its normalized result is available in status.exit_code.
ErrorThe canonical main process failed, or sandbox infrastructure failed. Inspect the condition reason and status.exit_code.
DeletingThe sandbox is being torn down. The system releases resources and purges credentials.

The compute backend can become ready before the sandbox supervisor connects to the gateway. During this interval, the sandbox remains in Provisioning and reports a Ready=False condition with the reason SupervisorNotConnected. After a gateway restart, an existing sandbox can return to Provisioning temporarily while its supervisor reconnects. Wait for the phase to return to Ready before you connect to the sandbox or execute commands.

The gateway records a successful canonical main-process exit as Ready=False with reason MainProcessCompleted. Nonzero and signal-normalized results use MainProcessFailed and the Error phase. It also sets status.exit_code; signal exits use the standard 128 + signal convention. Compute runtimes do not automatically restart that process.

Sandbox Compute Drivers

The gateway’s configured compute driver determines how OpenShell creates each sandbox. The CLI workflow stays the same across drivers: you create, connect to, inspect, and delete sandboxes through the gateway API.

For Docker, Podman, MicroVM, and Kubernetes behavior, refer to Sandbox Compute Drivers.

Next Steps