Instructions for AI coding agents (Claude Code and similar) working in this repo.
This is a regular git repository. It contains two kinds of things side by side:
- Dotfiles — the actual config files (
.gitconfig,.tmux.conf,.hammerspoon/,.config/karabiner/,.config/nvim/, etc.) - Nix configuration —
flake.nixandnix/which use nix-darwin and home-manager to deploy those dotfiles and manage the macOS system
home-manager deploys dotfiles by creating symlinks in $HOME that point back
into the repo checkout (via config.lib.file.mkOutOfStoreSymlink). The repo
must be cloned to ~/.dotfiles — this path is defined in
nix/modules/dotfile-links.nix as repoPath.
darwin-rebuild switch --flake ~/.dotfiles#personal-mbpOnly the owner of the machine can run this. Agents should never run it.
# Check flake evaluation (syntax + type errors)
nix flake check ~/.dotfiles
# Build without switching (downloads closures, verifies linkage)
nix build ~/.dotfiles#darwinConfigurations.personal-mbp.systemThese are safe to run. Run them after any change to nix/ files.
Follow Conventional Commits.
Format: <type>(<scope>): <subject>
Types: feat, fix, refactor, docs, chore, test, ci
Scopes for this repo: flake, homebrew, macos-defaults, packages,
git, shell, tmux, dotfile-links, readme, agents
- ≤50 characters
- Imperative mood: "add feature" not "added feature" or "adds feature"
- No trailing period
- Lowercase after the type prefix
- Separate from subject with a blank line
- Wrap at 72 characters
- Explain the WHY and WHAT, not the HOW (the diff shows the how)
- Use bullet points where appropriate (hyphen followed by a single space)
- Use a hanging indent for multi-line bullets
Split changes into multiple atomic commits when they touch different concerns. One commit per logical change.
| File Count | Minimum Commits |
|---|---|
| 3+ files | 2+ commits |
| 5+ files | 3+ commits |
Combine files in the same commit only when tightly coupled (e.g., implementation + its direct test, or a type definition + the only file that uses it).
- Commits must only introduce code changes relevant to the logical change at hand
- Never amend or rebase commits already pushed to shared branches
- Never commit secrets, API keys, or credentials
Parallel subagents require worktrees. Each subagent MUST work in its own
worktree (wt switch <branch>), not the main repo. Never share working
directories.
Describe what the code does now — not discarded approaches, prior iterations, or alternatives. Only describe what's in the diff.
Use plain, factual language. A bug fix is a bug fix, not a "critical stability improvement." Avoid: critical, crucial, essential, significant, comprehensive, robust, elegant.
Do not delete, overwrite, or reorganize these files — they are the active dotfiles or are retained for historical reference:
- Active dotfiles managed as symlinks: edit in place in the repo, changes are live immediately (no rebuild needed)
nix/modules/dotfile-links.nixrepoPathvariable — do not change without also updatingREADME.mdbootstrap instructions to match
- GUI app or font? → Add to
homebrew.casksinnix/modules/homebrew.nix - CLI tool available in nixpkgs? → Add to
environment.systemPackagesinnix/modules/packages.nix - CLI tool NOT in nixpkgs, or from a custom tap? → Add to
homebrew.brewsinnix/modules/homebrew.nix(add the tap tohomebrew.tapsif needed) - App Store app? → Add to
homebrew.masAppsinnix/modules/homebrew.nixwith"App Name" = <numeric-id>syntax
To check if a package exists in nixpkgs:
nix search nixpkgs <name>After any change to nix/ files, validate before committing:
nix flake check ~/.dotfilescd ~/.dotfiles
nix flake update # update all inputs (nixpkgs, nix-darwin, home-manager)
nix flake update nixpkgs # update a single input
# validate, then commit the lock file
nix flake check
git add flake.lock
git commit -m "chore(flake): update inputs"Use a programs.* home-manager module when:
- A module exists that understands the config's structure (e.g.
programs.git,programs.zsh,programs.tmux) - The file is never written to by an external tool
- Nix option validation or composability is genuinely useful
Use a mkOutOfStoreSymlink home.file entry (dotfile-links.nix) when:
- No useful Nix module exists — inlining the content as a raw
home.file.textstring buys nothing: no validation, no composability, just rebuild friction - An external tool writes back to the file (Karabiner exports
karabiner.jsonwhen you change settings in its UI; LazyVim writes into.config/nvim/) - The file needs a fast iteration loop — Hammerspoon Lua, Neovim config,
Karabiner mappings are edited, reloaded, and tweaked in seconds; a full
darwin-rebuild switchfor each tweak is the wrong tradeoff
Never use plain .source = ./path (without mkOutOfStoreSymlink) — that
copies the file into the read-only Nix store and breaks any tool that tries
to write to it.
All home.file entries live in nix/modules/dotfile-links.nix. Use the
link helper for simple paths:
home.file.".some-file".source =
config.lib.file.mkOutOfStoreSymlink "${repoPath}/.some-file";Or use the shorthand for paths that don't need an inline comment:
home.file.".some-config" = link ".some-config";Agents must never run git push or any command that modifies a remote
(git push, git push --force, gh pr create that triggers a push, etc.).
All commits are local only. Pushing is the owner's responsibility after reviewing the final state.