Skip to main content
Use write_file(path, content) to write bytes into the sandbox. It creates parent directories and returns after the write completes. read_file() reads the active VM without taking a snapshot. The next command reuses that VM, including its running processes. A cold session reads its saved image; pending staged files start a VM so reads include those inputs. This example uploads bytes as /input.txt, copies them to /output.txt remotely, and saves the result as output.txt in your local working directory. Configure a connection as described in Connection sources.
Both stdout and the local output.txt contain Hello from a file! followed by a newline. read_file() sees both writes without stopping the VM. Normal context exit saves them. With ContreeSession, each command saves by default; disposable=True leaves the previous image selected.
Async snippets with top-level await run inside an async function or a notebook that supports it. For a standalone script, use the asyncio.run(main()) structure from Your first sandbox.
For a local file, use run(shell="cat > /input.txt", stdin=Path("local.txt")). The default lazy sandbox does not support run(files=...) while a VM is active. The advanced examples below select ContreeSession or ContreeAsyncSession explicitly for startup attachments and per-command images.

Advanced: choose a file source

Destination paths refer to the remote filesystem. Source paths refer to your local machine. A duplicate destination in a list raises ValueError before execution. The standard transfer component uses the client’s ensure_file() to reuse uploads by content. A saved upload does not by itself change a session image. See Customize the SDK to replace file transfer.

Advanced: upload an executable script

Use UploadFileSpec to set a mode. This example runs the uploaded script directly. Its output is script output. It is disposable because the script is not needed by a later command.

Advanced: stage files for a later command

stage_files() uploads content and appends a stage history entry. It does not start a VM or change the image. pending_files() returns the uploaded UUID, destination path, and permissions for each pending attachment. Use a SQLite store to keep staging between processes. The next run() or spawn() attaches pending files from its source history entry. Local sources are no longer needed after staging succeeds. This example stages a configuration file, closes the store, and resumes the session. A committed run consumes the attachment. Rolling back that run restores the pending attachment with its original permissions.
Staging follows history ancestry. A new branch inherits the pending files at its starting entry. Staging another version of the same path changes only that branch. Rolling back a staging entry restores the previous version, if one exists. Explicit run(files=...) attachments take precedence over pending files with the same destination. A committed result records all attached paths as applied, including those explicit replacements. A disposable run, failed operation, cancelled operation, or failed commit leaves the source staging available. Nonzero command exit codes follow the session’s normal commit behavior. Concurrent operations retain their original parent. Their results become siblings; a late result does not replace a newer local selection. Use commit_result(operation, branch="result") to select its result explicitly. read_file() reads the current image; staged attachments appear there only after a committed run. For LazySession, pending attachments enter the VM at startup and are consumed when its snapshot commits. Stage inputs before starting that VM. Staging and pending-file resolution belong to Session. Store persists their nodes without interpreting file attachments. Rollback changes only the local selection; pass node_id=staged.id to resume that position in another process. Stored file references do not guarantee that the uploaded UUID still exists on the server.

Advanced: prepare directory trees and stream large files

Directory sources expand into regular-file attachments. For example, files={"/app": Path("project")} uploads the contents of project under /app. Use prepare_file_tree() for exclusions and ownership. It returns ordinary UploadFileSpec objects that can be passed to a session or a transfer component. Patterns match relative POSIX paths. Patterns without / also match basenames at any depth; matching directories are excluded with their contents. Permission bits are preserved unless mode= is supplied. Ownership defaults to uid=gid=0. Symlinks and special files raise ValueError; empty directories are omitted because spawn attachments represent files. Normalized destination conflicts are rejected before uploads start, including a file that would be another file’s parent. Client-backed transfers send local files as binary streams. Keep source files unchanged until the upload completes, or use Reuse uploads and retain their sources for a stable temporary snapshot. Bytes inputs are already in memory. read_file() still returns the whole file; use iter_file() or download_file() for large results. The examples upload a directory with an exclusion, then stream a result into a local file. Configure a connection as described in Connection sources.
max_concurrency limits uploads within each prepare_files() call. Sync defaults to one worker; async defaults to four. Sync callbacks run in upload worker threads when concurrency exceeds one, so callbacks and custom clients must support that usage. Async callbacks run on the event loop and must not block it. Upload events report a completed file’s size, including server deduplication. Download events report cumulative bytes received for one file. Callbacks are synchronous and their exceptions propagate. An upload failure cancels queued work and joins active workers. Async cancellation also joins local file I/O before closing files. Uploads already accepted by the server are not undone. Concurrent calls have separate worker limits. download_file() writes a temporary sibling, flushes it, and replaces the destination only after the stream completes. Failure, cancellation, or a progress callback error preserves the previous destination and removes the temporary file. The parent directory must exist. The replacement uses private temporary-file permissions; it does not retain the destination’s previous mode. If you iterate iter_file() directly, close it on early exit with contextlib.closing() or aclosing(). write_download() and write_download_async() provide the same atomic local writer for other byte iterables. These helpers do not own their input streams; the caller must close them. Custom transfer backends can override iter_file(); its default implementation buffers through read_file().

Inspect the active VM without stopping it

LazySession.inspect() returns a view of the current session. Each file request selects the active VM or, when the session is cold, its saved image. Retaining the view does not keep a VM running. Pending staged files start a VM before reading. The view provides list, grep, file_exists, archive_exists, read_file, iter_file, download_file, and archive. Paths use the filesystem root rather than the command’s working directory. File contents bypass the operation event log. download_file replaces the local destination only after the complete transfer.
Close streams when iteration stops early. Snapshot and normal session close wait for accepted live requests, including their streams. File requests count as idle activity; they do not increase CommandCountSnapshotPolicy’s command count. Requests arriving during a snapshot wait for it to finish. inspect(timeout=...) limits that admission and startup wait; client settings control HTTP timeouts. Live errors propagate without reading an older image as a fallback. Live reads can observe concurrent writes. They are not filesystem snapshots. Live grep also differs from image grep in regex support, encoding, ignore rules, and highlight spans. Unsupported patterns raise an error; check truncated on successful results. The live API limits transfers to 300 seconds and directory listings to 10,000 entries. Use inspect_snapshot() for a stable image view; its image_uuid remains fixed across later commands. Image metadata through view.info() requires this same snapshot and stops the VM.