Skip to content

Repository files navigation

Dotfiles

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

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

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

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

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.

Manage dotfile links

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:

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.

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

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.

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

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.

About

vim, zsh, git, homebrew, neovim - my whole world

Topics

Resources

Stars

3.0k stars

Watchers

79 watching

Forks

Used by

Contributors

Languages