Skip to content
 
 

Latest commit

 

History

51 Commits

Folders and files

Repository files navigation

dotfiles-nix

Discord

This repo is the public, reusable core of my development setup, on macOS, Linux, and WSL.

It is built with Nix, nix-darwin, Home Manager, and declarative Homebrew. The goal is to give developers a reproducible base they can fork and adapt without inheriting someone else's entire private dotfiles repo.

If you want the longer explanation, see the blog post.

What this repo does

It gives you a structured starting point for managing a development setup in code:

  • bootstrap a machine with one command, setup/install.sh, which detects macOS, Linux, or WSL
  • configure macOS defaults with nix-darwin
  • manage user packages and shell behavior with Home Manager, identically on every platform
  • install GUI apps and macOS-native tools declaratively with Homebrew
  • keep selected app config in the repo and link it into place

The same shell, prompt, git config, and CLI toolchain follow you across all three platforms. What differs is how deep the configuration reaches: on macOS it owns the whole machine, and on Linux and WSL it owns your user environment and leaves the distro alone. Windows itself is not a target, because Nix has no native Windows build; the supported path there is WSL2.

App configs kept in the repo and linked into place include WezTerm and herdr. Neovim config is deliberately not managed here; it lives in its own repo, kickstart.nvim, checked out at ~/.config/nvim.

What is intentionally not included

This repo does not try to mirror my entire machine.

I left out things that are too personal to make a good public repo, including:

  • secrets and tokens
  • private automation

The goal is to provide a reusable foundation that you can make your own.

Repo structure

  • setup/install.sh - detect the platform and run the right bootstrap (start here)
  • setup/mac.sh - bootstrap a fresh Mac
  • setup/linux.sh - bootstrap a Linux or WSL machine
  • setup/windows.ps1 - enable WSL2 on Windows, then run the Linux bootstrap inside it
  • setup/lib/platform.sh - shared platform detection and config-literal parsing, sourced by every bootstrap script and by files/bin
  • setup/README.md - bootstrap usage and testing notes
  • flake.nix - top-level Nix wiring
  • nix/host.nix - machine-level macOS config (nix-darwin)
  • nix/user.nix, nix/linux-user.nix, nix/wsl-user.nix - per-platform Home Manager entry points
  • nix/home/ - the layers those entry points compose: dotfiles.nix (the one definition of where this checkout lives, read by all of the below), common.nix (cross-platform), desktop.nix (fonts and linked app configs), darwin.nix, linux.nix, wsl.nix
  • files/.config/wezterm/wezterm.lua - WezTerm config linked into place
  • files/.config/herdr/config.toml - herdr config linked into place
  • files/bin/ - personal scripts kept on PATH, including sync-forks (daily fork sync), ic-link (symlink farm), and ic-doctor (health check)
  • files/skills/ - agent skills owned by this repo (ship plus the engineering skills listed under Agent skills below); setup/lib/skills.sh finds them
  • files/zsh/ic-workflow.zsh - IC workflow shell config sourced by zsh
  • tests/ - regression tests for the bootstrap scripts, the fork sync script, and the Linux end-to-end install
  • AGENTS.md - repo-specific notes for coding agents (CLAUDE.md imports it via @AGENTS.md)
  • blog.md - local copy of the blog post

How to use it

1. Clone the repo

git clone git@github.com:shreejitverma/dotfiles-nix.git ~/github/dotfiles-nix
cd ~/github/dotfiles-nix

2. Replace the placeholders

Update values like:

  • yourname
  • /Users/yourname (macOS, in nix/home/darwin.nix) or /home/yourname (Linux and WSL, in nix/home/linux.nix)
  • Your Name
  • you@example.com

yourname also appears as the username literal in flake.nix, which is what names the homeConfigurations outputs. The bootstrap reads that literal to derive the profile it activates, so replacing it there is enough; nothing else restates it.

On macOS the git config also signs commits by default with an SSH key at ~/.ssh/id_ed25519_signing. Generate that key before your first commit (see Step 1 of the full setup below), or remove programs.git.signing.signByDefault from nix/home/darwin.nix if you do not want signed commits.

If you are on an Intel Mac, change the system target in flake.nix from:

system = "aarch64-darwin";

to:

system = "x86_64-darwin";

3. Run the bootstrap

One command on every platform:

bash setup/install.sh

It detects whether the machine is macOS, Linux, or WSL, shows you what it found, and lets you confirm or pick a different target before anything is installed. Non-interactively (a pipe, CI) it uses the detected target without prompting rather than hanging.

bash setup/install.sh --dry-run          # print the plan, install nothing
bash setup/install.sh --target wsl --yes # skip detection and the prompt

What each platform gets

Platform Activated with Owns
macOS nix-darwin + Home Manager the whole machine: macOS defaults, Homebrew casks, launchd agents
Linux standalone Home Manager the user environment only
WSL standalone Home Manager the user environment, minus the desktop layer

The asymmetry is not an oversight. nix-darwin has no equivalent for an existing Linux distro, so on Linux and WSL this repo manages your packages, shell, git, and prompt, and leaves the kernel, services, display server, and distro packages alone.

Windows

Nix has no native Windows build, so there is nothing to install on Windows proper. The supported path is WSL2:

powershell -ExecutionPolicy Bypass -File setup\windows.ps1

That enables WSL2, installs a distro if you have none, clones this repo inside the distro, and runs setup/install.sh --target wsl there. The clone has to live inside the distro rather than on /mnt/c: the config links files out of the checkout by absolute path, and a Windows-side checkout would leave those links resolving only while the drive is mounted, over a much slower filesystem boundary. setup/linux.sh refuses such a checkout outright rather than half-installing.

If you already run WSL2, skip the PowerShell script and just run bash setup/install.sh inside your distro.

What the platform scripts do

  • macOS (setup/mac.sh): installs Determinate Nix and Homebrew if needed, applies nix-darwin + Home Manager, installs nvm and a default Node.js. On a fresh machine this completes in one run: after the installer, the script sources the Nix daemon profile into the current shell and uses an absolute nix path for the first activation, so no second shell or second run is needed.
  • Linux and WSL (setup/linux.sh): installs Determinate Nix if needed, builds the matching Home Manager profile out of this flake, and activates it. It builds the activation package from the pinned flake.lock rather than running nix run home-manager/master, so the Home Manager doing the activation is the one this repo pins instead of whatever is current upstream. Where systemd is not PID 1 (WSL without systemd=true, containers, non-systemd distros) it installs Nix with --init none, because the default Linux plan registers the daemon as a systemd service.

On Linux and WSL the Home Manager profile configures bash as well as zsh, because bash is the stock login shell on those platforms and the session PATH, environment, and aliases would otherwise only exist in the generated .zshrc. Bash gets the same alias set, rebuild included; the zsh workflow layer in files/zsh stays zsh-only, so setup/linux.sh finishes by printing the chsh command that switches the login shell if you want it. That also means Home Manager owns ~/.bashrc, ~/.bash_profile, and ~/.profile there, the same way it already owns ~/.zshrc, ~/.zshenv, and ~/.zprofile. On a machine that already had those files, setup/linux.sh names each one up front, before it installs or builds anything, and the originals are preserved as <file>.backup. If one of them is a symlink you own rather than a regular file, it stops there instead: Home Manager backs up a regular file it has to replace but never a symlink, so it tells you which paths to move aside first.

The NIX_DAEMON_PROFILE, DARWIN_REBUILD_BIN, WSL_OSRELEASE_FILE, and WSL_VERSION_FILE environment variables exist only so the regression tests can point the scripts at sandboxed paths. Normal use should leave them unset.

How I manage changes later

After the initial bootstrap, the usual workflow is:

  1. edit the Nix config
  2. run:
rebuild

The alias is included in the shell config, and each platform's profile defines it to mean the right thing:

# macOS (nix-darwin only activates as root, so the alias carries sudo)
sudo /run/current-system/sw/bin/darwin-rebuild switch --flake "$HOME/github/dotfiles-nix#mac"
# Linux and WSL
home-manager switch --flake "$HOME/github/dotfiles-nix#<user>@<linux|wsl>[-aarch64]"

Keep the flake reference quoted if you type these by hand: the zsh workflow layer enables EXTENDED_GLOB, which makes an unquoted # a glob operator, so the unquoted form fails with "no matches found" before the rebuild tool ever runs. The aliases quote it for the same reason.

up updates the whole machine: it refreshes the flake inputs, activates them the right way for the platform, and then updates Homebrew, rustup, and the tldr cache when those are present.

Note that up runs nix flake update first, so it bumps the pinned inputs. To apply a change without moving the pins, use rebuild.

Testing

Do not run setup/mac.sh, setup/linux.sh, or setup/install.sh against a development or CI machine just to test them. Every suite in tests/ runs the real script logic against a sandbox instead (stub executables, sandboxed HOME directories, local git fixtures), so nothing is ever installed, activated, or pushed anywhere real. The one exception is tests/linux_e2e_docker.sh, which performs a genuine Nix build and Home Manager activation, but only inside a Docker container; it skips itself without Docker.

Two gaps are deliberate: the Determinate installer branch of setup/linux.sh is not exercised (the container image already ships Nix), and setup/windows.ps1 is not covered at all, since it needs Windows and PowerShell. tests/README.md is the authoritative description of every suite, the scenarios it covers, how the sandbox is guarded, and what CI runs.

Where to add new tools

My rough rule of thumb:

  • use Home Manager / Nix for reproducible baseline CLI tools, fonts, shell utilities, and user environment packages
  • use Homebrew for GUI apps and macOS-native tools that fit naturally there
  • use ecosystem-specific package managers like npm when that is the right abstraction for the tool

Where a tool goes in nix/home/ decides which platforms get it:

Add it to Reaches
nix/home/common.nix macOS, Linux, and WSL
nix/home/desktop.nix macOS and Linux; skipped on WSL, which has no display server
nix/home/darwin.nix, linux.nix, wsl.nix that platform only
nix/host.nix (Homebrew, macOS defaults) macOS only, by nature

Default to common.nix. Only reach for a platform file when the option genuinely does not exist elsewhere, such as launchd on macOS or systemd.user on Linux.

A good setup does not force every tool through one package manager. It just makes the ownership of each layer clear. The complete software inventory below records which layer owns every installed piece of software.

Why this setup looks like this

I wanted a setup that was:

  • reproducible on a new Mac
  • structured enough to maintain
  • pragmatic about macOS
  • publishable without oversharing the rest of my workflow

That is why this repo focuses on the reusable core.

The agent-first IC toolchain

Beyond the base setup, this machine runs an integrated toolchain for working as a single individual contributor with a crew of coding agents. Every tool below is a fork of a kunchenguid repo, kept in sync with upstream automatically. This section documents how it is wired on my system today, and then gives the exact commands to reproduce the whole setup from scratch.

The tools

Tool What it does Local artifact
dotfiles-nix This repo: nix-darwin + Home Manager + Homebrew base system rebuild alias
axi The 10 AXI principles for agent-ergonomic CLIs, plus the SDK and skill skill only
gh-axi GitHub (issues, PRs, CI, releases, Projects) through an agent-ergonomic CLI gh-axi
tasks-axi Task and backlog manager for the current workspace, agent-driven tasks-axi
chrome-devtools-axi Real Chrome control for agents: navigate, click, inspect, screenshot, audit chrome-devtools-axi
lavish-axi Rich annotatable HTML artifacts from agent output, with a feedback loop lavish-axi
quota-axi Reports local LLM subscription quota windows so agents can pace spend quota-axi
no-mistakes git push no-mistakes: review, tests, lint, docs, PR, and CI gate before code ships no-mistakes
treehouse Git worktrees without managing worktrees; one worktree per stream of work treehouse
firstmate Talk to one agent, ship with a crew; agent-of-agents workspace and the default entry point for all AI work fm shell function
gnhf "Good night, have fun": supervised overnight agent coding runs gnhf
wheelhouse Cross-repo "what needs my decision" queue on GitHub Issues + Actions runs on GitHub

How it is integrated on this machine

The design has six layers, and each one is owned by exactly one mechanism.

1. Forks under ~/github. Every tool is cloned from my fork with the parent repo as upstream:

origin   -> https://github.com/shreejitverma/<repo>.git
upstream -> https://github.com/kunchenguid/<repo>.git

One parent is not under kunchenguid: wheelhouse tracks ImZoomBoy/wheelhouse, where the project lives since the original repository was removed. The fleet manifest records each repo's actual parent, and the fleet doctor checks every upstream remote against it.

This is what makes local commits safe: the sync is fast-forward-only, so a fork with its own commits (like wheelhouse's Configure fleet for shreejitverma) is reported as diverged and left untouched rather than merged or clobbered.

2. Binaries. Go tools are built with make and installed to ~/go/bin ($(go env GOPATH)/bin, on PATH via nix/home/common.nix):

  • no-mistakes from ~/github/no-mistakes (make install, which also restarts its daemon)
  • treehouse from ~/github/treehouse (make build plus an explicit install into ~/go/bin; the exact command lives in the fleet manifest)

Node tools are built with pnpm and put on PATH with npm link, so the linked binary in /opt/homebrew/bin always runs the current build of the clone:

  • chrome-devtools-axi, gh-axi, gnhf, lavish-axi, quota-axi, tasks-axi

firstmate is pure shell and is entered through the firstmate function in files/zsh/ic-workflow.zsh. axi ships no daily-use binary; it contributes its skill and SDK source. wheelhouse runs entirely on GitHub Actions inside the fork; nothing is built locally.

3. Agent skills. Skills live in the clones and are symlinked twice, so every agent runtime sees the same live copy and a rebuild of a clone updates the skill in place:

~/.agents/skills/<name>  -> ~/github/<repo>/skills/<name>
~/.claude/skills/<name>  -> ../../.agents/skills/<name>

A skill's name does not have to match its repo: lavish comes from lavish-axi, stow (sweep a session for durable knowledge before a context reset) comes from firstmate, and axi lives under .agents/skills inside its repo. Skills owned by this repo rather than a tool fork live in files/skills/, and every directory there with a SKILL.md is linked and checked automatically, so adding one needs no list edit:

  • ship encodes the end-to-end quality loop below as an invocable skill.
  • cpp-coding-standards, cpp-testing, python-testing, perf-loop, silent-failure-hunt, and search-first cover C++ and Python engineering, including hot-path rules and measured optimization.
  • mle-workflow, data-backfill, and decision-ledger cover research, ML, and data pipelines: point-in-time correctness, provable backfills, and experiment ledgers that survive selection bias.
  • loop-design-check reviews autonomous agent loops (overnight gnhf runs, green-keepers) before they run unattended, and learn-eval promotes a recurring lesson into a new skill behind a quality gate, complementing stow, which saves notes but never writes skills.

Those eleven are adapted from ECC under the MIT License; files/skills/THIRD_PARTY_NOTICES.md maps each to its source.

4. Daily sync. files/bin/sync-forks reads the fleet manifest (~/github/.fleet/manifest.yaml, the single source of truth for the repo list, maintained outside this repo), re-asserts commit identity, and fast-forwards each fork's default branch from upstream. It never merges: a fork with local commits is reported as diverged (or ahead) and left untouched, locally and on GitHub. When a fast-forward actually updated a repo, it pushes the fork and re-runs that repo's install command from the manifest. A launchd agent defined in nix/home/darwin.nix runs it daily at 10:00 on macOS, with RunAtLoad catching runs missed while the machine was off; nix/home/linux.nix schedules the same run as a systemd user timer, and nix/home/wsl.nix ships that timer disabled. Logs land in ~/github/.fleet/logs/ with 30-day rotation, and a desktop notification fires on failure only. syncforks and syncforks-dry run it by hand.

5. Shell ergonomics. files/zsh/ic-workflow.zsh wires the tools into the shell: th (treehouse), nm (no-mistakes), gn (gnhf), cda (chrome-devtools-axi), ta (tasks-axi), qa (quota-axi), fm (firstmate), syncforks, and icdoctor. On macOS it also wraps claude and grok so each invocation gets TYPESAFE_API_KEY from the login Keychain (service typesafe-api-key) for the compact-adviser plugin, and nothing else does. The key is scoped rather than exported because firstmate's typed dispatch presence-gates on the same variable, and Claude's /compact-adviser menu would save it into the git-tracked agents settings.json. Store or rotate it with security add-generic-password -U -a "$USER" -s typesafe-api-key -w; the first mate fm starts and unattended agent runs bypass the wrapper and get no key. files/bin/ic-doctor is the read-only health check for the whole system; run it whenever something feels off or after changing the setup.

6. Cross-tool defaults. Every tool reads its own manual, and no tool reads another's. ~/.claude/CLAUDE.md is Claude Code's, and ~/AGENTS.md carries the tool-neutral build that Codex and every other AGENTS.md reader resolves to. The "Default development system" section each of them carries makes firstmate the default entry point for all AI work and this toolchain the default for every development request. An agent in a plain session outside firstmate points at fm once before working directly. Grok Build and Gemini have their own files too, versioned next to Claude's in the private agents repo:

~/AGENTS.md                 -> ~/github/agents/AGENTS.md   (tool-neutral)
~/.codex/AGENTS.md          -> ~/AGENTS.md
~/.codex/skills/<name>      -> ../../.agents/skills/<name>
~/.grok/AGENTS.md           -> ~/github/agents/GROK.md     (only when ~/.grok exists)
~/.grok/agents/<name>.md    -> ~/github/agents/grok/agents/<name>.md
~/.grok/skills/<name>       -> ../../.agents/skills/<name>
~/.gemini/AGENTS.md         -> ~/github/agents/GEMINI.md   (only when ~/.gemini exists)

CLAUDE.md, GROK.md and GEMINI.md are the per-tool builds and AGENTS.md is the tool-neutral one, all generated in that repo from one shared source. Edit CORE.md, ROUTING.md or tools/<tool>.md there and run its bin/build-manuals; never hand-edit a generated manual. ic-link never points ~/.grok/AGENTS.md at Claude's file, never creates ~/.grok (the Grok installer owns that directory), and never touches ~/.grok/hooks/ (firstmate owns the turn-end hook). ~/.grok/config.toml is deliberately not linked: Grok owns that file and rewrites it in place, which replaces any symlink with a regular file. ic-link never creates, overwrites, or deletes it, and ic-doctor does not check it. The intended Grok settings are recorded in the agents repo README rather than as a versioned copy of that file. Gemini is wired the same way, with one addition: ~/.gemini/GEMINI.md is Gemini's own memory file, written by /memory add, so it is never linked or replaced, and the manual goes to ~/.gemini/AGENTS.md instead. Gemini loads it only when context.fileName in ~/.gemini/settings.json lists AGENTS.md, which ic-doctor checks and ic-link never writes, because that file is Gemini's to own. Cursor is not installed on this machine; when it is, point its User Rules at ~/AGENTS.md (or symlink a project's .cursor/rules to it) to join the same system.

The personal layer itself is version controlled in a private repo, ~/github/agents, so nothing exists only as loose files in the home directory:

~/.claude/CLAUDE.md      -> ~/github/agents/CLAUDE.md
~/.grok/AGENTS.md        -> ~/github/agents/GROK.md
~/.gemini/AGENTS.md      -> ~/github/agents/GEMINI.md
~/OPINIONS.md            -> ~/github/agents/OPINIONS.md
~/VOICE.md               -> ~/github/agents/VOICE.md
~/.claude/settings.json  -> ~/github/agents/claude/settings.json

Edits made through the symlinks land in the repo; commit and push there as usual. files/bin/ic-link is the idempotent script that creates this entire farm (skills, mirrors, instruction chain, personal layer), and ic-doctor verifies it.

Complete software inventory

Every piece of software on this system belongs to exactly one owner. This is the full map; if something is installed and not listed here, it is unmanaged and should be adopted into a layer.

Layer Owner What it installs
OS toolchain Apple Xcode Command Line Tools: git (pre-Nix), make, clang (xcode-select --install)
Bootstrap setup/install.sh -> setup/mac.sh Determinate Nix, Homebrew, nvm + Node LTS (fallback; the primary Node is Homebrew's)
System config nix/host.nix starship; brew formulas autoconf, herdr, and the C++ toolchain cmake, ninja, ccache, llvm (keg-only); casks wezterm, amethyst, opensuperwhisper; macOS defaults (incl. OpenSuperWhisper Cmd+` record hotkey)
User packages nix/home/common.nix home.packages git curl wget jq fd fastfetch ripgrep killall lazygit tree bun rustup zip unzip just dust duf procs sd btop tokei tealdeer uv ruff difftastic
User programs nix/home/common.nix programs.* git+delta, starship, bat, fzf, zoxide, atuin, direnv, zsh, eza
C++ lint tools nix/home/darwin.nix home.file links to Homebrew LLVM's clang-format, clang-tidy, run-clang-tidy, clang-apply-replacements, git-clang-format in ~/.local/share/ic/llvm-tools, on PATH without shadowing Apple clang
Desktop layer nix/home/desktop.nix fonts (Hack Nerd Font, Roboto, Noto, Font Awesome) and the linked WezTerm and herdr configs
Manual Homebrew brew (not yet declared in nix) formulas node, go, gh, gemini-cli; casks google-chrome (required by chrome-devtools-axi), codex
npm globals npm install -g pnpm (build tool for all Node forks)
Native installers vendor scripts claude (Claude Code, curl -fsSL https://claude.ai/install.sh | bash); grok (xAI Grok Build, curl -fsSL https://x.ai/cli/install.sh | bash, binary at ~/.local/bin/grok)
Go builds sync-forks / manual no-mistakes, treehouse into ~/go/bin
npm links sync-forks / manual chrome-devtools-axi, gh-axi, gnhf, lavish-axi, quota-axi, tasks-axi
Skills installer npx skills third-party skills: deploy-to-vercel, find-skills, vercel-*, web-design-guidelines, writing-guidelines
Symlink farm ic-link all skill, instruction-chain, and personal-layer links

Known gap, on purpose: the "Manual Homebrew" row is not yet declared in nix/host.nix. Moving those six entries into homebrew.brews/homebrew.casks would make them declarative; until then, this table is their record.

From scratch: the full setup, step by step

These are the exact commands to reproduce this system on a new Mac. Nothing is assumed beyond a fresh macOS install with an admin account. The IC toolchain below is macOS-first: the base Nix layer installs on Linux and WSL too, but several tools in this section come from Homebrew casks and have no Linux equivalent here yet.

Step 0: prerequisites and base system.

xcode-select --install          # Apple CLT: git, make, clang (accept the GUI prompt)
git clone https://github.com/<you>/dotfiles-nix.git ~/github/dotfiles-nix
cd ~/github/dotfiles-nix
bash setup/install.sh           # detects macOS; installs Nix, Homebrew, nix-darwin + Home Manager, nvm + Node LTS
exec zsh                        # pick up the new environment

On Linux or WSL the same first command applies, and setup/install.sh dispatches to the Home Manager path instead. On Windows, run powershell -ExecutionPolicy Bypass -File setup\windows.ps1 first; it prepares WSL2 and runs the Linux bootstrap inside it.

Then the runtimes and apps the toolchain needs, which the base config does not install:

brew install node go gh         # Node (primary), Go, GitHub CLI
brew install --cask google-chrome   # required by chrome-devtools-axi
npm install -g pnpm             # build tool for all the Node forks

And the AI coding tools themselves:

curl -fsSL https://claude.ai/install.sh | bash   # Claude Code -> ~/.local/bin/claude
brew install --cask codex                        # Codex CLI
curl -fsSL https://x.ai/cli/install.sh | bash    # Grok Build (xAI) -> ~/.local/bin/grok
brew install gemini-cli                          # Gemini CLI (Google); owns ~/.gemini, which ic-link links into

Step 1: authenticate GitHub and create the commit-signing key.

gh auth login
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519_signing        # dedicated signing key the git config points at
gh ssh-key add ~/.ssh/id_ed25519_signing.pub --type signing     # Verified badge on GitHub; needs the admin:ssh_signing_key scope

The git config in nix/home/common.nix signs commits with this key over SSH, and nix/home/darwin.nix turns signing on by default on macOS only, because the key is machine-local. The key is deliberately passphrase-less so headless commits sign without an agent prompt. Local verification (git log --show-signature) additionally needs the key listed in the allowed-signers file at ~/github/.fleet/allowed_signers, which lives with the fleet manifest outside this repo.

Step 2: fork and clone every tool with an upstream remote.

mkdir -p ~/github ~/.local/bin ~/go/bin ~/.agents/skills ~/.claude/skills
cd ~/github
me=$(gh api user -q .login)
for repo in axi chrome-devtools-axi firstmate gh-axi gnhf lavish-axi \
            no-mistakes quota-axi tasks-axi treehouse wheelhouse; do
  parent=kunchenguid
  case "$repo" in
    wheelhouse) parent=ImZoomBoy ;;
  esac
  gh repo fork "$parent/$repo" --clone=false
  git clone "https://github.com/$me/$repo.git"
  git -C "$repo" remote add upstream "https://github.com/$parent/$repo.git"
  git -C "$repo" fetch upstream
done

wheelhouse forks from ImZoomBoy/wheelhouse because the original kunchenguid/wheelhouse repository was removed.

Step 3: build the Go tools into ~/go/bin.

cd ~/github/no-mistakes && make install   # installs to $(go env GOPATH)/bin and restarts the daemon
cd ~/github/treehouse   && make build && install -m755 treehouse ~/go/bin/treehouse

treehouse gets an explicit install because its upstream make install reads the environment GOPATH, which is unset under launchd where sync-forks re-runs these commands.

Step 4: build and link the Node tools.

for repo in chrome-devtools-axi gh-axi gnhf lavish-axi quota-axi tasks-axi; do
  cd ~/github/$repo
  pnpm install --frozen-lockfile
  pnpm run build
  npm link
done

Step 5: clone the private personal layer. The agent operating manuals (the tool-neutral AGENTS.md and the per-tool CLAUDE.md, GROK.md, and GEMINI.md, each carrying the "Default development system" section), OPINIONS.md, VOICE.md, and Claude settings live in a private repo so they are version controlled without being published:

git clone https://github.com/<you>/agents.git ~/github/agents

If you are reproducing this setup for yourself, create that private repo first with your own shared source and its bin/build-manuals (see the cross-tool layer above); this repo's files/skills/ship/SKILL.md and the layer descriptions above tell you what the generated manuals need to contain.

Step 6: wire every symlink with one command.

ic-link

ic-link (in files/bin, on PATH) is the idempotent, versioned recipe for the whole farm: skill links into ~/.agents/skills, mirrors into ~/.claude/skills, ~/.codex/skills, and (when Grok is installed) ~/.grok/skills, the tool-neutral ~/AGENTS.md and ~/.codex/AGENTS.md chain, Grok's own AGENTS.md and agent definitions and (when Gemini is installed) ~/.gemini/AGENTS.md from ~/github/agents, Claude subagents and rules into ~/.claude/agents and ~/.claude/rules, and the rest of the personal-layer links (skipped with a note if that repo is absent). It links one file at a time and never writes into a real file or directory it did not create, so a skill installed by npx skills, a subagent made with /agents, or a hand-written rule is left alone with a warning. Rerun it any time; it repairs stale links in place.

Step 7: enable the daily sync. The sync engine and its launchd schedule are in this repo; the list of repos it syncs lives in the fleet manifest at ~/github/.fleet/manifest.yaml, which is maintained outside this repo. On a machine without that manifest the sync logs that there is nothing to sync and exits cleanly, so applying the config is safe either way:

rebuild
syncforks-dry   # verify: every manifest repo reports how far behind/ahead of upstream it is

Step 8: one-time per-tool setup.

quota-axi needs macOS Keychain access once to read live Claude quota (click "Always Allow"):

quota-axi --allow-keychain-prompt auth

Gemini needs AGENTS.md added to context.fileName in ~/.gemini/settings.json by hand, once; ic-link deliberately never writes that file (cross-tool layer above), and ic-doctor FAILs until the entry is there.

wheelhouse is configured on GitHub, not locally: commit your fleet of repos to your fork, enable Actions on it, and add the secrets its README lists. That fleet-config commit makes the wheelhouse fork diverged, so sync-forks reports it and leaves it untouched rather than ever merging over it.

firstmate is the default way in for all AI work and runs from inside its workspace; the shell function handles that:

fm          # cd ~/github/firstmate and launch Claude Code as the first mate
fm grok     # same, with Grok as the primary (coding quota fallback)
fm pi       # same, with another verified primary harness
cdfm        # just jump to the workspace (fleet alias)

FM_DEFAULT_HARNESS changes which harness a bare fm launches.

Optional ambient context: some tools can inject their state at agent session start (for Claude Code, Codex, and OpenCode) instead of waiting to be asked. Not currently enabled here; enable per tool with:

tasks-axi setup hooks             # backlog as ambient session context
chrome-devtools-axi setup hooks   # browser bridge ambient context

Since ~/.claude/settings.json is a symlink into the private agents repo, the hook edits land there; commit them.

Optional third-party skills come from the skills installer, not from ic-link:

npx skills add <owner>/<repo> --skill <name> -g   # -g = all projects (~/.claude/skills)

Step 9: verify everything.

ic-doctor

ic-doctor (in files/bin, already on PATH) is a read-only check with seven sections: this checkout's path against the dotfilesDir declared in the entry module for the detected platform, plus the app-config symlinks and shell hook that path feeds; every fork's clone, remotes, branch, and cleanliness; every binary's presence and --version, plus on macOS the linked C++ lint tools (a warning, since only C++ work needs them); every skill symlink in both directories; the daily sync schedule (launchd agent on macOS, systemd user timer on Linux) and its last log line; gh plus quota-axi auth; and the cross-tool default chain (~/AGENTS.md and the tool-neutral manual behind it, codex AGENTS.md and skills, Grok's separate AGENTS.md, skills, and agent definitions, plus the grok binary resolving to ~/.local/bin/grok with no second copy on PATH, and Gemini's ~/.gemini/AGENTS.md with the context.fileName entry that loads it). A ~/.grok/AGENTS.md or ~/.gemini/AGENTS.md that points anywhere other than its own tool's manual is a FAIL naming the actual target, reported whether or not that manual or the agents repo exists, because one tool silently loading another tool's operating manual is the fault this layer exists to prevent. A ~/AGENTS.md still resolving to ~/.claude/CLAUDE.md while the neutral build exists is the same FAIL; that target is only ok when the agents repo is absent, since there is then no neutral manual to point at. Agent definitions are optional: an agents repo with no grok/agents/*.md is only a warning, while a definition that exists but is not linked is a FAIL. A link in ~/.grok/agents left dangling because its definition was renamed or removed in the agents repo is a FAIL that names the link and the rm that clears it; ic-link only writes its own links and never deletes inside ~/.grok. When Grok is installed but ~/github/agents is not cloned, the remaining repo-backed Grok checks are skipped with a single warning, the same way the Claude personal layer is. With the agents repo cloned, the personal-layer checks also cover every Claude subagent and rule link (and dangling ones), every hook script that claude/settings.json runs around Bash and edit calls (the guard and the post-edit lint feedback), and bin/build-manuals --check, so a stale or hand-edited manual is caught on the machine and not only in CI. Checks that do not apply to a platform are reported as such rather than failed: WSL has no desktop layer, so the linked terminal configs are not expected there, and its sync timer is left disabled because systemd is off by default. It exits non-zero if anything needs attention, and every failure line names the command that fixes it. A healthy system ends with ic-doctor: all checks passed.

Maintenance: adding a new tool to the rotation

The checklist when adopting the next tool, so it inherits all five layers:

  1. Fork, clone, and set the upstream remote (Step 2 pattern).
  2. Build and put it on PATH: pnpm install --frozen-lockfile && pnpm run build && npm link for Node tools, or make build && install -m755 <bin> ~/go/bin/<bin> for Go tools.
  3. Add its skill to files/bin/ic-link (both the canonical link and the ALL_SKILLS mirror list) and run ic-link.
  4. Add the repo to the fleet manifest (~/github/.fleet/manifest.yaml), with its default branch and install command, so the daily sync picks it up.
  5. Add it to the FORKS, BINARIES, and SKILLS lists in files/bin/ic-doctor.
  6. Optionally add a short alias in files/zsh/ic-workflow.zsh.
  7. Run syncforks-dry and ic-doctor to confirm, then commit the dotfiles change.

A skill owned by this repo needs none of the list edits above: add files/skills/<name>/SKILL.md (the learn-eval skill does this behind its quality gate), run ic-link, confirm with ic-doctor, and ship through no-mistakes.

Troubleshooting

  • A Node tool's build fails under npm install. These repos use pnpm; npm install against a pnpm node_modules fails with errors like Cannot read properties of null (reading 'matches'). Always use pnpm install --frozen-lockfile.
  • A binary is on PATH but --version fails. The npm link points at a clone whose dist/ is stale or missing; rebuild the clone (pnpm run build) or relink (npm link).
  • Cmd+` moves window focus instead of, or as well as, toggling OpenSuperWhisper recording. Cmd+` is also macOS's default "Move focus to next window" shortcut; the record hotkey declared in nix/host.nix deliberately overlaps it and leaves the system binding untouched. If the overlap ever conflicts, disable the system shortcut manually in System Settings > Keyboard > Keyboard Shortcuts > Keyboard. The hotkey preference is applied by rebuild; restart OpenSuperWhisper after changing it.
  • Ctrl-R opens atuin's history search, not fzf's. That is deliberate: both programs.fzf and programs.atuin want Ctrl-R, and nix/home/common.nix cedes it to atuin with programs.fzf.historyWidget.command = "". fzf keeps Ctrl-T (files) and Alt-C (directories). To flip the ownership, drop that line and add "--disable-ctrl-r" to programs.atuin.flags.
  • The installer picked the wrong platform, or you want a different one. Run bash setup/install.sh --dry-run to see what it detected and what it would do, then --target <darwin|linux|wsl> to override it. WSL is detected from $WSL_DISTRO_NAME, $WSL_INTEROP, /proc/sys/kernel/osrelease, or /proc/version; any one of them is enough.
  • Could not find suitable profile directory during a Linux or WSL install. Home Manager will not activate without a per-user Nix profile directory, and it does not exist for a user who has never run a nix command. setup/linux.sh creates it, so this only appears if you activated by hand; run mkdir -p ~/.local/state/nix/profiles and retry.
  • The Nix installer fails on WSL or in a container complaining about systemd. The Determinate installer's default Linux plan registers the daemon as a systemd service, which cannot work where systemd is not PID 1. setup/linux.sh detects that and installs with --init none instead. WSL only runs systemd if you enable it explicitly in /etc/wsl.conf.
  • My ~/.bashrc was replaced on Linux or WSL. The Linux and WSL profiles manage the bash startup files (~/.bashrc, ~/.bash_profile, ~/.profile) as well as the zsh ones, which is what makes this repo's PATH, session variables, and aliases work in a bash login shell rather than only under zsh. setup/linux.sh names each file it is about to take over before it installs or builds anything, and every original is preserved next to it as <file>.backup, so mv ~/.bashrc.backup ~/.bashrc puts the distro default back. That restore only lasts until the next rebuild: programs.bash stays enabled, so Home Manager takes the file over again and backs up your restored copy in turn. To keep content for good, put it in programs.bash.initExtra (or bashrcExtra) in nix/home/linux.nix instead, where it becomes part of the generated file. If you later recreate a real ~/.bashrc while ~/.bashrc.backup is still there, the next activation stops with Existing file ... would be clobbered by backing up ... rather than overwriting the backup; move or delete the stale .backup file and retry.
  • setup/linux.sh stops because a startup file is a symlink Home Manager does not own. Home Manager backs up a regular file it needs to replace, but its collision check skips the backup entirely for a symlink, so activation would fail partway with would be clobbered after the whole build had already run. The installer checks for this before it installs or builds anything and names each path; mv ~/.bashrc ~/.bashrc.pre-home-manager and re-run. This also fires on a symlink whose content happens to match what Home Manager would write, which activation would have skipped harmlessly; moving it aside costs nothing and the check stays honest about what it can predict.
  • The daily fork sync never runs on WSL. That is deliberate: the timer is shipped disabled there because systemd is off by default. Run syncforks by hand, or enable systemd in /etc/wsl.conf and switch to the Linux profile behaviour.
  • setup/linux.sh refuses a checkout under /mnt/c. Also deliberate. The config links files out of the checkout by absolute path, so a Windows-side checkout resolves only while the drive is mounted, and is much slower over the filesystem boundary. Clone inside the distro at ~/github/dotfiles-nix.
  • quota-axi shows auth_required for claude. Keychain access has not been granted; run quota-axi --allow-keychain-prompt auth and click "Always Allow".
  • sync-forks skipped a repo. By design it skips uncommitted changes, an in-progress rebase or merge, a branch other than the manifest's default branch, an uncloned manifest entry, unpushed local commits, and sync: false entries; the log line names the reason. Fix the cause and rerun syncforks.
  • sync-forks reported a diverged fork. It never merges: a fork that is both ahead of and behind upstream is left untouched, locally and on GitHub. Reconcile by hand in that clone (for example git merge upstream/main, keeping your changes), push, and the next run takes over again.
  • Where are the sync logs? ~/github/.fleet/logs/sync-YYYYMMDD.log on every platform, one file per day with 30 days kept. On macOS launchd's own streams land next to them as launchd.out.log and launchd.err.log; on Linux the run's output is also in journalctl --user -u sync-forks.
  • A skill or instruction symlink is missing or points somewhere stale. Run ic-link; it recreates the entire farm idempotently.
  • Everything else. Run ic-doctor; each FAIL line names the fix.

How the pieces work together day to day

The point of the stack is that each tool owns one phase of the loop. The ship skill encodes the whole loop, so any agent in any tool can be told /ship and run it end to end.

  • Start the day with quota-axi to see how much agent headroom the session and week have left, and check the wheelhouse queue on GitHub for decisions other people are waiting on.
  • Pull the next piece of work with tasks-axi ready, and open an isolated worktree for it with th so streams of work never collide.
  • Do the work with agents that carry the axi skills: gh-axi for everything GitHub, cda for anything that needs a real browser, lavish when a plan or review is easier to judge as a rich artifact.
  • Ship through nm (no-mistakes), which runs review, tests, lint, docs, push, PR, and CI as one gate, so nothing reaches the remote unvalidated.
  • Before ending a long agent session, invoke the stow skill so preferences, project facts, and unfinished next steps land on disk instead of dying with the context window; when a stowed lesson keeps recurring, learn-eval turns it into a skill.
  • Before bed, hand the backlog to gn (gnhf) for a supervised overnight run, and read the results over coffee.
  • Every day at 10:00, sync-forks fast-forwards every manifest fork from its upstream, pushes, and reinstalls what changed, notifying only when something needs a human, so the whole toolchain stays current without a thought.

Related