diff --git a/README.md b/README.md
index 2401d7d5..07768232 100644
--- a/README.md
+++ b/README.md
@@ -264,6 +264,12 @@ Read more about E2B on the [E2B website](https://e2b.dev/?utm_source=github&utm_
TypeScript |
+ | Agent Client Protocol |
+ Drive OpenCode, goose or Claude Code inside a Sandbox with an ACP client |
+ Python |
+ TypeScript |
+
+
| Sandbox Agent SDK |
Run Sandbox Agent inside E2B and connect with the SDK |
- |
diff --git a/examples/acp-agents-js/.env.template b/examples/acp-agents-js/.env.template
new file mode 100644
index 00000000..11cbf9ca
--- /dev/null
+++ b/examples/acp-agents-js/.env.template
@@ -0,0 +1,5 @@
+E2B_API_KEY=
+# For the opencode and goose agents
+OPENAI_API_KEY=
+# For the claude agent
+ANTHROPIC_API_KEY=
diff --git a/examples/acp-agents-js/README.md b/examples/acp-agents-js/README.md
new file mode 100644
index 00000000..ac067db9
--- /dev/null
+++ b/examples/acp-agents-js/README.md
@@ -0,0 +1,38 @@
+# ACP coding agents in E2B Sandbox (TypeScript)
+
+Run a coding agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), such as OpenCode, goose or Claude Code, inside an E2B sandbox and drive it from your own code with the official ACP client.
+
+The same client works for all three agents: only the sandbox template and the command that starts the agent change.
+
+| `AGENT` | Template | Command | Key |
+|---|---|---|---|
+| `opencode` (default) | `opencode` | `opencode acp` | `OPENAI_API_KEY` |
+| `goose` | `goose` | `goose acp --with-builtin developer` | `OPENAI_API_KEY` |
+| `claude` | `claude` | `claude-agent-acp` | `ANTHROPIC_API_KEY` |
+
+## How it works
+
+The example creates a sandbox from the agent's template, then `ACPAgent.start()` from [`@e2b/acp`](https://www.npmjs.com/package/@e2b/acp) starts the agent inside it and connects your ACP client, built with `client()` from `@agentclientprotocol/sdk`, over the agent's stdin and stdout:
+
+```ts
+const agent = await ACPAgent.start(sandbox, 'opencode acp', app, { cwd, envs })
+
+const acp = agent.connection.agent
+await acp.request(methods.agent.initialize, { protocolVersion: PROTOCOL_VERSION })
+const { sessionId } = await acp.request(methods.agent.session.new, { cwd, mcpServers: [] })
+await acp.request(methods.agent.session.prompt, { sessionId, prompt: [{ type: 'text', text: '...' }] })
+
+await agent.stop()
+```
+
+`ACPAgent.start()` turns off the client filesystem and terminal capabilities in `initialize` and rejects agent requests for them, so the agent edits files and runs commands with its own tools inside the sandbox, never on the machine running your client. It does not restrict the agent's network access or the MCP servers you pass to it.
+
+Your client still answers permission requests and receives the agent's messages and tool calls. This example approves each request with its `allow_once` option and prints the agent's progress.
+
+## How to run
+
+1. Copy `.env.template` to `.env` and fill in `E2B_API_KEY` and the key for the agent you use.
+2. Install dependencies with `npm install`.
+3. Run `npm start`, or pick another agent with `AGENT=goose npm start` or `AGENT=claude npm start`.
+
+The agent writes and runs `fizzbuzz.py` in the sandbox, then the example runs the same file again and checks that it prints `FizzBuzz`, which shows the result lives in the sandbox.
diff --git a/examples/acp-agents-js/package.json b/examples/acp-agents-js/package.json
new file mode 100644
index 00000000..40d6e1fb
--- /dev/null
+++ b/examples/acp-agents-js/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "acp-agents",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "tsx src/index.ts"
+ },
+ "dependencies": {
+ "@agentclientprotocol/sdk": "^1.8.0",
+ "@e2b/acp": "^0.1.0",
+ "dotenv": "^17.4.2",
+ "e2b": "^2.56.1",
+ "tsx": "^4.19.4"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/examples/acp-agents-js/src/index.ts b/examples/acp-agents-js/src/index.ts
new file mode 100644
index 00000000..46e71ff0
--- /dev/null
+++ b/examples/acp-agents-js/src/index.ts
@@ -0,0 +1,96 @@
+import 'dotenv/config'
+import { Sandbox } from 'e2b'
+import { ACPAgent } from '@e2b/acp'
+import { client, methods, PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
+
+// Each agent is a sandbox template plus the command that starts it in ACP mode.
+const agents = {
+ opencode: {
+ template: 'opencode',
+ command: 'opencode acp',
+ envs: {
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '',
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({ model: 'openai/gpt-5.6-luna' }),
+ },
+ },
+ goose: {
+ template: 'goose',
+ command: 'goose acp --with-builtin developer',
+ envs: {
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '',
+ GOOSE_PROVIDER: 'openai',
+ GOOSE_MODEL: 'gpt-5.6-luna',
+ },
+ },
+ claude: {
+ template: 'claude',
+ command: 'claude-agent-acp',
+ envs: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '' },
+ },
+}
+
+const name = (process.env.AGENT ?? 'opencode') as keyof typeof agents
+const agentConfig = agents[name]
+if (!agentConfig) {
+ throw new Error(`Unknown AGENT "${name}", use one of: ${Object.keys(agents).join(', ')}`)
+}
+
+// The ACP client: it shows the agent's progress and answers permission requests.
+// File edits and shell commands happen inside the sandbox, not on this machine.
+const app = client({ name: 'e2b-acp-cookbook' })
+ .onRequest(methods.client.session.requestPermission, async ({ params: { toolCall, options } }) => {
+ const allow = options.find((o) => o.kind === 'allow_once') ?? options[0]
+ console.log(`\n[permission] ${toolCall.title} -> ${allow.name}`)
+ return { outcome: { outcome: 'selected', optionId: allow.optionId } }
+ })
+ .onNotification(methods.client.session.update, ({ params: { update } }) => {
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
+ process.stdout.write(update.content.text)
+ } else if (update.sessionUpdate === 'tool_call') {
+ console.log(`\n[tool] ${update.title}`)
+ }
+ })
+
+const sandbox = await Sandbox.create(agentConfig.template, { timeoutMs: 10 * 60_000 })
+console.log(`Sandbox ${sandbox.sandboxId} with ${name}`)
+
+try {
+ const cwd = '/home/user/project'
+ await sandbox.files.makeDir(cwd)
+
+ // Start the agent inside the sandbox and connect an ACP client to it.
+ const agent = await ACPAgent.start(sandbox, agentConfig.command, app, {
+ cwd,
+ envs: agentConfig.envs,
+ })
+ // Requests to the agent go through agent.connection.agent.
+ const acp = agent.connection.agent
+
+ const { agentInfo } = await acp.request(methods.agent.initialize, {
+ protocolVersion: PROTOCOL_VERSION,
+ })
+ console.log(`Connected to ${agentInfo?.name} ${agentInfo?.version}`)
+
+ const { sessionId } = await acp.request(methods.agent.session.new, { cwd, mcpServers: [] })
+ const { stopReason } = await acp.request(methods.agent.session.prompt, {
+ sessionId,
+ prompt: [
+ {
+ type: 'text',
+ text: 'Create fizzbuzz.py that prints FizzBuzz for 1 to 15, run it with python3 and show the output.',
+ },
+ ],
+ })
+ console.log(`\n\nTurn finished: ${stopReason}`)
+
+ // The agent worked inside the sandbox, so its result is there.
+ const result = await sandbox.commands.run(`python3 ${cwd}/fizzbuzz.py`)
+ console.log(`fizzbuzz.py in the sandbox prints:\n${result.stdout}`)
+ if (!result.stdout.includes('FizzBuzz')) {
+ throw new Error('fizzbuzz.py did not print FizzBuzz')
+ }
+
+ await agent.stop()
+} finally {
+ await sandbox.kill()
+}
diff --git a/examples/acp-agents-python/.env.template b/examples/acp-agents-python/.env.template
new file mode 100644
index 00000000..11cbf9ca
--- /dev/null
+++ b/examples/acp-agents-python/.env.template
@@ -0,0 +1,5 @@
+E2B_API_KEY=
+# For the opencode and goose agents
+OPENAI_API_KEY=
+# For the claude agent
+ANTHROPIC_API_KEY=
diff --git a/examples/acp-agents-python/.gitignore b/examples/acp-agents-python/.gitignore
new file mode 100644
index 00000000..685485c2
--- /dev/null
+++ b/examples/acp-agents-python/.gitignore
@@ -0,0 +1,10 @@
+# Virtual environments
+.venv
+
+# Environment files
+.env
+
+# Python caches
+__pycache__
+.pytest_cache
+.ruff_cache
diff --git a/examples/acp-agents-python/.python-version b/examples/acp-agents-python/.python-version
new file mode 100644
index 00000000..e4fba218
--- /dev/null
+++ b/examples/acp-agents-python/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/examples/acp-agents-python/README.md b/examples/acp-agents-python/README.md
new file mode 100644
index 00000000..2f5583be
--- /dev/null
+++ b/examples/acp-agents-python/README.md
@@ -0,0 +1,38 @@
+# ACP coding agents in E2B Sandbox (Python)
+
+Run a coding agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), such as OpenCode, goose or Claude Code, inside an E2B sandbox and drive it from your own code with the official ACP client (`agent-client-protocol`).
+
+The same client works for all three agents: only the sandbox template and the command that starts the agent change.
+
+| `AGENT` | Template | Command | Key |
+|---|---|---|---|
+| `opencode` (default) | `opencode` | `opencode acp` | `OPENAI_API_KEY` |
+| `goose` | `goose` | `goose acp --with-builtin developer` | `OPENAI_API_KEY` |
+| `claude` | `claude` | `claude-agent-acp` | `ANTHROPIC_API_KEY` |
+
+## How it works
+
+The example creates a sandbox from the agent's template, then `AsyncACPAgent.start()` from [`e2b-acp`](https://pypi.org/project/e2b-acp/) starts the agent inside it and connects your ACP client over the agent's stdin and stdout:
+
+```python
+agent = await AsyncACPAgent.start(sandbox, "opencode acp", client, cwd=cwd, envs=envs)
+
+await agent.connection.initialize(protocol_version=acp.PROTOCOL_VERSION)
+session = await agent.connection.new_session(cwd=cwd, mcp_servers=[])
+await agent.connection.prompt(session_id=session.session_id, prompt=[acp.text_block("...")])
+
+await agent.stop()
+```
+
+`AsyncACPAgent.start()` turns off the client filesystem and terminal capabilities in `initialize` and rejects agent requests for them, so the agent edits files and runs commands with its own tools inside the sandbox, never on the machine running your client. It does not restrict the agent's network access or the MCP servers you pass to it.
+
+Your client still answers permission requests and receives the agent's messages and tool calls. This example approves each request with its `allow_once` option and prints the agent's progress.
+
+The ACP Python SDK is asynchronous, so the example uses `AsyncSandbox`.
+
+## How to run
+
+1. Copy `.env.template` to `.env` and fill in `E2B_API_KEY` and the key for the agent you use.
+2. Run `uv run main.py`, or pick another agent with `AGENT=goose uv run main.py` or `AGENT=claude uv run main.py`.
+
+The agent writes and runs `fizzbuzz.py` in the sandbox, then the example runs the same file again and checks that it prints `FizzBuzz`, which shows the result lives in the sandbox.
diff --git a/examples/acp-agents-python/main.py b/examples/acp-agents-python/main.py
new file mode 100644
index 00000000..99b867dc
--- /dev/null
+++ b/examples/acp-agents-python/main.py
@@ -0,0 +1,109 @@
+import asyncio
+import json
+import os
+
+import acp
+from acp import schema
+from dotenv import load_dotenv
+
+from e2b import AsyncSandbox
+from e2b_acp import AsyncACPAgent
+
+load_dotenv()
+
+# Each agent is a sandbox template plus the command that starts it in ACP mode.
+AGENTS = {
+ "opencode": {
+ "template": "opencode",
+ "command": "opencode acp",
+ "envs": {
+ "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
+ "OPENCODE_CONFIG_CONTENT": json.dumps({"model": "openai/gpt-5.6-luna"}),
+ },
+ },
+ "goose": {
+ "template": "goose",
+ "command": "goose acp --with-builtin developer",
+ "envs": {
+ "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
+ "GOOSE_PROVIDER": "openai",
+ "GOOSE_MODEL": "gpt-5.6-luna",
+ },
+ },
+ "claude": {
+ "template": "claude",
+ "command": "claude-agent-acp",
+ "envs": {"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", "")},
+ },
+}
+
+
+class Client:
+ """The ACP client: it shows the agent's progress and answers permission requests.
+
+ File edits and shell commands happen inside the sandbox, not on this machine.
+ """
+
+ async def request_permission(self, session_id, tool_call, options, **kwargs):
+ allow = next((o for o in options if o.kind == "allow_once"), options[0])
+ print(f"\n[permission] {tool_call.title} -> {allow.name}")
+ return schema.RequestPermissionResponse(
+ outcome=schema.AllowedOutcome(outcome="selected", option_id=allow.option_id)
+ )
+
+ async def session_update(self, session_id, update, **kwargs):
+ if (
+ isinstance(update, schema.AgentMessageChunk)
+ and update.content.type == "text"
+ ):
+ print(update.content.text, end="", flush=True)
+ elif isinstance(update, schema.ToolCallStart):
+ print(f"\n[tool] {update.title}")
+
+
+async def main():
+ name = os.getenv("AGENT", "opencode")
+ if name not in AGENTS:
+ raise SystemExit(f'Unknown AGENT "{name}", use one of: {", ".join(AGENTS)}')
+ config = AGENTS[name]
+
+ sandbox = await AsyncSandbox.create(config["template"], timeout=600)
+ print(f"Sandbox {sandbox.sandbox_id} with {name}")
+ try:
+ cwd = "/home/user/project"
+ await sandbox.files.make_dir(cwd)
+
+ # Start the agent inside the sandbox and connect an ACP client to it.
+ agent = await AsyncACPAgent.start(
+ sandbox, config["command"], Client(), cwd=cwd, envs=config["envs"]
+ )
+ connection = agent.connection
+
+ init = await connection.initialize(protocol_version=acp.PROTOCOL_VERSION)
+ print(f"Connected to {init.agent_info.name} {init.agent_info.version}")
+
+ session = await connection.new_session(cwd=cwd, mcp_servers=[])
+ response = await connection.prompt(
+ session_id=session.session_id,
+ prompt=[
+ acp.text_block(
+ "Create fizzbuzz.py that prints FizzBuzz for 1 to 15, "
+ "run it with python3 and show the output."
+ )
+ ],
+ )
+ print(f"\n\nTurn finished: {response.stop_reason}")
+
+ # The agent worked inside the sandbox, so its result is there.
+ result = await sandbox.commands.run(f"python3 {cwd}/fizzbuzz.py")
+ print(f"fizzbuzz.py in the sandbox prints:\n{result.stdout}")
+ if "FizzBuzz" not in result.stdout:
+ raise RuntimeError("fizzbuzz.py did not print FizzBuzz")
+
+ await agent.stop()
+ finally:
+ await sandbox.kill()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/acp-agents-python/pyproject.toml b/examples/acp-agents-python/pyproject.toml
new file mode 100644
index 00000000..9d4abfc5
--- /dev/null
+++ b/examples/acp-agents-python/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "acp-agents-python"
+version = "0.1.0"
+description = "Drive ACP coding agents (OpenCode, goose, Claude Code) running inside an E2B sandbox"
+readme = "README.md"
+requires-python = ">=3.11"
+dependencies = [
+ "agent-client-protocol>=0.12.1,<0.13",
+ "e2b>=2.56.1",
+ "e2b-acp>=0.1.0",
+ "python-dotenv>=1.1,<2",
+]
diff --git a/tests/run-examples.ts b/tests/run-examples.ts
index 53546c5f..456c49d5 100644
--- a/tests/run-examples.ts
+++ b/tests/run-examples.ts
@@ -72,6 +72,8 @@ const scripts: {
// openai 7.33 (the first release with openai/helpers/computer) requires Node 22.
{ name: 'openai-computer-use-python', interpreter: 'uv', file: './examples/openai-computer-use-python/', entrypoint: '1-run.py' },
{ name: 'openai-computer-use-js', interpreter: 'npm', file: './examples/openai-computer-use-js/', nodeVersion: '22.22.0' },
+ { name: 'acp-agents-js', interpreter: 'npm', file: './examples/acp-agents-js/', nodeVersion: '22.22.0' },
+ { name: 'acp-agents-python', interpreter: 'uv', file: './examples/acp-agents-python/' },
{ name: 'playwright-in-e2b', interpreter: 'npm', file: './examples/playwright-in-e2b/' },
{ name: 'anthropic-claude-code-in-sandbox-js', interpreter: 'npm', file: './examples/anthropic-claude-code-in-sandbox-js/' },
{ name: 'anthropic-claude-code-in-sandbox-python', interpreter: 'uv', file: './examples/anthropic-claude-code-in-sandbox-python/', entrypoint: 'python -m anthropic_claude_code_in_sandbox.main' },