Sandbox

Run any task's terminals and hooks inside a sandbox. Three backends: a Lima VM that mounts only the task's work, nono's kernel-level access limits in place on the host, or a launcher of your own.

Choose a backend

Sandboxing is a per-terminal choice. Right-click a task and use Open in → Lima VM sandbox, Open in → nono sandbox, or Open in → Custom sandbox; an entry appears for each backend available to the project. Plain Open in → Terminal runs on the host, unsandboxed. Every terminal and hook opened for that task then runs under the chosen backend. A sandboxed terminal carries an outlined status dot and a backend suffix on its label.

Each backend has its own configuration under Project Settings → Sandbox.

Lima VM

Lima-sandboxed terminals run inside a Linux VM, on a parallel git branch, in a worktree that contains only tracked files.

What it does

For a Lima-sandboxed task, Ouijit creates a second git worktree at ~/Ouijit/sandbox-views/<project>/T-N-sandbox on a child branch s/<your-branch>. Because it is a plain git worktree add, only tracked files exist there: no .env, node_modules, build caches, or anything else gitignored. That sandbox-view directory is what gets mounted into a Lima VM as the agent's working directory. Your real worktree is never mounted.

When the agent commits inside the VM, the commit lands on s/<your-branch>. A host-side watcher fast-forwards your branch to match. If you committed in parallel on the host, the fast-forward fails and Ouijit surfaces the divergence.

How it works

  1. Open a Lima sandbox terminal from a task's Open in menu. Every terminal and hook for that task then runs in the VM.
  2. First terminal. Ouijit lazily creates an Ubuntu 24.04 VM via Lima on the first sandboxed spawn for a project. One VM per project, reused across that project's sandboxed tasks.
  3. Spawn progress. The dialog shows each step: checking VM status, creating or starting, SSH connection, provisioning, launching the shell. First boot takes 1-2 minutes; later spawns reuse the running VM.
  4. Shell sessions run inside the VM via limactl shell, with the working directory set to the sandbox-view path.
  5. VM lifecycle and resources. Manage the VM from Project Settings → Sandbox → Lima VM: see status, create, start, stop, recreate, open a console, and edit the merged Lima YAML config (memory, CPU, disk, mounts). Resource changes prompt a recreate.

Mount surface

Only two host paths are exposed to the VM:

Host pathIn guestWhy
~/Ouijit/sandbox-views/<proj>/Read/writeThe agent's tracked-files-only worktree.
<project>/.git/Read-only base, with read/write overlays at objects/, refs/, logs/, worktrees/Commits need to write packs, refs, and reflogs. hooks/, config, and packed-refs stay read-only.

Your home directory, source tree, system binaries, and any other host paths are not mounted at all.

Isolation properties

  • Gitignored content is not in the mount. .env, .npmrc, secrets/, node_modules, and build artifacts never leave the host.
  • Host toolchains stay separate. darwin-arm64 node_modules, Rust target/, and Python .venv don't collide with the Linux guest.
  • Parallel sandboxed tasks on the same project share the VM but work in separate sandbox-view directories.
  • .git/hooks/ and .git/config are read-only in the mount, so writes from the VM that would land there fail with EROFS.
  • No ouijit CLI in the VM. The binary isn't installed; the /api/* routes return 403 for sandbox-scoped tokens. Only the /hook endpoint accepts them.

Caveats

  • Committed files are always visible. The sandbox view is populated from the index, so anything you committed lives there regardless of .gitignore. To remove: git rm --cached <file> and rotate the secret.
  • Commits round-trip through fast-forward. Parallel commits on the host make the fast-forward fail; Ouijit surfaces the divergence.
  • The VM has no git credentials. Pushes happen from the host. Commits made inside the VM ride the next host-side push.
  • The VM's home directory is out of scope. The isolation above applies to mounts. Files copied into the VM's home (~/.netrc, ~/.git-credentials) aren't covered.

Customize the VM

VMs start from a clean Ubuntu 24.04 image with bash, git, curl, wget, nodejs, npm, python3, and build-essential pre-installed. Defaults are 4 GiB memory, 2 CPUs, 10 GiB disk.

Resource limits, extra packages, additional mounts, and provisioning scripts are controlled by the Lima YAML config in Project Settings → Sandbox → Lima VM. The editor shows the merged effective config and lets you write overrides; Ouijit prompts you to recreate the VM when changes require it. See the Lima configuration reference for the available keys.

Requirements

Ouijit bundles limactl; there is nothing extra to install.

  • macOS. Apple's Virtualization.framework (vz) with vzNAT networking.
  • Linux. QEMU with user-mode networking.

Limitations

  • First boot downloads an Ubuntu cloud image and provisions packages. It needs internet and takes 1-2 minutes.
  • GPU passthrough is not supported.
  • macOS-specific tooling (Xcode, Swift) is not available inside the Linux VM.
  • Docker-in-VM requires additional Lima configuration.
  • The VM home directory is ephemeral. Anything outside the mounted sandbox-views and .git overlays is lost on recreate.

nono

nono runs a task's commands in place on the host worktree under kernel-level access limits (Seatbelt on macOS, Landlock on Linux). There is no VM and no file sync, so it starts instantly. Compared to the Lima VM it is a lighter boundary: a kernel access policy on a host process rather than a separate machine.

nono is experimental. Turn it on under Project Settings → Experimental; the nono sandbox entry then appears in a task's Open in menu.

What it does

Terminals and hooks run on the real worktree, wrapped by nono. Filesystem access is limited to the worktree and the repository's git data, with the same read/write overlays at objects/, refs/, logs/, worktrees/, so commits land while hooks/ and config stay read-only. Ouijit derives these grants from the task's worktree path at spawn time. Network is allowed by default; the local hook-server port stays reachable so agent status still reports back.

Configuration

Under Project Settings → Sandbox → nono:

  • Additional folders. Extra paths granted read/write beyond the worktree and git data, e.g. a shared cache or a monorepo sibling.
  • Block outbound network. Deny outbound connections. The hook-server port stays open so agent status keeps working.
  • Allowed ports. Extra localhost ports to keep reachable, e.g. a dev server on 3000.

Requirements

Ouijit bundles the nono binary and the agent packs it uses; there is nothing extra to install.

  • macOS. Seatbelt (sandbox-exec).
  • Linux. Landlock, which needs kernel 5.13 or newer.

Custom

Read this first. A plain task terminal carries a host-scoped OUIJIT_API_TOKEN. A launcher you run by hand inside one passes that token into the sandbox, where an agent can rewrite hooks and this very setting, so the next spawn runs its command on the host. Either unset OUIJIT_API_TOKEN in a hand-run launcher (status reporting stops), or configure the launcher here so Ouijit spawns it with a sandbox-scoped token.

The Custom backend runs a task's terminals under a launcher you provide: a reviewed script in your team's control, with its own pinned sandbox binary, committed policy, and preflight checks. Ouijit grants nothing itself; the launcher owns the boundary. It is experimental. Turn it on under Project Settings → Experimental, then set the command under Project Settings → Sandbox → Custom or with ouijit sandbox-command set.

What Ouijit passes

Ouijit runs <launcher> [args] -- <shell> [shell args] with the working directory set to the task worktree and the full terminal environment: OUIJIT_API_URL and a sandbox-scoped OUIJIT_API_TOKEN, OUIJIT_PTY_ID, OUIJIT_WRAPPER_DIR, OUIJIT_CLI_PATH (when the bundled CLI is present), OUIJIT_SHELL_INTEGRATION_DIR, and the shell integration's ZDOTDIR. The launcher's argv is exactly the configured command, split on whitespace with shell-style quoting; nothing is expanded and nothing is added except the -- and the shell.

Everything Ouijit would otherwise have granted is exported as advisory hints. They are computed on the host from the project path and Ouijit's own directories, never from the worktree, and a launcher is free to ignore them.

VariableWhat Ouijit's other backends do with it
OUIJIT_SANDBOX_WORKTREEThe task worktree, read+write.
OUIJIT_SANDBOX_GIT_DIRThe repository's .git, read-only.
OUIJIT_SANDBOX_GIT_WRITABLE_DIRSColon-separated objects, refs, logs, worktrees under it, writable so commits land while hooks/ and config stay read-only.
OUIJIT_SANDBOX_HOOK_PORTThe localhost port that must stay reachable for agent status to report back.
OUIJIT_SANDBOX_CACHE_DIRA per-project package-manager cache outside any worktree, read+write. Shared by every task of the project.
OUIJIT_SANDBOX_WRAPPER_DIROuijit's agent shims and CLI reference, read-only. Its bin/ is first on PATH.
OUIJIT_SANDBOX_CLI_DIRThe bundled ouijit CLI the shim runs, read-only. Set only when the bundled CLI is present.

What the launcher is responsible for

  • Exec the argv after -- with the inherited environment intact: at least PATH, ZDOTDIR, every OUIJIT_* variable, TERM, HOME, SHELL, and the locale. Shell integration, the status dot, and the ouijit and agent shims are all carried by the environment; a launcher that scrubs it starts a bare shell.
  • Own the boundary. Ouijit adds no --allow or --read flags.
  • Keep the hook port reachable if you block the network.
  • Exit non-zero to refuse. Whatever it wrote to stderr appears in the terminal, the exit line prints red, and Ouijit raises a "sandbox failed to start" notice. It never falls back to an unsandboxed shell.

Where the launcher may live

An absolute path or a bare name resolved on PATH. Relative paths are refused, and so is any absolute path inside the worktree or the spawn directory: the sandboxed agent can write there, and a launcher it can edit is one it can turn off. Keep a versioned launcher in the repo if you like, but install it from a reviewed checkout to somewhere the sandbox cannot write, such as ~/.local/bin, and point the setting there. If your PATH contains relative entries, use an absolute path.

A restored terminal whose backend is no longer available (the flag was turned off) reopens as a host shell, as it does for the other backends.