A Go SDK for the E2B cloud sandbox API. E2B provides lightweight microVMs you can use to safely run arbitrary code in ephemeral environments.
go get github.com/matiasinsaurralde/go-e2b- Go 1.25+
- An E2B API key
package main
import (
"context"
"fmt"
"log"
"os"
e2b "github.com/matiasinsaurralde/go-e2b"
)
func main() {
client, err := e2b.NewClient(e2b.ClientConfig{
APIKey: os.Getenv("E2B_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
sandbox, err := client.NewSandbox(context.Background(), e2b.SandboxConfig{
Template: "base",
})
if err != nil {
log.Fatal(err)
}
defer sandbox.Close()
result, err := sandbox.Commands.Run(context.Background(), "echo hello, world")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Stdout) // hello, world
fmt.Println(result.ExitCode) // 0
}See examples/ for runnable programs:
| Example | Shows |
|---|---|
| basic | Create a sandbox and run a command |
| envvars | Passing environment variables |
| files | Read/write text and binary files |
| sandbox-info | Inspecting sandbox metadata |
| fork | Fork a sandbox into N copies and fan work out |
| volumes | Persistent volumes and mounting them |
| lifecycle | Pause/resume, timeout, and snapshots |
| network | Outbound network policy and allowlists |
client, err := e2b.NewClient(e2b.ClientConfig{
APIKey: "your-api-key", // or set E2B_API_KEY env var
APIBaseURL: "https://api.e2b.app", // optional
SandboxDomain: "e2b.app", // optional
})
if err != nil {
log.Fatal(err)
}
sandbox, err := client.NewSandbox(context.Background(), e2b.SandboxConfig{
Template: "base", // sandbox template (default: "base")
Timeout: 300, // lifetime in seconds (default: 300)
EnvVars: map[string]string{ // environment variables
"MY_VAR": "value",
},
})The base template includes Python 3.11, Node.js 20, npm, Yarn, git, and the GitHub CLI.
ListSandboxesV2 returns running and paused sandboxes. Filter by state,
metadata, template, or start time; sort with order; page with limit /
nextToken. Callers must re-pass the same filters on every page.
result, err := client.ListSandboxesV2(ctx,
e2b.WithSandboxState("running", "paused"),
e2b.WithSandboxTemplate("base"),
e2b.WithSandboxStartedAfter(time.Now().Add(-24*time.Hour)),
e2b.WithSandboxOrder(e2b.OrderDesc),
e2b.WithSandboxLimit(20),
)
if err != nil {
log.Fatal(err)
}
for _, s := range result.Sandboxes {
fmt.Println(s.ID, s.State, s.StartedAt)
}
for result.NextToken != "" {
result, err = client.ListSandboxesV2(ctx,
e2b.WithSandboxState("running", "paused"),
e2b.WithSandboxTemplate("base"),
e2b.WithSandboxStartedAfter(time.Now().Add(-24*time.Hour)),
e2b.WithSandboxOrder(e2b.OrderDesc),
e2b.WithSandboxLimit(20),
e2b.WithSandboxNextToken(result.NextToken),
)
if err != nil {
log.Fatal(err)
}
}Run takes a context.Context and a single shell command string, executed
through a login shell (/bin/bash -l -c), so pipes, redirection, and
environment expansion all work. It blocks until the command finishes.
ctx := context.Background()
// Simple command
result, err := sandbox.Commands.Run(ctx, "python3 -c 'print(1 + 1)'")
// Shell features
result, err = sandbox.Commands.Run(ctx, "echo one two three | wc -w")
// With options
result, err = sandbox.Commands.Run(ctx, "echo $FOO",
e2b.WithEnv(map[string]string{"FOO": "bar"}),
e2b.WithCwd("/tmp"),
e2b.WithUser("root"),
e2b.WithTimeout(30*time.Second),
)
fmt.Println(result.Stdout)
fmt.Println(result.Stderr)
fmt.Println(result.ExitCode)A non-zero exit code returns a *e2b.CommandExitError (the *CommandResult is
still returned so you can inspect the output):
result, err := sandbox.Commands.Run(ctx, "exit 3")
var exitErr *e2b.CommandExitError
if errors.As(err, &exitErr) {
fmt.Println(exitErr.ExitCode) // 3
}Stream stdout/stderr as it is produced via callbacks:
handle, err := sandbox.Commands.Start(ctx, "for i in 1 2 3; do echo $i; sleep 1; done",
e2b.WithOnStdout(func(b []byte) { fmt.Print(string(b)) }),
e2b.WithOnStderr(func(b []byte) { fmt.Fprint(os.Stderr, string(b)) }),
)
if err != nil {
log.Fatal(err)
}
result, err := handle.Wait(ctx) // drives the stream, returns the final result// Start a command in the background.
handle, err := sandbox.Commands.Start(ctx, "cat", e2b.WithStdin(true))
// Send data to its stdin, then signal EOF.
handle.SendStdin(ctx, []byte("hello\n"))
handle.CloseStdin(ctx)
// List running processes and kill one by PID.
procs, _ := sandbox.Commands.List(ctx)
ok, _ := sandbox.Commands.Kill(ctx, procs[0].PID) // false if not found
// Detach without killing, then reattach later by PID.
handle.Disconnect()
reattached, _ := sandbox.Commands.Connect(ctx, handle.PID())
result, err := handle.Wait(ctx)pty, err := sandbox.Pty.Create(ctx, 80, 24, // cols, rows
e2b.WithPtyOnData(func(b []byte) { os.Stdout.Write(b) }),
)
if err != nil {
log.Fatal(err)
}
sandbox.Pty.SendInput(ctx, pty.PID(), []byte("echo $TERM\n"))
sandbox.Pty.Resize(ctx, pty.PID(), 120, 40)
sandbox.Pty.SendInput(ctx, pty.PID(), []byte("exit\n"))
pty.Wait(ctx)sandbox.Filesystem provides read/write plus directory and metadata operations.
Text and byte helpers wrap the streaming Read/Write methods.
fs := sandbox.Filesystem
fs.WriteString(ctx, "/home/user/hello.txt", "hi\n")
content, _ := fs.ReadString(ctx, "/home/user/hello.txt")
entries, _ := fs.List(ctx, "/home/user") // []FileInfo
info, _ := fs.Stat(ctx, "/home/user/hello.txt")
ok, _ := fs.Exists(ctx, "/home/user/hello.txt")
fs.MakeDir(ctx, "/home/user/work") // no-op if it already exists
fs.Rename(ctx, "/home/user/hello.txt", "/home/user/work/hi.txt")
fs.Remove(ctx, "/home/user/work/hi.txt")
// Watch a directory for changes.
watch, _ := fs.WatchDir(ctx, "/home/user", true)
events, _ := watch.GetEvents(ctx)
watch.Stop(ctx)A paused sandbox retains its filesystem and memory, so work resumes exactly where it stopped.
sandbox.SetTimeout(600) // extend lifetime to 10 minutes
sandbox.Pause() // stop running, keep state
running, _ := sandbox.IsRunning() // false
sandbox.Resume(300) // resume with a fresh 5-minute lifetime
// Point-in-time snapshot you can reference later.
snap, _ := sandbox.CreateSnapshot(ctx, "checkpoint")
// Observability.
logs, _ := sandbox.Logs()
metrics, _ := sandbox.Metrics()Fork clones a sandbox from a snapshot into one or more independent copies. Every fork boots from the same state; after branching they no longer share anything.
forks, err := sandbox.Fork(ctx, e2b.WithForkCount(3))
if err != nil {
log.Fatal(err)
}
for i, f := range forks {
if f.Err != nil { // this fork failed to start
continue
}
defer f.Sandbox.Close() // each started fork must be closed
f.Sandbox.Commands.Run(ctx, fmt.Sprintf("echo worker %d", i))
}Volumes are persistent storage that outlives any single sandbox. Write to a volume directly through its content API, or mount it into a sandbox.
vol, _ := client.CreateVolume(ctx, "my-volume")
vol.WriteFileString(ctx, "/config.txt", "data\n")
got, _ := vol.ReadFileString(ctx, "/config.txt")
// Mount it into a sandbox — files appear on the sandbox filesystem.
sandbox, _ := client.NewSandbox(ctx, e2b.SandboxConfig{
Template: "base",
VolumeMounts: []e2b.VolumeMount{vol.AsMount("/mnt/data")},
})
client.DestroyVolume(ctx, vol.VolumeID)Restrict a sandbox's outbound access at creation, or change it on a running
sandbox. UpdateNetwork replaces the entire mutable config rather than merging.
// Allow only example.com; deny everything else.
sandbox, _ := client.NewSandbox(ctx, e2b.SandboxConfig{
Template: "base",
Network: e2b.AllowOutbound("example.com"),
})
// Later, deny all outbound traffic.
sandbox.UpdateNetwork(e2b.NetworkUpdateConfig{
DenyOut: []string{e2b.AllTraffic},
})Look up a template by alias (or namespaced project/name), list tags, and
assign or remove labels on a build. Assign uses a source selector
(alias:existingTag or alias:<buildID>); remove takes the template name,
not name:tag. Never delete the default tag.
alias, err := client.GetTemplateAlias(ctx, "my-template")
if err != nil {
log.Fatal(err)
}
tags, err := client.ListTemplateTags(ctx, alias.TemplateID)
assigned, err := client.AssignTemplateTags(ctx, "my-template:default", "staging")
err = client.RemoveTemplateTags(ctx, "my-template", "staging")A missing alias maps to *TemplateNotFoundError. HTTP 403 (no access to a
public alias such as base) is returned as *Error, not not-found.
| Option | Description |
|---|---|
WithEnv(map[string]string) |
Set environment variables for the command |
WithCwd(string) |
Set the working directory |
WithUser(string) |
Set the user to run the command as (default: user) |
WithTimeout(time.Duration) |
Set the command's maximum lifetime (default: 60s; ≤0 disables) |
WithStdin(bool) |
Keep stdin open for SendStdin |
WithOnStdout(func([]byte)) |
Stream decoded stdout chunks |
WithOnStderr(func([]byte)) |
Stream decoded stderr chunks |
Configuration can be provided via ClientConfig / SandboxConfig fields or environment variables:
| Field | Env Var | Default | Description |
|---|---|---|---|
ClientConfig.APIKey |
E2B_API_KEY |
— | E2B API key (required) |
ClientConfig.APIBaseURL |
E2B_API_URL |
https://api.e2b.app |
API base URL |
ClientConfig.SandboxDomain |
E2B_SANDBOX_URL |
e2b.app |
Sandbox domain |
SandboxConfig.Template |
— | base |
Sandbox template ID |
SandboxConfig.Timeout |
— | 300 |
Sandbox lifetime in seconds |
import e2b "github.com/matiasinsaurralde/go-e2b"
_, err := e2b.NewClient(e2b.ClientConfig{APIKey: apiKey})
switch {
case errors.As(err, &e2b.SandboxNotFoundError{}):
// sandbox not found
case errors.As(err, &e2b.CommandExitError{}):
// command ran but exited non-zero
case errors.As(err, &e2b.TimeoutError{}):
// operation timed out
case errors.As(err, &e2b.Error{}):
// generic E2B error
}The Connect RPC client is generated from e2b-dev/infra proto definitions using buf.
# Install tools
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
# Regenerate
make generate
# Sync proto from upstream
make proto-sync
# Upgrade to latest upstream commit
make proto-upgradeSee DEVELOPMENT.md for details.