Groundwork

Set up

One path, a few machine profiles.

Groundwork is organized around the learning path, not the operating system. The platform only decides which parts of the environment get installed: the shared terminal and agent layer everywhere, plus the Mac desktop layer where it makes sense.

The rule

.chezmoi.os decides what the operating system can support. The saved setup answers in ~/.config/chezmoi/chezmoi.toml decide the machine role: work or personal, normal Mac or server/CI Mac, optional Unity, optional Xcode.

#What changes by platform

macOS desktopThe full setup: shell, Git, tmux, Neovim, mise/uv, Homebrew formulae, casks, App Store apps, browser helpers, Raycast, Anybox, BetterDisplay, Karabiner, and selected macOS defaults.
macOS server / CISame operating system, different role. Answer yes to the server/CI Mac prompt to skip desktop apps, App Store apps, browser helpers, Obsidian setup, Karabiner, and macOS defaults.
Linux / WSL2The terminal layer: shell, Git, tmux, Neovim, shell tools, mise/uv, helper scripts, and agent instructions. Mac-only GUI apps, App Store apps, and macOS defaults are skipped by OS. Groundwork supports Linux directly — WSL2 is simply one supported way to run that Linux environment on a Windows-owned machine, and native Linux is the recommendation if you want no Microsoft dependency. Ubuntu LTS is the primary tested path (it is also what wsl --install sets up by default); Debian stable and Fedora stable are targeted next and move to supported as their CI coverage lands, with other mainstream distributions best effort until then. WSL1 is not supported — check with wsl --list --verbose and convert with wsl --set-version <DistributionName> 2. Groundwork updates only the developer tools it manages; operating-system packages stay with your distribution, and on WSL2 nothing on the Windows host is touched.
Headless DockerA non-interactive shell environment for agents, CI, and disposable containers. It uses docker/chezmoi.headless.toml, placeholder identity data, and no GUI apps.
Native WindowsNot supported. Groundwork is a zsh/tmux/Homebrew/mise Unix environment; a PowerShell or CMD port would be a separate project, and Git Bash is not an equivalent tier. On Windows, install WSL2 with Ubuntu and run Groundwork inside it — setup fails closed on native Windows with that guidance. Keep repositories in the WSL filesystem (~/code), not under /mnt/c.

#What stays the same

The durable development spine is the same everywhere: shell literacy, file paths, Git, diffs, tmux sessions, editor basics, project instructions, validation, and the habit of checking an agent's work before accepting it.

That is why most docs pages are not split by OS. The operating system changes the install surface; it does not change the core practice.

#Headless containers

Docker is an environment option here, not a separate curriculum track. Use it when you want the Groundwork terminal layer in a reproducible agent workspace, CI job, or disposable shell: no GUI apps, no macOS defaults, no browser helpers, just the command-line foundation agents and humans can share.

container.sh
$ docker build -t groundwork .
$ docker run -it --rm groundwork

On a personal Mac, Docker Desktop can provide the daemon. On a work Mac, Groundwork installs Colima plus the Docker CLI instead; run colima start before using Docker commands. Other daemons work too — OrbStack is a fast commercial Docker Desktop alternative on macOS, and Apple's open-source container tool (v1.0, macOS 26+) is one to watch; Groundwork's commands only assume a Docker-compatible CLI. Groundwork does not try to teach Dockerfiles, Compose, or container hosting here; that would be a separate learning goal.

The repo also ships a devcontainer (.devcontainer/devcontainer.json) built from the same Dockerfile, so VS Code, Cursor, or GitHub Codespaces can open Groundwork in this container directly — no manual docker build step.

Containers also give an agent a boundary. docker run --cpus 2 --memory 2g caps what a runaway process can consume, and a disposable container is the right place for an agent running with permission checks relaxed: the blast radius is the container, not your machine. Since early 2026 there is a stronger off-the-shelf tier for unattended work — Docker Sandboxes runs each coding agent in its own microVM (its own kernel, its own Docker daemon, per-sandbox network allow and deny lists) through a standalone sbx run command, and Anthropic's reference devcontainer for Claude Code pairs container isolation with a default-deny outbound firewall. The principle to carry: the less you plan to watch an agent, the stronger the isolation you give it.

#Containers at native speed

Your chip has an architecture — Apple Silicon and most new Windows laptops are arm64, older PCs and most cloud CI are amd64 — and every container image is built for one or more of them. An image that matches your chip runs at native speed; a mismatched one runs through an emulation layer and everything inside it slows down. So the first rule is about images, not settings: prefer images built for your architecture, and treat emulation as a fallback for the rare image that ships no native build. This matters doubly for agents, which may run entire toolchains inside a container without noticing it is emulated.

Mac, Docker DesktopSettings → General: keep Apple Virtualization framework as the VMM, VirtioFS for file sharing, and leave Use Rosetta for x86_64/amd64 emulation checked. Checked does not mean your containers use Rosetta — it only engages when an image has no arm64 build, and is then far faster than the QEMU fallback. If you notice containers running amd64 routinely, fix the image, not this setting. (Docker VMM is faster still for pure-arm64 work but, as of mid-2026, has no Rosetta support — switch to it only if nothing you run needs amd64.) These are app-owned settings, so Groundwork documents them rather than writing them for you.
Mac, Colima (work profile)Groundwork manages ~/.colima/_templates/default.yaml with the same policy baked in: vz, virtiofs, and Rosetta on Apple Silicon. New instances pick it up automatically; an existing instance keeps its old config until you run colima delete and colima start again.
Windows, WSLKeep your code inside the WSL filesystem (~/code in your Linux home), not under /mnt/c — bind mounts from the Windows side are slow and never receive file-change events, which silently breaks hot reload. Resource limits live in C:\Users\<you>\.wslconfig on the Windows side, outside what Groundwork manages; a sensible start is memory=8GB, processors=4, autoMemoryReclaim=gradual.
LinuxContainers are native processes; there is no VM layer and nothing to tune here.

Beyond architecture, three engine defaults are worth setting once on any machine — they prevent the two classic ways Docker quietly eats a disk, and unlock multi-platform builds:

containerd image storeDocker Desktop → Settings → General → Use containerd for pulling and storing images. On for new installs; older installs may still have it off. Required for building multi-platform images locally, which the repo-side standards assume.
Bounded container logsThe default json-file log driver never rotates — a chatty container grows its log forever. Set "log-driver": "local" (rotates automatically) in Docker Desktop → Settings → Docker Engine.
Build-cache ceilingBuildKit cache grows without a limit you'd notice until the disk fills. In the same Docker Engine JSON: "builder": {"gc": {"enabled": true, "defaultKeepStorage": "20GB"}}.
Resource slidersDocker Desktop's defaults are correct for most machines, because the sliders mean different things. CPU is a cap, not a reservation — leave it at all cores; idle vCPUs cost nothing and lowering it only slows builds. Memory is the one to size: the VM largely keeps what it touches, so the default (half your RAM, capped at 8 GB) is right unless you run a full monorepo dev stack inside Docker — then raise it to ~12 GB on a 24 GB machine rather than letting the kernel OOM-kill your dev servers. Disk is an on-demand ceiling, not an allocation — the full-disk default is fine once logs and build cache are bounded (above). Work Macs skip all this: the managed Colima template sizes CPU and memory from the machine's hardware at apply time.

On work Macs both engine settings ship baked into the managed Colima template, so colima start applies them without any UI visit. On Docker Desktop they live in the app's own settings store, so set them once through the Settings screens above.

One Docker Desktop offer to decline: Configure shell completions → Install. That button edits ~/.zshrc directly — a file Groundwork manages, so chezmoi would flag the edit as drift and revert it on the next apply. Groundwork wires the same completions in the managed config instead (generated into ~/.docker/completions on every chezmoi apply), which also clears the FPATH warning. If completions ever seem missing, run chezmoi apply and open a new shell.

One cleanup habit worth forming early: when disk fills up, resist docker system prune — it deletes every project's stopped containers, images, and caches at once, including state other repos still need. Well-run projects ship their own repo-scoped clean commands (Roost-scaffolded repos do); reach for those first, and keep global pruning for machines you're genuinely resetting. Run groundwork-doctor for a read-only report of the engine settings above, Docker disk usage, and leftover containers, images, and volumes — each paired with the owner-scoped way to clean it.

To check whether a container is native or emulated, ask for the image's architecture and compare it to your machine's:

arch-check.sh
$ uname -m                              # your chip: arm64 or x86_64
$ docker image inspect node:24 --format '{{.Architecture}}'
# arm64 on Apple Silicon = native. amd64 there = emulated — find an arm64 image.

#Ephemeral builds and the tidy habit

AI-native workflows build images constantly: every review, test, or agent session that touches a Dockerfile builds a full image to verify it, tags it something ad hoc (groundwork:test, groundwork:review-fix), and moves on. Tags never expire, each full build is gigabytes, and nothing in that flow prunes — so a few weeks of perfectly normal work can quietly accumulate several gigabytes of dead images on the disk. The fix is not deleting harder afterward; it is declaring intent at build time.

The contract: a build that exists only to verify something says so, with a scratch-namespace tag (<project>/scratch:<purpose>) and two labels — dev.roost.ephemeral=true (the consent) and dev.roost.built (the authoritative build clock the tidy strict-parses). Groundwork and Roost-scaffolded repos share the same label pair, so one tidy serves every repo on the machine. Don't hand-type any of that — the wrapper owns the syntax, so it cannot forget a label, typo the namespace, or emit a timestamp the tidy refuses to parse:

scratch-build.sh
$ groundwork-docker-build-scratch review .   # labels + groundwork/scratch:review, prints the image ID
$ docker run -it --rm groundwork/scratch:review
$ docker rmi groundwork/scratch:review  # best: clean up in the same session
$ groundwork-docker-build-scratch review . --rm-after  # or prove-and-delete in one step

Keep the plain groundwork tag for the image you actually run — it stays unlabeled on purpose, so no cleanup can ever take it. And never docker tag a scratch image into a real name: labels live on the image, not the tag, so the retagged image still carries the scratch metadata — promote by rebuilding under the real tag instead. Same-session cleanup is still the ideal; the label is the safety net for the sessions (yours or an agent's) that forget.

groundwork-docker-tidy enforces the contract by enumeration — the label is the consent, and the tidy honors it image by image, never by handing the decision to a broad prune. It is a dry run by default, and it reports every labeled image with one of four verdicts: eligible (its strict-parsed dev.roost.built is past the 72-hour grace, every tag sits inside the scratch namespace, and no container — running or stopped — references it), retained (inside the grace window), protected (a tag outside the scratch namespace or a container reference refuses deletion — a scratch image retagged groundwork:latest keeps its label, which is exactly why the tag check exists), or unverified (a missing or malformed build label, or no jq to verify it — retained and explained, never "probably old"). --yes removes exited containers carrying the label only when their CreatedAt is past the same grace window — container state under an ephemeral image is disposable only after the grace — with each container's status re-checked immediately before removal, and then the eligible images: deduplicated by image ID, re-checked immediately before deletion, deleted by explicit image ID. When a multi-tag image only partly cleans up (extra tags untagged, final delete refused), the report says partially cleaned and names the removed tags — never a plain "kept". It never touches volumes, running containers, unlabeled images — or dangling images and builder cache, which belong to the whole daemon. Run it weekly, or before a large build; update-all prints a one-line nudge when tidy-eligible leftovers have piled up, and groundwork-doctor reports the same scan's four counts in its read-only report (a fresh in-grace image is not a leftover).

Daemon-wide state is a separate decision. groundwork-docker-cache-tidy handles dangling images, stopped containers, and builder cache — resources from every project on this machine, which is why it is its own command: it says so when it runs, it is a dry run by default, and neither update-all nor any scheduled or agent path ever invokes it. Its builder-cache step (docker builder prune --keep-storage 5GB) asks Docker to prune eligible cache for the selected builder while retaining a target — an honest cap request, not a guarantee that total cache lands under the number.

#Mac choices

Most Macs should use the default desktop profile. The server/CI prompt exists for the unusual case: a Mac used as a remote runner, build box, or agent host with no logged-in desktop workflow.

Work machine?Turns off personal apps and uses Colima plus Docker CLI instead of Docker Desktop.
Server/CI Mac?Skips the desktop layer. Most personal and work laptops should answer no.
Unity tools?Optional. Installs Unity Hub and related editor extensions for the Unity FPS track.
Xcode?Optional. Attempts the Mac App Store Xcode install for native Apple/iOS development; first launch and signing remain manual.

#Check your saved profile

The setup answers live in one local config file. Inspect them before applying a change:

profile.sh
$ sed -n '/^\[data\]/,$p' ~/.config/chezmoi/chezmoi.toml
$ chezmoi diff
$ chezmoi apply

To change setup answers, re-run chezmoi init --source ~/code/groundwork, then inspect with chezmoi diff before applying.