• English
  • Sandbox

    The sandbox confines every shell command the agent runs inside an OS boundary — macOS Seatbelt or Linux bubblewrap — with restricted filesystem and network access. Where the permission gate decides whether a command runs, the sandbox constrains what it can touch while running — and because the boundary is enforced by the OS on the running process, it holds regardless of what the model decided to run. It cannot be talked out of with clever command strings. This is the second, independent layer of defense in depth.

    The adapter is @lite-agent/sandbox-anthropic, backed by @anthropic-ai/sandbox-runtime.

    Enable it

    pnpm add @lite-agent/sandbox-anthropic

    Pass sandboxRuntime() to createLiteAgent / query via the sandbox option:

    import { createLiteAgent } from "@lite-agent/sdk";
    import { anthropic } from "@lite-agent/provider";
    import { sandboxRuntime } from "@lite-agent/sandbox-anthropic";
    
    const agent = createLiteAgent({
      model: anthropic(),
      modelName: "claude-sonnet-4-6",
      workdir: process.cwd(),
      sandbox: sandboxRuntime({
        allowWrite: ["."],                 // writable paths (default: cwd)
        denyRead: ["~/.ssh", "~/.aws"],    // blocked reads (default shown)
        allowedDomains: ["api.github.com"],// network allow-list (default: none)
        onUnavailable: (err) => console.warn(`[sandbox] degraded to noop: ${err.message}`),
      }),
    });

    From here on, every command the agent runs through the bash tool is wrapped into the OS boundary before execution — no changes to tools or middleware needed.

    How it works

    Sandbox is a swappable strategy in @lite-agent/core. Its core operation is a pure command-string transformation:

    wrap(command: string, opts: SandboxWrapOptions): Promise<string> | string

    The core bash tool calls ctx.sandbox.wrap(command, { cwd }) right before execution, then executes the wrapped command. The wrapped string runs the original command inside the OS boundary, so the sandbox contains whatever the process actually does — filesystem writes, network connections, child processes (which inherit the boundary).

    PlatformMechanism
    macOSSeatbelt (sandbox-exec)
    Linux / WSL2bubblewrap + socat + ripgrep
    Native WindowsNot supported → graceful degradation (see below)

    The returned Sandbox exposes initialize(), wrap(command, opts), and dispose(); the kernel calls them for you. Initialization is lazy — the sandbox runtime (including its network proxy) starts on first use. When no sandbox is configured, core defaults to noopSandbox() and commands run unwrapped.

    Options

    sandboxRuntime(opts) accepts SandboxRuntimeOptions:

    OptionDefaultDescription
    allowWrite["."]Filesystem paths the command may write.
    allowRead[]Paths re-allowed inside a denied read region.
    denyRead["~/.ssh", "~/.aws"]Paths blocked from reading.
    denyWrite[]Additional paths blocked from writing.
    allowedDomains[]Network domains allowed. All outbound traffic is blocked unless allowed.
    deniedDomains[]Network domains denied.
    allowLocalBindingfalseAllow sandboxed commands to bind local ports.
    allowUnixSockets[]Unix socket paths the command may access.
    allowAllUnixSocketsfalseAllow access to all Unix sockets.
    enableWeakerNestedSandboxfalseWeaken isolation when already running inside a sandbox (e.g. a container).
    enableWeakerNetworkIsolationfalseWeaken network isolation for environments where full isolation is impossible.
    allowAppleEventsfalseAllow Apple Events (macOS automation) from sandboxed commands.
    requireSandboxfalsefalse → degrade to noop if init fails; true → throw.
    onUnavailableCalled once when degrading to noop.
    Tip

    The defaults are deliberately safe: nothing may be written outside the working directory, ~/.ssh and ~/.aws are unreadable, and no outbound network access is allowed. Opt in to more access explicitly.

    Graceful degradation

    The OS sandbox can't initialize everywhere — missing bubblewrap, native Windows, or an otherwise unsupported environment. sandboxRuntime handles this without blocking the agent:

    • Default (requireSandbox: false) — the adapter degrades to a no-op: wrap returns commands unchanged, and onUnavailable(err) fires exactly once so the host can log or surface the degraded state.
    • Strict (requireSandbox: true) — initialization failure throws, so a host that mandates a boundary fails fast instead of silently running unsandboxed.
    sandboxRuntime({
      requireSandbox: true, // production: no boundary, no run
    });
    Warning

    Degraded mode means no OS boundary. Commands still pass the permission gate, but nothing contains them at runtime. Use requireSandbox: true where a sandbox is a hard requirement.

    Defense in depth: permission gate vs. sandbox

    lite-agent separates "should this command run?" from "what can it touch once running?" — two orthogonal layers that must both exist:

    Permission gateSandbox
    QuestionShould this command run at all?What can it touch while running?
    TimingPre-execution decisionEnforced by the OS during execution
    Shapeallow / deny / ask rules, human approvalFilesystem + network boundary
    Bypasses model choice?No (judges the command string)Yes (OS-enforced, independent of the model)
    ImplementationPermissionPolicy + ApprovalHandler (via wrapToolCall middleware)Sandbox strategy (inside Tool.execute)

    The gate decides before execution; the sandbox contains whatever the gate lets through. Only a gate: an approved command can still read ~/.ssh or phone home. Only a sandbox: dangerous-but-in-bounds operations never get stopped for approval. The two compose naturally — no extra orchestration needed.

    createLiteAgent({
      model: anthropic(),
      modelName: "claude-sonnet-4-6",
      workdir: process.cwd(),
      // Gate: pre-exec decisions
      // permission: policy({ allow: ["read_file"], ask: ["bash", "write_file"] }),
      // Boundary: runtime containment
      sandbox: sandboxRuntime({
        allowedDomains: ["api.github.com", "registry.npmjs.org"],
        denyRead: ["~/.ssh", "~/.aws"],
        denyWrite: [".env"],
      }),
    });

    With this setup: curl evil.com → blocked (domain not allow-listed); cat ~/.ssh/id_rsa → read denied; rm -rf ~/project-outside → write outside boundary, rejected by the OS. None of this relies on model cooperation.

    Limitations

    • @anthropic-ai/sandbox-runtime is a Beta Research Preview — its API may change, and native Windows is unsupported (WSL2 works). That is exactly why the adapter is pluggable and degrades to noop by default.
    • Network filtering does not decrypt TLS — it trusts the client-declared hostname, so techniques like domain fronting can bypass it. Allow-listing a broad domain (e.g. github.com) opens an exfiltration channel. Stronger threat models need a custom MITM proxy (out of scope here).
    • Not for fully untrusted code — an OS-level sandbox is a guardrail for a trusted agent, not isolation for malicious code. For that, use a microVM (E2B, microsandbox) behind your own Sandbox implementation — same interface, swappable anytime.

    See also

    • Permissions — the pre-execution gate that composes with the sandbox.
    • Core strategies — the Sandbox strategy interface and the noopSandbox default.
    • Providers — model providers to pair with.