Files
pi-devbox/rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md
T
joakimp 71b12a9ed4
Lint / hadolint (push) Failing after 7s
Lint / actionlint (push) Successful in 31s
docs(skill): note macOS NFD-filename gotcha for dscp/scp
Accented filenames on a macOS host are stored decomposed (NFD), so a precomposed
(NFC) remote path in scp/dscp silently fails with 'No such file or directory'.
Document the wildcard / list-first workaround in the pi-devbox-environment skill,
next to the dssh/dscp alias table. (Hit while copying a screenshot named
'Skärmavbild ….png' from the host.)
2026-07-17 13:06:22 +02:00

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

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.
  • Touching tmux indexing? → don't (§5).