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