Repository navigation
Sandboxes
An additive, optional part of wip.yml: named sandboxes are long-lived containers declared
beside your app's stack, each with its own image and its own declared storage. They are what
wip sandbox operates on.
Nothing here changes the stack. dependencies:, compose: and sync: keep working exactly as
they did, and wip up / down / exec / sync never touch a sandbox. Declaring resources is
also inert on its own: loading wip.yml — or running wip config — never creates, mounts or
deletes anything. Only wip sandbox and wip volume do that.
version: 1
resource_namespace: example-project
volumes:
- name: project
persistent: true
mount: /workspace
- name: scratch
persistent: false
mount: /tmp/work
sandboxes:
- name: first
image: example/tool:1
volumes: [project, scratch]
- name: second
image: example/tool:1
volumes: [project]One explicit, stable identity for everything this wip.yml declares. It is required as soon as
volumes: or sandboxes: has an entry, and it must match [a-z][a-z0-9_-]{0,62}.
Backend names are derived from it, so changing it designates different resources rather than renaming the existing ones. Keep the old value around if you still have resources to clean up.
A sequence — not a mapping — so duplicate names are rejected instead of silently overwriting each other:
| Key | Required | Contract |
|---|---|---|
name |
yes | Matches [a-z][a-z0-9_-]{0,62}, unique within volumes |
persistent |
yes | A real YAML boolean. There is no default |
mount |
yes | Canonical absolute Linux path: no control characters, no empty segments, no . or .., no trailing slash (except / itself), no commas |
sync |
no | A host directory this volume is mirrored from. See below |
persistent has to be written out because the two values mean opposite things for your data, and
defaulting would mean guessing in the destructive direction:
-
persistent: true— kept when a sandbox is destroyed, and reused by the nextcreate. -
persistent: false— ephemeral. Retained while any container, including a stopped one, references it. After recorded use and removal of the last reference, reconciliation removes it; unused volumes and unknown states are retained. After cleanup, the nextcreategets fresh storage.
Sandbox destruction runs reconciliation. Removing a container directly through the backend does not run it automatically.
Also a sequence, also with unique names. This table covers the basic sandbox settings:
| Key | Required | Contract |
|---|---|---|
name |
yes | Matches [a-z][a-z0-9_-]{0,62}, unique within sandboxes |
image |
yes | Nonempty, no control characters. Its default command must keep running — a container that exits at once is a failed creation, not a sandbox |
volumes |
no | Names of volumes declared above. Omitted means no mounts |
There is no notion of a sandbox's role or purpose. wip infers nothing from a name, and
agent/purpose/mode-style keys are rejected rather than ignored.
Mount the same volume in two sandboxes and they share that storage. Only sandboxes referencing a volume are given that mount by wip. In the example below, "cannot see scratch" describes the mounts wip provides; host administrators and other backend clients may still access the storage:
sandboxes:
- name: first
image: example/tool:1
volumes: [project, scratch] # shares both
- name: second
image: example/tool:1
volumes: [project] # shares project, cannot see scratchCatching these when the file is read is the point of the sequence-with-explicit-names shape:
- a duplicate volume or sandbox name
- a sandbox referencing a volume that isn't declared, or referencing the same one twice
- two volumes mounted at the same path inside one sandbox (the same path in different sandboxes is fine)
- unsupported keys in a volume or sandbox entry
- a missing
resource_namespacewhen resources are declared - a
sync.mountoverlapping the volume's ownmount, or asyncwithout animage
A volume targeting / is accepted by configuration loading but cannot be mounted by
wip sandbox create.
Nested mounts are not a conflict: a shallower path is ordered before a deeper one so the deeper mount isn't shadowed.
See Configuration Errors for how wip reports a rejected file.
A volume can declare the host directory it is mirrored from. Editing stays on the host; the
sandboxes read the volume, which is native storage inside the VM rather than a bind mount shared
over virtiofs — the same reason sync: exists for the stack.
volumes:
- name: project
persistent: true
mount: /workspace
sync:
source: . # relative to wip.yml's directory; default "."
image: alpine-rsync # required; the mirror runs rsync from this image
exclude: [.git, node_modules]| Key | Required | Contract |
|---|---|---|
source |
no | Host directory, expanded against the wip.yml directory. Default .. It must exist |
mount |
no | Where the mirror sees the host tree, read-only. Default /host-src. Must not overlap the volume's mount
|
image |
yes | The mirror's own image, which has to provide rsync
|
exclude |
no |
--exclude patterns |
delete |
no | Whether the mirror deletes what the host no longer has. Default true
|
image is required rather than borrowed from a sandbox: the mirror runs in a throwaway container
of its own, and the images that mount the volume are not guaranteed to carry rsync.
Declaring it mirrors nothing. wip volume sync NAME runs one pass, over the volume that already
exists — it creates no storage, so wip volume create (or wip sandbox create) comes first. The
sandboxes that mount the volume keep running, and the host side is mounted read-only, so a mirror
never writes back to your tree. Re-run it whenever the host tree has moved on; there is no watch
mode for a declared volume.
delete: trueis the default, and it empties what the source does not have. The mirror makes the volume match the host directory, so anything written inside the sandbox under that mount is removed on the next pass. Keep a sandbox's own work on a different volume.
What survives which operation is the thing worth being precise about:
stop → create
|
destroy → create
|
|
|---|---|---|
| The container | kept, then resumed | removed, then a new one is run |
| Anything installed inside the container | still there | gone with the container layer |
persistent: true volumes |
kept | kept, and reused |
persistent: false volumes |
kept | fresh, once the last referencing container is gone |
So the rule for a sandbox you rely on: put it in the image, or put it on a persistent volume.
A package installed by hand into a running sandbox is gone the moment you destroy it.
If a CLI inside the sandbox writes configuration or credentials, and that has to outlive the container, mount a persistent volume over the directory it writes to:
volumes:
- name: tool-config
persistent: true
mount: /home/user/.config/example-tool
sandboxes:
- name: first
image: example/tool:1
volumes: [tool-config]Know where that directory actually is for the tool you are using — the path above is an example, not a convention wip imposes.
A persistent volume holding credentials is a secret store. It outlives the container by design, it is shared with every sandbox that mounts it, and wip neither encrypts it nor masks what is inside it. Restrict access to the machine it lives on, mount it only in the sandboxes that need it, keep it out of anything you publish, and prefer a short-lived credential where the tool supports one. (
wip configmasks secrets it can recognise inwip.yml— see Secret Masking — which says nothing about the contents of a volume.)
Volumes can be driven directly, without going through a sandbox:
wip volume create NAME
wip volume status NAME
wip volume destroy NAME
wip volume reconcile NAME
wip volume sync NAME
wip sandbox create already ensures the volumes its sandbox references, so reach for these when
you want to prepare or inspect storage independently of the container that uses it. The ephemeral
cleanup pass is reconcile. volume destroy explicitly deletes unreferenced owned storage,
including persistent volumes. volume sync mirrors a declared sync source into the volume.
- wip sandbox — creating, running, stopping and destroying
-
Configuration Reference — every other
wip.ymlkey - docs/sandbox-volume-contract.md — the full configuration and ownership contract
- docs/volume-lifecycle.md — declared volume storage in detail
Introduction
Modes
Configuration
- Configuration Reference
- Config File Discovery
- Dependencies
- Networking
- Interactions
- Sandboxes
- Restart Policies
- Env Files
- Secret Masking
- Dockerignore
- Shadow Build Context
- Source Sync
- Sync Modes
compose.yml support
- Compose File Support
- Compose Build
- Compose Depends On
- Compose Profiles
- Compose Variable Interpolation
Commands
- CLI Command Reference
- wip init
- wip version
- wip doctor
- wip help
- wip manual
- wip config
- wip build
- wip up
- wip stop
- wip down
- wip exec
- wip run
- wip shell
- wip logs
- wip sync
- wip dispatch
- wip sandbox
- Global Options
- Debug Output
- TTY Allocation
Guides
- Guides
- AI-Assisted Initialization
- Migrating from dip
- Reusing an Existing compose.yml
- Fixing a Slow Boot
- Continuous Sync
- Auto Restarting Containers
- Multi Arch Images
- Using wip in CI
Troubleshooting
- Troubleshooting & FAQ
- FAQ
- Configuration Errors
- WSLC Not Found
- Registry Authentication
- Architecture Mismatch
- Volume Limit Reached
- rsync Not Found
- Reporting Issues
Comparison
Ruby Implementation (legacy)