Watch the walkthrough: https://youtu.be/5N-okeDdIuI
My personal Mac setup, managed with nix-darwin and home-manager. One repo, one command, and a fresh Mac ends up configured the same way every time.
These are my personal dotfiles, shared publicly so people can read them, learn from them, and fork them freely. Feature requests and pull requests are not accepted here, and PRs are auto-closed. If you find a bug, please open a GitHub Issue using the bug report template.
Running the switch builds:
- System settings (dark mode, key repeat, dock, Finder, trackpad)
- Homebrew apps (casks and CLI tools, including VS Code Insiders)
- Nix user packages (ripgrep, fd, fzf, jq, lazygit, Neovim, pre-commit, gitleaks, Hack Nerd Font)
- Shell (zsh, aliases, starship prompt)
- Editor (Neovim config with the rose-pine moon theme)
- Terminal (WezTerm config with the rose-pine moon theme and dimmed unfocused windows)
- Agent configs (Claude, Codex, opencode all share one AGENTS.md)
- Optional Pi theme and local extensions, UI settings, a default model and model overrides, plus two deliberately pinned third-party Pi packages
- SSH server (Remote Login), key-only auth, for connecting from another machine on the LAN (e.g. VS Code Remote-SSH)
- Git identity, GitHub SSH auth, and commit signing, using a dedicated machine key pulled non-interactively from 1Password via a Service Account (no human required - this box runs unattended)
- Apple Silicon Mac, by default.
- Intel Mac: change one line.
In
configuration.nix, setnixpkgs.hostPlatform = "x86_64-darwin";(the comment right there tells you the same thing).
On a brand new Mac, from a bare clone of this repo:
git clone https://github.com/kunchenguid/dotfiles.git
cd dotfilesBefore you run it: review "Make it yours" below.
Change the host label or CPU architecture if needed, and read the Homebrew cleanup warning.
bootstrap.sh applies the config to your machine, so do this first.
./bootstrap.shbootstrap.sh does five things, in order:
- Installs Determinate Nix, if it isn't already installed.
- Symlinks this repo to
~/.dotfiles. This has to happen before the first build, becausehome.nixpoints at config files through~/.dotfiles. - Checks the
userconfigured inflake.nixagainst your actual macOS username, and offers to fix it for you if they differ. - Runs the first
darwin-rebuild switch. It fetches thedarwin-rebuildtool from the nix-darwin 26.05 release branch, then applies this repo's locked flake config. - Installs the local git pre-commit hooks (gitleaks secret scanning). See CONTRIBUTING.md if you need the one-line manual fallback.
After that, darwin-rebuild exists and you're on the normal workflow below.
Once Nix is installed (bootstrap.sh step 1 handles that), you can check that the config builds without touching your system - handy when you have edited something:
nix flake check --no-build
nix build .#darwinConfigurations.mac.system --dry-runIf you renamed the host label in "Make it yours", substitute your label for mac in these commands.
Edit the config files in place, then apply:
./rebuild.shThat's it. No separate build-and-copy step.
This repo is mine.
If you clone it, review these before you run bootstrap.sh:
- Username: run
./bootstrap.sh(it detects your macOS username and offers to set it) OR change the singleuser = "kunchen"line inflake.nix. Everything else (configuration.nix,home.nix, home directory paths) is threaded from that one variable. - Host label
"mac", in three places:flake.nix(thedarwinConfigurations."mac"name),rebuild.sh:5(the#macat the end of the flake reference), andbootstrap.sh's first-switch command (also#mac). All three have to match. - CPU architecture,
hostPlatforminconfiguration.nix(see Prerequisites above).
Git identity: user.name/email aren't in this repo at all - they come from a GIT_NAME/GIT_EMAIL item in 1Password, materialized by rebuild.sh straight to ~/.config/git/config-local (outside this repo, same as the SSH keys) and pulled in via programs.git.includes, so this public repo never hardcodes anyone's real name or email. user.signingkey points at ~/.ssh/id_ed25519_mac_signing, which likewise doesn't exist until "GitHub SSH authentication & commit signing" below is done. If you clone this repo, add your own GIT_NAME/GIT_EMAIL item instead (see that section for the exact item shape), or just hardcode programs.git.settings.user in home.nix if you don't want the 1Password indirection.
Homebrew cleanup warning: configuration.nix sets homebrew.onActivation.cleanup = "zap".
That means every time you switch, Homebrew removes any package or cask on your machine that isn't listed in the brews and casks arrays in configuration.nix.
If you already have Homebrew stuff installed that isn't in that list, the first switch will uninstall it.
Read through brews and casks before you run bootstrap.sh or rebuild.sh for the first time, and add anything you want to keep.
About herdr: it's in the brews list.
It's a real public Homebrew formula (brew info herdr finds it in homebrew-core, no tap needed), so it will install fine.
If you don't use it, just remove it from brews in your copy.
About Claude Code: it's installed via npm (home.nix's npm-global activation script), not a Homebrew cask.
The Homebrew cask build carries a quarantine xattr that Apple System Policy suspends before it reaches main, hanging forever with no output.
npm is the supported install path until upstream fixes the cask.
Heads-up:
home/AGENTS.mdis my personal agent policy, andhome.nixinstalls it for Claude, Codex, and opencode. If you clone this repo, you'd silently inherit my agent instructions - edit or deletehome/AGENTS.mdif you don't want that.- The
ccandcoshell aliases inhome.nixare high-agency shortcuts:claude --dangerously-skip-permissionsandcodex --full-auto. They're convenient for me, but know what they do before you use them.
flake.nix- the entry point. Wires up nixpkgs, nix-darwin, home-manager, and nix-homebrew, and declares themacmachine.configuration.nix- system-level config: macOS defaults, Homebrew.home.nix- user-level config: shell, packages, prompt, and the symlinks described below.rebuild.sh- re-applies the config after the first switch. Run this every time you make a change.home/- the actual config files that get symlinked into place; the sections below explain the shared symlink model and Pi's narrower selective setup.
The files under home/ are the real files - editing them here is editing your live config, no rebuild needed to see the change in your editor.
home.nix uses mkOutOfStoreSymlink to point paths like ~/.config/nvim straight at home/.config/nvim in this repo, so the two never drift out of sync.
You only run ./rebuild.sh when you change something that isn't just a symlinked file, like a package list or a system default.
This repo installs the Pi CLI declaratively: the official pi-coding-agent Homebrew formula is in configuration.nix's brews list, so pi resolves on your PATH after a normal rebuild. Everything below is the configuration this repo layers on top of that install.
Pi Launcher is a separate, optional GUI installed from its owner, not declared by this config:
brew install --cask kunchenguid/tap/pi-launcherHome Manager owns exactly two repository-authored Pi directories: ~/.pi/agent/themes and ~/.pi/agent/extensions. It also links models.json and settings.json as individual files. Because those links point back into the clone, Pi rewrites settings.json in place - it serializes without a trailing newline (do not add one) and re-adds churn keys such as lastChangelogVersion on upgrade; discard those with git checkout instead of committing them. The local extension directory is for public, repository-authored extensions only - third-party package code never belongs there. Run /reload after editing a local extension or other Pi resources. The terminal-title extension shows a spinner while Pi is working, then a completion mark with the session name or current directory. The rose-pine-moon theme was authored clean-room from the public Rosé Pine Moon palette and Pi's public theme schema, not from a private or live theme file.
home/.pi/agent/extensions/calm is a standalone local Pi extension. Home Manager's existing global extensions-directory link makes Pi auto-load it without another declaration. /calm toggles a conversation-only presentation mode and is off by default. Its choice is stored locally in ~/.pi/agent/calm (or the directory selected by PI_CODING_AGENT_DIR), not in this repository or Home Manager. Adapted from Firstmate under the bundled MIT license, Calm imports no Firstmate modules and has no Firstmate runtime dependency.
When enabled, Calm hides collapsed thinking and the call/result shells for Pi's seven built-in tools (read, bash, edit, write, grep, find, and ls) without leaving blank transcript rows. During an active run it replaces Pi's working row with a two-line animated blue-water, yellow-boat widget. /calm restores Pi's stock rendering and preserves the existing Ctrl+O tool-expansion choice.
Calm never changes prompts, tool execution, model context, session data, or ordering. /share and /export use the complete stock transcript. Generic custom tools, images, and unsupported Pi transcript classes deliberately remain visible because Pi has no safe general-purpose transcript filter. If a future Pi release no longer exports the exact collapsed-thinking rendering seam, Calm logs one diagnostic and leaves only that adapter disabled; all other behavior remains available.
Pi's package system declares two third-party sources in the linked global settings.json:
npm:@ryan_nookpi/pi-extension-codex-fast-mode@0.2.6- the exact public npm release fromryan_nookpi.git:github.com/algal/pi-openai-server-compaction@c6d593087709e9481223dc6c6c2269b371b5e055- the exact publicalgalcommit for experimental OpenAI server-side compaction.
The version and commit are immutable pins, so Pi does not move them during package updates. Deliberate updates require a new source and security audit, followed by an explicit pin change in home/.pi/agent/settings.json. On Pi 0.82.0, global settings declarations install missing pinned packages automatically at startup. No one-time install command is required. Pi keeps the downloaded npm and git package trees in its own unmanaged ~/.pi/agent/npm and ~/.pi/agent/git runtime directories, outside Home Manager and Git tracking.
Both packages execute with your full user permissions and must be trusted like any other executable code. The compaction package is experimental, sends the relevant OpenAI compaction and continuity data to OpenAI, and upstream declares the stale peer range >=0.80.9 <0.81.0; this exact immutable ref was locally proven to load and perform remote compaction on Pi 0.82.0. Do not treat that proof as a guarantee for a different Pi version or a different package ref.
Home Manager deliberately does not manage ~/.pi/agent itself, or Pi authentication, sessions, trust decisions, caches, npm/git package trees, or any other runtime state. settings.json does pick a default provider, model, and thinking level (xai / grok-4.5 / medium); change those three keys if you use a different provider. Neither those settings nor the model overrides contain credentials or endpoint settings, and both only take effect after you authenticate Pi yourself. This remains an additive post-video layer for ~/.pi/agent config: beyond the pi-coding-agent CLI declared in configuration.nix, it does not install a launcher or vendor package source code into this repository.
configuration.nix declares services.openssh.enable = true;, which turns on macOS's built-in Remote Login (the same sshd behind System Settings > Sharing) on every switch, and disables password authentication (PasswordAuthentication no, KbdInteractiveAuthentication no) so only key-based logins are accepted.
home/.ssh/authorized_keys is symlinked to ~/.ssh/authorized_keys and ships with a placeholder line. To let another machine (e.g. a Windows PC running VS Code Insiders' Remote-SSH extension) connect:
- On that machine, generate a keypair if you don't already have one:
ssh-keygen -t ed25519. - Replace the placeholder line in
home/.ssh/authorized_keyswith the contents of the resulting.pubfile. - Run
./rebuild.shon the Mac.
This setup assumes both machines are on the same local network - it doesn't open any port on your router or configure a tunnel. For access from outside your LAN, put something like Tailscale in front of it rather than port-forwarding SSH directly to the internet.
visual-studio-code@insiders is in the casks list, giving this Mac its own local VS Code Insiders install. That's separate from the Remote-SSH connection itself: when you connect from the Windows PC's VS Code Insiders, the Remote-SSH extension downloads and runs its own remote server component on the Mac automatically over the SSH connection the first time you connect - no separate install step for that part, since macOS already ships the curl/tar it needs.
This Mac runs unattended - nobody is sitting at it to approve a Touch ID prompt, and VNC-ing in every time git wants to push or sign a commit isn't "hands off." So this isn't 1Password's interactive SSH agent (that always requires a human to approve each use, every time it locks or restarts - fine for a laptop, not for a server). Instead:
- A dedicated, non-default 1Password vault (
mac-automation) holds two SSH keys used only by this machine:dotfiles-mac-authanddotfiles-mac-signing, kept separate so a problem with one never touches the other. - A Service Account scoped read-only to just that vault authenticates non-interactively - no vault unlock, no biometrics, no human required.
home/.ssh/*.tmplare committed templates containing onlyop://references (safe - no secrets).rebuild.shrunsop injectafter everydarwin-rebuild switchto materialize the real private keys, public keys, andallowed_signersstraight into~/.ssh/, entirely outside both this git repo and the Nix store (which is world-readable, so secrets must never pass through it).home/.ssh/configroutesgithub.comat the local materialized key directly (IdentityAgent none), and falls back to 1Password's interactive agent for every other host - so a human still gets the vault-gated, private-key-never-touches-disk experience for their own ad hoc SSH use.- Git signing uses git's default
ssh-keygen-based signer against the local key file - noop-ssh-sign, no 1Password dependency at commit time. - The same vault also holds a
dotfiles-personalitem withGIT_NAME/GIT_EMAILfields, injected into~/.config/git/config-localand pulled in viaprograms.git.includes- so this repo'shome.nixnever hardcodes anyone's real name or email either. - The GitHub CLI (
gh, and thegh-axiwrapper firstmate uses to open, poll, and merge pull requests) talks to the GitHub API, which an SSH key can't authenticate. So the same vault holds adotfiles-gh-tokenitem whose password is a GitHub Personal Access Token;rebuild.shinjects it into~/.config/gh/token(mode 600, outside repo and Nix store), andhome.nixexports it asGH_TOKEN/GITHUB_TOKENfrom.zshenvfor every shell. Git operations still go over SSH with the auth key; only the API uses this token.
The trade-off, stated plainly: the private keys now exist as ordinary files on this machine's disk, protected by Unix permissions and FileVault-at-rest - not "held only inside 1Password's vault." That's the same security posture as any standard CI/deploy key, not stronger. It's the accepted trade-off for unattended automation; it is a real downgrade from the interactive-agent model, not a wash.
One-time setup:
- In 1Password, create the
mac-automationvault (Service Accounts can't be granted access to your Personal/Private/Shared vault, so it has to be a fresh one). - In that vault, create
dotfiles-mac-authanddotfiles-mac-signingas SSH Key items (+ New Item > SSH Key > Generate, type ed25519), and adotfiles-personalitem (any item type with custom text fields) withGIT_NAMEandGIT_EMAILfields set to your actual name and email. Also create adotfiles-gh-tokenPassword item whosepasswordis a GitHub Personal Access Token for the GitHub CLI/API. A classic PAT needsrepoandworkflowscopes (addread:orgif you work in org repos); a fine-grained PAT needs Contents, Pull requests, and Workflows read/write plus Actions/Checks read, on the repos you ship to. - Create a Service Account, read-only, scoped to only the
mac-automationvault. Copy its token immediately - 1Password shows it exactly once. - On this machine:
mkdir -p ~/.config/op && chmod 700 ~/.config/op, save the token to~/.config/op/service-account-token, thenchmod 600it. That path is outside~/.dotfiles, so it can never end up in this repo. - Run
./rebuild.sh. It installs1password-cli(theopbinary, via thecaskslist), then injects the keys, git identity, and GitHub token into~/.ssh/,~/.config/git/config-local, and~/.config/gh/token. - On GitHub, go to Settings > SSH and GPG keys > New SSH key. Add
~/.ssh/id_ed25519_mac_auth.pub's contents with key type "Authentication Key", and~/.ssh/id_ed25519_mac_signing.pub's with key type "Signing Key". - Point this clone at GitHub over SSH:
git remote set-url origin git@github.com:<you>/<repo>.git. - Verify:
ssh -T git@github.comshould greet you by username with no prompt at all, andgit commit --allow-empty -m test && git log --show-signature -1should show a good SSH signature. GitHub also shows a "Verified" badge on pushed commits signed this way.
Rotating a key later is just: generate a new one in the 1Password item, re-run ./rebuild.sh (op inject --force overwrites the local file), and update the GitHub-registered public key to match.
If you're setting this up on a laptop you actually sit at instead of a headless box, skip all of this and just use 1Password's interactive SSH agent directly - point home/.ssh/config's IdentityAgent at 1Password's socket for all hosts, point git's gpg.ssh.program at op-ssh-sign, and accept the occasional Touch ID prompt in exchange for the private key never touching disk at all. That's a better trade for a machine a person is actually present at.
The first time you launch nvim, it bootstraps lazy.nvim by cloning plugins from GitHub.
That needs network access once; after that it's offline.
Neovim and WezTerm both use the rose-pine moon theme.
Neovim keeps italics off and uses a transparent background on macOS, Windows, and WSL so it matches the terminal setup.
This repo is licensed under MIT No Attribution.
See LICENSE.