Personal machine-provisioning repo: one unified installer for macOS, Linux
desktop (Ubuntu/Arch), and an Ubuntu Server LTS home server, plus per-platform
configs and docs. A Raspberry Pi (<pi-hostname>, Debian) node lives in linux-pi/
β currently its own Docker Compose service stacks (a secondary AdGuard resolver);
folding it into the setup.sh base provisioning is still tracked in docs/TODO.md.
This file is the shared instruction set for every coding agent working in
this repo (Claude Code, opencode, Codex, β¦). CLAUDE.md imports it and adds
Claude-Code-only notes on top; keep cross-agent guidance here, not there.
setup.shβ installs everything for the detected platform. Flags:--optional --work --personal --base --tags <csv> --dotfiles --dry-run --platform <macos|ubuntu|arch|server> --profile <desktop|server>. The server platform is never auto-detected (--profile serveror--platform serverrequired).--baseinstalls only the high-priority base set;--tags development,terminalinstalls base + thosepackages.jsontag categories; a bare TTY run with no selection flag prompts interactively except on the server profile. Selection mechanics and the custom-step gating:docs/PACKAGES.md.--dotfilesdeploys only the shared dotfiles set and installs no packages β it short-circuits insetup.shbefore tag validation and the interactive prompt, so it ignores every selection flag.deploy_dotfiles()inlib/core.shis the single owner of that set and is what bothplatform_mains call.verify.shβ read-only health check mirroringsetup.sh's selection logic. Flags:--optional --work --personal --all --platform <macos|ubuntu|arch|server>(no--dry-run). The server profile additionally checks NUT configuration and services. Unknown flags warn and are ignored, they don't abort.
packages.jsonβ single source of truth for all package data. Managers are keyed by platform ({macos, ubuntu, arch, server});<platform>_nameoverrides the install token;environmentgates on--work/--personal;custommanagers carry aninstall_command(auto-run whenhandled_by_setup, else a reminder);tagsis a required category array. The tier/gating fieldspriority,optional,environment, andinstall_commandcan be a scalar or a per-platform object. Full schema, per-platform resolution, theenvironmentcaveat, and the tag filter live indocs/PACKAGES.mdβ read it before editingpackages.json.lib/core.shβ shared engine: arg parsing, platform detection, env filter, jq selection, install loops, config deploys.lib/verify.shβ check engine.platforms/<platform>.shβ per-platform quirks only (bootstrap, manager invocations).dotfiles/β configs shared across platforms (tmux.conf,ghostty.config,zshrc.example,zsh_plugins.txt); the engine deploys them from here. One zshrc base serves every platform including the headless server β macOS-specific bits guard on/opt/homebrewor$OSTYPE, and the desktop-only bits self-disable headless (notify hook no-ops without$DISPLAY/$WAYLAND_DISPLAY, fastfetch keys off Ghostty or$SSH_CONNECTION, version managers/zoxide are command-guarded). The override system (deploy_zshrc) still lets a platform folder ship its ownzshrc.exampleto win over the base, but no platform currently does.macOS/,linux-desktop/,linux-server/β platform-specific configs, docs, and thin shim scripts that exec the root entrypoints.linux-pi/holds the Raspberry Pi node's Docker Compose service stacks (same<service>/{docker-compose.yml,.env.example,ts-serve.json}layout aslinux-server/), not yet wired intosetup.sh.scripts/dryrun-smoke.shβ runssetup.sh --dry-runfor every platform and asserts it exits clean with install actions; also run in CI.
docs/UNIFICATION.md is the design doc for this layout; docs/CHANGELOG.md records
what shipped and docs/TODO.md tracks remaining work.
- Pre-commit runs
shellcheck --severity=warningon all shell scripts;zsh -nchecks.zshfiles andzshrc.example;scripts/validate-packages.shenforces thepackages.jsonschema (platform vocabulary, controlled tag set, and the "no silent drop" rule β every platform a package targets must resolve a valid priority tier and a boolean optional). All three also run in CI. - Probe semantics in
lib/verify.share platform-faithful ports β macOS has nocommand -vfallback for casks/pipx/app-store, Linux falls back everywhere. Don't "fix" the asymmetry without checkingdocs/UNIFICATION.mdhistory. --dry-runmust print every command without executing anything. Before committing changes tosetup.sh,lib/,platforms/, orpackages.json, exercise it across all four platforms; only one platform can run live.- Before committing changes to
.envhandling,custominstall_commandshell execution, or path/network code, perform a security review. - App-store packages and
priority: "none"entries are reminders only β never auto-installed.
These apply to every agent (the repo is almost entirely Bash).
- Comments: none by default. Add one only when the why is non-obvious β a hidden constraint, a bug workaround, a subtle invariant. Never narrate what the code does; well-named identifiers cover that.
- No speculative design. Don't build for hypothetical future requirements; three similar lines beat a premature abstraction. Don't add features, refactors, or abstractions beyond what the task needs.
- Validate only at boundaries (user input, external APIs). Trust internal code and framework guarantees β no error handling or fallbacks for cases that can't happen.
- Prefer editing existing files to creating new ones; delete removed code cleanly rather than leaving back-compat shims.
- Bash style:
#!/usr/bin/env bashshebang;set -euo pipefailat the top of every non-trivial script.[[ ]]not[ ]; quote all expansions ("$var","${arr[@]}").printfnotecho; declare function-local vars withlocal; write errors to stderr (printf 'error: %s\n' "$msg" >&2).command -v foooverwhich foo; herestring (<<< "$var") overecho "$var" |.
This repo is public. Never commit identifying or secret information.
- Keep these out of tracked files entirely: tailnet names / MagicDNS suffixes
(
tailXXXXXX.ts.net), real hostnames, server IPs, usernames, emails, tokens, auth keys, and personal absolute paths. - Put any machine-specific or private value in a
.envfile (gitignored repo-wide) and ship a committed.env.examplewith placeholders instead β e.g.linux-server/forgejo/.env.example,macOS/forgejo-runner/.env.example. Scripts read these via${VAR:-<placeholder>}and source a local.envwhen present; they never hardcode the real value. - In docs and configs use placeholders:
<tailnet>,<server-ip>,<username>,<hostname>. Default to.envwhenever a value is identifying β prefer one more env var over leaking a real value. - When editing, scan the diff for accidentally introduced real identifiers before committing.