Dotfiles

September 1, 2026 ยท View on GitHub

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.

capture_20260822_095935

What this sets up

AreaCurrent choice
Machine setupMise bootstrap and tasks
TerminalGhostty, with WezTerm and Kitty configs still tracked
Shell and promptHomebrew zsh and Starship
Multiplexertmux
EditorNeovim with lazy.nvim
Window managementAeroSpace, SketchyBar, Borders, and Karabiner-Elements
CLI agentsPi and Claude Code
Agent orchestrationFleet
Fonts and colorMonaspace, Symbols Nerd Font, Tokyo Night

Install

Run this on a Mac:

curl -fsSL https://raw.githubusercontent.com/nicknisi/dotfiles/main/install.sh | bash

The 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-git

The 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.

Preview the install

curl -fsSL https://raw.githubusercontent.com/nicknisi/dotfiles/main/install.sh | bash -s -- --dry-run

Set NO_COLOR=1 for plain output.

Run it by hand

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-dirty

The explicit MISE_GLOBAL_CONFIG_FILE is only needed before bootstrap creates ~/.config/mise.

How bootstrap works

config/mise/config.toml is the machine manifest. A full mise bootstrap does the following work:

  1. On macOS, runs the pre-packages hook that installs Homebrew and the tap packages mise cannot resolve.
  2. Installs the remaining OS-specific packages and macOS apps from [bootstrap.packages].
  3. Clones missing repositories from [bootstrap.repos] without changing existing checkouts.
  4. Applies the [dotfiles] symlinks.
  5. Applies macOS defaults where available and sets the OS-specific login shell.
  6. Installs the runtimes and command-line tools from [tools].
  7. Runs the bootstrap task 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

mise tasks
TaskPurpose
mise run bootstrapRegister the pi-settings Git clean filter
mise run install-homebrewInstall Homebrew with the official installer if needed
mise run install-tap-packagesInstall the macOS packages unavailable to mise
mise run setup-gitWrite the machine-local Git identity
mise run update-allRun every scoped update task in sequence
mise run update:systemUpdate Homebrew packages
mise run update:toolsUpdate mise-managed tools, uv tools, and Pi extensions
mise run update:pluginsUpdate Neovim and zsh plugins
mise run update:dotfilesFast-forward this repo when it is on main
Software managed by Mise

Runtimes

  • Node.js 24 and Python 3.14.7
  • pnpm, Bun, Deno, Lua, and tree-sitter

Command-line tools

  • 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, and sessions from my GitHub repositories. Linux ARM skips these until their releases include ARM assets.

System packages and macOS apps

  • 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

Additional repositories

Bootstrap clones these over SSH:

  • ~/Developer/pi-extensions
  • ~/Developer/ideation
  • ~/Developer/claude-plugins

Repository layout

PathWhat it containsDestination
config/App configuration~/.config/*
home/Home-directory configuration~/.claude, ~/.pi, and ~/.zshenv
bin/Personal commands placed on PATHUsed directly from this checkout
tools/Larger one-off tools and build helpersRun from the repository
install.shBare-machine bootstrapRun directly or through curl
.devcontainer/Ubuntu development environment and smoke testLocal 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/nvim

Unapply 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 {} \; -print

Those commands only print candidates. Check each target before removing it.

Shell and prompt

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/sbin paths
  • sets CODE_DIR to ~/code when it exists, otherwise ~/Developer
  • installs the local zsh plugins with the zfetch function
  • loads ~/.zshenv.local, ~/.localrc, and ~/.zshrc.local when 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.

Terminal and macOS desktop

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:

KeysAction
alt+h/j/k/lFocus a window
alt+shift+h/j/k/lMove a window
alt+1..9 or alt+letterSwitch workspace
alt+shift+1..9 or alt+shift+letterMove 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.

tmux

The prefix is control-a. I remap Caps Lock to Control, so this is less awkward than the default control-b.

Key after the prefixAction
h, j, k, lMove between panes
H, J, K, LResize a pane by ten cells
``
-Split below
gOpen lazygit in a popup
sOpen the tm session picker
yOpen Fleet in a popup
nJump to the next waiting agent pane
fToggle the Fleet sidebar
TToggle 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=1

The 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.

Neovim

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 UI

Open :Lazy inside Neovim to inspect or update individual plugins.

Git and worktrees

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 prune

git 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.

Pi, Claude Code, and Fleet

Both agent configurations are tracked, but their runtime data is not.

Pi

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.

Claude Code

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-statusline renders the Claude status and pushes usage updates to SketchyBar
  • claude-tmux-cleanup resets pane state when a Claude session ends
  • claude-notify sends 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

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.

Commands in bin/

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.

CommandWhat it does
batteryPrint the current macOS battery percentage
brew-whyShow installed Homebrew formulae and their installed dependents
digestBuild a bounded text digest of a Git repository for model input
npm-trust-setupBootstrap npm packages and GitHub Actions trusted publishing
wifi-passwordRead a Wi-Fi password from the macOS keychain
thisisfinePrint 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.

Updating

Run every update in sequence with:

mise run update-all

Run 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.

Linux devcontainer

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.

What the files do

File or servicePurpose
.devcontainer/devcontainer.jsonTells a Dev Container launcher which image, mount, environment, and lifecycle commands to use
mcr.microsoft.com/devcontainers/base:ubuntu24.04Supplies the Ubuntu image, Git, zsh, and the non-root vscode user
config/mise/config.devcontainer.tomlOverrides the macOS login-shell path with /usr/bin/zsh
.devcontainer/github_known_hostsPins GitHub's published SSH host key
.devcontainer/smoke-test.shChecks 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.

Start and enter it

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 -l

Inside that shell, use nvim, tmux, Git, and the other installed tools normally. Changes under ~/Developer/dotfiles are changes to the host checkout.

Check, rerun, and rebuild

Run the smoke test again:

devcontainer exec --workspace-folder . .devcontainer/smoke-test.sh

Rerun 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-container

Docker 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.

Local changes and forks

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.

License and questions

The repository is MIT licensed. For questions, use GitHub Discussions.