ai-sdk-harness
Packages

ai-sdk-sandbox-runtime

Anthropic Sandbox Runtime (srt) for AI SDK harness agents: Claude Code, Codex and friends on your machine, fenced by the operating system, with no container.

Run AI SDK harness agents (Claude Code, Codex, OpenCode…) on your own machine, fenced by the operating system, with no container and no virtual machine: Anthropic Sandbox Runtime (srt), the sandbox Claude Code puts around its own commands, put around the whole agent.

The package gives HarnessAgent.createSession({ sandboxSession }) what it expects, a HarnessV1NetworkSandboxSession, like ai-sdk-sandbox-sbx does for Docker Sandboxes:

  • Fenced by the kernel: srt runs the sandbox under bubblewrap on Linux and sandbox-exec on macOS. The agent reads the host but your home directory, writes its own directory and its workspace only, and reaches the hosts you allow, nothing else.
  • Credentials stay out: the sandbox holds a placeholder; srt's proxy puts the real value in the requests to the model's API.
  • Starts in milliseconds: nothing to pull or boot. A command runs in a few milliseconds, and the agent works on your files where they are.
  • Your machine's tools: the agent uses the Node.js, git and compilers of the host, read-only.

This package is in its 0.x series, and srt is itself a research preview: try it, and tell what works and what does not in the issues. Until 1.0.0, a minor release may break its API, and its changelog says how; a patch release never does. It is a community package, not affiliated with Vercel or Anthropic.

How it works

Each sandbox is one process srt wraps, a small supervisor: every command, file operation and connection of the sandbox goes through it, on its standard input and output. Its processes share one filesystem view and, on Linux, one network namespace of their own, whose loopback the harness bridge listens on: the package forwards the bridge's port to a free port of this host's loopback. Every request a process makes leaves through srt's proxy, which only lets the allowed hosts through and puts the credentials in.

Requirements

  • Node.js 24 or later, on Linux or macOS. Windows is not supported.
  • @ai-sdk/harness and a harness adapter, such as @ai-sdk/harness-claude-code.
  • pnpm on the PATH, which the Claude Code and Codex bridges install their dependencies with. Corepack's will do.

Linux

srt needs three programs, and user namespaces it may use:

ProgramWhat srt does with itDebian, UbuntuFedoraArch
bubblewrapThe sandbox itself: namespaces, mountsapt install bubblewrapdnf install bubblewrappacman -S bubblewrap
socatRelays the proxy into the sandbox's network namespaceapt install socatdnf install socatpacman -S socat
ripgrepFinds the files srt must protectapt install ripgrepdnf install ripgreppacman -S ripgrep

All three in one go on Debian or Ubuntu:

sudo apt install bubblewrap socat ripgrep

Binaries off the PATH are named with runtime: { bwrapPath, socatPath, ripgrepPath }.

srt's seccomp filter on Ubuntu. Inside the sandbox, srt adds a seccomp filter that blocks Unix sockets, which needs a nested user namespace with capabilities. Recent Ubuntu releases refuse it two ways: the sandbox then fails to start with apply-seccomp: write /proc/self/setgroups … Permission denied, and the error says which applies.

  • Ubuntu 24.04 restricts unprivileged user namespaces (kernel.apparmor_restrict_unprivileged_userns=1). Lift the restriction:

    sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
    # and to keep it after a reboot:
    echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/60-srt-userns.conf
  • Releases that ship the bwrap-userns-restrict AppArmor profile (/etc/apparmor.d/bwrap-userns-restrict, checked on Ubuntu 26.04) confine everything bwrap runs to unpriv_bwrap, which denies every capability, whatever the sysctl says, and a local rule cannot lift a deny. The filter cannot run there.

Where the filter cannot run, run without it, runtime: { allowAllUnixSockets: true }: the sandbox keeps its filesystem and network rules, but a process in it can connect to the Unix sockets it can see. The sockets that drive the host are hidden for you either way — the session and system D-Bus, and the Docker, containerd and Podman sockets — so the agent cannot reach the host's service managers or its container engine through them. Hide any other socket a process should not reach with denyRead:

await createSrtNetworkSandboxSession({
  runtime: { allowAllUnixSockets: true },
  denyRead: ['/run/my-service.sock'],
  ports: [4000],
});

srt masks a directory or a file, not a socket itself, so a socket whose parent directory stays visible (a bare .sock under /run) is hidden by name only, and a socket that appears after the sandbox started is not. Keep high-value sockets in a directory you pass to denyRead, or on a host where srt's seccomp filter runs.

Inside a Docker container without privileges, add runtime: { enableWeakerNestedSandbox: true }, srt's mode for a host that already isolates it.

macOS

sandbox-exec ships with macOS; srt only needs ripgrep: brew install ripgrep.

macOS has no network namespace: the bridge listens on the host's own loopback, which the sandbox may therefore reach, services of the host listening on 127.0.0.1 included. Its other rules are the same.

Installation

pnpm add ai-sdk-sandbox-runtime @ai-sdk/harness @ai-sdk/harness-claude-code

Usage

agent.ts
import { HarnessAgent } from '@ai-sdk/harness/agent';
import { createClaudeCode } from '@ai-sdk/harness-claude-code';
import { createSrtNetworkSandboxSession } from 'ai-sdk-sandbox-runtime';

// Authenticated from CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY.
const agent = new HarnessAgent({ id: 'coder', harness: createClaudeCode() });

const sandboxSession = await createSrtNetworkSandboxSession({
  // The Claude Code bridge listens on the first port; the harness reaches it on the loopback.
  ports: [4000],
  // The agent works on this directory, read-write. Without it, in a directory of its own.
  workspace: '/path/to/project',
  // Installs Claude Code in the sandbox before it is handed over.
  template: await agent.getSandboxTemplate(),
});
const session = await agent.createSession({ sandboxSession });

try {
  const result = await agent.generate({
    session,
    prompt: 'Write a script that prints the first ten primes, then run it.',
  });
  console.log(result.text);
} finally {
  await session.destroy();
  await sandboxSession.destroy();
}

The caller owns the sandbox: ending the harness session leaves it in place. stop() ends its processes and keeps its files; destroy() removes its directory. destroy() never touches the workspace, which is yours.

agent.stream() works the same way, and its result's toUIMessageStreamResponse() feeds useChat straight away. The Next.js example runs in one with EXAMPLE_SANDBOX=srt.

In a Next.js application, keep the package and srt out of the server bundle: srt finds its helpers next to its own files.

next.config.ts
const config = {
  serverExternalPackages: ['ai-sdk-sandbox-runtime', '@anthropic-ai/sandbox-runtime'],
};

What the sandbox may read, write and reach

AccessAllowedNever
ReadThe whole host, but your home directory: inside it, only the sandbox's directory, the workspaces, Node.js, srt and the PATHYour home: SSH keys, cloud logins, the claude and gh ones; denyRead
WriteThe sandbox's own directory (its home and temporary files), the workspace, allowWriteAnything else, denyWrite, the sandbox's own settings
ReachallowedDomains (by default the npm registry and the model APIs), and the hosts brokered credentials go toAny other host

Each sandbox lives in a directory of its own, ~/.ai-sdk-sandbox-runtime/<sandboxId>/ (directory moves them):

<sandboxId>/
├── sandbox.json     its settings, out of its reach: what it was created with, read again on resumption
├── .supervisor/     the supervisor, copied there at each start, readable but not writable
└── sandbox/         all it may write
    ├── home/        HOME: the harness's own state, ~/.claude, global npm installs
    ├── tmp/         TMPDIR
    └── workspace/   its working directory, when it has no `workspace`

Your home directory is hidden, not just read-only: inside the sandbox it is an empty directory, and what a process writes there is gone with it. Set hideHome: false to let the sandbox read it, or re-open a part of it with allowRead:

await createSrtNetworkSandboxSession({
  ports: [4000],
  workspace: '/home/me/project',
  // A tool installed in the home, which the sandbox runs.
  allowRead: ['/home/me/.cargo'],
  readOnlyWorkspaces: ['/home/me/docs'],
  denyWrite: ['/home/me/project/.git/hooks'],
  allowedDomains: ['registry.npmjs.org', 'api.anthropic.com', 'github.com', '*.github.com'],
});

The sandbox starts with nothing of your application's environment: only PATH, LANG and USER come from it, and HOME and TMPDIR are its own. A global npm install lands in its home, whose .local/bin comes first on its PATH. The variables a command is given are added to that environment for that command alone.

Credentials

A harness that supports credential brokering never puts the real credential in the sandbox. It hands the sandbox a random placeholder, an aisdkhc_… string, and asks the sandbox session to transform the requests on their way out. This package registers each placeholder with srt's proxy, with the real value and the API's host: the proxy terminates TLS, finds the placeholder in the request's headers, and puts the real value in its place. The agent can use the credential to call its API; it cannot read it.

  • The real value lives in your application's memory, and in srt's, which runs in the same process. Never in the sandbox, its files or its environment.
  • srt trusts its own certificate authority in the sandbox (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE…), which TLS termination needs. A client that pins its certificates cannot reach an API through it.
  • srt matches the host alone: the path or method a transformation may also name is not checked. The placeholder, random and sent only by the harness, is what scopes the swap.
  • The host a credential goes to becomes reachable, on top of allowedDomains.
  • srt swaps what the request carries; it cannot add a header. Codex, logged in with ChatGPT, also asks for a ChatGPT-Account-ID header: it is left out with a warning, and Codex works without it.
  • release() and stop() withdraw the placeholders.

Brokering can be turned off with brokerCredentials: false: the harness then forwards the real credential into the sandbox environment, where the agent can read it. Keep it on.

Network

srt's proxy lets the sandbox reach the hosts of allowedDomains, in srt's syntax (example.com, *.example.com, an optional :port), and refuses every other connection. By default: registry.npmjs.org, api.anthropic.com, api.openai.com, chatgpt.com and ai-gateway.vercel.sh (DEFAULT_ALLOWED_DOMAINS).

setNetworkPolicy() replaces them while the sandbox runs: { mode: 'deny-all' }, or { mode: 'custom', allowedHosts, allowedCIDRs } with single addresses. srt allows what it lists and nothing else, so allow-all, ranges of addresses and deniedCIDRs are refused with HarnessCapabilityUnsupportedError.

One list per process with srt 0.0.79. srt has a single proxy per process, and its 0.0.79 release a single allow list: every sandbox of a process would otherwise reach what any of them is allowed. So this package fails closed there — a second sandbox of the process whose hosts differ is refused with HarnessCapabilityUnsupportedError rather than quietly sharing the union; sandboxes with the same hosts run side by side. The release after 0.0.79 keeps a list per sandbox, which the package uses as soon as it is installed and which lifts the restriction (sandboxSession.isolatesNetwork says which). Give the sandboxes of an old-srt process the same hosts, or run them in processes of their own.

Ports

ports lists the ports inside the sandbox the harness may reach. Each one is forwarded the first time getPortEndpoint() asks for it, to a free port of this host's loopback (127.0.0.1:<free port>), through the supervisor; release(), stop() and setPorts() stop forwarding it. A port outside the list is refused with HarnessCapabilityUnsupportedError.

Templates

A harness bootstraps itself in the sandbox before its first turn: Claude Code installs @anthropic-ai/claude-agent-sdk and the claude CLI with pnpm. Pass the agent's template, await agent.getSandboxTemplate(), and it happens when the sandbox is created, before createSrtNetworkSandboxSession() returns; the setup commands run before it. A sandbox that fails either is removed.

There is no image to save: each new sandbox prepares the template in its own home, in a few seconds when the npm registry is close. Resume a sandbox to keep what it installed.

Resuming a sandbox

Creation never resumes: a sandbox already named sandboxId is a conflict. Name the sandbox, keep its id, and reattach to it, from the same process or another one:

import {
  createSrtNetworkSandboxSession,
  resumeSrtNetworkSandboxSession,
  SrtSandboxNotFoundError,
} from 'ai-sdk-sandbox-runtime';

async function openSandbox() {
  try {
    return await resumeSrtNetworkSandboxSession({ sandboxId: 'my-agent', ports: [4000] });
  } catch (error) {
    if (!(error instanceof SrtSandboxNotFoundError)) throw error;
    return createSrtNetworkSandboxSession({ sandboxId: 'my-agent', ports: [4000] });
  }
}

A resumed sandbox has the files and the rules it was created with. Not its processes: they end with the process that ran them, whatever the way it went, so nothing of a previous run holds a port.

Lifecycle

MethodWhat it does
fork({ ports })Another view of the same sandbox, with ports, processes and credentials of its own
release()Stops the processes this view started, stops forwarding its ports, withdraws its placeholders
killAllProcesses()Stops every process of the sandbox, whichever view started it
stop()release(), then ends the supervisor. Its files stay; the next command starts it again
destroy()stop(), then removes the sandbox's directory. Never the workspace
restricted()The files-and-processes view of the same sandbox, to hand to tools (see below)

restricted() returns an Experimental_SandboxSession: it can run commands and read and write files, but cannot stop the sandbox, forward ports or touch credentials. Pass it to AI SDK tools that accept experimental_sandbox.

fork() and release() are what sharedSandbox() of ai-sdk-harness-sessions needs to run several conversations in one sandbox.

Options

createSrtNetworkSandboxSession(options) takes every option below; resumeSrtNetworkSandboxSession(options) takes sandboxId (required), abortSignal and the connection settings.

OptionDefaultDescription
sandboxIdsrt-<hex>The name of the sandbox's directory. Creation fails if it is taken
templatenoneawait agent.getSandboxTemplate(): prepared in the new sandbox
abortSignalnoneAborts the setup and the template
workspacenoneHost directory the sandbox works in, read-write
readOnlyWorkspaces[]More host directories it may read
allowedDomainsDEFAULT_ALLOWED_DOMAINSThe hosts it may reach
hideHometrueHide your home directory, but for what the sandbox needs
denyRead[]More paths it may not read
allowRead[]Paths it may read inside a hidden region
allowWrite[]More paths it may write
denyWrite[]Paths it may never write
setup[]Commands run once after creation, in the sandbox, before the template
ports[]Connection. Ports the harness may reach, forwarded on demand
brokerCredentialstrueConnection. Keep credentials out of the sandbox, put in by srt's proxy
directory~/.ai-sdk-sandbox-runtimeConnection. Where the sandboxes live
runtime{}Connection. srt's own settings, for the whole process (below)

runtime configures srt itself. srt keeps one configuration per process: the first sandbox created or resumed applies it, and a later one asking for other settings is refused.

runtime optionDefaultDescription
allowAllUnixSocketsfalseRun without srt's seccomp filter (Linux), where Ubuntu refuses it
bwrapPathon the PATHThe bwrap binary (Linux)
socatPathon the PATHThe socat binary (Linux)
ripgrepPathon the PATHThe rg binary
enableWeakerNestedSandboxfalsesrt's mode for running inside an unprivileged container (Linux)

Errors

  • SrtSandboxNotFoundError: resumeSrtNetworkSandboxSession() found no sandbox of that id. Carries sandboxId.
  • SrtError: srt cannot run on this host (it says what is missing), refused the sandbox, or the supervisor stopped; and a setup command that failed.
  • HarnessCapabilityUnsupportedError (from @ai-sdk/harness): a port that is not exposed, a network policy or a credential rule srt cannot express.

Security checklist

What the sandbox does, or does not, against the essential risks of running an agent's code: ✅ covered, ⚠️ partly or with care, ❌ not covered. The same table describes every sandbox package of this repository. The rest is your application's and the harness's: approving sensitive actions, budgets, audit, and reviewing what leaves the sandbox before it runs anywhere else.

ControlStatusHow
Isolation boundary⚠️bubblewrap or sandbox-exec: the sandbox shares the host's kernel
Nothing of the host by default⚠️Your home is hidden; the sockets that drive the host (the session and system buses, Docker, containerd, Podman) are kept out whether or not srt's seccomp filter runs, but the rest of the host is readable
Writes to the host⚠️workspace and allowWrite are read-write: review what the agent writes there. readOnlyWorkspaces and denyWrite narrow it
Privileges✅The sandbox runs as your user, without root
Docker and Compose❌srt shares the host's kernel, without root: there is no in-sandbox Docker, and the host's is kept out of reach. For docker compose, use ai-sdk-sandbox-sbx or ai-sdk-sandbox-microsandbox
Outbound allow list⚠️allowedDomains and setNetworkPolicy(), applied by srt's proxy. With srt 0.0.79 a process keeps one list, so a second sandbox with different hosts is refused rather than sharing the union; a release that isolates networks lifts this (isolatesNetwork)
Private networks and metadata⚠️Unreachable unless listed; on macOS, the host's loopback is reachable
Inbound ports⚠️Declared ports only, forwarded to the host's loopback, where any local process can connect
Credentials out of the sandbox✅The real value only lives in your application's memory
Credential scope⚠️Only to its API's host, but in any header that carries the placeholder
Environment✅Only PATH, LANG and USER, and the variables a command names
Resource limits❌None
Lifetime limits❌None
Teardown✅destroy() removes the sandbox's directory, never the workspace; its processes end with your application

Limitations

  • A guard rail on your machine, not a virtual machine. The sandbox shares the host's kernel, and reads its files but your home. Prefer ai-sdk-sandbox-sbx for code you do not trust at all, or for projects that need Docker.
  • One allow list per process with srt 0.0.79: see Network.
  • /tmp is read-only. Programs that write to /tmp by name rather than to TMPDIR fail. Corepack is kept to the pnpm release it knows (COREPACK_DEFAULT_TO_LATEST=0): pnpm 12 locks its store under /tmp. A project whose packageManager asks for pnpm 12 still does.
  • No template images: each new sandbox prepares the template; see Templates.
  • Linux and macOS, the latter without a network namespace of its own; see Requirements.

Development

This package lives in the ai-sdk-harness monorepo. pnpm test runs the unit tests against a fake srt, the supervisor running on this host; pnpm test:e2e creates real sandboxes, and runs a real Claude Code turn in one. It reads SRT_SOCAT, SRT_RIPGREP and SRT_ALLOW_ALL_UNIX_SOCKETS=1 for a host without the binaries on its PATH, or with restricted user namespaces.

License

MIT

On this page