Keeping a home environment consistent across machines usually means scattered configs, manual installs, and "works on my machine" drift. This flake declares the entire home environment as code — packages, dotfiles, and tool configs all version-controlled in one place — so one command applies a fully reproducible setup on any supported machine.
New machine, no Nix installed — one line: no clone, no git, nothing to edit. It
installs Nix (via the Determinate installer) if
missing, then applies this flake with the account set to your $USER:
curl -fsSL https://raw.githubusercontent.com/NoSugarCoffee/dotnix/main/scripts/bootstrap-macos.sh | bashIf Nix wasn't installed yet, open a new terminal and re-run. If no proxy is listening on the
port in lib/proxy.nix, it installs and opens only Clash Verge, then stops: import a
subscription and re-run; uncached downloads in the full switch use the proxy. Skip that gate
(no proxy or a TUN / system VPN) with DOTNIX_DIRECT=1; a different proxy port requires
updating lib/proxy.nix first. Clash-managed shells already export http(s)_proxy toward
127.0.0.1:7890, so drop those for both curl and the piped bash or the fetch still
hits the dead proxy:
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u all_proxy -u ALL_PROXY \
curl -fsSL https://raw.githubusercontent.com/NoSugarCoffee/dotnix/main/scripts/bootstrap-macos.sh \
| env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u all_proxy -u ALL_PROXY DOTNIX_DIRECT=1 bashRe-run any time to update. On a machine that already wrote impure-env and whose proxy is
down, bootstrap tells you to start the Clash Verge you already have instead of trying a
download that cannot succeed — or re-run with DOTNIX_DIRECT=1 to drop the daemon proxy
line and switch with uncached fetches going direct.
Already have Nix, no home-manager yet — bootstrap the first switch. This targets the
account configured in flake.nix (liangliangdai until you fork and rename it), which only
matches $USER after scripts/bootstrap-macos.sh has run on this checkout:
# Linux
nix run .#home-manager -- switch --flake ".#$(nix eval --raw .#username)-x86_64-linux"
# macOS (Apple Silicon)
nix run .#home-manager -- switch --flake ".#$(nix eval --raw .#username)-aarch64-darwin"
# macOS (Intel)
nix run .#home-manager -- switch --flake ".#$(nix eval --raw .#username)-x86_64-darwin"Day to day — once just is on PATH:
just switch| 🍎 🐧 Both | 🍎 macOS only | 🐧 Linux only | |
|---|---|---|---|
| AI | codex · claude-code (not Intel macOS) · claude-desktop · pi · apm · ccstatusline · paperclip · orca · agent-access · bitwarden-cli · grok-bot | ChatGPT | — |
| Terminal | kitty · zellij · zoxide · yazi · nix-zsh-completions | — | — |
| Editors | intellij-idea-ultimate · cursor | jetbrains-air · pulsar | — |
| Dev | git · git-open · gh · glab · docker (client) · docker-compose · asdf (go, nodejs, java, maven) · pnpm · python3 (ipython, pip) · ruby · just · android-sdk · lark-cli | colima · cocoapods | — |
| Browsers | — | google-chrome · ego-lite | — |
| Screen | obs-studio | macshot | — |
| Network | clash-verge-rev | tailscale (app + CLI) | tailscale (CLI + daemon) |
| Utilities | — | albert · maccy · scroll-reverser · cida · digup | copyq |
| Command | Description |
|---|---|
just switch |
Apply the configuration |
just build |
Build without switching |
just generations |
Show Home Manager generations |
just update |
Update flake inputs |
just show |
Show flake outputs |
just test |
Run the Python tests with CI's bash, rsync and GNU coreutils |
just nix-proxy |
Route the Nix daemon's uncached downloads through Clash (macOS, sudo) |
update-flake-lock (.github/workflows/flake-update.yml)
opens a daily PR bumping flake inputs and enables auto-merge (squash) on it, so it only lands
once CI goes green — it never merges directly. Three repository settings are required for that
to actually work end to end:
UPDATE_PATsecret. Must be a fine-grained PAT (or other long-lived token) withcontents: writeandpull-requests: writeon this repo — not a GitHub App installation token, which expires after an hour and would silently break the next scheduled run once it does. The workflow falls back to the defaultGITHUB_TOKENif the secret is unset, butGITHUB_TOKEN-authoredpull_requestruns are held for manual approval rather than running automatically, so setUPDATE_PATfor the PR to get CI (and therefore auto-merge) on its own.Allow auto-mergemust be enabled in the repository's General settings. Without it,gh pr merge --autofails outright instead of queuing the PR for merge once checks pass.- Branch protection on
mainmust require thelint,build-linux, andbuild-macosstatus checks (theci.ymljobs) before merging. Auto-merge only completes the squash once those checks report success.
The username is a single source of truth in flake.nix:
let
username = "liangliangdai";Everything else (homeConfigurations attribute names, home.username,
homeDirectory, CI) reads from there directly or via nix eval --raw .#username.
Fork the repo and change just that string — that's the whole rebranding step.
A new machine doesn't even need that edit: scripts/bootstrap-macos.sh rewrites the
username/homeDirectory lines to the machine's $USER/$HOME in a throwaway copy of the
source (via scripts/inject-account.sh). Keep each on one line — the rewrite is
line-level and fails loudly if either doesn't match.
- zsh is managed. Move an existing
~/.zshrcaside before the first switch; machine-local config goes in an untracked~/.zshrc-local. - git config is not. Per-checkout identity needs
includeIfpaths, so~/.gitconfigis hand-written. - Codex and Claude Code settings stay writable. A switch seeds them if missing and resets only the keys declared in
apps/codex/managed.tomlandapps/claude-code/settings.nix. - asdf pins Go / Node / Java / Maven to explicit versions in
apps/asdf/default.nix. Python comes from nixpkgs instead, since asdf compiles it from source. - Docker on macOS is colima. A launchd agent starts the VM at login; resize it with
colima stop && colima start --cpu 4 --memory 8. On Linux the daemon is a system service, out of scope here. - Tailscale on macOS is the standalone app. Open it after switching, then approve its VPN configuration and system extension when prompted. On Linux, the package provides
tailscaleandtailscaled; the host must run the daemon as a system service beforetailscale upcan connect. - Ad-hoc-signed macOS apps are re-signed locally. Nix builds like kitty and Albert carry a signature that changes with every rebuild, which silently invalidates Accessibility / Screen Recording grants. A switch re-signs their copies in
~/Applications/Home Manager Appswith a self-signeddotnix local code signingcertificate it creates in the login keychain, so grants survive updates. The first switch that migrates an app to this identity still changes its code identity once -- macOS treats that as a new app, so expect to re-grant Accessibility / Screen Recording for kitty and Albert one more time right after that switch; it then sticks across every rebuild after. Without a login keychain at~/Library/Keychains/login.keychain-db(headless activation, CI) it warns and leaves the copy ad-hoc signed instead of failing. - Packages nixpkgs lacks (the macOS repacks of OBS, Clash Verge, Pulsar, Albert, …) come from NoSugarCoffee/nur-packages, which follows each upstream release 4× a day and serves builds from
nosugarcoffee.cachix.orgon Apple Silicon and Linux (Intel Macs build them locally against the 26.05 pin); AI agents come from numtide/llm-agents.nix, which has no Intel Mac builds. Neither needs anything here beyondnix flake update. - China mirrors.
scripts/bootstrap-macos.shadds SJTU/TUNA/USTC substituters; check withnix config show | grep substitut. - On macOS, the Nix daemon downloads through Clash. Anything no cache has (unfree packages like the Android SDK, GitHub release assets) is fetched by the daemon, which ignores the shell's proxy. Bootstrap sets
impure-envin/etc/nix/nix.custom.confto the proxy inlib/proxy.nix; on a machine set up before that, runjust nix-proxyonce. Those downloads then fail while Clash Verge is down.DOTNIX_DIRECT=1(bootstrap andjust nix-proxy) skips the write and, if a proxy-routingimpure-envline is already there, removes it and restarts the daemon so uncached downloads go direct.
MIT — see LICENSE.
