Guide · Pre-release

Read Markdown
On this page

Get started with Ukai Jail

Start with a small command, see what the jail recorded, then use the same workflow for your own tools. You don’t need to change agents or adopt a new way of working: ukai-jail wraps the command you already run.

You choose the files it can read or change, the services it can contact, and how long it can run. A receipt tells you which controls were applied and how the run ended.

Choose your host: Linux x86_64/ARM64 runs ukai-jail directly. Apple Silicon Macs can use the separate macOS VM preview, which runs offline Linux commands in disposable VMs. Both are developer-preview software; native macOS programs and online agents are outside the Mac preview.

Already have the binary? Jump to your first command. Writing an integration? Start with agents and scripts.

Install the developer preview

The same installer detects your platform. The following commands are for Linux; Mac users should follow the Mac quickstart.

Ukai was called Ouroboros until 15 October 2026. The current previews were published under that name (ouro-jail, ouro-vm) and keep those command names. The installation examples below use the published commands. Later sections use the current ukai-jail source build; for the published Linux binary’s complete guide, use the release guide.

Linux preview 0.1.0-rc.2 ships Linux x86_64 and ARM64 packages. Install your distribution’s bubblewrap package first. The binaries require glibc 2.39 or newer; Alpine/musl and macOS execution are unavailable in this preview. The installer needs Bash, curl, tar, and sha256sum or shasum; no Minisign, Rust compiler or sudo is needed.

curl --proto '=https' --tlsv1.2 -fsSL \
  https://ukai.sh/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
ouro-jail version --json
ouro-jail doctor --profile tool --json

The installer embeds this preview’s SHA-256 hashes for both architectures. It checks the downloaded archive before extracting or executing it, then installs outside your project in ~/.local/bin. The HTTPS-served installer is the source of the trusted hashes; a downloaded checksum manifest cannot override them. Repeat to upgrade after a new installer is published. Pass --version through bash -s -- --version VERSION to select a release already pinned in the script; an unpinned version refuses before any download. Downgrades require --allow-downgrade. Replacing a source-built 0.1.0 without a release record also needs this explicit flag to install an RC version.

The supported-host notes remain in the release: Ubuntu 26.04 is the x86_64 reference host; stock Ubuntu 24 containment is refused. The tested Pi lacks memory cgroups and Landlock, so its default build profile remains unavailable. Run doctor before relying on a profile. Archived runtime releases are separate; use the linked preview release; future releases use ukai-jail-v... tags. The ledger and fleet are not included in these packages.

macOS VM preview

Ukai VM preview 0.1.0-preview.1 is a signed, notarized download for Apple Silicon and macOS 27+. Execution was tested on macOS 27.0.1; installation and CLI launch were also checked in a fresh macOS VM. Allow 8 GiB free APFS storage, two virtual CPUs and 2 GiB RAM per running attempt. Intel Macs and nested VM execution are unsupported.

curl --proto '=https' --tlsv1.2 -fsSL \
  https://ukai.sh/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
ouro-vm version
ouro-vm doctor

The installer checks an embedded SHA-256 pin before mounting the 512 MiB DMG. It installs the bundled runtime and guest image under ~/.local/share/ouro-vm/0.1.0-preview.1, with a command in ~/.local/bin. Python, Rust, Swift, Homebrew, sudo and Endpoint Security approval are not needed. Use bash -s -- --vm-prefix /absolute/path to change the root containing bin and share. An already installed version is preserved and repeat installation refuses; use a fresh prefix to evaluate another copy. --bin-dir, its Linux alias --prefix, and --allow-downgrade apply only to Linux.

Run a small offline Linux command:

vm_demo="$(mktemp -d "$HOME/ukai-vm-demo.XXXXXX")"
mkdir "$vm_demo/input"
printf 'hello\n' > "$vm_demo/input/input.txt"
ouro-vm run --workspace "$vm_demo/input" --out "$vm_demo/result" \
  --profile build --wall 30 -- \
  /bin/sh -c 'cat input.txt; printf "created in Linux\n" > output.txt'
cat "$vm_demo/result/workspace/output.txt"

Use a new output directory for every attempt. Inputs are copied into a private guest disk; review the returned files before adopting them. Both tool and build allow writes to that disposable workspace copy. Protected system paths remain read-only. The guest includes a shell and Python, rather than a full build SDK. Each attempt boots a VM; measured startup was about 22 seconds.

This preview has no network adapter, host directory shares, credential forwarding, interactive stdin, background/resumable VM or agent/provider integration. It runs Linux ARM64 commands, not macOS binaries, Xcode or Apple SDKs. Input/output limits are 5,000 regular files, 512 KiB per file and 32 MiB total. Symlinks, devices and FIFOs are refused; .git, target, node_modules, .venv, __pycache__ and .DS_Store are excluded. Empty directories are not preserved. Commands have 512 MiB memory, zero swap, 64 processes and a 1–300 second wall limit.

A completed attempt requires a verified Linux Jail receipt and acknowledged VM stop. Incomplete evidence returns 125. Files and Linux receipts remain in <out>/workspace/ and <out>/evidence/; attempt.json records Mac supervision. The package qualification passed all 14 measured teardown cases. Simultaneous suspension of all three host controllers, host sleep/wake, kernel failure and arbitrary guest compromise are outside qualified coverage. The native macOS cleanup blocker remains open.

For offline installation, save this installer and the DMG from the release, then run bash install.sh --from-dir /path/to/artifacts. The same embedded hash is required. The DMG also contains a standalone installer and build-time notes; its notes describe the candidate before publication. The public release notes record publication of those exact tested bytes.

To uninstall the default installation, remove ~/.local/bin/ouro-vm and ~/.local/share/ouro-vm/0.1.0-preview.1. Keep or delete attempt directories as needed. A crashed launcher can leave a disk there; never boot or reuse it.

Build and check your host

The remaining Linux examples use the current Ukai source build. The published previews retain their original commands, data paths and receipt schemas; renaming an executable does not migrate those contracts.

You need Git, Rust 1.98.1, and your distribution’s bubblewrap package. The reference host runs Ubuntu 26.04.1 and bubblewrap 0.11.1. Run as your normal user; the jail does not install a backend or change host security settings.

git clone --branch dev https://github.com/monocursive/ukai.git
cd ukai
cargo +1.98.1 build --release -p ukai-jail
mkdir -p "$HOME/.local/bin"
install -m 0755 target/release/ukai-jail "$HOME/.local/bin/ukai-jail"
export PATH="$HOME/.local/bin:$PATH"

ukai-jail version
ukai-jail doctor --profile tool

If you already have the checkout, start at the build command. Install the binary outside any workspace you’ll make writable: the jail refuses a grant that would let a child modify its executable.

doctor checks whether your host can run the selected profile. Read its report before continuing. A required control must be available; a preferred limit may be left unapplied and reported as such. Explicit PID, memory, swap, and CPU limits require a delegated cgroup v2 scope. The host setup reference explains that setup and its effect on process cleanup.

For a source-built native ukai-jail on macOS, version and explain work; doctor reports why execution is unavailable. Other Linux architectures are not yet supported for execution. Signed Jail packages are linked above; a Homebrew tap remains unconfigured. Older v0.1.x releases belong to the archived agent runtime.

The separate VM preview source documents its build process and lifetime design. Use the Mac quickstart above for the packaged release.

Verify a supplied package

For offline installation, save the Bash installer and the archive for your architecture from the release. The installer verifies its embedded SHA-256 pin even when the archive comes from a local directory. No checksum manifest or signature tool is needed:

bash /path/to/install.sh --from-dir /path/to/artifacts
ouro-jail version --json
ouro-jail doctor --profile tool --json

Use --bin-dir /absolute/path for a different destination, or --base-url https://mirror.example/release for an HTTPS mirror. Both paths must provide exactly the archive pinned in the installer. Re-running replaces an existing installation; downgrades require --allow-downgrade. A failed checksum leaves the installed binary, license notices and release record unchanged. doctor still decides whether the host can enforce the requested policy.

Run your first command

Let’s write a file in a fresh directory. This example needs no network or agent credentials.

jail_demo="$(mktemp -d "$HOME/ukai-jail-demo.XXXXXX")"
cd "$jail_demo"

ukai-jail run --profile tool --workspace "$PWD" --limit wall=30s -- \
  /bin/sh -c 'printf "hello from the jail\n" > greeting.txt'

cat greeting.txt
ukai-jail tail

You should see hello from the jail when you read the file. tail then shows the journal for the most recently updated attempt. The command had a writable workspace, no network access, and a 30-second deadline.

Everything after -- is the program and its literal arguments. This example names /bin/sh because it needs shell redirection. For an ordinary program, pass its executable and arguments directly.

The workspace is yours after the command exits. Changes persist, so use a disposable checkout when trying an unfamiliar tool.

Choose a profile

A profile is a starting policy. Pick the one closest to your task, then add only the paths and services it needs.

Profile Good starting point for Default access
tool Tests and local commands Writable workspace and scratch; no network. Existing .git and .ukai trees are protected from writes.
agent Coding agents that call a provider Writable workspace, scratch, and temporary agent state. Network destinations need explicit grants. Git metadata stays writable except the hooks and configs that exist at launch. That does not make git metadata safe to use after a run; see Git metadata under agent.
build Builds from read-only inputs Writable scratch; add read-only inputs and writable outputs. No network. Requires an explicit memory limit and host support.
none Deliberately uncontained execution Host access. Filesystem and network containment are disabled.

Git metadata under agent

Do not treat git metadata as safe to use after an agent run. The protection below raises the cost of accidental damage and of naive planting, such as a package’s install script adding a hook; it does not stop a run that sets out to make your next git command run its code.

agent keeps .git writable, so commits, fetches, checkouts, git submodule update, moving and removing linked worktrees and git rm of a submodule work inside the run. Where they exist at launch, it binds read-only:

  • .git/hooks and .git/config of the workspace’s repository and of nested repositories at any depth, and of a git directory granted with --rw <repo>/.git;
  • the hooks directory of each submodule’s git directory under .git/modules/.

What stays open:

  • Submodule configs. .git/modules/<name>/config stays writable, since git submodule update rewrites it on every run. One core.fsmonitor line there runs a command on your plain git status in the superproject, and core.hooksPath there makes the read-only hooks directory beside it moot.
  • A git directory reached through a gitlink. A linked worktree or git init --separate-git-dir keeps its git directory elsewhere, behind a file-form .git; when that directory lies in a writable path under another name, its hooks and config are writable. A gitlink itself is not protected: binding it read-only broke git worktree remove and git rm of a submodule, and left half-removed worktrees behind.
  • commondir and config.worktree. A run can plant a commondir file in a writable git directory, or rewrite the one a linked worktree’s administrative directory holds, naming a copy with its own config and hooks; git then reads the copy. Where extensions.worktreeConfig is enabled, it can write config.worktree.
  • Included files. A protected config that includes another file (include.path, includeIf) is only as protected as that file.
  • core.hooksPath into the worktree. When your config points hooks at a directory in the checkout, as husky does with .husky/_, the run can write the hooks there.
  • A repository without a hooks directory at launch. Nothing existed to bind, so the run can create .git/hooks and fill it.
  • Replacement. A run can move a .git, or a nested repository’s whole directory, aside and create another in its place. Repositories created during the run, such as a fresh git clone, and bare repositories are not protected.

When the workspace is a linked worktree of a repository outside it, the run can commit only with a grant of that repository (--rw <repo>) or of its git directory (--rw <repo>/.git); either way that repository’s hooks and config stay read-only.

The protection has costs inside the run. Commands that write a protected config fail, git config, git remote add and git submodule init among them; git submodule deinit empties the submodule but cannot unregister it, and reports the error yet exits 0. Deleting a repository that existed at launch fails partway and leaves its hooks and config behind. Each nested repository takes two rows of the receipt’s mount table, whose bound of 1024 rows refuses a workspace of more than about 500 nested repositories; the largest real tree measured held 122. The table is also bounded by size, to 1 MiB of the receipt: repositories nested under very long paths, or paths with control characters or bytes that are not UTF-8 (which a receipt must escape or encode), refuse sooner. A symlinked .git, hooks directory or config refuses the run and names the path, since no coverage can be claimed through it.

After an agent run, before you run git in the workspace, check:

  • hooks you did not install, in .git/hooks, in nested repositories, in .git/modules/*/hooks, and in the directory core.hooksPath names;
  • .git/modules/*/config and every file a config includes;
  • a commondir or config.worktree file in any git directory, including .git/worktrees/*/;
  • whether .git (or a nested repository) is still the directory it was, and whether a file-form .git still names the git directory it did.

Contained profiles also expose system runtime files needed to execute programs. Inspect the resolved policy before running a new command:

ukai-jail explain --profile tool --workspace "$PWD" --limit wall=30s --json

explain resolves configuration without probing the host or executing the command. It shows what you are requesting; the receipt later records what was actually applied.

Add access as the task needs it

  • --ro PATH makes a file or directory visible for reading.
  • --rw PATH permits changes to that path.
  • --allow-host HOST[:PORT] allows a proxied destination with the agent profile. Without a port, the grant is TLS only: it admits a tunnel to port 443, and the proxy refuses plaintext HTTP to it (except inside a tunnel to a bare IP address, which it does not inspect). Plaintext HTTP needs the port written out (HOST:80, or HOST:443 for plaintext on 443); the receipt lists TLS-only grants in applied.network.tls_only.
  • --limit wall=5m sets a deadline. Resource limits use forms such as --limit mem=512MiB, --limit pids=64, or --limit cpu=100 for one core’s worth of CPU bandwidth. Every explicit limit is required.

For tests, start with tool, a disposable checkout, and read-only access to installed dependencies. For a build, keep source inputs read-only and give the compiler a separate writable output directory. Toolchains installed in your home directory may need explicit paths; your usual home and environment are not automatically inherited.

Bound RAM, swap and writable storage

RAM and swap have separate ceilings. Combine --limit mem=512MiB with --limit swap=0 to cap charged RAM and disable swap, or use a positive swap budget such as --limit swap=128MiB. An omitted swap limit is not implied by the memory limit.

--limit storage=64MiB --limit inodes=4096 requires operator-provisioned bounded tmpfs volumes or ext4/XFS filesystems with enforced user hard quotas. Their combined capacity must fit those ceilings. The workspace, explicit --scratch PATH, extra writable grants and agent state all count; aliases of the same filesystem count once. Existing files and concurrent users of the same budget consume capacity too. The jail does not provision volumes or copy the workspace automatically.

For disk storage, use a dedicated non-root worker account and a filesystem with user quota accounting and enforcement already enabled. For example, on a prepared quota filesystem, an operator with quota-tools can set a 64 MiB block hard limit and 4096-inode hard limit with sudo setquota -u WORKER 0 65536 0 4096 /WORKER_VOLUME (block limits are KiB). Put both workspace and scratch there, owned by WORKER. Ukai queries the kernel as that user before releasing the target. Soft limits alone, disabled enforcement, foreign-owned writable files, idmapped writable mounts and XFS realtime volumes refuse. This budget covers that uid across the filesystem, including files outside the granted directories; it is not a per-run reservation.

For storage-bounded runs, /dev/shm is read-only and new hard links return EPERM. The hard-link restriction prevents importing another user’s inode into the writable tree; these seccomp denials have no observer event. Existing hard links within the admitted ownership rules remain usable. The storage ceiling covers allocated file data; use mem to bound charged RAM as well. A storage receipt’s hit=null means saturation was not established, not that the limit was never reached. The limit specification defines the scope and remaining backend limitations. Project quotas and automatic volume provisioning are not implemented.

Run an existing agent

Launch profiles supply starter file, environment, credential, and network settings for an agent. They don’t install it or choose its command line. Fourteen profiles are embedded, including OpenCode, Codex, and Claude Code. All remain experimental until a compatibility record covers the specific agent version and jail build.

Here’s an OpenCode example. It assumes OpenCode is already installed at $HOME/.opencode/bin, your distribution’s ripgrep package supplies rg on PATH, and you’re still in the disposable directory above:

git init
rg --version
ukai-jail doctor --launch opencode
ukai-jail explain --launch opencode --workspace "$PWD" \
  --ro "$HOME/.opencode/bin" --json

ukai-jail run --launch opencode --workspace "$PWD" \
  --ro "$HOME/.opencode/bin" -- \
  "$HOME/.opencode/bin/opencode" run "Create greeting.txt containing hello."

Review the resolved file and network grants before the run. Change the executable path and read-only grant to match your installation. A provider that needs authentication also needs explicit credential configuration in your launch profile; normal login state is not exposed automatically. See the launch profile reference for the configuration format.

OpenCode 1.18.32 uses rg for file search. Without it, OpenCode attempts a GitHub download that the starter profile refuses. Provision the tool before launch instead of widening the network policy. For a private tool installation, grant its directory with --ro and set the child’s path explicitly, for example -- /usr/bin/env PATH=/private/tools:/usr/local/bin:/usr/bin:/bin /path/to/opencode run …. Contained profiles replace the host’s PATH, so changing it only outside the jail does not expose a private tool directory to the agent. doctor --launch checks Jail capabilities and profile inputs; it does not verify every vendor tool dependency or provider availability.

OpenCode has recorded live runs. The compatibility table names the tested versions, jail builds, and remaining gaps. Check it before assuming a profile will work with your setup.

Inspect a run

Each attempt saves a jail.json receipt, its resolved policy, and an event journal. By default, these live under ~/.local/share/ukai/attempts/. UKAI_DATA_DIR can select a different private data directory.

ukai-jail tail
ukai-jail tail --follow
ukai-jail tail --json

For a particular attempt, use ukai-jail tail --attempt ID --json, replacing ID with its receipt’s attempt_id. This matters when several commands run at once: the default follows the most recently updated attempt.

When reading the receipt, start with these fields:

Field What to check
phase settled means the process tree was verified dead. refused means execution was refused or the command could not be started. An unfinished phase needs investigation.
outcome The child’s result and the reason a run stopped.
containment and applied The boundary, mounts, network mode, and limits actually applied.
lifetime Whether the tree is empty and whether its integrity was verified.
coverage and errors Which operation classes were observed and where evidence is missing.

Observation is on by default and covers a defined set of operations. It does not record every read or every action. Strict evidence is also the default: loss of required evidence stops the run. A zero event count only means no observed events when that class had active coverage.

Learn missing permissions

If a command hits a permission error, learn can help identify the access it needs. It runs the command inside containment and writes a proposal from that run’s evidence:

ukai-jail learn --launch opencode --workspace "$PWD" \
  --ro "$HOME/.opencode/bin" --out "$HOME/ukai-proposal.toml" -- \
  "$HOME/.opencode/bin/opencode" run "Run this project's tests."

The proposal can include exact read-only paths and denied network destinations. A refused tunnel to port 443 is proposed as the bare host, which grants TLS only; a destination is proposed with its port only when the run sent plaintext HTTP there. Denied writes are reported without becoming write grants. Read the proposal, check that the access fits the task, and then apply only what you intend. --adopt shows the change and requires confirmation in an interactive terminal.

Learning executes real work and can change writable files. Its proposal covers one observed run; it is not a complete list of everything that program might ever need. Nothing is adopted automatically.

When something doesn’t work

The host check refuses the profile. Read doctor’s missing requirements. Explicit resource limits need host support. For native ukai-jail on macOS, refusal is the current expected behavior. Changing to none would remove containment.

The command cannot find a file, executable, or library. Check explain and the receipt. The program, its interpreter, and its dependencies must be visible. Add the specific read-only path the task needs, then try again.

The agent cannot connect or authenticate. Check the launch profile’s provider hosts and credential configuration. --allow-host grants a network destination; it does not supply a login or an API key.

A short command reports exec_unconfirmed. With observation off, the command may finish before the jail independently confirms execution. Keep the default observation mode for the first run, and inspect the receipt before repeating work that might have side effects.

The command returned a nonzero status. It may be a child failure, a refusal, or a problem with evidence or cleanup. Read the receipt alongside the exit code. A child’s own exit code can overlap the jail’s codes.

A policy, workspace or environment is too large to record. Every receipt and resolved policy is held to 4 MiB, and a receipt lists at most 1024 grants, credentials, allowed hosts or environment names. A policy whose records could not fit, such as one with thousands of --ro paths, refuses with invalid_config before anything runs, in explain and --label-only as in run; so does a --profile none run started from an environment with more than 1024 variables, or with names too long to record, since its receipt lists every name it inherits. A workspace whose mount table would not fit refuses with invalid_config while it is prepared. Narrow the grants or the workspace, or start the run with fewer variables. A refused run whose error messages alone would not fit still writes its receipt: messages are cut and say so, and every error is printed whole on stderr.

For agents and scripts

The website provides /guide.md and /roadmap.md from these exact source documents, plus /llms.txt as a short discovery index. Give an agent the Markdown guide URL directly; automatic discovery depends on the client. Roadmap entries describe future work, not commands available in the CLI.

Use this sequence when integrating the jail:

  1. Read ukai-jail version --json to identify the build and its schemas.
  2. Run doctor --profile NAME --json or doctor --launch NAME --json and inspect the result. doctor does not accept run overrides such as --workspace or --limit; run checks the final requested policy.
  3. Use explain --json with the same policy flags you will pass to run. Keep the requested access within the user’s authorized task.
  4. Run the command with an explicit --receipt PATH. Use a unique path outside every child-writable grant so concurrent runs cannot overwrite it.
  5. Read that receipt even when the process exits nonzero. Use its attempt_id with tail --attempt ID --json for the matching journal.

For example, from the disposable workspace:

jail_records="$(mktemp -d "$HOME/ukai-jail-records.XXXXXX")"
ukai-jail version --json > "$jail_records/version.json"
ukai-jail doctor --profile tool --json > "$jail_records/doctor.json"
ukai-jail explain --profile tool --workspace "$PWD" --limit wall=30s \
  --json > "$jail_records/policy.json"

After inspecting those reports, run the command and save its receipt:

ukai-jail run --profile tool --workspace "$PWD" --limit wall=30s \
  --receipt "$jail_records/receipt.json" -- /usr/bin/true

run has no --json flag. The child’s output is passed through, and jail diagnostics may also appear on stderr. tail --json emits one JSON event per line (NDJSON), not a single JSON array. A setup error may happen before a receipt is created, so handle a missing file and retain stderr. Don’t fabricate a successful result from the exit code alone.

On failure, report the requested policy, missing capability or evidence, and receipt path. Do not silently widen grants, switch to none, disable observation, or weaken evidence requirements to make a run pass. A permission proposal needs the same review as any other access change. Review output files and test results separately: containment does not establish that the agent completed its task correctly.

The receipt schema and exit code reference cover the machine interfaces in detail.

Performance

Containment adds startup time. Observation also adds work while the command runs, especially when it creates, renames, or deletes many files.

In the recorded Linux benchmark from 29 September 2026, a plain-session fixture of 5,000 file-operation rounds had these median total runtimes:

Execution mode Median runtime
Direct, without the jail 190 ms
Jailed, observation off 375 ms
Jailed, observation on (default) 1,167 ms

Each mode had 30 measured samples. This was an earlier development build on the Ubuntu reference host, using synthetic workloads. It does not predict the speed of your agent.

Across workloads and sessions, the highest p95 added startup time with observation off was 131 ms. The full benchmark includes 540 measured launches; all 60 observation-off no-op runs returned exec_unconfirmed and exit 1. Their timings are included, but they are not successful run receipts. The full report includes the method, build, host, raw data, and limitations.

Understand the boundary

An agent can change files you make writable and send readable data to services you allow. Keep grants small and review the resulting changes. Containment relies on the host kernel, and security review remains part of the work before release.

The network proxy supports HTTP and SOCKS5 TCP connections; UDP is not supported. Optional HTTP(S) credential vaulting supplies placeholders to the child and substitutes secrets at the proxy. An authorized upstream could still return a secret in its response. Command deny-rules help catch mistakes; another program can perform the same operation under a different name.

For more detail, use the operator reference. See the roadmap for what we’re working on next.