Already have code running on E2B? Moving to Tenki is a small, mechanical change. Both give you a disposable cloud sandbox with the same core loop — create → run a command → capture output → dispose — so most scripts port in a few minutes with the official @tenkicloud/sandbox SDK.
This example shows the two scripts side by side, an API map, and a run.mjs you can run against live Tenki.
E2B (e2b):
import { Sandbox } from "e2b";
const sandbox = await Sandbox.create(); // auth via E2B_API_KEY
const r = await sandbox.commands.run("echo hello from e2b"); // runs via a shell
console.log(r.stdout.trim(), "· exit", r.exitCode); // hello from e2b · exit 0
await sandbox.kill();Tenki (@tenkicloud/sandbox):
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox";
const tenki = new TenkiSandbox({ authToken: process.env.TENKI_AUTH_TOKEN });
const sandbox = await tenki.createAndWait({
cpuCores: 1,
memoryMb: 1024,
workspaceId: process.env.TENKI_WORKSPACE_ID,
});
try {
// exec() runs a bare binary + args (no shell). Wrap shell syntax in `sh -c`.
const r = await sandbox.exec("sh", { args: ["-c", "echo hello from tenki"] });
console.log(stdoutText(r).trim(), "· exit", r.exitCode); // hello from tenki · exit 0
} finally {
await sandbox.close();
}| Concept | E2B (e2b) |
Tenki (@tenkicloud/sandbox) |
|---|---|---|
| Install | npm i e2b |
npm i @tenkicloud/sandbox |
| Import | import { Sandbox } from "e2b" |
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox" |
| Auth | implicit E2B_API_KEY env var |
new TenkiSandbox({ authToken }) (+ workspaceId) |
| Create | await Sandbox.create() |
await tenki.createAndWait({ cpuCores, memoryMb, workspaceId }) |
| Run a command | await sandbox.commands.run("ls -la") (shell) |
await sandbox.exec("ls", { args: ["-la"] }) (no shell — see note) |
| stdout / stderr | r.stdout / r.stderr (strings) |
stdoutText(r) / stderrText(r) (decode the byte fields) |
| Exit code | r.exitCode |
r.exitCode |
| Env vars | commands.run(cmd, { envs }) |
exec(cmd, { env }) or createAndWait({ env }) |
| Working dir | commands.run(cmd, { cwd }) |
exec(cmd, { cwd }) |
| Write a file | await sandbox.files.write(path, data) |
await sandbox.writeFile(path, data) |
| Read a file | await sandbox.files.read(path) |
await sandbox.readFile(path) |
| Sandbox ID | sandbox.sandboxId |
sandbox.id |
| Reconnect | await Sandbox.connect(id) |
await tenki.get(id) |
| List sandboxes | await Sandbox.list() |
await tenki.list() |
| Dispose | await sandbox.kill() |
await sandbox.close() (or await using auto-disposes) |
This is the change that bites most ports. E2B's commands.run("echo $HOME && ls") runs your string through a shell, so pipes, globs, &&, and $VARS just work. Tenki's exec(command, { args }) runs a bare binary plus its argument array — nothing is interpreted.
// E2B
await sandbox.commands.run("echo $HOME && ls /tmp");
// Tenki — ask for a shell explicitly
await sandbox.exec("sh", { args: ["-c", "echo $HOME && ls /tmp"] });
// Tenki — a single binary needs no shell at all
await sandbox.exec("python3", { args: ["-c", "print(6 * 7)"] });npm install
export TENKI_AUTH_TOKEN=... # from `tenki login` (~/.config/tenki/config.yaml)
export TENKI_WORKSPACE_ID=...
node run.mjs # hello from tenki · exit 0verify.mjs is the CI proof: it reads your token from TENKI_AUTH_TOKEN / TENKI_API_KEY or ~/.config/tenki/config.yaml, then does the full loop against live Tenki — create → exec a shell command and a bare binary → assert the captured output → terminate — and exits non-zero on any failure.
node verify.mjs # ✓ create → exec sh + python3 → "hello-from-tenki" / 42 → dispose- Output is bytes.
ExecResult.stdout/.stderrareUint8Array; usestdoutText(r)/stderrText(r)to get strings (E2B hands you strings directly). - Auth is explicit. E2B reads
E2B_API_KEYimplicitly; Tenki takes anauthTokenon the client, plus aworkspaceIdfor placement (fromtenki login). - Disposal.
await sandbox.close()maps to E2B'ssandbox.kill(). BecauseSessionis anAsyncDisposable, you can also writeawait using sandbox = await tenki.createAndWait(...)and let the scope terminate it for you. Either way the microVM is billed per second and self-reaps on its idle / lifetime caps. - Networking is off by default. If your code needs outbound network, pass
createAndWait({ allowOutbound: true }). - Requires Node 20+ for
await using(therun.mjshere uses explicitclose(), so Node 18+ is fine).
Built with the official @tenkicloud/sandbox SDK. Learn more at tenki.cloud.