From 1fc014a7dd8102045d22880c2c9df5c7bb7363b0 Mon Sep 17 00:00:00 2001 From: Tomas Beran Date: Wed, 29 Jul 2026 15:59:20 +0200 Subject: [PATCH 1/2] docs(commands): document reconnecting to a background command --- docs/commands/background.mdx | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/commands/background.mdx b/docs/commands/background.mdx index cf12e8e2..ab03b2f5 100644 --- a/docs/commands/background.mdx +++ b/docs/commands/background.mdx @@ -43,3 +43,71 @@ for stdout, stderr, _ in command: command.kill() ``` + +## Reconnect to a background command + +A background command keeps running inside the sandbox even after the SDK disconnects. Because sandboxes are long-lived, you can start a long task in one process, return immediately, and reconnect from a completely different process later to wait for it and collect the result. + +This is a common pattern for kicking off slow work (code generation, a build, a data job) from a serverless function or API handler: you start the command, store the sandbox ID and process ID, and let a later request pick the work back up. + +The example below starts a long-running command (`run-codegen` here is a placeholder for your own binary or script) and returns the two identifiers you need to reconnect: the sandbox ID and the command's process ID (`pid`). + + +```js JavaScript & TypeScript highlight={11} +import { Sandbox } from 'e2b' + +// Start the task and return right away with the IDs needed to reconnect. +async function startGeneration(prompt: string) { + const sandbox = await Sandbox.create({ timeoutMs: 15 * 60_000 }) + + // Redirect output to a file so it survives after we disconnect. + const handle = await sandbox.commands.run( + `run-codegen ${JSON.stringify(prompt)} > /home/user/gen.log 2>&1`, + { background: true, timeoutMs: 0 } + ) + + return { sandboxId: sandbox.sandboxId, pid: handle.pid } +} + +// From a separate process, reconnect and wait for the result. +async function collectGeneration(sandboxId: string, pid: number) { + const sandbox = await Sandbox.connect(sandboxId) + const handle = await sandbox.commands.connect(pid) + await handle.wait() + return sandbox.files.read('/home/user/gen.log') +} +``` +```python Python highlight={9} +import shlex +from e2b import Sandbox + +# Start the task and return right away with the IDs needed to reconnect. +def start_generation(prompt: str): + sandbox = Sandbox.create(timeout=15 * 60) + + # Redirect output to a file so it survives after we disconnect. + handle = sandbox.commands.run( + f"run-codegen {shlex.quote(prompt)} > /home/user/gen.log 2>&1", + background=True, + timeout=0, + ) + + return {"sandbox_id": sandbox.sandbox_id, "pid": handle.pid} + + +# From a separate process, reconnect and wait for the result. +def collect_generation(sandbox_id: str, pid: int): + sandbox = Sandbox.connect(sandbox_id) + handle = sandbox.commands.connect(pid) + handle.wait() + return sandbox.files.read("/home/user/gen.log") +``` + + +A few details make this reliable across processes: + +- **Redirect output to a file.** The streamed `stdout`/`stderr` from a background command is only delivered to the process that started it. Writing to a file (`> /home/user/gen.log 2>&1`) gives you durable output you can read after reconnecting. +- **Disable the command timeout.** `timeoutMs: 0` (`timeout=0` in Python) removes the command's connection time limit so it isn't killed while running long. Set the sandbox `timeoutMs`/`timeout` high enough to outlast the whole job (15 minutes above). See [sandbox persistence](/docs/sandbox/persistence) if you need it to survive even longer. +- **Persist the IDs.** Return or store `sandboxId` and `pid` (for example in a database or the response of an API call). They are all a later process needs to call `Sandbox.connect()` and `commands.connect()`. + +`commands.connect(pid)` reattaches to the running command, and `handle.wait()` blocks until it finishes. If you don't know the `pid`, you can look up running processes with `sandbox.commands.list()`. From 9e8bad5a181fd2534fb158d692460c3096bd8177 Mon Sep 17 00:00:00 2001 From: Tomas Beran Date: Thu, 30 Jul 2026 13:07:33 +0200 Subject: [PATCH 2/2] docs(commands): pass prompt via env var to avoid shell injection --- docs/commands/background.mdx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/commands/background.mdx b/docs/commands/background.mdx index ab03b2f5..9c6645eb 100644 --- a/docs/commands/background.mdx +++ b/docs/commands/background.mdx @@ -53,17 +53,18 @@ This is a common pattern for kicking off slow work (code generation, a build, a The example below starts a long-running command (`run-codegen` here is a placeholder for your own binary or script) and returns the two identifiers you need to reconnect: the sandbox ID and the command's process ID (`pid`). -```js JavaScript & TypeScript highlight={11} +```js JavaScript & TypeScript highlight={20} import { Sandbox } from 'e2b' // Start the task and return right away with the IDs needed to reconnect. async function startGeneration(prompt: string) { const sandbox = await Sandbox.create({ timeoutMs: 15 * 60_000 }) - // Redirect output to a file so it survives after we disconnect. + // Pass the prompt as an env var, never interpolated into the shell string. + // Write output to a file so a reconnecting process can read it later. const handle = await sandbox.commands.run( - `run-codegen ${JSON.stringify(prompt)} > /home/user/gen.log 2>&1`, - { background: true, timeoutMs: 0 } + 'run-codegen "$PROMPT" > /home/user/gen.log 2>&1', + { background: true, timeoutMs: 0, envs: { PROMPT: prompt } } ) return { sandboxId: sandbox.sandboxId, pid: handle.pid } @@ -77,19 +78,20 @@ async function collectGeneration(sandboxId: string, pid: number) { return sandbox.files.read('/home/user/gen.log') } ``` -```python Python highlight={9} -import shlex +```python Python highlight={22} from e2b import Sandbox # Start the task and return right away with the IDs needed to reconnect. def start_generation(prompt: str): sandbox = Sandbox.create(timeout=15 * 60) - # Redirect output to a file so it survives after we disconnect. + # Pass the prompt as an env var, never interpolated into the shell string. + # Write output to a file so a reconnecting process can read it later. handle = sandbox.commands.run( - f"run-codegen {shlex.quote(prompt)} > /home/user/gen.log 2>&1", + 'run-codegen "$PROMPT" > /home/user/gen.log 2>&1', background=True, timeout=0, + envs={"PROMPT": prompt}, ) return {"sandbox_id": sandbox.sandbox_id, "pid": handle.pid} @@ -107,6 +109,7 @@ def collect_generation(sandbox_id: str, pid: int): A few details make this reliable across processes: - **Redirect output to a file.** The streamed `stdout`/`stderr` from a background command is only delivered to the process that started it. Writing to a file (`> /home/user/gen.log 2>&1`) gives you durable output you can read after reconnecting. +- **Pass untrusted input as an environment variable, not string interpolation.** The prompt is provided through `envs` and referenced as `"$PROMPT"`, so the shell inserts it as a literal value. Interpolating user input directly into the command string (for example with a template literal) would let inputs like `$(...)` or backticks run as commands inside the sandbox. - **Disable the command timeout.** `timeoutMs: 0` (`timeout=0` in Python) removes the command's connection time limit so it isn't killed while running long. Set the sandbox `timeoutMs`/`timeout` high enough to outlast the whole job (15 minutes above). See [sandbox persistence](/docs/sandbox/persistence) if you need it to survive even longer. - **Persist the IDs.** Return or store `sandboxId` and `pid` (for example in a database or the response of an API call). They are all a later process needs to call `Sandbox.connect()` and `commands.connect()`.