Files
pi-devbox/rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md
T
joakimp 4d4abd9a9f
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 19s
skill(pi-devbox-environment): resolve a skill symlink before editing it
~/.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.
2026-07-29 19:37:59 +02:00

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 -studio variant. 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.
  • ~/.pi is 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.

~/.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-extensionsthe 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 redirecting ControlPath into the writable ~/.ssh-local/cm (because ~/.ssh is typically bind-mounted read-only, so a master socket can't be created under it), plus Include ~/.ssh/config.
  • Aliases host / machost.docker.internal (user comes from HOST_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/config pins a ControlPath under 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 the pi-extensions skill 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 listing nameserver lines. Reading them (over ssh … mac) is a fine way to learn the real split-DNS map — for that one machine.
  • Linux host: typically systemd-resolved split DNS (per-link Domains= routing) or /etc/resolv.conf search/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:

  1. Never hardcode a domain→nameserver mapping or a specific nameserver IP — it is per-deployment and changes between users and even VPN states.
  2. Verify by reading the live config, e.g. cat /etc/resolv.conf in the container, or ssh … mac 'cat /etc/resolver/* 2>/dev/null' on a macOS host.
  3. 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."
  4. 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 python3 exists, but prefer uv for 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 … (the pyproject.toml + uv.lock travel with the repo — the durable choice).
  • 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 -v or 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. (Changes host.docker.internal semantics — weigh against §3 LAN jump.)
  • studio-expose bridge (STUDIO_EXPOSE=1 or run studio-expose &): a socat relay from the container's external interface to its loopback, so a published 127.0.0.1:PORT + ssh -L PORT:127.0.0.1:PORT host reaches 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/ll in 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).