pi setup

Host

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#

CommandWhat 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 --allStops 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 listOne 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 listOne 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 listSession 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> / --allDeletes 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> / --legacyDeletes 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 helpUsage.

Examples#

bash
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 --all

Project 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, default private work; see Domains);
  • <path> is one or more folders below the domain, at any depth, e.g. sandbox-test or git.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 &lt;d&gt; days). Retention: SESSIONS_RETENTION_DAYS (default 30) and per domain SESSIONS_<ID>_RETENTION_DAYS, whole days, 0 = keep forever. Details: state.md.

Flags#

FlagCommandsEffect
--with-domain <name>start, shell, selftestAlso 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-domainstart, shell, selftestShortcut for --with-domain <the other one> while exactly two domains are configured; refused (exit 2) otherwise.
--key-from-keychainstartReads 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>startPassed to pi unchanged.

Exit codes#

No container starts on any error. Messages start with pi-sandbox: error:.

CodeMeaning
2Bad usage or unknown flag
10The project path does not exist
11The 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
12Unknown 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)
13pi-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
20The 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
22TELEGRAM_BOT_TOKEN_CMD or FIGMA_API_KEY_CMD failed or printed nothing; the session does not start
23A 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)
30memory-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)
31memory-check: warnings or possible personal data found, nothing blocking
32memory-check: at least one blocking finding (secret, project folder in the core store) or the check itself failed
60Push 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
61Push 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.)
63known-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
40The container runtime is not reachable (start Docker Desktop)
50workspace clone/refresh: ghorg failed or at least one repository failed (listed in the output)
51workspace 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
41The 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)#

WarningMeaning
remote <name> embeds credentials; the agent can read themA 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 .envNo 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 imageThe registry was unreachable (offline, no login); the session runs the image already on this Mac.
image <image> not available from the registry; building it locallyPull 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 itA memory repo is missing; the session gets an empty read-only placeholder.

Summary line#

Every start, shell and selftest prints one line to stderr:

text
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.

VariableRead by the wrapper fromPurpose
PI_ROOT.env, default: parent of the sandbox repoRoot of the four areas
GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL.envIdentity for local commits
PI_DOMAINS.env (or caller’s environment), default private workThe configured domains (Domains)
MCP_<ID>_<NAME>_CMD.envOptional 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 30Days 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).envMemory push URLs, sync only; the domain’s URL reaches the sync container as BRAIN_DOMAIN_URL
ANTHROPIC_API_KEYANTHROPIC_API_KEY_CMD or Keychain, only with --key-from-keychain; any host value is discardedModel key for one session
TELEGRAM_BOT_TOKENTELEGRAM_BOT_TOKEN_CMD in .env, else empty; any host value is discardedTelegram bot token for pi-telegram, read at every start (exit 22 if the command fails or prints nothing)
FIGMA_API_KEYFIGMA_API_KEY_CMD in .env, else empty; any host value is discardedFigma 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.envOptional 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.envPush 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 sshRemote 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.envPublic 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-hostsSSH host keys (known_hosts format, one file per host) trusted in every session, for any host; no .env setting
PUSH_PROTECTED_BRANCHES.envBranch globs the pre-push guard refuses; replaces the built-in list
BRAIN_CORE_USER, BRAIN_<ID>_USER.envUsername prefixed to a memory token stored without user:
PI_SANDBOX_IMAGE_REPO.env, default codeberg.org/black0ut-pi-setup/pi-sandboxImage repository; the tag is the release version in VERSION
PI_SANDBOX_PULL.env or caller’s environment, default alwaysalways: 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_IMAGEset by the wrapperThe resolved image reference, used by compose.yaml / compose.sync.yaml (not passed into the container)
PI_SANDBOX_DOCKERcaller’s environmentTest hook: path of the docker binary (default docker)

just recipes#

RecipeWhat it runs
just checkshellcheck, 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 quickstartRuns 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 gateConstitution 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