~/.agents/skills/ lives in the ephemeral container layer and is rebuilt by
entrypoint-user.sh on every start from two sources, so an edit made through the
symlink may vanish on the next recreate. Adds to §1 (persistence tiers):
- `readlink -f ~/.agents/skills/<name>` as the first move, with a tier table:
resolves under /workspace/skillset → edit in place; resolves under
/usr/local/share/pi-devbox/skills → image layer, edit the canonical repo and
`sudo cp` to activate for the running session.
- Canonical owner per baked skill (pi-devbox-environment → this repo;
pi-extensions → the package repo's skill/, plus this repo's floor snapshot;
mempalace → the private skillset repo), pointing at VENDORED.md as
authoritative.
- The shadowing gotcha: image-baked links are created first and only when
absent, and deploy-skills.sh --prune-stale leaves foreign links alone, so for
a name present in BOTH sources the image copy wins and a skillset edit has no
effect in the container. Documented with the live example found while writing
this: the baked mempalace snapshot carries a Temporal grounding section
(904fe85) that skillset at its snapshot point (8e8db64) lacks.
- Checklist gets a matching line.
Found while adding session findings to the pi-extensions skill (e73cb9f), where
the same resolve-first step was what kept the edit out of the image layer.
14 KiB
name, description
| name | description |
|---|---|
| pi-devbox-environment | Operate correctly inside a pi-devbox container. Load when running inside pi-devbox (detection: the directory `/usr/local/lib/pi-devbox/` exists, the shell prompt is prefixed `[devbox]`, or `~/.ssh-local/config` is present) and the task touches any of: reaching the Docker host or its LAN, SSH, DNS name resolution, what survives container recreate (persistence vs ephemerality), running Python or other REPLs, tmux, or the pi-studio browser UI. Covers the persistence model, the interactive-vs-tool-shell alias gotcha (dssh/dscp/cat=bat exist only in interactive bash), host + LAN SSH reachability and ControlMaster, split-horizon DNS mechanisms, the tmux 0-index constraint, uv-first Python, and pi-studio reachability. This skill teaches MECHANISMS only — concrete hostnames, usernames, internal domains, nameservers, and even the host OS vary per deployment and MUST be discovered at runtime, never assumed or hardcoded. |
pi-devbox environment
You are (or may be) running inside pi-devbox: a Docker container that ships
pi, MemPalace, and a curated tool stack, with the host source tree mounted at
/workspace. This skill is about the container-shaped facts that change how
you should act — things that are easy to get wrong because they differ from a
normal workstation shell.
Golden rule: this environment is a template, not a fixed deployment. The host could be macOS, Windows, or Linux. There may or may not be LAN peers, a VPN, split-DNS, a skillset mount, or the
-studiovariant. Detect and verify the specifics live (commands below) — do not assume any particular hostname, domain, nameserver, or OS. Where this skill shows example values they are illustrative placeholders.
0. Am I in pi-devbox, and what's true here?
Cheap detection signals (any one is sufficient):
[ -d /usr/local/lib/pi-devbox ] && echo "pi-devbox image"
[ -r "$HOME/.ssh-local/config" ] && echo "LAN/host SSH sidecar present"
case "$PS1" in *'[devbox]'*) echo "interactive devbox shell";; esac
Then orient before acting:
cat /etc/os-release | head -2 # container distro (usually Debian)
ls -la /usr/local/lib/pi-devbox/ # which devbox helpers exist
sed -n '/^Host /,$p' ~/.ssh-local/config 2>/dev/null # host/LAN reachability, if any
mount | grep -E ' /workspace | /home/\S+/\.ssh ' # what's bind-mounted
1. Persistence vs ephemerality — know before you write
The container has three storage tiers with very different lifetimes. Pick the right one or work is silently lost on the next recreate/update.
| Tier | Examples | Survives down? |
Survives down -v? |
Survives image update / --force-recreate? |
|---|---|---|---|---|
| Host bind-mount | /workspace, usually ~/.ssh (ro), often ~/.mempalace |
yes | yes (lives on host) | yes |
| Named volume | ~/.pi, ~/.ssh-local, ~/.cache/bash, ~/.local/share/{uv,nvim,zoxide} |
yes | no | yes |
| Writable container layer | anything else: sudo apt install …, rustup/ghc/R toolchains, files in /tmp, /opt edits |
yes | no | no |
Practical consequences:
- Durable work goes in
/workspace(it's the host filesystem, UID-aligned — what you write appears with the user's normal ownership on the host). - Runtime-installed system packages and language toolchains are ephemeral.
If a task needs them reproducibly, it belongs in the image (Dockerfile) or a
project manifest, not an ad-hoc
apt install. Tell the user when you install something that won't survive. ~/.piis a named volume, so things baked into the image under/home/<user>/...are shadowed by the volume on existing containers and only seen on a fresh volume. Image-owned content that must always be live belongs under an image path like/usr/local/...or/opt/...and is linked in by the entrypoint — not dropped into a home directory that a volume covers.
Editing a skill: resolve the symlink before you touch it
~/.agents/skills/ itself is in the ephemeral container layer, rebuilt by
entrypoint-user.sh on every start from two sources — so where a skill really
lives decides whether your edit survives:
readlink -f ~/.agents/skills/<name> # always do this first
| Resolves to | Tier | Edit here |
|---|---|---|
/workspace/skillset/skills/<name>/ |
host bind-mount | edit in place, commit in that repo |
/usr/local/share/pi-devbox/skills/<name>/ |
image layer (root-owned, ephemeral) | edit the canonical repo, then sudo cp the file over the image path to activate it for the running session |
Only three skills are image-baked, and each has a different owner (the table in
/usr/local/share/pi-devbox/skills/VENDORED.md is authoritative):
| Baked skill | Canonical source to edit |
|---|---|
pi-devbox-environment |
pi-devbox repo → rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/ (authored there; this file) |
pi-extensions |
the pi-extensions package repo → skill/. Dockerfile.variant copies it over the vendored snapshot at build, so also refresh pi-devbox's rootfs/.../pi-extensions/ copy to keep the fallback floor from diverging |
mempalace |
the private skillset repo → skills/mempalace/ (manual snapshot refresh per release) |
Editing through the symlink into /usr/local/... is silently lost on the next
recreate — and worse, it diverges from the canonical repo that every other
consumer (host pi, opencode) reads.
Shadowing gotcha: image-baked links are created first and only when the
name is absent, and the later deploy-skills.sh --bootstrap --prune-stale pass
treats them as foreign links and leaves them alone. So for a name present in
both the image and skillset — currently mempalace and pi-extensions —
the image copy wins, and a skillset edit to that skill has no effect in
the container. Verified 2026-07-29: the baked mempalace snapshot carries a
Temporal grounding section (pi-devbox 904fe85) that the skillset copy at
its snapshot point (8e8db64) lacks — containers load the richer baked text
while skillset consumers get the older one. When you change one of those two,
decide deliberately which copy is canonical and sync the other.
2. Interactive shell vs. your tool shell (a real footgun)
The conveniences below are defined in ~/.bash_aliases and only exist in an
interactive login shell. Your bash tool runs non-interactively, so these
are "command not found" there — you must spell out the underlying command.
| Interactive alias | Non-interactive equivalent to actually run |
|---|---|
dssh <host> |
ssh -F "$HOME/.ssh-local/config" <host> |
dscp … |
scp -F "$HOME/.ssh-local/config" … |
cat file (→ bat) |
cat file works, but output differs; use command cat for raw |
ll, la (→ eza/ls) |
ls -lh, ls -lha |
If a command "works in my terminal but not when the agent runs it," this alias gap is the first thing to suspect.
dscp/scp with accented filenames on a macOS host. macOS stores filenames
in Unicode NFD (decomposed — e.g. ä is a + combining U+0308), while the
string you type or paste is usually NFC (precomposed ä, U+00E4). The bytes
differ, so a precomposed remote path silently fails to match on the host —
scp … "mac:'~/Desktop/Skärmavbild ….png'" returns No such file or directory
even though the file plainly exists. Sidestep the encoding entirely: let the
remote shell expand a wildcard, or list the directory first and copy the
exact name it prints.
# glob dodges the NFC/NFD mismatch (the remote shell matches the real bytes):
scp -F "$HOME/.ssh-local/config" "mac:~/Desktop/Sk*rmavbild*.png" ./
# or read the exact filename first, then copy that:
ssh -F "$HOME/.ssh-local/config" mac 'ls -1 ~/Desktop/*.png'
3. Reaching the Docker host and its LAN over SSH
When the host is VM-backed (e.g. OrbStack / Docker Desktop on macOS) the
entrypoint's setup-lan-access.sh writes a writable SSH sidecar at
~/.ssh-local/config. It always provides:
- A
Host *block redirectingControlPathinto the writable~/.ssh-local/cm(because~/.sshis typically bind-mounted read-only, so a master socket can't be created under it), plusInclude ~/.ssh/config. - Aliases
host/mac→host.docker.internal(user comes fromHOST_SSH_USER) — i.e. SSH back into the Docker host. - On VM-backed hosts only: an SSH-jump-via-host block so the container can
reach the host's directly-attached LAN peers (
ProxyJump host). On a native Linux host the LAN is usually reachable directly and this jump block is omitted — so don't assume a jump path exists; read the sidecar.
Use it (remember §2 — spell it out in tool bash):
ssh -F "$HOME/.ssh-local/config" mac 'hostname; whoami' # reach the host
ssh -F "$HOME/.ssh-local/config" <lan-peer> '…' # reach a LAN peer (if configured)
Two related mechanisms (don't reinvent them):
- ControlMaster multiplexing is preconfigured (
/tmp/sshcm/) to survive CGNAT per-destination flow caps on residential ISPs. If~/.ssh/configpins aControlPathunder the read-only~/.ssh, override with-o ControlPath=none(or use the sidecar, which already redirects it). pi --ssh <host>rewires pi's own read/write/edit/bash tools to run on a remote host; it has its own writable-socket fallback. See thepi-extensionsskill for that path.
4. DNS / name resolution — environment-specific, verify live
How a name resolves here is not universal and depends on the host's
networking. The container's own resolver is just /etc/resolv.conf, but the
host (which you reach via §3, and whose DNS the container may inherit) can use
split-horizon DNS to send certain internal domains to specific nameservers
while everything else goes to a default resolver/VPN gateway. The mechanism is
OS-specific and may not be present at all:
- macOS host: per-domain files in
/etc/resolver/<domain>, each listingnameserverlines. Reading them (overssh … mac) is a fine way to learn the real split-DNS map — for that one machine. - Linux host: typically
systemd-resolvedsplit DNS (per-linkDomains=routing) or/etc/resolv.confsearch/nameserver. - Windows host: the NRPT (Name Resolution Policy Table) plays the per-suffix role; WSL2 inherits host resolution via mirrored networking + DNS tunneling.
Operating rules:
- Never hardcode a domain→nameserver mapping or a specific nameserver IP — it is per-deployment and changes between users and even VPN states.
- Verify by reading the live config, e.g.
cat /etc/resolv.confin the container, orssh … mac 'cat /etc/resolver/* 2>/dev/null'on a macOS host. - Reachability needs both DNS and a route. A name resolving to an internal address is useless if packets to that subnet don't have a path (e.g. via the VPN or the §3 jump). Check both when something "resolves but won't connect."
- If you discover deployment-specific facts (a domain, a nameserver, a reachable peer), prefer recording them in MemPalace over baking them into code or this skill.
5. tmux is 0-indexed — don't change it
The image ships /etc/tmux.conf with base-index 0 / pane-base-index 0
because pi-studio hard-codes its tmux send target to <session>:0.0. If you
(or a user ~/.tmux.conf) set base-index 1, pi-studio fails with "can't find
window: 0". Leave the indexing alone in this environment.
6. Python and other languages: uv-first, toolchains are ephemeral
- A system
python3exists, but preferuvfor REPLs and project envs — it's installed and its store (~/.local/share/uv) is a persisted volume.- Throwaway REPL:
uv run --with ipython ipython - Project env:
cd /workspace/proj && uv init && uv add <pkgs> && uv run …(thepyproject.toml+uv.locktravel with the repo — the durable choice).
- Throwaway REPL:
- Other language toolchains (Rust via rustup, R, GHC, Clojure, Go) are
runtime opt-ins on the ephemeral layer unless baked into the image — they
do not survive
down -vor an image update. Flag this when installing.
7. pi-studio reachability (only in the -studio variant)
Present only if /opt/pi-studio exists / the studio_* tools are in your tool
list. pi-studio binds to 127.0.0.1 inside the container with no host-bind
flag, so a plain docker -p publish can't reach it. Two supported paths:
- Host networking (
network_mode: host): container loopback == host loopback; open the tokenized URL on the host. (Changeshost.docker.internalsemantics — weigh against §3 LAN jump.) studio-exposebridge (STUDIO_EXPOSE=1or runstudio-expose &): asocatrelay from the container's external interface to its loopback, so a published127.0.0.1:PORT+ssh -L PORT:127.0.0.1:PORT hostreaches it.
The real auth token comes from the /studio slash command (/studio --status
to reprint), not from studio-expose. For Graphviz, use dot-watch →
PNG (Studio renders Mermaid natively and previews PNG, but not SVG/DOT).
8. MemPalace is the shared brain
MemPalace data is usually a host bind-mount, so a pi on the host and a pi in
this container share one palace (SQLite WAL: many readers, one writer). Use it
to persist the deployment-specific facts this skill deliberately refuses to
hardcode. Details are in the mempalace skill.
Checklist before acting in this environment
- Writing durable output? →
/workspace, not the ephemeral layer. - Using
dssh/dscp/llin the bash tool? → spell out the real command. - Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
- "Resolves but won't connect"? → check route and DNS (§3 + §4).
apt/toolchain install? → tell the user it's ephemeral unless imaged.- Editing a skill? →
readlink -f ~/.agents/skills/<name>first (§1). - Touching tmux indexing? → don't (§5).