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
- Open a Lima sandbox terminal from a task's Open in menu. Every terminal and hook for that task then runs in the VM.
- 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.
- 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.
- Shell sessions run inside the VM via
limactl shell, with the working directory set to the sandbox-view path. - 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 path | In guest | Why |
|---|---|---|
~/Ouijit/sandbox-views/<proj>/ | Read/write | The 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, Rusttarget/, and Python.venvdon'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/configare read-only in the mount, so writes from the VM that would land there fail withEROFS.- No
ouijitCLI in the VM. The binary isn't installed; the/api/*routes return 403 for sandbox-scoped tokens. Only the/hookendpoint 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
.gitoverlays 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.
| Variable | What Ouijit's other backends do with it |
|---|---|
OUIJIT_SANDBOX_WORKTREE | The task worktree, read+write. |
OUIJIT_SANDBOX_GIT_DIR | The repository's .git, read-only. |
OUIJIT_SANDBOX_GIT_WRITABLE_DIRS | Colon-separated objects, refs, logs, worktrees under it, writable so commits land while hooks/ and config stay read-only. |
OUIJIT_SANDBOX_HOOK_PORT | The localhost port that must stay reachable for agent status to report back. |
OUIJIT_SANDBOX_CACHE_DIR | A per-project package-manager cache outside any worktree, read+write. Shared by every task of the project. |
OUIJIT_SANDBOX_WRAPPER_DIR | Ouijit's agent shims and CLI reference, read-only. Its bin/ is first on PATH. |
OUIJIT_SANDBOX_CLI_DIR | The 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 leastPATH,ZDOTDIR, everyOUIJIT_*variable,TERM,HOME,SHELL, and the locale. Shell integration, the status dot, and theouijitand agent shims are all carried by the environment; a launcher that scrubs it starts a bare shell. - Own the boundary. Ouijit adds no
--allowor--readflags. - 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.