My Dotfiles for macOS
July 7, 2026 ยท View on GitHub

๐คฉ Highlights
- Neovim editor configured with LazyVim๐ค
- Starship prompt
- Zsh shell with zsh-abbr for abbreviations
- Flexible, terminal-based dev environment with ghostty ๐ป + Tmux!
- Claude Code with custom skills, permission presets, and safety hooks
- Fast, idempotent setup with GNU Stow
- New Mac bootstrap based on thoughtbot's Laptop
- Support for both Apple Silicon and Intel Macs
Note
This project previously supported Fish shell alongside Zsh. Fish support was removed in PR #135 (f158de9). If you were using the Fish configuration, you can reference that PR to see what changed or recover code for your own setup.
โก๏ธ Quick Setup
Make sure macOS is up to date and you have installed the required software.
Clone this repo.
git clone https://github.com/joshukraine/dotfiles.git ~/dotfiles
Read the setup script and check available options.
less ~/dotfiles/setup.sh
~/dotfiles/setup.sh --help
Preview what the setup script will do (dry-run mode).
~/dotfiles/setup.sh --dry-run
Run the setup script.
~/dotfiles/setup.sh
โ Prerequisites
The dotfiles assume you are running macOS with (at minimum) the following software pre-installed:
All of the above and more are installed with my fork of Laptop.
๐ New Mac Bootstrap
This is what I would do if I bought a new Mac computer today. The steps below assume you have already completed the basics:
- Log in to iCloud
- Check for software updates
- Install Xcode Command Line Tools
๐ป 1. Run my fork of thoughtbot's Laptop
โน github.com/joshukraine/laptop
Download the mac script:
curl --remote-name https://raw.githubusercontent.com/joshukraine/laptop/main/mac
Download .local.laptop for additional customizations:
curl --remote-name https://raw.githubusercontent.com/joshukraine/dotfiles/master/laptop/.laptop.local
Review both scripts before proceeding:
less mac
less .laptop.local
Execute the mac script:
sh mac 2>&1 | tee ~/laptop.log
I've made the following changes to my fork of Laptop:
- Install asdf via git instead of Homebrew
- Comment out Heroku-related code
- Comment out unused Homebrew taps and formulae
It is worth noting that the Laptop script (mac) is idempotent and can be safely run multiple times to ensure a consistent baseline configuration.
โ ๏ธ 2. Check for Stow conflicts
The dotfiles setup.sh script uses GNU Stow to symlink all the config files to your $HOME directory. If you already have an identically-named file/directory in $HOME (e.g. ~/.zshrc leftover from installing Laptop), this will cause a conflict, and Stow will (rightly) abort with an error.
The setup script will try to detect and backup these files ahead of Stow, but it's still a good idea to check your $HOME directory as well as $HOME/.config and $HOME/.local/bin.
On a fresh machine, setup.sh also pre-creates ~/.claude as a real directory so Stow links the Claude config files individually instead of folding the whole directory into one symlink (which would route Claude Code's runtime state into the repo). If you forked before this behavior existed and see Claude runtime files appearing in git status, see Troubleshooting: ~/.claude folding.
๐ 3. Clone and setup the dotfiles
Clone
git clone https://github.com/joshukraine/dotfiles.git ~/dotfiles
Read and preview
less ~/dotfiles/setup.sh
~/dotfiles/setup.sh --help
~/dotfiles/setup.sh --dry-run # Preview changes without applying them
Setup
~/dotfiles/setup.sh
If you do encounter Stow conflicts, resolve these and run setup again. The script is idempotent, so you can run it multiple times safely.
โก๏ธ 4. Install Zap
Zap describes itself as a "minimal zsh plugin manager that does what you expect."
โน zapzsh.com
Important
After copying/pasting the install command for Zap, be sure to add the --keep flag to prevent Zap from replacing you existing .zshrc file.
๐บ 5. Install remaining Homebrew packages
Review the included Brewfile and make desired adjustments.
less ~/Brewfile
Install the bundle.
brew bundle install
๐ ๏ธ 6. Complete post-install tasks
- Launch LazyVim (
nvim) and run:checkhealth. Resolve errors and warnings. Plugins should install automatically on first launch. - Add personal data as needed to
*.localfiles such as~/.gitconfig.local,~/.laptop.local. - (Optional) Set up 1Password CLI for managing secrets.
- (Optional) Set up 1Password SSH key management.
- Install Tmux plugins with
<prefix> + I(https://github.com/tmux-plugins/tpm)
Zsh Setup
Zsh is now the default shell on macOS. However, it's helpful to add an entry enabling the Homebrew version of Zsh (/opt/homebrew/bin/zsh on Apple Silicon, /usr/local/bin/zsh on Intel) instead of the default (/bin/zsh) version.
Ensure that you have Zsh from Homebrew. (which zsh) If not:
brew install zsh
Add Zsh (Homebrew version) to /etc/shells:
# Apple Silicon Macs:
echo /opt/homebrew/bin/zsh | sudo tee -a /etc/shells
# Intel Macs:
echo /usr/local/bin/zsh | sudo tee -a /etc/shells
# Or use this universal command:
echo $(which zsh) | sudo tee -a /etc/shells
Set it as your default shell:
chsh -s $(which zsh)
Install Zap.
Restart your terminal.
Abbreviations
One of the best ideas I picked up from using Fish shell is abbreviations over aliases. zsh-abbr brings this functionality to Zsh.
Abbreviations are managed directly in zsh/.config/zsh-abbr/abbreviations.zsh. You can edit this file directly, or use the abbr add/abbr remove commands in your shell.
Smart Git Functions
The configuration includes intelligent git functions that automatically detect your main branch:
gpum- Push current branch to origin with upstream trackinggrbm- Rebase on main/mastergcom- Checkout main/mastergbrm- Remove branches merged into main/master
These functions work with both main and master branch names automatically.
Claude Code
Claude Code is Anthropic's CLI tool for AI-assisted development. This repo includes a full configuration under the claude/ directory, stowed to ~/.claude/.
Tip
Claude Code pairs well with Neovim via the sidekick.nvim plugin, which is how I use it most of the time during development. My preferred layout runs Claude as a sibling tmux pane (60% editor / 40% Claude) rather than nested inside the editor โ see Claude Code + Neovim for the setup, the reasoning, and how sidekick sends editor context across panes.
Skills
Custom skills provide structured workflows for the full development lifecycle:
| Skill | Purpose |
|---|---|
/autopilot | Carry one well-scoped issue through the full dev loop autonomously, to a review-ready PR (or merge for small reversible changes) |
/autopilot-batch | Fan out a queue of issues to parallel worktree subagents, each running /autopilot, with an Opus gating review per PR |
/autopilot-triage | Vet open issues for autonomous resolution and queue the qualifying ones for /autopilot-batch |
/bootstrap-prd | Scaffold PRD-driven development infrastructure into a new project |
/checkpoint | Quick status update โ what's done, in progress, and blocked |
/create-pr | Create a PR with auto-linked issues and formatted description |
/debrief | Comprehensive walkthrough of recent work with architecture rationale |
/drift-check | Pre-PR advisory check for deviations from the project spec (read-only) |
/dustoff | Read-only re-entry assessment for a dormant project, with a prioritized plan |
/md2pdf | Convert a Markdown file to PDF with GitHub-style formatting |
/merge-pr | Merge a PR with status checks, squash merge, and branch cleanup |
/plan-phase | Draft implementation plan and create GitHub issues (no code written) |
/prd-view | Render a PRD Markdown file as a rich HTML reading view in the browser |
/qa-handoff | Prepare a hands-on QA testing guide (Rails apps or static/Hugo sites) for a completed PRD phase |
/qa-triage | Triage a QA-labeled report, classify it, and draft the technical issue(s) |
/qa-triage-batch | Fan out /qa-triage across the open QA reports, cluster shared root causes across them, and create the tech issues behind one gate |
/readme-refresh | Audit and update a project README, or bootstrap a new one |
/resolve-issue | Structured workflow for resolving a GitHub issue end-to-end |
/sidecar | Research-only mode for a secondary session sharing a working dir โ no tracked-file edits or git mutations |
/todoist-cli | Manage Todoist tasks, projects, and labels via the td CLI |
/update-deps | Dependabot-aware dependency updates with security audit, CI validation, and a unified PR |
/walkthrough | Generate a browser walkthrough of a PR's user-facing changes |
Permission Presets
Composable permission presets control what Claude Code is allowed to do in each project. A base preset covers universal operations (git, file editing, Unix tools), and framework overlays add project-specific tooling.
cc-rails # base + Rails overlay
cc-hugo # base + Hugo overlay
cc-js # base + JavaScript/Node overlay
cc-dotfiles # base + dotfiles overlay
cc-sprint # all-inclusive sprint preset
cc-default # base only
cc-clean # remove project settings
cc-perms # show active permission counts
Presets live in claude/.claude/presets/ with a README explaining the design.
Safety
- Hooks block
rm -rfpatterns and log all Bash commands - Deny rules prevent force push, hard reset,
sudo, and credential reads - Sandbox mode enabled by default with filesystem and network restrictions
Shell Abbreviations
Zsh abbreviations for quick access (via zsh-abbr):
| Abbreviation | Expands to |
|---|---|
cl | claude |
clr | claude --resume |
clc | claude --continue |
clup | claude update |
See zsh/.config/zsh-abbr/abbreviations.zsh for the full set (clsp, clh, clv, clp, clcp, clmcp).
Other Configuration
CLAUDE.mdโ Global development philosophy and coding standards applied across all projectscheatsheet.mdโ Quick reference for keyboard shortcuts (including fullscreen scrolling and custom keybindings), commands, and context management tipsstarship.tomlโ Custom Starship prompt showing model, context window status, and token cost
Troubleshooting: ~/.claude folding
setup.sh creates ~/.claude as a real directory before stowing, so a fresh install links the Claude config files individually. If you cloned before that fix landed, you may have a folded ~/.claude โ a single symlink pointing back into the repo โ which makes Claude Code write its runtime state (sessions/, projects/, history.jsonl, โฆ) straight into your dotfiles. The tells: a flood of untracked claude/.claude/โฆ entries in git status, and ls -ld ~/.claude showing a symlink (lrwxโฆ) rather than a directory (drwxโฆ).
To repair an already-folded ~/.claude, quit all claude sessions and run:
cd ~/dotfiles
stow -D claude/ # detach the folded ~/.claude symlink
mkdir -p ~/.claude # recreate it as a real directory
git restore --staged claude/.claude/ # unstage anything Claude's writes added
git restore claude/.claude/settings.json # discard Claude's auto-edit, if present
git clean -fd claude/.claude/ # delete the leaked runtime state
stow claude/ # re-link only the managed config
Then confirm the result:
ls -ld ~/.claude # expect a real directory (drwxโฆ), not a symlink (lrwxโฆ)
ls -la ~/.claude | grep ' -> ' # expect exactly: CLAUDE.md, settings.json, starship.toml, skills, docs, presets
If a stray symlink such as sessions still points into the repo, remove just that link (rm ~/.claude/sessions) and re-run the git clean above so a later stow -R can't fold it back.
About Neovim Distributions
Tip
TL;DR: Just install LazyVim๐ค
Back in the day if you wanted to use Vim (and later Neovim) you had to code a ton of configuration on your own. With Vim we got Vimscript ๐คข, but then came Neovim which brought us Lua ๐คฉ. I went from ye olde crunchy .vimrc to the more adventurous init.vim to the blessed path of init.lua. ๐
Meanwhile, there were the VS Code boys across the fence, bragging about their fancy icons, shiny tabs, and the oh-so-cool LSP. I confess, I even tried VS Code for a bit. That didn't last long. ๐ฌ
But Neovim has caught up. And wow have they. caught. up. Not only do we have native LSP support in Neovim (have had for a while now โ v0.5), but we are solidly in the era of pre-baked Neovim distributions that are really challenging the notion of Vim/Neovim as austere, command-line editors. (I will say that I think we owe a lot to VS Code for raising the bar here. But I'm still glad I'm with Neovim. ๐)
If you want a quick primer on Neovim distros, check out the YouTube video below. I started with LunarVim (my first entry into distro-land) and now I'm with LazyVim and the Folke gang. Bottom line: you can still config Neovim from scratch if you want to, but you can get a HUGE head-start by just grabbing a distro and tweaking it to your needs.
๐บ I tried Neovim Distributions so you don't have to
Boy, when I reminisce about the days of writing PHP for Internet Explorer in BBEdit...
My Favorite Programming Fonts
Over the years, I've branched out to explore a variety of mono-spaced fonts, both free and premium. Here is a list of my favorites.
Free Fonts
Included in my Brewfile and installed by default via Homebrew Cask Fonts
- Cascadia Code
- Fira Code
- Hack
- JetBrains Mono
- Monaspace Argon
- Monaspace Neon
- Symbols Nerd Font Mono (for icons only)
Premium Fonts
You have to give people money if you want these. ๐ค
Ligatures
I first discovered ligatures through Fira Code, which IMHO is probably the king of programming fonts. After using Fira Code, it's hard to go back to a sans-ligature typeface. Therefore all the fonts I've included in my fave's list do include ligatures, although some have more than others.
Note
Operator Mono does not include ligatures but can be easily patched to add them.
Nerd Fonts and Icons
Back in the day, I started using the VimDevicons plugin so I could have fancy file-type icons in Vim. (Remember NERDTree?) In order for this to work, one had to install patched "Nerd-font" versions of whatever programming font one wanted to use. For example:
# Original font
$ brew install --cask font-fira-code
# Patched variant
$ brew install --cask font-fira-code-nerd-font
Patching fonts with icons still works fine of course, and is, I think, pretty widely used. However, during my exploration of kitty, I discovered that there is a different (better?) approach to icon fonts. It turns out, you don't need a patched version of your chosen mono-spaced font. You can get most if not all the icons you need and use them alongside any font by just installing the Symbols Nerd Font Mono font.
Leveraging this approach depends on your terminal. In iTerm2, for example, you need to check "Use a different font for non-ASCII text" in the Preferences panel. Then select Symbols Nerd Font Mono font under "Non-ASCII font". (see screenshot below)

kitty does things a little differently. If you install a patched font, it will mostly work. Mostly. But the "kitty way" can be broken down in three steps:
- Install a normal, un-patched mono-spaced font, such as
Cascadia Code - Install a dedicated icon font, such as
Symbols Nerd Font Mono - Create a set of Unicode symbol maps1 to tell kitty which font to use for which icons (symbols)
More work up front, maybe, but less guesswork in the long-term once you understand what's going on. And if you're using my dotfiles, you have it easy. All the fonts you need are installed in Brewfile, and I have a set of Unicode symbol maps ready to go. ๐
Note
To learn more about Nerd fonts in terminals, as well as Unicode symbol maps and all the rest, be sure to check out Effective Nerd Fonts in Multiple Terminals by Elijah Manor
๐งช Nerd Font Smoke Test
If you want to check whether icons and ligatures are working properly, try running the included nerd-font-smoke-test.sh script from the root of the dotfiles folder like so:
bash nerd-font-smoke-test.sh
If your terminal is configured correctly, the output of the test should look like this:

Again, thank you, Elijah Manor!
Useful Font Links
A Note about Vim performance and Ruby files
Once upon a time, I almost left Vim due to some crippling performance issues. These issues were particularly painful when editing Ruby files. I documented what I learned here:
โน What I've learned about slow performance in Vim
Identifying Sources of Slow Startup Times (Zsh)
The .zshrc script can be profiled by touching the file ~/.zshrc.profiler and starting a new login shell. To see the top 20 lines that are taking the most time use the zshrc_profiler_view. zshrc_profiler parameters are number of lines to show (20) and path to profiler log file ($TMPDIR/zshrc_profiler.${PID}log).
Awesome Neovim Dotfiles, Distros, and Starters
- https://github.com/LazyVim/lazyvim
- https://github.com/LunarVim/LunarVim
- https://github.com/NvChad/NvChad
- https://github.com/LunarVim/Launch.nvim
- https://github.com/folke/dot
- https://github.com/nvim-lua/kickstart.nvim
- https://github.com/ThePrimeagen/init.lua
- https://github.com/elijahmanor/dotfiles
- https://github.com/cpow/cpow-dotfiles
- https://github.com/josean-dev/dev-environment-files
- https://github.com/glepnir/nvim
- https://github.com/numToStr/dotfiles
- https://github.com/jdhao/nvim-config
- https://github.com/brainfucksec/neovim-lua
- https://github.com/disrupted/dotfiles
- https://github.com/topics/neovim-dotfiles
- https://github.com/topics/neovim-config
Some of my favorite dotfile repos
- Pro Vim (https://github.com/Integralist/ProVim)
- Trevor Brown (https://github.com/Stratus3D/dotfiles)
- Chris Toomey (https://github.com/christoomey/dotfiles)
- thoughtbot (https://github.com/thoughtbot/dotfiles)
- Lars Kappert (https://github.com/webpro/dotfiles)
- Ryan Bates (https://github.com/ryanb/dotfiles)
- Ben Orenstein (https://github.com/r00k/dotfiles)
- Joshua Clayton (https://github.com/joshuaclayton/dotfiles)
- Drew Neil (https://github.com/nelstrom/dotfiles)
- Kevin Suttle (https://github.com/kevinSuttle/OSXDefaults)
- Carlos Becker (https://github.com/caarlos0/dotfiles)
- Zach Holman (https://github.com/holman/dotfiles/)
- Mathias Bynens (https://github.com/mathiasbynens/dotfiles/)
- Paul Irish (https://github.com/paulirish/dotfiles)
Helpful web resources on dotfiles, et al
- http://dotfiles.github.io/
- https://medium.com/@webprolific/getting-started-with-dotfiles-43c3602fd789
- http://code.tutsplus.com/tutorials/setting-up-a-mac-dev-machine-from-zero-to-hero-with-dotfiles--net-35449
- https://github.com/webpro/awesome-dotfiles
- http://blog.smalleycreative.com/tutorials/using-git-and-github-to-manage-your-dotfiles/
- http://carlosbecker.com/posts/first-steps-with-mac-os-x-as-a-developer/
- https://mattstauffer.co/blog/setting-up-a-new-os-x-development-machine-part-1-core-files-and-custom-shell
Development
This repo ships a small test and lint suite. Run both from the dotfiles root.
Lint shell scripts with shellcheck:
./scripts/lint-shell
Run the bats test suite:
./scripts/run-tests # all tests
./scripts/run-tests git # git function tests
./scripts/run-tests abbr # abbreviation tests
./scripts/run-tests presets # permission-preset merge tests
Both checks also run in CI on every push and pull request via GitHub Actions (.github/workflows/test.yml), which additionally lints Markdown with markdownlint-cli2.
Customization
Local customizations should be placed in *.local files:
~/.gitconfig.local- Personal git configuration~/.laptop.local- Additional laptop setup customizations~/.config/ghostty/config.local- Personal ghostty overrides (keybinds, fonts, theme)~/.config/tmux/tmux.conf.local- Personal tmux overrides (extra plugins, key bindings, options)~/.config/tmux/project-accent.local.sh- Per-project status bar accent colors (see below)
Per-project tmux accent
When several projects are open at once โ one tmux session each, created with tat โ the status bar can be color-coded per project so they're easy to tell apart. Both ends of the bar are accented: the session-name "pill" on the left and the hostname pill on the right, giving a matching color band on each side that's easy to catch at a glance. Sessions without a mapping keep the theme's default blue, so this is a no-op until you opt in.
To enable it, copy the example map and edit it:
cd ~/.config/tmux
cp project-accent.local.sh.example project-accent.local.sh
$EDITOR project-accent.local.sh
Map each session to a color in the accent_for function, keyed by the tmux session name (the #S shown in the pill):
accent_for() {
case "\$1" in
my-work-app) printf '%s' "#ff966c" ;; # orange
my-side-proj) printf '%s' "#c3e88d" ;; # green
*) printf '%s' "" ;;
esac
}
Colors can be hex (#rrggbb), a named color (red), or colour0โcolour255. The real project-accent.local.sh is gitignored, so your project names and color choices never leave your machine.
Note: the lookup key is the tmux session name, not the directory path. With
tatthe session name is the current directory's basename with dots converted to dashes (afoo.bardirectory becomes sessionfoo-bar), so map it asfoo-bar. Keying on the session name lets two checkouts that share a directory name (e.g.bfo1andbfo2) still get distinct colors.
Colors apply automatically to new sessions (via a session-created hook) and backfill existing sessions whenever tmux is reloaded. The color is driven entirely by the @accent user option, which the theme renders into both pills; if you switch away from TokyoNight Moon, parameterize the new theme's pills with #{@accent} โ see the comments in ~/.config/tmux/themes/tokyonight_moon.tmux.
License
Copyright ยฉ 2014โ2026 Joshua Steele. MIT License