Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Sandbox SDK extensions framework
description: Attach opt-in capabilities to your Sandbox subclass with the new extension framework. The code interpreter and Git client are now shipped as extensions.
products:
- sandbox
date: 2026-07-03
---

The Sandbox SDK now has an **extension framework** that lets you attach opt-in capabilities to your `Sandbox` subclass instead of relying on features baked into the core SDK or container image. Extensions come in two flavors: SDK-only extensions that orchestrate existing container APIs from the Durable Object side, and sidecar extensions that ship a bundled runtime provisioned inside the container on first use.

The framework also delivers on our previously announced [plan to move non-core features into helpers](/changelog/2026-06-09-deprecating-sandbox-sdk-features/). Two Cloudflare-authored extensions ship alongside the framework:

- **`@cloudflare/sandbox/interpreter`** — the Python and JavaScript code interpreter, now provisioned as a sidecar the first time you use it.
- **`@cloudflare/sandbox/git`** — the Git client, driving `exec()` from the Durable Object side.

Attach them to your `Sandbox` subclass:

```ts
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";
import { withGit } from "@cloudflare/sandbox/git";

export class Sandbox extends BaseSandbox<Env> {
interpreter = withInterpreter(this);
git = withGit(this);
}
```

And call them from a Worker as a nested namespace on the sandbox stub:

```ts
await sandbox.interpreter.runCode('print("hello")', { context });
await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 });
```

The extension framework, `withInterpreter()`, and `withGit()` are currently available in the [`@cloudflare/sandbox@next`](https://www.npmjs.com/package/@cloudflare/sandbox/v/next) prerelease of the SDK. Install it explicitly to try it today:

```sh
npm install @cloudflare/sandbox@next
```

The `next` channel will become the default `latest` release in a future version. Note that on `next`, `sandbox.gitCheckout()` and the interpreter methods (`createCodeContext`, `runCode`, `runCodeStream`, `listCodeContexts`, `deleteCodeContext`) no longer exist on the base `Sandbox` class — attach the extension and, optionally, add a delegate method to preserve the previous flat API.

For the full authoring guide, examples, and migration notes, refer to the new [Extensions concept page](/sandbox/concepts/extensions/).
290 changes: 290 additions & 0 deletions src/content/docs/sandbox/concepts/extensions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
---
title: Extensions
description: Add opt-in capabilities to a Sandbox subclass with the extension framework.
pcx_content_type: concept
sidebar:
order: 7
products:
- sandbox
---

import { TypeScriptExample, PackageManagers } from "~/components";

The extension framework lets you attach opt-in capabilities to your `Sandbox` subclass instead of relying on features baked into the core SDK or container image. Extensions are how larger, optional features (such as the code interpreter and Git client) now ship in the Sandbox SDK.

:::caution[Available in the `next` release only]
The extension framework, the `withInterpreter()` extension, and the `withGit()` extension are only available in the [`@cloudflare/sandbox@next`](https://www.npmjs.com/package/@cloudflare/sandbox/v/next) prerelease of the SDK. Install it explicitly:

<PackageManagers pkg="@cloudflare/sandbox@next" />

The `next` channel will become the default `latest` release in a future version. Until then, code samples on this page will not work against the current stable release.
:::

## What extensions are for

The base `Sandbox` class exposes a small, general-purpose surface: commands, files, processes, ports, and sessions. Extensions layer higher-level capabilities on top of that surface without bloating it:

- **Keep the core lean.** You only pay for what you opt into. Extensions that are not attached are not shipped, provisioned, or executed.
- **Isolate feature runtimes.** An extension can ship its own long-running process inside the container (a sidecar) without polluting the base container image, and without exposing extra ports.
- **Encapsulate cross-layer logic.** An extension can drive existing container APIs from the Durable Object side, own its own container-side runtime, or do both.
- **Provide a stable authoring pattern.** Every extension follows the same `SandboxExtension` + `withX(this)` shape, so features written by Cloudflare and features you write yourself look identical to a caller.

The code interpreter and Git client are the first two Cloudflare-authored extensions. Both were previously methods on the base `Sandbox` class and are now opt-in extensions instead.

## How extensions work

Every extension is a subclass of `SandboxExtension` from `@cloudflare/sandbox/extensions`, attached to a `Sandbox` subclass via a `withX(this)` factory:

<TypeScriptExample>
```
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";
import { withGit } from "@cloudflare/sandbox/git";

export class Sandbox extends BaseSandbox<Env> {
interpreter = withInterpreter(this);
git = withGit(this);
}
```
</TypeScriptExample>

From a Worker, you call extension methods as a nested namespace on the sandbox stub:

<TypeScriptExample>
```
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const ctx = await sandbox.interpreter.createCodeContext({ language: "python" });
const result = await sandbox.interpreter.runCode('print("hello")', {
context: ctx,
});

await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 });
```
</TypeScriptExample>

Under the hood, the SDK routes `sandbox.<extension>.<method>(...)` calls across the Worker to Durable Object boundary and dispatches them to the matching `SandboxExtension` instance. You can still add flatter delegate methods on your `Sandbox` subclass if you want to keep an existing API shape (`sandbox.runCode(...)`), but they are optional.

Extension methods called from a Worker must return serializable data. You cannot hand back class instances that only make sense inside the Durable Object.

### Two flavors: SDK-only and sidecar

Extensions come in two flavors, depending on whether they need their own container-side runtime:

- **SDK-only extensions** orchestrate existing container APIs (`commands`, `files`, `processes`, and so on) from the Durable Object side. Nothing extra runs inside the container. The Git extension works this way, because `git` is just shell commands: the extension calls `exec()` with the right arguments and parses the output.
- **Sidecar extensions** ship a bundled runtime that runs as a supervised process inside the container. The SDK talks to it over a private channel, with no extra network ports and no HTTP. The code interpreter works this way, because its Python and JavaScript executors need a long-running process pool inside the container.

Sidecars are provisioned lazily the first time you call the extension, cached across sandboxes, and restarted automatically if they crash. You never manage the lifecycle yourself.

As a rule of thumb: if existing container APIs are enough to implement your feature, use an SDK-only extension. Reach for a sidecar only when you need a long-running process inside the container.

## Author an extension

Every extension follows the same shape: a class extending `SandboxExtension`, and a `withX(sandbox)` factory. The base class captures the sandbox and exposes `this.client` (the container API surface) and, for sidecar extensions, `this.sidecar<T>()` (a typed remote handle to the sidecar process).

### SDK-only extension

An SDK-only extension calls existing container APIs. Here is a minimal example that reports basic system information by shelling out via `commands.execute`:

<TypeScriptExample>
```
import {
SandboxExtension,
type SandboxLike,
} from "@cloudflare/sandbox/extensions";

class SystemInfo extends SandboxExtension {
constructor(sandbox: SandboxLike) {
super(sandbox);
}
async kernelVersion(sessionId: string) {
const { stdout } = await this.client.commands.execute(
"uname -a",
sessionId,
);
return stdout.trim();
}
}
export const withSystemInfo = (s: SandboxLike) => new SystemInfo(s);
```
</TypeScriptExample>

Wire it onto a `Sandbox` subclass:

<TypeScriptExample>
```
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withSystemInfo } from "./system-info";

export class Sandbox extends BaseSandbox<Env> {
systemInfo = withSystemInfo(this);
}
```
</TypeScriptExample>

Callers use `sandbox.systemInfo.kernelVersion(sessionId)` from a Worker.

If your extension needs to pass caller-supplied values into a shell command, do not string-interpolate them into the command. Untrusted input can contain shell metacharacters and alter the command that runs. Validate the input against an allowlist, or escape it before use.

### Sidecar extension

A sidecar extension additionally passes a tarball to `super()` and reaches its container-side runtime through `this.sidecar<T>()`:

<TypeScriptExample>
```
import sidecarTarballBytes from "./sidecar-package.tgz";
import {
SandboxExtension,
type SandboxLike,
} from "@cloudflare/sandbox/extensions";
import type { MySidecarAPI } from "./shared";

class MyExtension extends SandboxExtension {
constructor(sandbox: SandboxLike) {
super(sandbox, { tarball: new Uint8Array(sidecarTarballBytes) });
}
async run(input: string) {
const api = await this.sidecar<MySidecarAPI>();
return api.run(input);
}
}
export const withMyExtension = (s: SandboxLike) => new MyExtension(s);
```
</TypeScriptExample>

The sidecar itself is a small npm package that extends `SandboxSidecar` and calls `serveSandboxSidecar(...)`. It listens on the `EXT_SOCKET` Unix socket the container injects and serves a capnweb session per connection:

<TypeScriptExample>
```
import {
SandboxSidecar,
serveSandboxSidecar,
} from "@cloudflare/sandbox/sidecar";

class MySidecar extends SandboxSidecar {
async run(input: string) {
return { result: input.toUpperCase() };
}
}
serveSandboxSidecar(new MySidecar());
```
</TypeScriptExample>

Streaming is expressed as a typed callback parameter on a sidecar method. The callback is stubbed across both the Worker to Durable Object and Durable Object to sidecar hops, so there is no separate streaming protocol to learn:

<TypeScriptExample>
```
import {
SandboxExtension,
type SandboxLike,
} from "@cloudflare/sandbox/extensions";
import type { InterpreterSidecarAPI } from "./shared";

class Interpreter extends SandboxExtension {
constructor(sandbox: SandboxLike) {
super(sandbox, { tarball: new Uint8Array() });
}
async runCode(
code: string,
onEvent: (event: { kind: string; data: unknown }) => void,
) {
const api = await this.sidecar<InterpreterSidecarAPI>();
return api.runCode(code, onEvent);
}
}
```
</TypeScriptExample>

## Practical examples

### Code interpreter (sidecar extension)

The Python/JavaScript interpreter ships as `@cloudflare/sandbox/interpreter`. Its container-side runtime (the process pool and language executors) runs as a sidecar that is provisioned the first time you use it, so you only pay for it when you opt in.

<TypeScriptExample>
```
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox<Env> {
interpreter = withInterpreter(this);
}
```
</TypeScriptExample>

From a Worker:

<TypeScriptExample>
```
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const context = await sandbox.interpreter.createCodeContext({
language: "python",
});
const result = await sandbox.interpreter.runCode('print("hello")', {
context,
});
```
</TypeScriptExample>

Python execution still requires the `-python` container image variant. `runCode()` resolves to an `ExecutionResult` (a plain, serializable object) so it can safely cross the Worker to Durable Object boundary.

### Git client (SDK-only extension)

Git ships as `@cloudflare/sandbox/git`. Because `git` is just shell commands, no sidecar is needed: the extension calls `exec()` with the right arguments and parses the results.

<TypeScriptExample>
```
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withGit } from "@cloudflare/sandbox/git";

export class Sandbox extends BaseSandbox<Env> {
git = withGit(this);
}
```
</TypeScriptExample>

From a Worker:

<TypeScriptExample>
```
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 });
await sandbox.git.checkoutBranch("feature/my-work");

const branch = await sandbox.git.getCurrentBranch();
const branches = await sandbox.git.listBranches();
```
</TypeScriptExample>

### Migrate from the base class

In the `next` release, `sandbox.gitCheckout()` and the interpreter methods (`createCodeContext`, `runCode`, `runCodeStream`, `listCodeContexts`, `deleteCodeContext`) no longer exist on the base `Sandbox` class. To keep existing call sites working, attach the extension and add a delegate method:

<TypeScriptExample>
```
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withGit } from "@cloudflare/sandbox/git";

export class Sandbox extends BaseSandbox<Env> {
git = withGit(this);
// Optional: preserve the previous flat API.
gitCheckout(url: string, options?: { depth?: number }) {
return this.git.checkout(url, options);
}
}
```
</TypeScriptExample>

## Related resources

- [Architecture](/sandbox/concepts/architecture/): how Workers, Durable Objects, and containers fit together
- [Use code interpreter](/sandbox/guides/code-execution/): running Python and JavaScript via the interpreter extension
- [Git workflows](/sandbox/guides/git-workflows/): cloning and managing repositories via the Git extension
1 change: 1 addition & 0 deletions src/content/docs/sandbox/concepts/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ These pages explain how the Sandbox SDK works, why it's designed the way it is,
- [Preview URLs](/sandbox/concepts/preview-urls/) - How to expose sandboxed services on the public internet.
- [Security model](/sandbox/concepts/security/) - Isolation, validation, and safety mechanisms
- [Terminal connections](/sandbox/concepts/terminal/) - How browser terminal connections work
- [Extensions](/sandbox/concepts/extensions/) - Add opt-in capabilities with the extension framework

## Related resources

Expand Down