Shell Sessions
Long-lived bash sessions inside a sandbox. State persists across commands.
A shell session is a single long-lived bash process inside the
sandbox that survives across many commands. cd, export, shell
variables, and background jobs all persist between calls, so the
per-command fork cost a regular sandbox.exec() pays disappears.
Reach for this primitive when an AI agent runs many small commands
in sequence, cd /repo && git diff && pytest && grep ..., and for
interactive workflows where the user expects state to stick.
Why it's fast
A normal sandbox.exec("ls") does this every call:
- HTTP round-trip from your client to the runner.
- A connection from the runner to the in-sandbox agent.
- The agent forks
sh -c "ls". The shell forksls. lsruns.- The agent reads stdout, sends it back over the runtime channel.
- HTTP response back to your client.
For 100 commands you pay all of that 100 times.
Inside a shell session:
- One WebSocket connection for the whole session.
- One persistent runtime channel between the runner and the agent.
- One long-running
bashprocess inside the VM. Commands stream in over its stdin; output streams out over stdout/stderr. - The agent injects a sentinel after each command so it knows where one command's output ends and the next begins. Exit codes ride along on the sentinel.
Result: each command is a write to the already-running bash, you skip the per-call setup a separate exec pays every time.
Open a session over WebSocket
The TypeScript SDK ships a sandbox.shell() helper in an upcoming
release. Until it lands, talk to the WebSocket endpoint directly using
your client of choice. The protocol below is the wire format the
helper will speak.
sandbox.shell() isn't in the SDK yet. The WebSocket protocol shown here is stable and is what the helper will wrap once it ships.
import { WebSocket } from 'ws'
import { Isorun } from 'isorun'
const isorun = new Isorun()
const sandbox = await isorun.create({ image: 'python:3.12-slim' })
const ws = new WebSocket(
`${isorun.apiUrl.replace(/^http/, 'ws')}/v1/runs/${sandbox.id}/shell`,
{ headers: { Authorization: `Bearer ${process.env.ISORUN_API_KEY}` } },
)
await new Promise<void>((resolve, reject) => {
ws.once('open', () => resolve())
ws.once('error', reject)
})
function run(id: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
return new Promise((resolve) => {
let stdout = '', stderr = ''
const handler = (raw: any) => {
const msg = JSON.parse(raw.toString())
if (msg.id !== id) return
if (msg.type === 'shell_out') stdout += msg.data
else if (msg.type === 'shell_err') stderr += msg.data
else if (msg.type === 'shell_exit') {
ws.off('message', handler)
resolve({ exitCode: msg.code, stdout, stderr })
}
}
ws.on('message', handler)
ws.send(JSON.stringify({ type: 'shell_run', id, command }))
})
}
try {
await run('r1', 'cd /tmp') // cwd persists
await run('r2', 'export FOO=bar') // env persists
const r = await run('r3', 'echo $FOO')
console.log(r.stdout) // bar
} finally {
ws.close()
await sandbox.destroy()
}What persists across commands
- cwd (
cdworks) - environment variables (
export FOO=bar) - shell variables (
X=42; echo $X) - shell functions and aliases
- background jobs (
./long-running &keeps running) - shell history (in-memory)
What doesn't persist: anything that requires re-launching the shell
itself (exec to replace the shell, exit). If your command kills
the shell, the session ends and the client receives a shell_closed
frame.
Streaming output
The shell streams stdout and stderr separately and incrementally, you
don't have to wait for the command to exit to see partial output. The
WebSocket frames are shell_out and shell_err chunks that arrive in
real time. The helper class will surface this as a callback API; the
raw WebSocket above already gives you the chunks.
Limits
| Constraint | Value |
|---|---|
| Sessions per sandbox | unbounded; each is one bash process |
| Session timeout | sandbox lifetime (or until you close the WebSocket) |
| Per-command overhead | minimal: one write to the persistent bash |
| Bash death detection | shell_closed frame surfaces immediately |
Choose between exec and shell
Use sandbox.exec() | Use the shell session |
|---|---|
| One-off command per sandbox | Many commands in sequence |
| You want command isolation | You want shared cwd / env / state |
| Don't care about per-call latency | Latency matters (agent loop, REPL) |
| Stateless tools | Stateful workflows |
Next steps
- Create and execute, the stateless
execpath. - Computer use (CDP + VNC), another long-lived in-sandbox service over WebSocket.
- TypeScript SDK, full method reference.