Skip to content

Permissions

How Linux (bubblewrap / Landlock), macOS Seatbelt and the Windows ACL runner differ, and the read-only / workspace-write / danger-full-access modes.

qialike runs the confined bash (or pwsh on Windows) through the harness’s ctx.sandbox.confine(). All three platforms get it out of the box: Linux prefers the host’s bubblewrap and falls back to qialike’s own Landlock (which needs the LSM enabled in the kernel, ≥5.13); macOS uses the system Seatbelt; Windows uses the harness’s ACL restricted-token runner. danger-full-access runs unconfined.

The sandbox’s boundary is “a child process sharing the host’s kernel and filesystem”. When you need to isolate the whole environment, use a container, a microVM or a remote executor.

Three modes

Press Tab in the composer to cycle them; the status line shows the current one:

ModeStatus-line labelEffect
read-onlyRead OnlyWrites are denied, except required sinks such as /dev/null.
workspace-writeWorkspace WriteWrites are allowed inside the workspace root and in the temporary areas the backend defines.
danger-full-accessFull accessBypasses confinement; the raw argv is spawned directly.

A few semantics:

  • The default is workspace-write. When a session is resumed, the mode comes from the session’s last persisted sandbox/mode event — that is your last choice, not a fresh default.
  • The mode and the approval policy are decoupled. Switching to danger-full-access raises the file boundary alone; it does not quietly turn the approval policy into “never ask”. Turning approvals off is always an explicit human action (choosing “Allow always” for a tool in the approval dialog).
  • A refused call carries the [sandbox: file access denied under <mode> mode] marker; the model may retry the identical call once with sandbox_permissions (the narrowest wider mode that would suffice) plus a justification, and a human approves it. An escalation has to be strictly wider than the mode in effect, and it applies to that one call only.

Linux: bubblewrap → Landlock, two rungs

Confined bash runs through bwrap (bubblewrap) first, and needs a kernel with unprivileged user namespaces enabled. Where bubblewrap is installed this is the rung taken, and it is the stronger one:

Terminal window
# Debian / Ubuntu
sudo apt install bubblewrap
# Fedora / RHEL
sudo dnf install bubblewrap
# Arch
sudo pacman -S bubblewrap

No bubblewrap does not mean no sandbox: qialike carries its own Landlock rung (its launcher is implemented in apps/tui-bin/src/landlock-shim.ts), taken automatically whenever the kernel has the Landlock LSM enabled (≥5.13) — still a kernel-level constraint.

Only when both rungs are unavailable does the following happen (that, and not a missing bwrap, is what triggers SANDBOX_UNAVAILABLE):

  • The app boots as usual.
  • Every non-shell tool works (the pure-JS fs-sandbox fence is still there).
  • But under workspace-write / read-only every bash call fails closed with SANDBOX_UNAVAILABLE rather than running unconfined.
  • Only danger-full-access (no confinement) runs.
  • Where the executor offers no kernel confinement, per-call shell approval takes over — the prompt states that the command has your full user authority.

This is deliberate fail-closed design: when the requested mode cannot be enforced, the call fails with SANDBOX_UNAVAILABLE instead of being silently let through unconfined. The error text names the missing platform runner.

A kernel with unprivileged user namespaces disabled can produce the same outcome:

Terminal window
# check
cat /proc/sys/kernel/unprivileged_userns_clone # Debian/Ubuntu family
cat /proc/sys/user/max_user_namespaces

macOS: Seatbelt

macOS uses the system’s built-in Seatbelt (sandbox-exec). The workspace-write writable scope is derived from one shared writable root used by both the Seatbelt profile and the in-process fs fence, so the two grant exactly the same roots.

There is nothing to install. If sandbox-exec is unavailable the handling matches Linux: a mode that cannot be enforced fails with SANDBOX_UNAVAILABLE.

Windows: the ACL restricted-token runner

Windows gets OS-level isolation from the harness’s ACL restricted-token runner, out of the box like the other two platforms. Two things have to be carried for it to run inside the single-file build: the native koffi it depends on (replaced by apps/tui-bin/stub/koffi.js), and the runner process itself — the harness locates it by module specifier, which a compiled binary cannot answer, so qialike carries the harness’s own runner and acts as its launcher (see apps/tui-bin/src/windows-acl-shim.ts).

The permission (presets) row is enabled on every platform, Windows included.

What that means in practice:

  • The pure-JS fs-sandbox fence plus the workspace-write + ask approval boundary remain the gate on file effects.
  • The ACL runner provides kernel-level process isolation.
  • You can still use qialike inside WSL2 (which gets the Linux bubblewrap / Landlock rungs), but that is optional, not required. See Windows and WSL.

Design boundaries

  • Enforcement completeness is reported per call: full means every file operation the backend’s governing mode promises, partial means only a subset is governed. A consumer that needs an absolute boundary can refuse or surface partial.
  • Policy travels with the call, not fixed on the provider: two consumers can confine under different policies at the same time; an approved escalation retry is simply a new call made with a wider policy.
  • Secret files cannot be read: the .env family, .git internals and harness credential documents are refused outright (.env.example excepted). This holds in every sandbox mode, and danger-full-access does not lift it. It fences the read tools only: a shell command that names those files still reads them — a gap recorded honestly here, and one Gemini CLI shares.
  • A repository cannot loosen your sandbox: rows that change (or disable) sandbox / sandbox-policy / fs-sandbox / bash-sandbox / pwsh-sandbox / approval / permission / fs-observation-policy can only be written in your own overlay; such rows in a repository-level overlay are always refused. Use --no-project-overlay (QIALIKE_NO_PROJECT_OVERLAY=1) to ignore that layer for one run. See Config.

Next steps