These are my actual dotfiles, not a starter kit. The host setup assumes Apple Silicon macOS, a checkout at ~/Developer/dotfiles, and several of my other repositories under ~/Developer. The devcontainer runs the same bootstrap in Ubuntu so I can edit and check the portable terminal configuration without changing the host.
Note
If you came here from my vim + tmux talk, the repository at the time of that recording is still available. The current setup is substantially different.
| Area | Current choice |
|---|---|
| Machine setup | Mise bootstrap and tasks |
| Terminal | Ghostty, with WezTerm and Kitty configs still tracked |
| Shell and prompt | Homebrew zsh and Starship |
| Multiplexer | tmux |
| Editor | Neovim with lazy.nvim |
| Window management | AeroSpace, SketchyBar, Borders, and Karabiner-Elements |
| CLI agents | Pi and Claude Code |
| Agent orchestration | Fleet |
| Fonts and color | Monaspace, Symbols Nerd Font, Tokyo Night |
Run this on a Mac:
curl -fsSL https://raw.githubusercontent.com/nicknisi/dotfiles/main/install.sh | bashThe installer checks for Git, clones this repository, installs Mise, and runs one full bootstrap. On a Mac without the Xcode Command Line Tools, the first run opens Apple's installer and stops. Run the command again after the tools finish installing.
The bootstrap expects:
- Apple Silicon Homebrew paths under
/opt/homebrew - a working GitHub SSH key for the additional repositories declared in
[bootstrap.repos]
After the install, open a new login shell and configure the machine-local Git identity:
mise run setup-gitThe Git task asks for a name, email, and GitHub username, then writes ~/.gitconfig-local. That file is included by the tracked Git config but never committed.
curl -fsSL https://raw.githubusercontent.com/nicknisi/dotfiles/main/install.sh | bash -s -- --dry-runSet NO_COLOR=1 for plain output.
xcode-select --install # only when Git is missing
git clone https://github.com/nicknisi/dotfiles.git ~/Developer/dotfiles
curl -fsSL https://mise.run | sh
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
MISE_GLOBAL_CONFIG_FILE=~/Developer/dotfiles/config/mise/config.toml \
mise bootstrap --yes --skip-dirtyThe explicit MISE_GLOBAL_CONFIG_FILE is only needed before bootstrap creates ~/.config/mise.
config/mise/config.toml is the machine manifest. A full mise bootstrap does the following work:
- On macOS, runs the pre-packages hook that installs Homebrew and the tap packages mise cannot resolve.
- Installs the remaining OS-specific packages and macOS apps from
[bootstrap.packages]. - Clones missing repositories from
[bootstrap.repos]without changing existing checkouts. - Applies the
[dotfiles]symlinks. - Applies macOS defaults where available and sets the OS-specific login shell.
- Installs the runtimes and command-line tools from
[tools]. - Runs the
bootstraptask to register the repository's Git clean filter.
The installer points Mise at the cloned config, so the same manifest handles the first run and every later run.
mise tasks| Task | Purpose |
|---|---|
mise run bootstrap |
Register the pi-settings Git clean filter |
mise run install-homebrew |
Install Homebrew with the official installer if needed |
mise run install-tap-packages |
Install the macOS packages unavailable to mise |
mise run setup-git |
Write the machine-local Git identity |
mise run update-all |
Run every scoped update task in sequence |
mise run update:system |
Update Homebrew packages |
mise run update:tools |
Update mise-managed tools, uv tools, and Pi extensions |
mise run update:plugins |
Update Neovim and zsh plugins |
mise run update:dotfiles |
Fast-forward this repo when it is on main |
Software managed by Mise
- Node.js 24 and Python 3.14.7
- pnpm, Bun, Deno, Lua, and tree-sitter
- 1Password CLI, Claude Code, Pi, Wrangler, Greptile, and the WorkOS CLI
- bat, delta, eza, fd, fzf, GitHub CLI, glow, gum, jq, lazygit, ripgrep, shellcheck, Starship, StyLua, tmux, zoxide, and superfile
- Neovim
diffdad,fleet,tm, andsessionsfrom my GitHub repositories. Linux ARM skips these until their releases include ARM assets.
- newer Bash, Git, zsh, grep, and Vim builds
- btop, cloc, entr, fswatch, GnuPG, highlight, tree, wdiff, and wget through Homebrew or apt
- noti, trash, AeroSpace, Ghostty, WezTerm, Karabiner-Elements, SketchyBar, Borders, Monaspace, and Symbols Nerd Font on macOS
Bootstrap clones these over SSH:
~/Developer/pi-extensions~/Developer/ideation~/Developer/claude-plugins
| Path | What it contains | Destination |
|---|---|---|
config/ |
App configuration | ~/.config/* |
home/ |
Home-directory configuration | ~/.claude, ~/.pi, and ~/.zshenv |
bin/ |
Personal commands placed on PATH |
Used directly from this checkout |
tools/ |
Larger one-off tools and build helpers | Run from the repository |
install.sh |
Bare-machine bootstrap | Run directly or through curl |
.devcontainer/ |
Ubuntu development environment and smoke test | Local Docker container |
Mise links directories rather than copying individual files. The two declarations are intentionally broad:
[dotfiles]
"~/.config/*" = "~/Developer/dotfiles/config/*"
"~/.??*" = "~/Developer/dotfiles/home/.??*"The fixed source path is why the repository must live at ~/Developer/dotfiles unless you edit the manifest.
mise bootstrap dotfiles status
mise bootstrap dotfiles apply --yes
mise bootstrap dotfiles apply --yes ~/.config/nvim
mise bootstrap dotfiles unapply --yes
mise bootstrap dotfiles unapply --yes ~/.config/nvimUnapply a target before deleting or renaming its source. Mise refuses to overwrite a real file with a symlink unless you pass --force.
To inspect old dangling links:
find ~/.config -type l ! -exec test -e {} \; -print
find ~ -maxdepth 1 -type l ! -exec test -e {} \; -printThose commands only print candidates. Check each target before removing it.
home/.zshenv establishes the XDG paths, finds the repository through the ~/.zshenv symlink, and exports EDITOR=nvim and GIT_EDITOR=nvim. config/zsh/.zshrc handles the interactive shell.
The shell config:
- activates Mise for per-directory tool versions
- initializes completion, fzf, and zoxide
- adds the repository's
bin/,~/bin,~/.local/bin, Bun, Cargo, pnpm, GNU grep, and/usr/local/sbinpaths - sets
CODE_DIRto~/codewhen it exists, otherwise~/Developer - installs the local zsh plugins with the
zfetchfunction - loads
~/.zshenv.local,~/.localrc, and~/.zshrc.localwhen present
The configured plugins are zsh-async, zsh-syntax-highlighting, zsh-autosuggestions, zsh-npm-scripts-autocomplete, and fzf-tab. mise run update:plugins pulls their Git checkouts.
Starship renders a two-line prompt. The left side shows the full directory and a Node version when the directory contains package.json or node_modules. The right side shows Git state, the branch, and suspended jobs. The prompt symbol is cyan after success and red after failure.
Ghostty is the terminal this tmux config targets. Its config uses Tokyo Night light and dark themes, Monaspace, a translucent blurred background, CSI-u modified keys, and cmd+s as a prefix for native splits and tabs.
The bootstrap installs WezTerm too, and a Kitty config remains in the tree.
AeroSpace starts at login and launches SketchyBar. The basic movement scheme is:
| Keys | Action |
|---|---|
alt+h/j/k/l |
Focus a window |
alt+shift+h/j/k/l |
Move a window |
alt+1..9 or alt+letter |
Switch workspace |
alt+shift+1..9 or alt+shift+letter |
Move a window to a workspace |
alt+shift+; |
Enter the AeroSpace service mode |
The AeroSpace rules route terminals, browsers, chat apps, mail, and other applications to named workspaces. SketchyBar shows those workspaces with app icons, the focused window title, the current layout, Fleet state, GitHub review requests, agent spend, Claude usage, and now-playing information. Borders runs as a Homebrew service.
The prefix is control-a. I remap Caps Lock to Control, so this is less awkward than the default control-b.
| Key after the prefix | Action |
|---|---|
h, j, k, l |
Move between panes |
H, J, K, L |
Resize a pane by ten cells |
| ` | ` |
- |
Split below |
g |
Open lazygit in a popup |
s |
Open the tm session picker |
y |
Open Fleet in a popup |
n |
Jump to the next waiting agent pane |
f |
Toggle the Fleet sidebar |
T |
Toggle the status bar |
The status bar sits at the top. It shows the session on the left, then Fleet state and Git status on the right. The theme follows the macOS light or dark appearance and uses Nerd Font separators.
tm is installed as a compiled Mise tool. bin/tm is the fallback implementation and uses fzf to switch, create, refresh, and delete sessions.
Set TMUX_MINIMAL=1 in a local shell file to hide the status bar while a session has one window:
export TMUX_MINIMAL=1The tmux config also forwards truecolor, italics, undercurl, modified Enter keys, OSC 8 links, and terminal graphics through Ghostty. Those settings are there for Neovim and terminal agents, not decoration.
config/nvim/init.lua calls the local nisi module. That module bootstraps lazy.nvim, loads the plugin specs under config/nvim/lua/nisi/plugins/, and enables the Copilot, Python, and fzf extras.
The active setup uses a transparent background and chooses the Tokyo Night colorscheme after checking the macOS appearance. The first launch needs network access because it clones lazy.nvim and the plugin set.
Useful commands:
nvim # Open the editor
vimu # Run Lazy sync without opening the UIOpen :Lazy inside Neovim to inspect or update individual plugins.
The tracked Git config lives at config/git/config. It sets main as the default branch, uses delta for paging, rebases pulls, enables rerere, auto-stashes rebases, and includes the untracked ~/.gitconfig-local identity file.
The worktree tooling is available through Git's external-command convention:
git wt create my-feature
git wt create --pr 123
git wt status
git wt go
git wt prunegit wt create resolves local branches, remote branches, and GitHub pull requests before creating a new branch. New branches default to the prefix from git config github.user. git wt status shows dirty, merged, closed, and prunable worktrees.
Both agent configurations are tracked, but their runtime data is not.
home/.pi/agent/settings.json is the Pi configuration. It points at packages from ~/Developer/pi-extensions, the Claude plugin repository, Ideation, Fleet, and several npm or Git packages. Most extension source code lives outside this repository. A fresh clone will only have the extension repositories declared in Mise; some local package paths still require their own checkouts.
Because ~/.pi is a directory symlink into this repository, .gitignore excludes auth, sessions, memory databases, relay state, subagent runs, package installs, and other runtime files. A Git clean filter strips lastChangelogVersion from settings.json before Git compares or stages it.
home/.claude/settings.json and home/.claude/CLAUDE.md are the only tracked Claude files. The settings configure the status line, SessionEnd cleanup, permissions, plugin marketplaces, and enabled plugins. Sessions, caches, downloaded plugins, and credentials remain untracked.
Three small scripts connect the agent tools to the terminal environment:
claude-statuslinerenders the Claude status and pushes usage updates to SketchyBarclaude-tmux-cleanupresets pane state when a Claude session endsclaude-notifysends attention notifications through tmux
MCP servers are user-scoped in ~/.claude.json and are not part of bootstrap. bin/setup-mcp-servers is a one-shot mutating script for the GPT-5, Playwright, and Context7 servers. It requires OPENAI_API_KEY and CONTEXT7_API_KEY, and it runs immediately when invoked.
Fleet is installed by Mise and lives in its own repository. tmux uses it for agent status, the next-waiting-agent jump, the popup, the sidebar, and window titles. SketchyBar consumes the same state through sketchybar-fleet-watch.
bin/ is on PATH. The table calls out the larger standalone commands; many of the remaining files support tmux, SketchyBar, Git aliases, or agent status.
| Command | What it does |
|---|---|
battery |
Print the current macOS battery percentage |
brew-why |
Show installed Homebrew formulae and their installed dependents |
digest |
Build a bounded text digest of a Git repository for model input |
npm-trust-setup |
Bootstrap npm packages and GitHub Actions trusted publishing |
wifi-password |
Read a Wi-Fi password from the macOS keychain |
thisisfine |
Print the "This is fine" scene in terminal color |
Read a script before running it. Some are one-off commands and do not implement --help or a dry run.
Run every update in sequence with:
mise run update-allRun one area with mise run update:system, mise run update:tools, mise run update:plugins, or mise run update:dotfiles. The wrapper stops when a task fails.
The devcontainer is a disposable Ubuntu 24.04 environment that runs the real bootstrap. Docker bind-mounts this checkout at /home/vscode/Developer/dotfiles, so source edits persist on the Mac. Installed tools, cloned companion repositories, and editor plugins live in the container and disappear when it is replaced.
| File or service | Purpose |
|---|---|
.devcontainer/devcontainer.json |
Tells a Dev Container launcher which image, mount, environment, and lifecycle commands to use |
mcr.microsoft.com/devcontainers/base:ubuntu24.04 |
Supplies the Ubuntu image, Git, zsh, and the non-root vscode user |
config/mise/config.devcontainer.toml |
Overrides the macOS login-shell path with /usr/bin/zsh |
.devcontainer/github_known_hosts |
Pins GitHub's published SSH host key |
.devcontainer/smoke-test.sh |
Checks packages, tools, repositories, links, SSH, zsh, Git, Neovim, and tmux |
There is no custom Dockerfile. The official base image already has the prerequisites that install.sh needs, and Mise owns the rest.
Install the reference CLI once and start OrbStack or Docker Desktop:
brew install devcontainer
devcontainer up --workspace-folder .The first up creates the container, forwards the macOS SSH agent without copying key files, runs install.sh, and runs the smoke test. Unlock 1Password if it asks to approve an SSH operation. Enter the resulting zsh login shell with:
devcontainer exec --workspace-folder . zsh -lInside that shell, use nvim, tmux, Git, and the other installed tools normally. Changes under ~/Developer/dotfiles are changes to the host checkout.
Run the smoke test again:
devcontainer exec --workspace-folder . .devcontainer/smoke-test.shRerun bootstrap after changing the Mise manifest or installer:
devcontainer exec --workspace-folder . zsh -lc 'NO_COLOR=1 bash install.sh'Replace the container and prove setup works from an empty home directory:
devcontainer up --workspace-folder . --remove-existing-containerDocker is the verified container engine. OrbStack and Docker Desktop both expose the SSH agent at the mounted /run/host-services/ssh-auth.sock path. DevPod can read the same devcontainer.json with its Docker provider, but adds no value for this local workflow. Podman is not verified because it does not provide this Docker-specific SSH bridge.
Machine-only shell changes belong in one of these ignored files:
~/.zshenv.local~/.localrc~/.zshrc.local
Before using this repository as your own, search for nicknisi, /Users/nicknisi, and ~/Developer. The Mise manifest, Claude marketplaces, agent package paths, Git aliases, and application rules all contain personal assumptions.
For hardware and software that do not belong in dotfiles, see nicknisi.com/uses.
The repository is MIT licensed. For questions, use GitHub Discussions.