> ## Documentation Index
> Fetch the complete documentation index at: https://e2b.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Flue

> Give a Flue agent an E2B sandbox with the flue add sandbox e2b blueprint adapter.

[Flue](https://flueframework.com) is a TypeScript agent framework from the Astro team. An agent is a function marked with `'use agent'` and configured with React-like hooks: `useModel()` picks the LLM, `useSandbox()` attaches a workspace. That second hook is what gives the agent its `read`, `write`, `edit`, `bash`, `grep`, and `glob` tools — without it the agent has no filesystem and no shell at all.

Flue's [E2B adapter](https://flueframework.com/docs/ecosystem/sandboxes/e2b/) points those tools at an E2B sandbox. It ships as a **blueprint**: `flue add sandbox e2b` prints a Markdown implementation guide that your coding agent applies, leaving the adapter in your repo as `src/sandboxes/e2b.ts`. There is no `@e2b/flue` package — the only E2B dependency is the [`e2b`](/docs/sdk-reference/js-sdk) SDK.

<Note>
  Flue adapters are deliberately thin. Your application creates the E2B sandbox and decides when it dies; Flue wraps the handle you hand it and never destroys provider infrastructure.
</Note>

## Why E2B as the sandbox

Flue ships two built-in environments — an in-memory [just-bash](https://github.com/vercel-labs/just-bash) sandbox and `local()`, which binds the agent to your host.

|                             | E2B                                                                                                                             | `bash()` virtual sandbox                       | `local()`                          |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------- |
| Where commands run          | Firecracker microVM in E2B's cloud                                                                                              | Emulated in your Node.js process               | Your host, as real processes       |
| Isolation                   | Full VM boundary                                                                                                                | No host access, but no real processes either   | None, by design                    |
| Toolchain                   | Debian 12, `apt`, any runtime you install or bake into a [template](/docs/template/quickstart)                                  | The unix subset just-bash implements           | Whatever the host already has      |
| Filesystem lifetime         | Lives with the sandbox: up to 24 hours running (1 hour on the Base tier), indefinitely once [paused](/docs/sandbox/persistence) | Rebuilt empty every time the agent initializes | The host disk                      |
| Per-conversation workspaces | One sandbox keyed on the agent instance id                                                                                      | Ephemeral                                      | Every conversation shares one host |

Use E2B when agent-written code must not touch your host, when the work needs a real Linux toolchain, or when each conversation needs a workspace that outlives the process that started it.

## Prerequisites

* Node.js 22.19 or later
* An [E2B API key](https://e2b.dev/dashboard?tab=keys)
* A model provider key — this guide uses Anthropic

## Create a project

Skip to the next section if you already have a Flue project.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
mkdir flue-e2b-agent && cd flue-e2b-agent
npx @flue/cli@latest init . --target node
npm install
npm install e2b
```

`flue init` writes `flue.config.ts`, `package.json`, `tsconfig.json`, `.env`, `src/agents/hello.ts`, `src/db.ts`, `AGENTS.md`, and `README.md`. It does not install anything, hence the separate `npm install`.

Put both keys in `.env` — `flue run` loads it automatically, and the E2B SDK reads `E2B_API_KEY` from the environment:

```bash title=".env" theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
E2B_API_KEY="e2b_***"
ANTHROPIC_API_KEY="sk-ant-***"
```

## Add the adapter

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npx flue add sandbox e2b --print | claude
```

`flue add` is not a package installer. It fetches the blueprint from Flue's registry and prints it; the coding agent you pipe it into writes the file. Swap `claude` for `codex`, `opencode`, or whatever you use — or read the guide yourself with `npx flue add sandbox e2b --print | less`. Inside a coding agent session the CLI detects that and writes to stdout without `--print`.

The result is `src/sandboxes/e2b.ts`, tagged `// flue-blueprint: sandbox/e2b@1`, exporting `e2b(sandbox)` — a `SandboxFactory` rooted at `/home/user`. It implements Flue's `SandboxDriver` directly against the E2B SDK rather than shelling out:

| Flue driver operation       | E2B call                                                                                                       |
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `readFile`, `writeFile`     | `sandbox.files.read`, `sandbox.files.write`                                                                    |
| `readdir`, `exists`, `stat` | `sandbox.files.list`, `.exists`, `.getInfo`                                                                    |
| `mkdir`                     | `sandbox.files.makeDir` (always recursive on E2B)                                                              |
| `rm`                        | `sandbox.files.remove` — throws on `recursive` or `force`, which E2B's direct remove API has no equivalent for |
| `exec`                      | `sandbox.commands.run`, with `timeoutMs` forwarded unchanged                                                   |

To pull in a later revision of the adapter, pipe `npx flue update sandbox e2b --print` into your coding agent the same way; the marker comment is how the guide recognizes an existing install.

## Write the agent

The factory is where you own the sandbox: create it with the E2B SDK, then hand it to `e2b()`.

```typescript title="src/agents/analyst.ts" theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
'use agent';
import { useModel, useSandbox } from '@flue/runtime';
import { Sandbox } from 'e2b';
import { e2b } from '../sandboxes/e2b.ts';

export function Analyst() {
	useModel('anthropic/claude-sonnet-4-6');

	useSandbox({
		// Lazy: createSandbox() runs once, when the runtime initializes the
		// agent — not on every render.
		async createSandbox(options) {
			const sandbox = await Sandbox.create({
				timeoutMs: 10 * 60_000,
				metadata: { framework: 'flue', agent: 'analyst' },
			});
			console.error(`[e2b] sandbox ${sandbox.sandboxId}`);
			return e2b(sandbox).createSandbox(options);
		},
	});

	return 'You work in a Linux sandbox. Use bash for anything about the machine or its files, and quote the exact command output in your answer.';
}
```

Two details that matter:

* **`createSandbox` is called once per agent initialization**, never on a re-render. Creating the sandbox inside it — not in the module body — is what keeps one sandbox per conversation instead of one per render.
* **`timeoutMs` is E2B's, not Flue's.** The default sandbox timeout is 5 minutes; a conversational agent usually wants more. Nothing in Flue extends it for you.

## Run it

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npx flue run src/agents/analyst.ts --message "Which Linux kernel is this box running, and how much free disk does /home/user have? Run the commands."
```

Flue streams tool activity to stderr and the final reply to stdout:

```
[e2b] sandbox ic1av8osjd5gtxrsegyfz
tool bash
tool done bash
  1. Linux Kernel: 6.1.158+
  2. Free Disk Space for /home/user: 22 GB total, 1.3 GB used, 20 GB available
```

The sandbox is now in your [E2B dashboard](https://e2b.dev/dashboard). The metadata you set at creation is how you find it again from the SDK:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { Sandbox } from 'e2b'

const [sandbox] = await Sandbox.list({
  query: { metadata: { framework: 'flue' }, state: ['running', 'paused'] },
}).nextItems()

console.log(sandbox.sandboxId, sandbox.state) // ic1av8osjd5gtxrsegyfz running
```

## Keep the same sandbox across turns

Flue conversations persist across `flue run` invocations. Sandboxes do not, unless you make them: the default factory above creates a fresh one every time the agent initializes. `createSandbox(options)` receives `options.id` — the agent instance id — so store it in metadata and look it up first.

```typescript title="src/agents/durable.ts" theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
'use agent';
import { useModel, useSandbox } from '@flue/runtime';
import { Sandbox } from 'e2b';
import { e2b } from '../sandboxes/e2b.ts';

export function Durable() {
	useModel('anthropic/claude-sonnet-4-6');

	useSandbox({
		async createSandbox(options) {
			// `options.id` is the agent instance id — key the sandbox on it so
			// the next message in this conversation gets the same workspace.
			const [existing] = await Sandbox.list({
				query: { metadata: { flueInstance: options.id }, state: ['running', 'paused'] },
			}).nextItems();

			// connect() never shortens the window; passing timeoutMs extends it
			// back to a full 10 minutes for this turn.
			const sandbox = existing
				? await Sandbox.connect(existing.sandboxId, { timeoutMs: 10 * 60_000 })
				: await Sandbox.create({
						timeoutMs: 10 * 60_000,
						metadata: { flueInstance: options.id },
					});

			console.error(`[e2b] ${existing ? 'reconnected' : 'created'} ${sandbox.sandboxId}`);
			return e2b(sandbox).createSandbox(options);
		},
	});

	return 'You work in a Linux sandbox at /home/user. Use bash.';
}
```

Two runs of the same conversation id now share one workspace, across separate processes:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npx flue run src/agents/durable.ts --id demo-1 --message "Write the text 'hello from turn one' to /home/user/note.txt, then confirm."
# [e2b] created ivk69fg95t9bndyf9odko

npx flue run src/agents/durable.ts --id demo-1 --message "cat /home/user/note.txt and quote it exactly."
# [e2b] reconnected ivk69fg95t9bndyf9odko
# > hello from turn one
```

Including `'paused'` in the state filter covers longer gaps: `Sandbox.connect()` resumes a [paused](/docs/sandbox/persistence) sandbox with its filesystem intact, so a conversation can go quiet for days and come back to the same disk.

Reconnecting can only ever lengthen a sandbox's window. `connect()` keeps whatever time is left and raises it to the requested timeout — the 5-minute default, or the `timeoutMs` you pass — when the remainder is shorter than that. A sandbox with 20 minutes left keeps its 20 minutes either way.

## Use a custom template

`Sandbox.create()` takes a [template](/docs/template/quickstart) name or ID as its first argument. Bake the language runtimes, system packages, and repo checkout your agent needs into a template so no turn is spent on `apt-get`:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const sandbox = await Sandbox.create('your-template-name', {
  timeoutMs: 10 * 60_000,
})
```

## Lifecycle is your job

The adapter never creates or kills anything — it only wraps the sandbox you pass in. What that leaves you:

* **Timeout is the backstop.** `timeoutMs` at creation decides how long an unattended sandbox lives; the sandbox dies at that point unless you [pause](/docs/sandbox/persistence) it.
* **Nothing kills the sandbox when the agent finishes.** `flue run` exits and the sandbox keeps running. Call `sandbox.kill()` from a harness tool, from your server's teardown, or from a sweep keyed on metadata.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { Sandbox } from 'e2b'

for (const sandbox of await Sandbox.list({
  query: { metadata: { framework: 'flue' }, state: ['running', 'paused'] },
}).nextItems()) {
  await Sandbox.kill(sandbox.sandboxId)
}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="A failing command shows the agent only 'exit status 1'">
    The blueprint's `exec()` calls `sandbox.commands.run()` without a `try/catch`, and the E2B SDK **throws** `CommandExitError` on any non-zero exit instead of returning a result. Flue turns that throw into a bare tool error, so the model sees `<error>exit status 1</error>` and never sees stderr.

    Unwrap the error into the result the `SandboxDriver` contract expects:

    ```typescript title="src/sandboxes/e2b.ts" theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
    import { CommandExitError } from 'e2b';

    // inside E2BSandboxDriver.exec()
    try {
      const result = await this.sandbox.commands.run(command, {
        cwd: options?.cwd,
        envs: options?.env,
        timeoutMs: options?.timeoutMs,
      });
      return {
        stdout: result.stdout ?? '',
        stderr: result.stderr ?? '',
        exitCode: result.exitCode ?? 0,
      };
    } catch (error) {
      if (error instanceof CommandExitError) {
        return {
          stdout: error.stdout ?? '',
          stderr: error.stderr ?? '',
          exitCode: error.exitCode ?? 1,
        };
      }
      throw error;
    }
    ```

    The same `cat` of a missing file then reaches the model as `cat: /home/user/does-not-exist.txt: No such file or directory` plus the exit code — which is what it needs to fix its own command.
  </Accordion>

  <Accordion title="AuthenticationError: API key is required">
    ```
    AuthenticationError: API key is required, please visit the API Keys tab at
    https://e2b.dev/dashboard?tab=keys to get your API key.
    ```

    The E2B SDK reads `E2B_API_KEY` from `process.env`. `flue run` loads the project's `.env` for you (`--env <file>` picks a different one). Anywhere else — a dev server, a built server, CI — put the key in the process environment yourself, or pass it explicitly with `Sandbox.create({ apiKey: process.env.MY_KEY })`.
  </Accordion>

  <Accordion title="ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX when importing the adapter">
    The blueprint's driver class uses a TypeScript parameter property (`constructor(private sandbox: E2BSandbox) {}`), which Node.js's built-in type stripping refuses to compile — including on Node 24:

    ```
    ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
    ```

    `flue run` compiles the file properly, so agents are unaffected. A standalone script that imports the adapter directly needs `node --experimental-transform-types`.
  </Accordion>

  <Accordion title="rm fails with SandboxOperationUnsupportedError">
    ```
    SandboxOperationUnsupportedError: E2B does not support rm with "recursive".
    ```

    E2B's `files.remove()` has no recursive or force flags, so the adapter rejects both rather than silently ignoring them. When a tree has to go, let the agent run `rm -rf` through `bash` instead — that path goes through `commands.run()` and works.
  </Accordion>
</AccordionGroup>

## How the integration works

| Component              | Responsibility                                                                         |
| ---------------------- | -------------------------------------------------------------------------------------- |
| Flue runtime           | Runs turns, persists the conversation, calls `createSandbox()` once per initialization |
| `useSandbox()`         | Attaches the environment and adds the file and shell tools to the agent                |
| Your factory           | Creates or reconnects the E2B sandbox and owns its lifetime                            |
| `src/sandboxes/e2b.ts` | Maps Flue's `SandboxDriver` onto `sandbox.files` and `sandbox.commands`                |
| E2B template           | Base OS, runtimes, and pre-installed dependencies                                      |

## Learn more

* [Flue: Sandboxes guide](https://flueframework.com/docs/guide/sandboxes/) — `useSandbox()`, the virtual sandbox, and `local()`
* [Flue: E2B adapter](https://flueframework.com/docs/ecosystem/sandboxes/e2b/) — the blueprint's own reference page
* [Flue: Sandbox Adapter API](https://flueframework.com/docs/reference/sandbox-api/) — the `SandboxDriver` contract the adapter implements
* [`flue add`](https://flueframework.com/docs/cli/add/) and [`flue run`](https://flueframework.com/docs/cli/run/) — the two CLI commands used here

## Related guides

<CardGroup cols={3}>
  <Card title="Templates" icon="layer-group" href="/docs/template/quickstart">
    Build custom sandbox templates with pre-installed dependencies
  </Card>

  <Card title="Sandbox persistence" icon="clock" href="/docs/sandbox/persistence">
    Pause, resume, and manage sandbox lifecycle
  </Card>

  <Card title="JavaScript SDK" icon="js" href="/docs/sdk-reference/js-sdk">
    The `e2b` package the adapter is built on
  </Card>
</CardGroup>
