docs/host-setup.md states the contract (which tools a host must provide and which procedure stops without each one, added in #482). This issue collects the implementation: how each tool actually gets installed, per host type.
The split is deliberate. The contract is identical on every platform, so it belongs in a doc. The install command is not, so it belongs in a matrix that whoever runs that platform can fill in and verify.
The matrix
Fill in a cell only when you have run it on that platform. An unverified install command in a setup guide is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded.
| Tool |
Linux (apt) |
Windows (winget) |
WSL2 |
macOS (brew) |
git |
apt install git |
winget install -e --id Git.Git |
apt install git (inside the distro) |
|
gh |
apt install gh (or the GitHub apt repo for a current version) |
winget install -e --id GitHub.cli |
apt install gh (inside the distro; Debian ships an old one — see notes) |
|
python3 |
apt install python3 |
winget install -e --id Python.Python.3.13 (registers python/py, not python3 — see notes) |
apt install python3 (inside the distro; do not use the Windows install — see notes) |
|
docker |
Docker Engine via the official apt repo |
winget install -e --id Docker.DockerDesktop (Docker Desktop; needs WSL2 backend) |
Docker Desktop on Windows + Settings → Resources → WSL integration enabled for the distro (installs nothing inside it), or Docker Engine via the official apt repo inside the distro — see notes |
|
uv / uvx |
curl -LsSf https://astral.sh/uv/install.sh | sh |
winget install -e --id astral-sh.uv (ships uv + uvx) |
curl -LsSf https://astral.sh/uv/install.sh | sh (inside the distro) |
|
| write-safety kit |
host-setup/agent-safety/install.sh |
host-setup\agent-safety\install.ps1 |
install.sh |
install.sh |
Verified on Linux (this host)
Versions observed on the proxmox Debian host, recorded as a known-good reference rather than a floor:
git 2.47.3
gh 2.46.0
python3 3.13.5
docker 29.6.2
uv 0.11.31
One caveat worth carrying: this gh is 2.46.0, old enough that gh pr checks --json does not exist, so a watcher written against it prints nothing rather than failing loudly. If a platform's package manager ships an old gh, note the version in its cell.
Verified on Windows (this host)
Versions observed on a native Windows 11 host (winget v1.29.280), recorded as a known-good reference rather than a floor. Left is the tool-reported version; right in parentheses is the winget package id and its version:
git 2.55.0.windows.3 (Git.Git 2.55.0.3)
gh 2.96.0 (GitHub.cli 2.96.0)
python 3.13.14 (Python.Python.3.13 3.13.14)
docker 29.6.2 (Docker.DockerDesktop 4.83.0)
uv 0.11.32 (astral-sh.uv 0.11.32)
Unlike the Linux host, this gh is 2.96.0, so gh pr checks --json exists here — the old-gh caveat above is Linux-only. Install commands use -e --id (exact match on the package id) so a name search cannot silently resolve to a different package.
Verified on WSL2 (this host)
Versions observed on Debian 13 (trixie) under WSL 2.7.10.0 / kernel 6.18.33.2 on Windows 11 (10.0.26200.8973), recorded as a known-good reference rather than a floor:
git 2.47.3 (Debian trixie main, 1:2.47.3-0+deb13u1)
gh 2.46.0 (Debian trixie main, 2.46.0-3)
python3 3.13.5 (Debian trixie main, 3.13.5-1)
docker 29.6.2 (Docker Desktop WSL integration — no docker package inside the distro)
uv 0.11.17 (astral installer, ~/.local/bin; the installer currently ships 0.12.1)
This distro's gh is 2.46.0, the same Debian-repo build as the Linux host, so the old-gh caveat carries here too: gh pr checks has --watch, --fail-fast and --interval but no --json, confirmed against this binary's own help.
What was actually run to fill the column, since the rule above is worth honoring precisely:
git / gh / python3: not re-installed (this host has no non-interactive sudo), but each was confirmed to be the stock Debian package from trixie/main at the version above via dpkg -S + apt-cache policy, which is exactly what apt install <pkg> yields on this distro. Treat these three as provenance-verified rather than command-verified.
docker: two of the four pinned linter images were run end to end from inside the distro — docker run --rm --pull=always -v "$PWD":/repo ... rhysd/actionlint:latest (which also carries shellcheck) and the davidanson/markdownlint-cli2:latest equivalent both pulled and exited 0 against this repo. cspell was not run, though it is the same invocation shape.
uv: the installer was run for real, into a throwaway UV_INSTALL_DIR, and produced a working uv/uvx 0.12.1. uvx ruff@0.14.4 --version resolves and runs.
- write-safety kit:
host-setup/agent-safety/install.sh was run against a throwaway CLAUDE_HOME, twice. First run reported hook self-test: PASS and safety block appended; second reported safety block updated, so the idempotency claim holds on WSL2. The live ~/.claude copy passes --selftest and has both agent-safety v markers.
- the rest of the contract:
git -c gpg.format=ssh commit -S --allow-empty produces a Good "git" signature in a throwaway repo, gh auth status is authenticated with a file token store (matching the WSL2 row in docs/devcontainer.md), and python3 spec/validate.py plus the 159 scripts/ unit tests pass with no packages installed, which is the stdlib-only claim in the contract.
Per-platform notes to capture
Beyond the bare install command, the cells that have bitten us before:
- Windows: native PowerShell is enough to install all five tools via winget, but three cells bite. (1)
python3: the winget package registers python, py, and python3.13 — not python3. On a default box python3 resolves first to the Microsoft Store alias stub (%LOCALAPPDATA%\Microsoft\WindowsApps\python3.exe), which errors Python was not found… (exit 9009) instead of running; use python/py, disable the Store alias under Settings → Apps → Advanced app settings → App execution aliases, or run under WSL2. (2) docker: Docker Desktop requires the WSL2 backend, matching docs/host-setup.md on the devcontainer flow. (3) write-safety kit install.ps1: it is a thin wrapper that runs install.py under Python 3, resolving the interpreter py -3 → python3 → python — so it needs Python 3 on PATH, and it sidesteps the python3 Store-alias trap above because py -3 is tried first. Running it needs script execution allowed: a git clone carries no Mark-of-the-Web so it runs under the default RemoteSigned, but a browser-downloaded ZIP is blocked as unsigned until Unblock-File (or invoke it as pwsh -ExecutionPolicy Bypass -File host-setup\agent-safety\install.ps1). The POSIX snippets still do not run as-is in PowerShell.
- WSL2 (answered below; kept as a note because the choices still bite): five things.
- Docker provenance is a real fork, and this host takes the Desktop branch. Evidence it is the integration rather than an in-distro engine:
/usr/bin/docker is a symlink into /mnt/wsl/docker-desktop/cli-tools/, docker info reports Docker Desktop / docker-desktop, and no docker* or containerd* package is installed. Socket access still looks native — /var/run/docker.sock is root:docker, so the account needs to be in the docker group exactly as with an in-distro engine, and the default context (not desktop-linux, which is a Windows named pipe) is the one that works from inside the distro. Bind mounts work from the distro's own filesystem with no path translation: a repo under $HOME mounts natively, the container sees the caller's uid/gid, and files it writes come back owned correctly. The engine-inside-the-distro branch is untested here.
- Keep the repo on the distro's ext4, not under
/mnt/c. Both bind-mount successfully, but /mnt/c is a 9p/drvfs mount that reports every file as 0777. Verified side by side: the same file is 644 1000:1000 from $HOME and 777 1000:1000 from /mnt/c. That is a broken exec bit for git and for any mode-sensitive check, on top of 9p being far slower.
- Docker Desktop's credential helper is a Windows
.exe (~/.docker/config.json has "credsStore": "desktop.exe", resolved via /usr/bin/docker-credential-desktop.exe). So an uncached docker pull depends on WSL interop being healthy, and the linters use --pull=always. When systemd leaves the WSLInterop binfmt handler unregistered, any .exe gives exec format error and the pull fails on the credential helper while cached images still run. Check with cat /proc/sys/fs/binfmt_misc/WSLInterop (want enabled, interpreter /init); fix with wsl --update, or an /etc/binfmt.d/WSLInterop.conf of :WSLInterop:M::MZ::/init:PF plus systemctl restart systemd-binfmt. Fully quit Docker Desktop before any wsl --update / wsl --shutdown, since a running or paused Desktop blocks the WSL service from stopping.
- Install the tools inside the distro; the Windows copies are on
$PATH but never win. WSL2 appends the Windows PATH by default (51 entries here), so python3.exe, docker.exe and friends are reachable — but type -a confirms the Linux binaries resolve first for all five tools (docker finds /usr/bin/docker before the interop /Docker/host/bin/docker). The practical consequence is the inverse of the Windows column's trap: the Store-alias python3.exe stub exists at .../WindowsApps/python3.exe yet is shadowed, so python3 just works here. Note there is no bare python on stock Debian — install.sh is fine because it tries python3 first, but a snippet written for python will not run.
- The doc's systemd
ssh-agent.socket step assumes an opt-in. WSL2 does not run systemd unless /etc/wsl.conf carries a [boot] section with systemd = true; this host does, and ssh-agent.socket is enabled + active. On a distro without that opt-in, docs/host-setup.md's Linux/WSL2 SSH section only offers the ~/.bashrc fallback, which is the branch to take.
- macOS: the Keychain path for the SSH agent, which
docs/host-setup.md already covers for signing but not for tooling.
- Proxmox: whether the host is a shell for agent work or also runs containers, which decides whether
docker is required there at all.
Related
Assignment
Linux, Windows and WSL2 are filled in and verified above. macOS still needs someone on that platform, as does the Proxmox question in the notes. The engine-inside-the-distro variant of the WSL2 docker cell is also still unverified — this host only exercised the Docker Desktop integration branch.
docs/host-setup.mdstates the contract (which tools a host must provide and which procedure stops without each one, added in #482). This issue collects the implementation: how each tool actually gets installed, per host type.The split is deliberate. The contract is identical on every platform, so it belongs in a doc. The install command is not, so it belongs in a matrix that whoever runs that platform can fill in and verify.
The matrix
Fill in a cell only when you have run it on that platform. An unverified install command in a setup guide is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded.
gitapt install gitwinget install -e --id Git.Gitapt install git(inside the distro)ghapt install gh(or the GitHub apt repo for a current version)winget install -e --id GitHub.cliapt install gh(inside the distro; Debian ships an old one — see notes)python3apt install python3winget install -e --id Python.Python.3.13(registerspython/py, notpython3— see notes)apt install python3(inside the distro; do not use the Windows install — see notes)dockerwinget install -e --id Docker.DockerDesktop(Docker Desktop; needs WSL2 backend)uv/uvxcurl -LsSf https://astral.sh/uv/install.sh | shwinget install -e --id astral-sh.uv(shipsuv+uvx)curl -LsSf https://astral.sh/uv/install.sh | sh(inside the distro)host-setup/agent-safety/install.shhost-setup\agent-safety\install.ps1install.shinstall.shVerified on Linux (this host)
Versions observed on the proxmox Debian host, recorded as a known-good reference rather than a floor:
One caveat worth carrying: this
ghis 2.46.0, old enough thatgh pr checks --jsondoes not exist, so a watcher written against it prints nothing rather than failing loudly. If a platform's package manager ships an oldgh, note the version in its cell.Verified on Windows (this host)
Versions observed on a native Windows 11 host (winget
v1.29.280), recorded as a known-good reference rather than a floor. Left is the tool-reported version; right in parentheses is the winget package id and its version:Unlike the Linux host, this
ghis 2.96.0, sogh pr checks --jsonexists here — the old-ghcaveat above is Linux-only. Install commands use-e --id(exact match on the package id) so a name search cannot silently resolve to a different package.Verified on WSL2 (this host)
Versions observed on Debian 13 (trixie) under WSL 2.7.10.0 / kernel 6.18.33.2 on Windows 11 (10.0.26200.8973), recorded as a known-good reference rather than a floor:
This distro's
ghis 2.46.0, the same Debian-repo build as the Linux host, so the old-ghcaveat carries here too:gh pr checkshas--watch,--fail-fastand--intervalbut no--json, confirmed against this binary's own help.What was actually run to fill the column, since the rule above is worth honoring precisely:
git/gh/python3: not re-installed (this host has no non-interactivesudo), but each was confirmed to be the stock Debian package fromtrixie/mainat the version above viadpkg -S+apt-cache policy, which is exactly whatapt install <pkg>yields on this distro. Treat these three as provenance-verified rather than command-verified.docker: two of the four pinned linter images were run end to end from inside the distro —docker run --rm --pull=always -v "$PWD":/repo ... rhysd/actionlint:latest(which also carriesshellcheck) and thedavidanson/markdownlint-cli2:latestequivalent both pulled and exited 0 against this repo.cspellwas not run, though it is the same invocation shape.uv: the installer was run for real, into a throwawayUV_INSTALL_DIR, and produced a workinguv/uvx0.12.1.uvx ruff@0.14.4 --versionresolves and runs.host-setup/agent-safety/install.shwas run against a throwawayCLAUDE_HOME, twice. First run reportedhook self-test: PASSandsafety block appended; second reportedsafety block updated, so the idempotency claim holds on WSL2. The live~/.claudecopy passes--selftestand has bothagent-safety vmarkers.git -c gpg.format=ssh commit -S --allow-emptyproduces aGood "git" signaturein a throwaway repo,gh auth statusis authenticated with a file token store (matching the WSL2 row indocs/devcontainer.md), andpython3 spec/validate.pyplus the 159scripts/unit tests pass with no packages installed, which is the stdlib-only claim in the contract.Per-platform notes to capture
Beyond the bare install command, the cells that have bitten us before:
python3: the winget package registerspython,py, andpython3.13— notpython3. On a default boxpython3resolves first to the Microsoft Store alias stub (%LOCALAPPDATA%\Microsoft\WindowsApps\python3.exe), which errorsPython was not found…(exit 9009) instead of running; usepython/py, disable the Store alias under Settings → Apps → Advanced app settings → App execution aliases, or run under WSL2. (2)docker: Docker Desktop requires the WSL2 backend, matchingdocs/host-setup.mdon the devcontainer flow. (3) write-safety kitinstall.ps1: it is a thin wrapper that runsinstall.pyunder Python 3, resolving the interpreterpy -3→python3→python— so it needs Python 3 on PATH, and it sidesteps thepython3Store-alias trap above becausepy -3is tried first. Running it needs script execution allowed: agit clonecarries no Mark-of-the-Web so it runs under the defaultRemoteSigned, but a browser-downloaded ZIP is blocked as unsigned untilUnblock-File(or invoke it aspwsh -ExecutionPolicy Bypass -File host-setup\agent-safety\install.ps1). The POSIX snippets still do not run as-is in PowerShell./usr/bin/dockeris a symlink into/mnt/wsl/docker-desktop/cli-tools/,docker inforeportsDocker Desktop/docker-desktop, and nodocker*orcontainerd*package is installed. Socket access still looks native —/var/run/docker.sockisroot:docker, so the account needs to be in thedockergroup exactly as with an in-distro engine, and thedefaultcontext (notdesktop-linux, which is a Windows named pipe) is the one that works from inside the distro. Bind mounts work from the distro's own filesystem with no path translation: a repo under$HOMEmounts natively, the container sees the caller's uid/gid, and files it writes come back owned correctly. The engine-inside-the-distro branch is untested here./mnt/c. Both bind-mount successfully, but/mnt/cis a 9p/drvfs mount that reports every file as0777. Verified side by side: the same file is644 1000:1000from$HOMEand777 1000:1000from/mnt/c. That is a broken exec bit for git and for any mode-sensitive check, on top of 9p being far slower..exe(~/.docker/config.jsonhas"credsStore": "desktop.exe", resolved via/usr/bin/docker-credential-desktop.exe). So an uncacheddocker pulldepends on WSL interop being healthy, and the linters use--pull=always. When systemd leaves theWSLInteropbinfmt handler unregistered, any.exegivesexec format errorand the pull fails on the credential helper while cached images still run. Check withcat /proc/sys/fs/binfmt_misc/WSLInterop(wantenabled,interpreter /init); fix withwsl --update, or an/etc/binfmt.d/WSLInterop.confof:WSLInterop:M::MZ::/init:PFplussystemctl restart systemd-binfmt. Fully quit Docker Desktop before anywsl --update/wsl --shutdown, since a running or paused Desktop blocks the WSL service from stopping.$PATHbut never win. WSL2 appends the WindowsPATHby default (51 entries here), sopython3.exe,docker.exeand friends are reachable — buttype -aconfirms the Linux binaries resolve first for all five tools (dockerfinds/usr/bin/dockerbefore the interop/Docker/host/bin/docker). The practical consequence is the inverse of the Windows column's trap: the Store-aliaspython3.exestub exists at.../WindowsApps/python3.exeyet is shadowed, sopython3just works here. Note there is no barepythonon stock Debian —install.shis fine because it triespython3first, but a snippet written forpythonwill not run.ssh-agent.socketstep assumes an opt-in. WSL2 does not run systemd unless/etc/wsl.confcarries a[boot]section withsystemd = true; this host does, andssh-agent.socketisenabled+active. On a distro without that opt-in,docs/host-setup.md's Linux/WSL2 SSH section only offers the~/.bashrcfallback, which is the branch to take.docs/host-setup.mdalready covers for signing but not for tooling.dockeris required there at all.Related
Assignment
Linux, Windows and WSL2 are filled in and verified above. macOS still needs someone on that platform, as does the Proxmox question in the notes. The engine-inside-the-distro variant of the WSL2
dockercell is also still unverified — this host only exercised the Docker Desktop integration branch.