pi-sandbox commands
bin/pi-sandbox is the only supported way to run pi (spec FR-005, FR-010). It resolves values
and selects compose files; every mount is declared in the compose files
(path-contract.md). Put bin/ on your PATH or call it by path.
Commands#
| Command | What it does |
|---|---|
pi-sandbox start <project-path> [--with-domain <name>] [--key-from-keychain] [-- <pi args>…] | Validates the path, creates the pi-agent-state volume, pulls the pinned released image <PI_SANDBOX_IMAGE_REPO>:<VERSION> (if the pull fails: the local image, else a local build, each with a warning — release.md), prints a one-line summary, then runs pi in a fresh container (docker compose run --rm). Everything after -- goes to pi unchanged. |
pi-sandbox shell <project-path> [--with-domain <name>] | Same container and mounts as start, but opens bash instead of pi. Use it to inspect or clean the agent state. |
pi-sandbox selftest <project-path> [--with-domain <name>] | Same container and mounts, runs the isolation test built into the image. The first output line is pi <version>, the last is isolation: <passed>/<total> checks passed. Exit code is non-zero if any check fails. |
pi-sandbox stop <project-path> / pi-sandbox stop --all | Stops the running sandbox containers of that project, or all of them (by the pi-sandbox.project label). |
pi-sandbox rebuild [--no-cache] | Builds <PI_SANDBOX_IMAGE_REPO>:<VERSION> locally from the Dockerfile with the pinned base digest and pi version. The state volume is untouched. |
pi-sandbox memory-sync <domain> | Reviews and pushes core memory and that domain’s memory in a separate sync container (memory.md). Runs the memory check first: secrets block a store, possible personal data must be confirmed. |
pi-sandbox memory-check <domain> | Read-only hygiene check of core and that domain’s memory (secrets, possible personal data, format) in a network-less container; prints a report and exits 0 / 31 / 30 / 32 (memory.md). Nothing is written, committed or pushed. |
pi-sandbox workspace clone <domain> <host>/<group path> [--user] [--include-archived] [--include-forks] | Clones every repository of a forge group (with GitLab subgroups) into ws/<domain>/<host>/<group>/… with ghorg in a separate workspace container that sees only ws/<domain>; existing repositories are only fast-forwarded when clean; archived repositories and forks only with the flags; --user for a GitHub/Codeberg user (workspace.md, pi-config spec 009). |
pi-sandbox workspace refresh <domain> [<host>/<group path>] | Runs clone again with the recorded options, for one group or for every recorded group of the domain. |
pi-sandbox workspace list [<domain>] | Recorded groups per domain and the token source per host (cmd, keychain, missing); reads no token, starts nothing. |
pi-sandbox known-hosts add <host> [--port <n>] [--fingerprint <SHA256:…>]... | Trusts a host’s SSH host keys in every session: ssh-keyscan runs on this machine (ed25519, ecdsa, rsa), the fingerprints are shown and stored in known_hosts.d/<host> (<host>_<port>) only after you answer y. With --fingerprint (repeatable, take it from the server admin or the forge) nothing is asked: only keys with a listed fingerprint are stored, and every listed one must be presented (exit 63 otherwise). Replaces an existing entry of that host. |
pi-sandbox known-hosts list | One line per stored key: host, key type, fingerprint. |
pi-sandbox known-hosts remove <host> [--port <n>] | Deletes that host’s entry (exit 2 if there is none). |
pi-sandbox domain add <name> | Adds a memory domain (pi-config spec 005): checks the name, adds it to PI_DOMAINS in .env (every other line stays), creates ws/<name>/, the memory store brain/pi-memory-<name>/ (git repo with README.md, briefing.md, index.md, log.md, notes/, no commit) and the configuration store config/pi-config-<name>/ (git repo with a README.md, no commit; config.md) where they do not exist yet, and prints the remaining steps: BRAIN_<ID>_URL, the Keychain item brain-<name>-token (or BRAIN_<ID>_TOKEN_CMD), then memory-sync <name>. Existing folders are kept untouched. There is no remove command: delete a domain by editing PI_DOMAINS and the folders by hand. |
pi-sandbox domain list | One line per configured domain: WS (folder exists), STORE (memory repo exists), CONFIG (configuration store config/pi-config-<domain>/ exists), URL (BRAIN_<ID>_URL set), CREDENTIAL (cmd, keychain, missing, or unknown without the security tool). Never reads a secret. |
pi-sandbox config init <domain> | Creates the configuration store config/pi-config-<domain>/ (git repo with a README.md, no commit) of an already configured domain — for domains from before spec 007; domain add does it for new ones. Prints created or kept; an existing store is never changed. Unconfigured domain → exit 2. |
pi-sandbox config show <project-path> | The merged settings and MCP servers for that project (pi-config spec 007, config.md): contributing files, each setting with the level that set it, each MCP server with state, level and secrets (NAME→slot (keychain|cmd|missing), with , all domains for a shared secret); instructions: (level, file, size), skills: and prompts: (name, [level]) (pi-config spec 008). Never reads a secret value, runs no *_CMD, copies nothing, starts no container. |
pi-sandbox sessions list | Session history per domain (pi-config spec 006): one line per configured domain with SESSIONS, SIZE, OLDEST (date of the oldest session) and RETENTION; a legacy line while history from before spec 006 is left in the shared state; a (not configured) line for pi-sessions-<name> volumes of removed domains. Runs in the network-less sessions container; creates nothing. |
pi-sandbox sessions prune <domain> / --all | Deletes that domain’s (every configured domain’s) sessions older than its retention now and prints <domain>: removed <n> session(s) older than <d> days; retention 0 → retention 0, nothing pruned. |
pi-sandbox sessions clear <domain> / --legacy | Deletes all session history of that domain (or the leftover history from before spec 006) after delete all <n> session(s) …? [y/N]; only y/yes deletes. |
pi-sandbox resolve <project-path> | Diagnostic: prints domain=… project=… path=… for a project path, or the error it would cause. |
pi-sandbox help | Usage. |
Examples#
pi-sandbox start ~/pi/ws/private/homelab # new session
pi-sandbox start ~/pi/ws/private/homelab -- --continue # continue the last session
pi-sandbox start ~/pi/ws/work/shop -- --model sonnet:high
pi-sandbox start ~/pi/ws/work/shop --with-domain private # private memory read-only, this run only
pi-sandbox domain add club # new domain: ws/club/, brain/pi-memory-club/
pi-sandbox start ~/pi/ws/private/homelab --key-from-keychain
pi-sandbox selftest ~/pi/ws/private/sandbox-test
pi-sandbox stop --allProject paths#
<project-path> may be absolute or relative. It is resolved with all symlinks followed and must
then lie at <PI_ROOT>/ws/<domain>/<path>:
<domain>is one of the configured domains (PI_DOMAINS, defaultprivate work; see Domains);<path>is one or more folders below the domain, at any depth, e.g.sandbox-testorgit.example.org/group/repo(pi-config spec 004); every folder name matches[A-Za-z0-9._-]+(dots allowed for host names) and none is.git;- the project does not need to be a git repository (meeting notes, docs), but a folder
inside a git repository that is not its root is refused (
…/repo/src), so a subfolder never becomes its own project or memory level; - the domain folder itself, and symlinks that land somewhere else (for example
ws/private/x→ an work repo), are refused.
The domain is always read from the resolved path; it picks the memory scope (spec FR-017).
Domains#
The domains are the names in PI_DOMAINS in .env (space-separated; unset or empty means
private work). A name is 1–32 lowercase letters, digits or -, starts with a letter and is
never core; duplicates are refused. Every command checks the list first and stops with exit 12
naming a bad entry. Each domain has ws/<name>/, brain/pi-memory-<name>/ and, for the sync,
settings named by one rule: <ID> is the name in uppercase with - → _, so my-club uses
BRAIN_MY_CLUB_URL, BRAIN_MY_CLUB_TOKEN_CMD, BRAIN_MY_CLUB_USER and the Keychain account
brain-my-club-token. Add one with pi-sandbox domain add <name> or just domain-add <name>.
Session history#
Each domain’s session history is its own volume pi-sessions-<domain>, created on the domain’s
first start/shell/selftest; at that moment the domain’s history from the old shared
location moves over once (moved <n> session folder(s) of <domain> from the shared history;
leftovers of no configured domain are reported with sessions clear --legacy). Before pi and
shell start, sessions older than the retention are deleted (removed <n> session(s) older than <d> days). Retention: SESSIONS_RETENTION_DAYS (default 30) and per domain
SESSIONS_<ID>_RETENTION_DAYS, whole days, 0 = keep forever. Details: state.md.
Flags#
| Flag | Commands | Effect |
|---|---|---|
--with-domain <name> | start, shell, selftest | Also mounts that configured domain’s memory read-only, for this run only (FR-018). Exactly one extra domain; its own domain, an unknown name or a second opt-in are refused (exit 2). |
--with-other-domain | start, shell, selftest | Shortcut for --with-domain <the other one> while exactly two domains are configured; refused (exit 2) otherwise. |
--key-from-keychain | start | Reads the model API key — from ANTHROPIC_API_KEY_CMD if set in .env, else from Keychain item pi-sandbox / anthropic-api-key — and passes it through the environment for this session only (FR-008b, credentials.md). |
-- <pi args> | start | Passed to pi unchanged. |
Exit codes#
No container starts on any error. Messages start with pi-sandbox: error:.
| Code | Meaning |
|---|---|
| 2 | Bad usage or unknown flag |
| 10 | The project path does not exist |
| 11 | The resolved path is not a project under <PI_ROOT>/ws/<domain>/<path>: outside ws/, the domain folder itself, an invalid folder name (named in the message), a symlink landing elsewhere, or a folder inside a git repository that is not its root |
| 12 | Unknown domain folder (the message lists the configured domains); an invalid PI_DOMAINS entry (invalid name, core, duplicate); domain add with an invalid, reserved or already configured name; an invalid SESSIONS_RETENTION_DAYS / SESSIONS_<ID>_RETENTION_DAYS (named in the message); a configuration file that is not valid JSON or not an object, packages below the global level, more than 16 MCP secrets in use; instructions over 64 KiB; a symlinked level folder or level AGENTS.md; an invalid level skill or prompt template (not a regular file or folder, invalid name, SKILL.md missing) or more than 10 MiB / 2,000 files of them (sizes per level in the message) (config.md) |
| 13 | pi-config/agent not found at <PI_ROOT>/pi-config/agent, or PI_ROOT does not exist |
| 14 | .env missing in the sandbox repo — copy .env.example |
| 20 | The API key could not be read: Keychain item locked or missing, or ANTHROPIC_API_KEY_CMD failed or printed nothing; there is no fallback |
| 21 | --key-from-keychain while a stored login exists in the agent state (the stored login would win); run /logout first or start without the flag |
| 22 | TELEGRAM_BOT_TOKEN_CMD or FIGMA_API_KEY_CMD failed or printed nothing; the session does not start |
| 23 | A sidecar (kube-proxy of a level kubeconfig, ssh-agent of the SSH hosts) did not start or stopped right after its start; the session does not start (config.md, SSH hosts) |
| 30 | memory-sync failed: a store is missing, a credential could not be read (Keychain or BRAIN_<ID>_TOKEN_CMD), a token has no user, a store was blocked by the memory check, or the sync failed for a store; local changes are kept. memory-check: a store is missing (and nothing blocks) |
| 31 | memory-check: warnings or possible personal data found, nothing blocking |
| 32 | memory-check: at least one blocking finding (secret, project folder in the core store) or the check itself failed |
| 60 | Push from sessions: the credential of the project’s host cannot be read (WS_<ID>_<HOST_ID>_PUSH_KEY / _PUSH_KEY_CMD / _PUSH_TOKEN_CMD or the Keychain item: file unreadable, command failed or printed nothing, key given twice), or WS_<HOST_ID>_KNOWN_HOSTS_FILE is unreadable; nothing started |
| 61 | Push from sessions: the key is not an unencrypted private key (a passphrase is not supported), the token contains whitespace, or a line of WS_<HOST_ID>_KNOWN_HOSTS_FILE or of a known_hosts.d/ file is not <host> <key type> <key>; nothing started. (Inside the container problems with the credential end with exit 62.) |
| 63 | known-hosts add: ssh-keyscan/ssh-keygen missing, no host keys received, a --fingerprint the host did not present, or the keys were not confirmed; nothing written |
| 40 | The container runtime is not reachable (start Docker Desktop) |
| 50 | workspace clone/refresh: ghorg failed or at least one repository failed (listed in the output) |
| 51 | workspace clone/refresh: no forge token for that domain and host (WS_<ID>_<HOST_ID>_TOKEN_CMD or Keychain ws-<domain>-<host>-token), or the command failed or printed nothing (password manager locked?); nothing started |
| 41 | The session history of the domain could not be set up (moving the old shared history into the new pi-sessions-<domain> volume failed); the new volume is removed again, nothing is deleted, the next start retries |
Warnings (the session still starts)#
| Warning | Meaning |
|---|---|
remote <name> embeds credentials; the agent can read them | A git remote URL of the project contains user:token@. Remove it on the host (FR-022). |
commits will fail until GIT_AUTHOR_* are set in .env | No git identity; the agent’s commits fail with git’s identity error. |
pi-docker-sandbox is on branch <b>, not main; … | The sandbox checkout is not on main (or is a detached HEAD). VERSION, and with it the pinned image, comes from this checkout, so it may be outdated or unreleased. Run git switch main && git pull in pi-docker-sandbox. Only for start, shell and selftest; no network access. |
could not pull <image>; using the local image | The registry was unreachable (offline, no login); the session runs the image already on this Mac. |
image <image> not available from the registry; building it locally | Pull failed and no local copy exists; the image is built from the Dockerfile with the same tag. |
MCP server <s> disabled: secret <NAME> not found (…) | A placeholder of that server has no Keychain item mcp-<domain>-<NAME> and no MCP_<ID>_<NAME>_CMD; the server is off for this session (config.md). |
<file>: <server> overrides a server no level above defines; ignored / <file>: … looks like a literal secret; use ${NAME} | Configuration hints, the session starts. |
brain store <id> not found at <path>; starting without it | A memory repo is missing; the session gets an empty read-only placeholder. |
Summary line#
Every start, shell and selftest prints one line to stderr:
pi-sandbox: project=<path> domain=<d> memory=core:rw,<d>:rw[,<other>:ro] credentials=<stored-login|keychain> image=<repo>:<release-version> pi=<pi-version> [mode=shell|selftest]Environment variables#
The wrapper reads .env (parsed, never executed) and passes only the variables declared in the
compose files. The full list, with where each value comes from, is in
path-contract.md.
| Variable | Read by the wrapper from | Purpose |
|---|---|---|
PI_ROOT | .env, default: parent of the sandbox repo | Root of the four areas |
GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL | .env | Identity for local commits |
PI_DOMAINS | .env (or caller’s environment), default private work | The configured domains (Domains) |
MCP_<ID>_<NAME>_CMD | .env | Optional command printing the MCP secret <NAME> of domain <ID>; replaces the Keychain item mcp-<domain>-<NAME>; also the kubeconfig of a level kube.json and the SSH keys and host-key commands of ssh.json (config.md) |
SESSIONS_RETENTION_DAYS, SESSIONS_<ID>_RETENTION_DAYS | .env, default 30 | Days to keep session history, all domains / one domain; 0 = keep (Session history); reaches the session container as SESSION_RETENTION_DAYS |
BRAIN_CORE_URL, BRAIN_<ID>_URL (e.g. BRAIN_PRIVATE_URL, BRAIN_WORK_URL) | .env | Memory push URLs, sync only; the domain’s URL reaches the sync container as BRAIN_DOMAIN_URL |
ANTHROPIC_API_KEY | ANTHROPIC_API_KEY_CMD or Keychain, only with --key-from-keychain; any host value is discarded | Model key for one session |
TELEGRAM_BOT_TOKEN | TELEGRAM_BOT_TOKEN_CMD in .env, else empty; any host value is discarded | Telegram bot token for pi-telegram, read at every start (exit 22 if the command fails or prints nothing) |
FIGMA_API_KEY | FIGMA_API_KEY_CMD in .env, else empty; any host value is discarded | Figma token for the figma MCP server, read at every start (exit 22 if the command fails or prints nothing) |
ANTHROPIC_API_KEY_CMD, BRAIN_CORE_TOKEN_CMD, BRAIN_<ID>_TOKEN_CMD, TELEGRAM_BOT_TOKEN_CMD, FIGMA_API_KEY_CMD | .env | Optional host command that prints the secret (1Password op read …, Bitwarden bw get password …); replaces the Keychain lookup. Never passed into a container |
WS_<ID>_<HOST_ID>_PUSH_KEY, WS_<ID>_<HOST_ID>_PUSH_KEY_CMD | .env | Push from sessions, remote form ssh: path of, or command printing, an unencrypted private key for that domain and git host; the key reaches only sessions of projects below ws/<domain>/<host>/ (credentials.md) |
WS_<ID>_<HOST_ID>_PUSH_TOKEN_CMD, WS_<ID>_<HOST_ID>_PUSH_USER | .env (token: or Keychain ws-<domain>-<host>-push-token) | Push from sessions, remote form https: command printing the access token; optional user name (default oauth2, x-access-token on GitHub) |
WS_<ID>_<HOST_ID>_REMOTE | .env, beats WS_<HOST_ID>_REMOTE, default ssh | Remote form for that domain and host: chooses key or token for push and the remote URLs workspace clone sets (workspace.md) |
WS_<HOST_ID>_KNOWN_HOSTS_FILE | .env | Public SSH host keys (known_hosts format) of a host beyond codeberg.org, github.com and gitlab.com, for ssh push |
known_hosts.d/ | folder next to bin/ (git-ignored), written by pi-sandbox known-hosts | SSH host keys (known_hosts format, one file per host) trusted in every session, for any host; no .env setting |
PUSH_PROTECTED_BRANCHES | .env | Branch globs the pre-push guard refuses; replaces the built-in list |
BRAIN_CORE_USER, BRAIN_<ID>_USER | .env | Username prefixed to a memory token stored without user: |
PI_SANDBOX_IMAGE_REPO | .env, default codeberg.org/black0ut-pi-setup/pi-sandbox | Image repository; the tag is the release version in VERSION |
PI_SANDBOX_PULL | .env or caller’s environment, default always | always: pull the pinned image on every start/shell/selftest/memory-sync; never: no pull, use the local image or build it (offline, or to test a fresh local build — just gate and just quickstart use it) |
PI_SANDBOX_IMAGE | set by the wrapper | The resolved image reference, used by compose.yaml / compose.sync.yaml (not passed into the container) |
PI_SANDBOX_DOCKER | caller’s environment | Test hook: path of the docker binary (default docker) |
just recipes#
| Recipe | What it runs |
|---|---|
just check | shellcheck, bats tests/, compose config lint, scripts/check-sandbox-definition.sh, scripts/check-sandbox-docs.sh |
just domain-add <name> | bin/pi-sandbox domain add <name> (Domains) |
just quickstart | Runs scripts/quickstart.sh: every non-interactive quickstart step with a PASS/FAIL table, then the steps that need you (login, commit identity, real memory sync) |
just gate | Constitution sandbox gate: rebuild, then selftest on ws/<first configured domain>/.pi-sandbox-gate (created and removed automatically); checks the pinned pi version and that every isolation check passed |