Skip to main content
A Sailbox is a Linux VM on the Sail platform: a full cloud environment designed for long-horizon agents. Create one, then run commands, transfer files, expose ports, and checkpoint or pause it. For a guided walkthrough, see the Sailboxes guide; this page is the API reference. In a language without a Sail SDK, reach for the HTTP API.

Sync and async

  • Python methods are synchronous, and every method that does I/O has an async twin under .aio (await sb.exec.aio(...)). Handles returned by async calls are already async: await sb.exec.aio(...) returns a process whose own methods (wait, cancel, resize) are awaited directly, with no .aio. The one exception to the rule is shell, which is sync-only since it drives your local terminal.
  • TypeScript is async-only: every operation returns a Promise.
  • Rust is async-only, on a Tokio runtime. Synchronous code can drive any call with sail::block_on.

Naming

The three SDKs expose the same operations with each language’s conventions: Python is snake_case with keyword arguments, TypeScript is camelCase with options objects and unit-suffixed durations (timeoutSeconds), and Rust pairs a Sailbox object with explicit argument structs. Parameter tables on this page use the Python spelling; each section’s TypeScript signature shows the real names. Every TypeScript static also accepts an explicitly constructed client in its options object (see Configuration); the signatures on this page omit it.

Attributes

Every Sailbox carries its identity and lifecycle state: sailbox_id, name, and status (running, paused, sleeping, failed, terminated). A Sailbox returned by get or list also carries the monitoring snapshot from that fetch: owning app, image, configured and observed resource usage, timestamps, and its egress policy (see snapshot fields). Treat sailbox_id as the durable handle: store the id, and turn it back into a usable Sailbox any time with Sailbox.get (which fetches fresh state), or without a network call via Sailbox.from_id / Sailbox.fromId / client.sailbox(id). The get result reflects the Sailbox at the time of the call; call it again for fresh state.

Sailbox.create

Creates a new Sailbox. The SDK builds any custom image first, then blocks until the VM is running or creation fails. size is a resource ceiling, not a billing reservation; ongoing Sailbox billing uses your actual CPU, memory, and disk consumption. Each size also has a one-time creation charge. memory_limit_gib and disk_limit_gib optionally tune the size’s ceilings, in whole GiB within the size’s range; raising a ceiling does not increase the ongoing rate, and lowering one caps what the Sailbox can use. See Sailbox pricing.
Volumes are currently in Alpha. To pilot them, reach out in the Sail Slack.
Returns a running Sailbox. Raises a creation error when Sail cannot create the Sailbox, a permission error on 401/403, and an invalid-argument error for an unknown size or invalid ingress ports and volume mounts.
In Python, pin image=sail.Image.debian_arm64 or debian_amd64 when using @sail.function; those images match your local Python version so function bytecode can deserialize in the guest. See Images & Functions.

SSH

For a quick shell, use shell() or sail box shell: it streams a PTY over the same channel as exec and uses no ingress port. SSH is opt-in, for when you want a standard SSH endpoint: a devbox, your own client, scp, or port forwarding. Enable SSH after create with enable_ssh, which trusts your org’s SSH certificate authority, starts sshd, and exposes guest port 22 as raw TCP. See the SSH access guide for the access model, how to connect, and source restrictions. visibility chooses who may operate the Sailbox, fixed for its life. "org", the default, lets any credential in your org exec, copy files, SSH into, or run lifecycle operations on it. "private" restricts all of that to you: only your credential can operate the Sailbox (your org can still see it in listings, but not act on it). It requires an API key minted by your user, not a service key. Org admins can override the restriction for exec, file, setting a wake time, and the pause, sleep, resume, terminate, and upgrade operations by setting SAIL_OWNER_OVERRIDE_REASON (or the X-Sail-Owner-Override-Reason header on raw HTTP calls); exposing or removing listeners, checkpoint, and restore stay creator-only. Every override requires that reason, and your org’s audit log records it. SSH has no override: a private Sailbox’s SSH server accepts only its creator’s certificates. From the CLI, pass --visibility private to sail box create.

Sailbox.get

Fetches a Sailbox by id and returns a fully usable Sailbox: run commands, read and write files, and manage listeners on it directly. get never wakes a paused or sleeping Sailbox. Operations that run inside it (commands, file reads and writes, network traffic) wake a sleeping Sailbox on demand; a paused Sailbox rejects them until you call resume. The returned object reflects the Sailbox at the time of the call, so call get again for fresh state. Unknown and wrong-org ids both raise a not-found error, so you cannot tell an unknown id from one owned by another org. In Rust, client.sailbox(id) binds the id without a network call; fetch current state with sb.info().

Sailbox.list

Lists Sailboxes for the current org. Python and TypeScript fetch pages internally until every match (or limit of them) is collected; limit caps the total returned, bounding the fetch for large orgs. (Rust’s list_sailboxes returns one page with its envelope.) app_id filters by the owning app id (resolve a name through App.find first). search filters by name substring. order is "newest_active" (most recently active first, the default) or "newest_created" (newest-created first). Use Sailbox.list_page to control paging yourself or to read the pagination envelope.

Sailbox.list_page

Same call as Sailbox.list, but returns a SailboxPage with the Sailboxes plus the limit/offset/total/has_more pagination envelope. (In Rust, list_sailboxes always returns the page.)

exec

Runs a command in the Sailbox and returns a process handle. By default a stream you are reading pauses the command when you fall behind, so nothing is lost until a cancel or the exec timeout ends the pauses, and a stream you are not reading keeps only its most recent 1 MiB. To get every byte, start reading right after exec returns. output_mode and output_buffer_bytes (outputMode and outputBufferBytes in TypeScript) change that; what counts as reading a stream differs by language. See Exec process. A string command runs via /bin/sh -lc, so shell syntax (pipes, redirects, &&) works. Python and TypeScript also accept an argv list, which execs the program directly with no shell interpretation, and Rust separates the two as exec_shell (string) and exec (argv). Python’s exec additionally accepts a @sail.function-decorated Python callable; it then blocks and returns the function’s return value directly. For a function, output_mode must stay auto, and the function’s complete encoded response (its serialized return value, captured stdout and stderr, and any error details, as encoded on the wire) must fit output_buffer_bytes; a larger response raises SailboxFunctionSerializationError. A second call with the same idempotency_key while the function runs takes over its output, and the earlier call may then fail to decode its result. See Images & Functions.
Multiple execs can run on the same Sailbox concurrently; coordinate access to shared files and ports in your own commands. Running a command on a sleeping Sailbox wakes it. A paused Sailbox rejects commands until you call resume.

run

Runs a command to completion and returns its buffered result: a one-shot convenience over exec followed by wait(). A string command runs via /bin/sh -lc; a list is exec’d directly. cwd sets the working directory for string commands only (like exec, an argv command with cwd raises). user picks the user the command runs as (see exec). By default a nonzero exit code returns normally on the result; check exit_code. With check set, a nonzero exit or a timeout raises CommandFailedError carrying the completed result instead. (Rust reports everything through the returned ExecResult.) A command that exceeds timeout is killed and reports timed_out on the result; without check, a timeout alone does not raise. idempotency_key makes a retried run wait on the original command instead of launching it again, so a control-loop retry cannot double-execute. While the first call is still running, a second call with the same key takes over its output stream, and the earlier call’s result may come back truncated. In TypeScript, aborting signal force-cancels the remote command and rejects. The result’s stdout and stderr hold only the most recent output_buffer_bytes (outputBufferBytes in TypeScript) of each stream (1 MiB by default, up to 64 MiB), with stdout_truncated and stderr_truncated set when older output was dropped; the command never pauses for unread output. To get every byte, use exec and read the stream (see Exec process). The interactive and detached exec options (open_stdin, pty, background) and the pipe output mode are not available on run.

shell

Opens an interactive pty session on the Sailbox and bridges it to your local terminal. With no command, runs a login shell; pass command to run that under a pty instead (e.g. a REPL or vim). Keystrokes (including Ctrl-C, Ctrl-Z, and Ctrl-D) reach the remote process, its output renders locally, and terminal resizes propagate. Blocks until the remote process exits and returns its exit code. Requires an interactive local terminal (stdin and stdout must be TTYs) on a Unix machine, so it suits CLIs and dev tools rather than server-side harnesses. This is the equivalent of ssh-ing into the Sailbox, without running an SSH server. shell overrides the login shell (default $SHELL, else /bin/bash); it is ignored when command is given. env adds environment variables to the session, with the same precedence and reserved names as for exec. The session runs as the image’s USER when the image sets one, root otherwise: the same identity exec uses. user runs it as someone else instead (a user name or numeric uid, optionally with a group after a colon, like "alice", 1000, "alice:staff"); user="0:0" is always root. A user other than "0:0" requires a Sailbox whose guest honors requested users; on older Sailboxes the session fails until upgrade is called. While the session is open, browser opens and localhost servers in the Sailbox are forwarded to your machine. When a program in the Sailbox opens a browser (a login like claude login or gh auth login), the page opens in your local browser, and a login that redirects to a localhost callback completes end to end. A server the Sailbox starts on localhost keeps serving inside the Sailbox the whole time (code and agents there reach it as usual); while the shell is open it is also mirrored to the same port on your machine, unless that port is already in use locally. The mirror lasts only for the session. Files dragged onto the terminal upload into the Sailbox and paste as their guest paths, and Ctrl+V forwards your clipboard. On devbox images the clipboard is two-way: a pasted image or text lands on the Sailbox’s clipboard, and text copied inside the Sailbox comes back to yours. Other images upload a pasted image as a file and paste its path. Pass no_forward=True (TS noForward) to turn all of it off, for example for an untrusted or automated session. Plain exec forwards nothing; for the same forwarding on sail box exec --tty, see the CLI reference.
From the CLI:

The fs namespace

File and directory operations live under the fs namespace: sb.fs in Python and TypeScript, sb.fs() in Rust. Reads and writes stream bytes to and from the guest, and Python paths accept str or PurePosixPath. The directory helpers mkdir (creates missing parents), remove (deletes recursively), and exists behave like mkdir -p, rm -rf, and test -e, and upload_dir and download_dir transfer whole directories. Writes give what they create to the image’s USER by default, or to root when the image sets none. That is the same identity exec runs commands as, so an uploaded file is usable by the code in the Sailbox. Reads and the directory helpers act as root by default, so they work on any path. Each operation except the reads and the directory download takes an optional user in the Docker USER syntax: a user name or numeric uid, optionally with a group after a colon ("alice", 1000, "alice:staff"). The other directory helpers then run as that user, with its permissions enforced. The writes give what they create that owner (like COPY --chown) while the write itself always runs as root, so it works even where the owner cannot write. "0:0" is always root, and the directory upload gives what it creates the same way. Reads and the download take no user: a user only decides which paths an operation may touch and who owns what it creates, a read creates nothing in the Sailbox, and a download reads any path as root the way the reads do. A user other than "0:0" requires a Sailbox whose guest honors requested users; on older Sailboxes the operation fails until upgrade is called.

fs.read

Reads a regular file from the Sailbox as bytes. Loads the whole file into memory; for very large files (checkpoints, datasets) prefer read_stream. Raises a file-not-found error if the path does not exist.

fs.read_stream

Yields a regular file’s contents in chunks without buffering the whole file in memory. Iterate to completion (or close the stream) so it is released. The Python iterator supports both for and async for.

fs.write

Writes data to a regular file in the Sailbox. Missing parent directories are created by default in every language. path must be absolute. To write several files in one call, see fs.write_files.

fs.write_files

Writes several complete files in one call. files maps each absolute guest path to its contents; every file gets the same create_parents, mode, and user as fs.write. In Rust, files is any iterator of path and contents pairs, borrowed or owned: each entry is streamed from the buffer you pass, not copied first. How a batch runs, what happens on a failure, and when to stream instead are covered in Filesystem.

fs.write_stream

Opens a streaming write and returns a writer: push chunks with write and confirm with finish. Only finish commits the write; a writer that goes away without finishing (an explicit abort, an exception, or dropping it) cancels the transfer instead, and the guest file state is then unspecified. Use it when your data arrives incrementally (streaming logs, assembling an archive on the fly) rather than from a source write can consume whole. The options match write, including user for the written file’s owner. Python makes the file mode explicit and defaults it to 0o644.

fs.mkdir / fs.remove / fs.exists

mkdir creates a directory and any missing parents (like mkdir -p) and is a no-op if it already exists. remove deletes a file or a whole directory tree (like rm -rf) and is a no-op if the path is already absent. exists reports whether a path exists; it follows symlinks (like test -e), so a dangling symlink reports false even though fs.ls lists it. A failure (for example a permission error) raises with the guest’s stderr in the message. With user, the helper runs as that user with its permissions enforced: mkdir leaves the created directories owned by it, remove can only delete what that user may delete, and exists reports what that user can observe (a path it lacks permission to reach reports false).

fs.ls

Lists a directory’s immediate entries (no recursion) as DirEntry records. A missing path raises, as does a path that is not a directory and a listing too large for the exec output cap. An entry whose name is not valid UTF-8 fails the listing, since the path API cannot address it. With user, the listing runs as that user, so a directory it cannot read raises a permission error.

fs.upload_dir

Uploads a local directory’s contents into a Sailbox directory. The local directory’s entries land inside the Sailbox directory, which is created if needed. Entries the upload does not name are left in place; a same-named file is replaced. Uploaded files keep their permission bits (the setuid, setgid, and sticky bits are cleared) and belong to the image’s USER (root when the image sets none or its identity cannot be read). A user (the same syntax the other operations take) gives the entries to that user instead, like COPY --chown; it must exist in the Sailbox, and like the other operations’ user it requires a Sailbox whose guest honors requested users. The Sailbox’s image must provide tar and gzip, which the transfer uses to ship the directory as one compressed archive; the default images do.

fs.download_dir

Downloads a Sailbox directory’s contents to a local directory. The Sailbox directory’s entries land inside the local directory, which is created if needed. Entries the download does not name are left in place; a same-named file is replaced. The transfer reads every file in the tree, so download directories of ordinary files: system trees like /proc or /sys hold files that cannot be read as plain data, and downloading them fails. A file that is being written while the download runs is captured as it is at that moment, the way copying a live file would; download after writers finish for a consistent copy. On Windows, a directory that contains symbolic links cannot be downloaded, since Windows restricts creating them. The Sailbox’s image must provide tar and gzip, which the transfer uses to ship the directory as one compressed archive; the default images do.

listener / listeners

Look up the exposed guest ports and how to reach them. Waiting blocks until the route is active and the endpoint is reachable (an HTTP probe to the URL, or a TCP connectivity check to the host/port), then returns the ready listener; it raises a timeout error otherwise.
Ports are exposed at create time via ingress_ports, or at runtime with expose/unexpose (see Networking).

enable_ssh

Prepares this Sailbox for SSH (idempotent): installs your org’s SSH certificate authority as trusted, starts sshd, and exposes guest port 22 as raw TCP once the CA-only server owns it. See the SSH access guide for the access model and how to connect. By default it blocks until SSH is reachable, up to timeout seconds, and returns the endpoint to dial; pass wait=False to skip the probe and return nothing. allowlist restricts which source addresses or ranges may connect to port 22, replacing any existing restriction. Left empty, a first enable opens the port to any source, and a re-enable keeps the existing restriction. Disabling SSH removes the port-22 listener along with its restriction, so enabling again starts fresh.

checkpoint

Creates a durable checkpoint handle for this Sailbox. Running Sailboxes are snapshotted first. Paused and sleeping Sailboxes return a handle to their existing checkpoint without waking. Upgrade a Sailbox that uses an older guest payload before you create a checkpoint handle. name sets a display name for the handle. ttl_seconds (ttl in Rust), when set, must be > 0 and overrides the server’s default retention window. Set it when you keep a checkpoint to reuse as a template, so the handle does not expire while you still need it. The returned handle carries expires_at, a timestamp (datetime in Python, Date in TypeScript, OffsetDateTime in Rust) for when the checkpoint expires: seven days out unless you asked for a different window. Starting a Sailbox from it after that fails.

from_checkpoint

Creates a new running Sailbox, called name, from a durable checkpoint handle returned by checkpoint. The new Sailbox gets a fresh network identity; existing TCP connections do not carry over, and ingress ports are not inherited. It keeps the original’s egress policy. timeout (seconds, > 0 when set) bounds the call, since a restore can block for many minutes while the new Sailbox queues for capacity. A call that times out fails, and the restore may still finish in the background; the new Sailbox then shows up in Sailbox.list. Omit timeout to wait without a client-side bound. The new Sailbox restores the memory saved in the checkpoint as well as the writable disk, so processes the original was running carry on there. Commands you started with exec stop in the new Sailbox, though their writes up to the checkpoint are kept, and one you started with the background option keeps running there. Start the other commands you need again. Sometimes the new Sailbox comes up cold instead, with the disk intact and nothing running. Write code that expects a cold start.
checkpoint() does not support a Sailbox that has volume mounts. Create a separate Sailbox without volume mounts before you create a checkpoint handle.

upgrade

Upgrades this Sailbox’s runtime to the latest version, picking up new Sailbox features, fixes, and performance improvements without recreating the Sailbox. A running Sailbox reboots in place on its current disk: all filesystem state is preserved, but processes restart as they would after a machine reboot (any application state not yet written to disk is lost, as after a sudden power loss). A paused or sleeping Sailbox is upgraded without waking; the upgrade is recorded and applied at the next wake. Returns an UpgradeResult: applied is true when the upgrade happened immediately (the Sailbox was running) and false when it will apply at the next wake, and status is the Sailbox’s lifecycle status after the call. Already-up-to-date Sailboxes report immediate success without rebooting.

pause / sleep / resume / terminate

  • pause checkpoints and pauses the Sailbox in memory until you explicitly resume it. Commands and network traffic do not wake a paused Sailbox.
  • sleep checkpoints the Sailbox to disk; inbound traffic, an operation, or an explicit resume wakes it. An optional wake time schedules a wall-clock wake (see Lifecycle).
  • resume wakes a paused or sleeping Sailbox. Raises a not-found error if the Sailbox is terminated.
  • terminate permanently ends the Sailbox. Idempotent: terminating an already-terminated Sailbox succeeds.
Sail may also sleep a Sailbox on its own, but only when nothing would notice: no CPU or network activity, no process waiting on a timer, and no open connections a sleep would break. A slept Sailbox wakes transparently on traffic or the next operation. See Lifecycle for how these interact with checkpoints, and Pricing for billing.

Volumes

A Volume is an org-scoped shared filesystem (NFS) that can be mounted into one or more Sailboxes. Resolve one by name, then pass it (or its id) in the volumes mapping of Sailbox.create, keyed by the absolute guest path to mount it at:
The management surface:
  • find looks up a volume by name; mint_if_missing creates it when no volume with that name exists.
  • list returns the org’s active volumes, newest first; max_objects caps the count.
  • delete deletes the volume. With allow_missing, deleting an already-deleted volume succeeds instead of raising a not-found error.
Each handle carries volume_id, name, backend, status, and created_at / updated_at timestamps.

Egress policy

An egress policy limits which hosts a Sailbox can connect to and can add credentials to the HTTPS requests it sends. It is chosen at creation with egress_policy (TypeScript egressPolicy, Rust CreateSailboxRequest.egress_policy) and can be replaced at any time:
egress_policy at creation and set_egress_policy take the same values:
  • a saved EgressPolicy from EgressPolicy.create or EgressPolicy.get, its id string, or a summary row from EgressPolicy.list() (Rust EgressPolicySpec::Named, or (&policy).into()).
  • a document: EgressPolicy.allow_only(hosts) (Rust EgressPolicyDocument::allow_only) to reach only the listed destinations, EgressPolicy.no_egress() (Rust no_egress) for no outbound connections, EgressPolicy.no_network() (Rust no_network) for no network at all, or the JSON document itself as a dict (TypeScript object, Rust EgressPolicyDocument). A document that references a secret must be saved first. At creation, an invalid document fails the call with InvalidArgumentError before a Sailbox is created, and omitting the argument (or passing None) lets the Sailbox reach any host.
no_network() cannot be combined with ingress_ports at creation, and is refused with ApiError while the Sailbox exposes ports or SSH. The other documents can be used alongside exposed ports and SSH. from_checkpoint copies the source’s policy.
  • set_egress_policy replaces the policy and returns the new one.
  • clear_egress_policy removes all restrictions and rules and returns the new policy, whose document is {}.
  • egress_policy is the policy the Sailbox runs under, as a SailboxEgressPolicy: the document plus the policy_id and name of the saved policy it came from (None for an inline document). get, list, set_egress_policy, and clear_egress_policy fill it; the handle create returns does not carry it.
A change applies to connections opened after the call. A sleeping or paused Sailbox takes the change when it next runs. Save policies and store the secrets their rules reference with the organization-wide EgressPolicy and Secret classes (Client methods in Rust). The egress policy guide covers the document, and credential injection walks through the secrets flow in every language.

Supporting types

Field names below use the Python spelling; TypeScript exposes the same fields in camelCase.

DirEntry

One entry in a directory listing from fs.ls. Reported for the entry itself, so a symlink’s type is "symlink" regardless of what it points at.

IngressPort

A guest port to expose for ingress. In Python, Sailbox.create also accepts a bare int as shorthand for IngressPort(port) (an HTTP port). Address and range entries work for HTTP and TCP listeners. App-name entries match authenticated traffic from another Sailbox and work on HTTP listeners only, because raw-TCP connections carry no source app identity (so a "tcp" allowlist must contain only addresses and ranges). An entry that reads as an address or a range is taken as one, so an app name cannot read as either, and cannot contain a /. An address must not carry an IPv6 zone, such as fe80::1%eth0, which names an interface on one machine rather than a source. To send that authenticated traffic, pass headers=sail.ingress_auth_headers() on the request when calling from inside a Sailbox. From the host, fetch a specific Sailbox’s headers with sb.ingress_auth_headers() (this needs an organization-scoped API key). Reserved ports: guest port 22 cannot be an HTTP port (expose it as tcp for SSH) and 10000/10001/15001/15002 are reserved for Sail’s in-guest services. Leaving allowlist empty normally makes the port publicly reachable, with one exception: for well-known database, cache, and search ports (e.g. 5432, 6379), a raw-TCP expose with no allowlist is rejected, so you can’t accidentally publish an unprotected Postgres or Redis to the whole internet. To make one of these ports public on purpose, say so explicitly with allowlist=["0.0.0.0/0", "::/0"].

Exec process

A handle to a command running in the Sailbox (ExecProcess in Python, ExecProcess in TypeScript and Rust). The command runs inside the Sailbox, and a temporary network interruption does not kill it. Sail may reattach after an interruption, but reattachment does not guarantee exact output replay. Closing the handle abandons the live attachment without killing the command. Live output. stdout and stderr are iterators (for in Python, for await in TypeScript, next().await in Rust). The Python and TypeScript iterators yield text, incrementally decoded from the raw stream (a multibyte character split across chunks arrives whole); the raw byte stream is available as stdout_bytes / stderr_bytes in Python, .raw() on the stream in TypeScript, and is what the Rust reader yields directly. Bytes travel exactly as the command wrote them (escape sequences and binary payloads included). Output limits. Each stream has a buffer, 1 MiB by default. output_buffer_bytes (outputBufferBytes in TypeScript) sets its size, from 64 KiB to 64 MiB. The output_mode option (outputMode in TypeScript) sets what happens when a buffer fills. This applies to commands started without a pty; a pty command always behaves like tail.
  • auto, the default. If you are reading a stream and fall behind, the command pauses when the buffer fills and resumes as you read, like a pipe. If you are not reading a stream, the command never pauses and the stream keeps only its most recent bytes. Reading a stream is how you get every byte, and it slows the command when you cannot keep up.
  • pipe. The command pauses when either buffer fills and stays paused until you read that stream, so nothing is lost while you are late to start reading. Read both streams, or the command stays paused on the one you ignore; 2>&1 or 2>/dev/null in a shell command is the easy way out. Once you release a reader, its stream goes back to keeping only its most recent bytes. wait() without a reader waits for as long as the command stays paused. Not available with a pty.
  • tail. The command never pauses for you. Each stream keeps only its most recent bytes, even while you are reading it, so a slow reader skips output without notice (a Rust StreamReader can check took_drop). stdout_truncated and stderr_truncated say only that the result holds less than the command wrote, which is also the case after a reader consumed everything.
Stdout and stderr are handled separately. Sending cancel(), and the exec timeout, end every pause: from then on each stream keeps only its most recent bytes, so a reader more than a buffer behind skips ahead. A command that ignores the cancel signal keeps running that way; cancel with force to stop it. The command keeps its original timeout. If a handle attaches to a command launched earlier under the same idempotency_key, that handle’s pause deadline starts when the attachment succeeds, so it can release the pauses one full timeout after that; cancel() and close() release them at once. A command that never pauses for you still runs no faster than your connection carries its output; when output outruns the connection, the command pauses on the Sailbox until the backlog drains. With auto, start reading right after exec() returns to get every byte (what starts a read differs by language; see below). You can read stdout without holding stderr, or the reverse. The stream you are not holding keeps its most recent bytes and never pauses the command when it fills, and a reader that starts on it late begins with whatever is still held; with pipe, that is everything. If you hold both readers, read them at the same time, each from its own thread or task (asyncio.gather in Python, Promise.all in TypeScript, two tasks in Rust). The exit code is available from poll() (Python also has exit_code) as soon as the streams end. Claiming and releasing a stream. Each stream can be read once; a second attempt fails (InvalidArgumentError in Python and TypeScript, SailError::InvalidArgument in Rust), before or after the first reader is released. After release, Sail again keeps only the stream’s most recent bytes, in every mode.
  • Python. Accessing proc.stdout or proc.stdout_bytes (and the stderr twins) claims the stream and returns a generator; from that access on the command pauses rather than lose output (see Output limits for the exceptions). The stream is released when the generator ends, when you call close() on it (await its aclose() for an async generator), or when nothing references it any more (a for loop over proc.stdout drops it when the loop ends, including by break). To stop early on purpose, keep the generator in a variable and call close().
  • TypeScript. Accessing proc.stdout or proc.stderr claims nothing. The stream is claimed when iteration starts (for await, .raw(), .text(), .bytes()) or when you call .toReadable(). It is released when the iteration finishes or you leave it (break, return, or a thrown error inside for await), when .text() or .bytes() reaches the end, or when the Readable is destroyed (released at once, even while a read is waiting for output).
  • Rust. reader(OutputStream::Stdout) or reader_async(...) claims the stream. It is held while the reader value exists and released when the reader is dropped.
In every language, close() on the handle, or your process exiting, releases both streams. The command keeps running and never pauses. wait() fails after close() unless it already resolved a result. If the connection to the Sailbox is interrupted, the command keeps running and does not pause while Sail reconnects; output produced in the meantime can be missing, and stdout_truncated / stderr_truncated report when that happened. Reattachment is best-effort, not an exact replay. A pty command never pauses: Sail keeps only its most recent output, up to the buffer size, so a reader further behind than that misses older output. Use resync() to ask the command to repaint its current screen. stdin. With open_stdin=True, write to the command’s stdin and deliver EOF by closing it (Python proc.stdin.write/close, TypeScript writeStdin/closeStdin, Rust write_stdin/close_stdin). Writes block (like a pipe write) while the command is not reading. Writing to a completed command, or after the command closed its stdin, raises a broken-pipe error. wait() waits for the command to finish and returns its result, holding each stream’s buffer, its most recent output (see Output limits above). It never pauses the command itself and can be called while a reader is still open; with pipe, it waits for as long as an unread stream keeps the command paused. After close() it fails, unless a result was already resolved; a repeat call returns that result. For foreground execs it waits for the command to finish; for background execs it waits only for the detached launcher. An exec that ended without a real exit code (the machine hosting the Sailbox was lost before the command finished) raises a host-lost error instead of returning a result. In Python and the sail CLI, Ctrl-C during a wait additionally sends SIGINT to the remote command and resumes waiting, and a second Ctrl-C escalates to SIGKILL; terminal-facing surfaces forward the interrupt like a local foreground job. The TypeScript and Rust libraries leave process signal handling to your application; wire the same behavior with cancel if you want it. poll() (Rust: try_wait) returns the exit code once the output stream has ended, else nothing. It never blocks and never drops output, so it is the way to get the exit code after reading the streams yourself. If the connection was lost for good mid-command, the stream ends early with the outcome still unknown: poll() stays empty and wait() fetches the result Sail recorded. cancel() signals the guest command: SIGINT by default, SIGKILL with force. Idempotent on the server. If the Sailbox is sleeping, cancel wakes it to deliver the signal; if you paused the Sailbox, resume it first (cancel raises rather than waiting, since the guest cannot receive the signal while paused). close() abandons the handle without killing the command. It releases both streams. The command keeps running and never pauses, and Sail keeps only the most recent output of each stream. Call cancel() instead if the command should stop. wait() fails after close() unless it already resolved a result. resize(cols, rows) adjusts a pty command’s window. resync() asks a pty command to repaint its current screen on the output stream. A command runs at full speed and never waits for a slow reader, so if you render its output yourself and fall far behind, the oldest output is dropped and the screen can end up garbled. Call resync() to receive the current screen instead of a broken, partial one. It does nothing for a command with no pty. The interactive shell helper calls it for you.

Exec result

The output of a completed exec (ExecResult in Python, ExecResult in TypeScript and Rust).

Listener

An exposed guest port and how to reach it. Every listener carries its guest port, protocol, route status, and a typed endpoint: an HttpEndpoint (with url) or a TcpEndpoint (with host/port), absent until routable. In TypeScript the endpoint is a union discriminated on kind; in Rust it is the ListenerEndpoint enum returned by listener.endpoint(). Listeners are snapshots; re-fetch with sb.listener(guest_port).

HttpEndpoint

The routable HTTPS address of an "http" listener.

TcpEndpoint

The host and port to connect to for a "tcp" listener.

Sailbox snapshot fields

The monitoring snapshot carried by every Sailbox returned from Sailbox.get and Sailbox.list.
Observed-usage fields reflect the latest live sample within roughly the last two minutes, falling back to zero when no recent sample is available.

SailboxPage

One page of Sailbox.list_page results.