Skip to content
GitHubRSS

Sandboxing tasks

Prove a task reads only what it declares.

  1. Add sandbox to the task’s exec. sandbox: {} allows nothing in the workspace, not even the package. Outside the workspace root (~/.cache, /etc) reads are open and fold into no key: declare what the output depends on as a key input.
  2. Grant the package: allow: { read: ['.'] }. Its node_modules is readable already. Another package of yours, linked there, is readable when the task depends on one of that package’s command tasks; an uncached task reads every linked package.
  3. Grant each output directory in write, and each host in network. The sandbox does not read cache: declare both.
  4. Run the task. An undeclared read or write fails it and names the path.
  5. Declare that path, or silence a noisy tool’s path with ignore.
packages/app/vx.config.ts
import { defineProject } from '@vzn/vx'
export default defineProject({
tasks: {
build: {
exec: {
command: 'vite build',
sandbox: {
allow: {
read: ['.', '~/.cache/ms-playwright'],
write: ['dist/**'],
network: ['registry.npmjs.org'],
},
deny: { network: ['telemetry.example.com'] },
ignore: { write: ['*.bun-build'] },
},
},
cache: { inputs: { files: ['src/**', 'index.html'] }, outputs: { files: ['dist/**'] } },
},
},
})

allow takes every key below. deny takes only network. Domain lists are one union per run, which every sandboxed task reaches; deny.network is refused to every sandboxed task. ignore takes read, write, systemInfo and network, as patterns, and refuses the rest.

KeyGrants
readpaths or globs: package-relative, absolute or ~/
writepaths or globs; a directory ends in / or is a glob (dist/**)
networktrue, or a list of domains (*.sentry.io)
localBindingbind localhost ports; a list ([3000]) makes them reachable from outside
unixSocketstrue, or socket paths
systemInfosysctl names a tool probes (vfs.disk-space)
machLookupmacOS services (com.apple.FSEvents)
ptya terminal
gitConfigwrites to .git/config

A task never reaches another package or a root file you did not grant, bar the linked packages of step 2. That wall is silent. An undeclared touch of the task’s own files fails the task, and a failed task is never cached.

  • Linux: bubblewrap (bwrap), socat and ripgrep (rg); strace to name an undeclared read. vx info says if your host can.
  • macOS: the system sandbox. Its report can miss a record under load; the denial never does.
  • Windows: under WSL.
  • A group task: it has no command.
  • A task that itself sandboxes, on macOS (a sandbox cannot nest).
  • write /proc/self/uid_map: Operation not permitted. You are root in a container. Run as a normal user, or set weakerWhenNested: true.
  • File exists from the task’s own mkdir. A write grant with no trailing slash is a file. Write 'coverage/'.
  • On Linux a file made during the run is denied. A glob expands when the task starts: grant its directory.
  • read packages/ui through packages/app/node_modules/@x/ui, and its key folds no task of @x/ui. The task imports a sibling its key never sees. Depend on a command task of it (dependsOn: ['^source'], or ^build), or grant and key the files yourself.