joshukraine/dotfiles: A macOS Neovim, Zsh and Tmux Setup Driven by GNU Stow
:round_pushpin: My dotfiles for macOS using Neovim, Zsh, and Ghostty + Tmux
At a glance
- What is it?
- This repository is one engineer's personal macOS environment, packaged so it can be reproduced on a new machine. Its most interesting parts are not the editor config but the setup script, the Stow conflict handling, and a drift reporter called bubo.
- Who is it for?
- Adopt this if you run macOS and want a Neovim, Zsh and Tmux environment that someone else already debugged, and you are willing to read setup.sh before running it. Do not adopt it if you are on Linux or Windows, or if you want a config you can install without understanding.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
Who this repository is actually for
This is a personal dotfiles repository, not a framework. The README describes one engineer's macOS environment built around Neovim with LazyVim, Zsh with Starship and zsh-abbr, Ghostty plus Tmux, and Claude Code with custom skills, permission presets and safety hooks. It is published so the author can rebuild a machine quickly, and so others can read it. The topics list includes ai-agents, claude-code, asdf, rails and ruby, which tells you the intended user is a macOS developer working in Ruby or Rails who spends most of the day in a terminal.
The problem it solves is not configuration in the abstract. It is the gap between a fresh Mac and a working environment: Homebrew formulae, asdf tool versions, shell plugins, editor plugins, Tmux plugins, and a set of dotfiles that need to land in $HOME without clobbering anything already there. The README treats that gap explicitly, with a numbered bootstrap sequence and a dry-run mode. If you are looking for a config you can install in one command and never think about again, this is not aimed at you. If you want to see how one person sequences that work, it is.
How the setup works: Stow, symlinks and the folding problem
The mechanism is GNU Stow. The setup script symlinks config files from the repository into $HOME. Because Stow refuses to overwrite an existing file of the same name, a leftover ~/.zshrc from a previous tool causes Stow to abort. The README is direct about this: Stow will, in its words, rightly abort with an error. The script tries to detect and back up those files before Stow runs, but the README still tells you to inspect $HOME, $HOME/.config and $HOME/.local/bin yourself.
The more interesting detail is directory folding. On a fresh machine, setup.sh pre-creates ~/.claude as a real directory so that Stow links the Claude config files individually rather than folding the whole directory into a single symlink. Without that step, Claude Code's runtime state would be written inside the repository and show up in git status. The README links a Troubleshooting section for people who forked before this behavior existed. That is a real design constraint, not a cosmetic one: Stow's default directory handling and an application that writes state into its config directory are in conflict, and the fix is to force per-file linking.
The setup script is idempotent, and the README repeats this twice, which matters because the intended workflow is run, hit a conflict, resolve it, run again. There are no releases in the repository metadata, so updates arrive as commits on master rather than tagged versions.
Getting it running: the actual commands
The README gives a short sequence. Clone the repository to ~/dotfiles, read the script, preview it, then run it:
git clone https://github.com/joshukraine/dotfiles.git ~/dotfiles less ~/dotfiles/setup.sh ~/dotfiles/setup.sh --help ~/dotfiles/setup.sh --dry-run ~/dotfiles/setup.sh
The prerequisites are listed as Git, Homebrew including coreutils, asdf, Ruby, Node.js, Zsh, Neovim and Starship, all of which the author installs through a fork of thoughtbot's Laptop. That fork is fetched as a script named mac, along with a .laptop.local file from this repository, reviewed with less, and then executed with sh mac 2>&1 | tee ~/laptop.log. The README notes the author's changes to the fork: asdf installed via git rather than Homebrew, Heroku-related code commented out, and unused taps and formulae commented out.
Two more steps sit after the dotfiles. Zap is installed as the Zsh plugin manager, and the README carries an explicit warning to add the --keep flag so Zap does not replace an existing .zshrc. Then brew bundle install runs against the included Brewfile. Post-install tasks include launching nvim and running :checkhealth, adding personal data to *.local files such as ~/.gitconfig.local and ~/.laptop.local, optionally configuring 1Password CLI and SSH key management, and installing Tmux plugins with <prefix> + I.
Zsh, abbreviations and the smart git functions
The shell layer has two parts worth separating. The first is shell selection. macOS ships Zsh, but the README prefers the Homebrew build at /opt/homebrew/bin/zsh on Apple Silicon or /usr/local/bin/zsh on Intel. The commands are given for both, plus a universal form: echo $(which zsh) | sudo tee -a /etc/shells, followed by chsh -s $(which zsh).
The second is abbreviations, implemented with zsh-abbr. The README explains the reasoning by pointing at a blog post about when an alias should actually be an abbreviation, and notes the idea came from Fish shell. Abbreviations live in zsh/.config/zsh-abbr/abbreviations.zsh and can be edited directly or managed with abbr add and abbr remove. The distinction matters in practice: an abbreviation expands in place, so the command you see in your shell history is the full command, while an alias stays abbreviated. That is a deliberate choice about what your history is worth later.
The configuration also ships four git functions that detect whether the main branch is called main or master: gpum pushes the current branch to origin with upstream tracking, grbm rebases on main or master, gcom checks out main or master, and gbrm removes branches merged into main or master. These are small, but they encode a real annoyance, since repositories in the same working week can use either name. Note that Fish support was removed in PR #135, commit f158de9. If you were relying on it, the README says the PR is the reference for recovering that code.
bubo and the drift problem
The most distinctive piece of tooling here is bubo, described as a staleness reporter. It reports what in the environment has drifted across every surface setup.sh installs: Homebrew, Zap and its plugins, tmux and tpm plugins, asdf plugins and tool versions, Neovim and lazy, and Mason. It prints the command, according to the README, though the supplied text is truncated at that point, so the exact output format and the full command set are not something I can confirm.
The value of this is easy to miss. A dotfiles repository is a snapshot of intent, and the machine it runs on drifts continuously: Homebrew formulae update, asdf tool versions move, LazyVim plugins pull new commits. Most setups have no way to answer the question of what no longer matches. bubo appears to be an attempt to answer it across all the package managers this repository touches, which is a broader scope than most dotfiles tooling attempts.
What I cannot tell from the material is how bubo decides something has drifted, whether it compares against a lockfile, a recorded baseline, or the upstream latest, and whether it can fix anything or only report. Treat it as a reporting tool until you read the script.
Where this setup will fight you
The first limitation is platform. This is macOS only, and the README says so at the top. The bootstrap depends on Homebrew paths that differ between Apple Silicon and Intel, and on a fork of thoughtbot's Laptop. On Linux, the Stow layer would still work, but the Laptop script, the Brewfile and the shell path instructions would not.
The second is the Stow conflict model. Because Stow aborts on collisions, the first run on a machine that already has dotfiles will fail partway through. The script backs up what it can detect, but the README's own advice is to check $HOME, $HOME/.config and $HOME/.local/bin yourself. If you have a heavily customized existing environment, the migration is a manual reconciliation, not an install.
The third is the ~/.claude folding issue. If you forked this repository before the pre-creation behavior existed, Claude Code's runtime state can end up inside the repository and appear in git status. The README points to a Troubleshooting section rather than fixing it for you, which is reasonable for a personal repository but means you carry the fix.
Finally, this is one person's configuration. The README does not claim otherwise. There is no release history in the repository metadata, so there is no version to pin and no changelog to read. If the author changes something you depended on, as happened with the Fish removal in PR #135, you find out from the commit.
How it compares to chezmoi or a bare Git repository
The common alternatives take different positions on the same problem. A bare git repository with a $HOME work tree avoids Stow entirely: you check out directly into $HOME and git handles collisions by refusing to overwrite, which is the same failure mode with less tooling. chezmoi instead keeps a source directory and applies a target state, and it supports templating and per-machine differences, so the same repository can produce different files on a work laptop and a personal one. This repository uses plain files plus Stow plus *.local include files, which is a simpler model with a narrower scope.
The difference shows up when machines diverge. With Stow and static files, divergence is handled by .local files and by editing the repository. With chezmoi, divergence is a first-class feature of the tool. On the other side, Stow is a small, well-understood program, and the entire behavior of this setup is visible in setup.sh and the directory tree. There is no template language to learn and no apply step to reason about. If your machines are similar and you want to read every line that touches your $HOME, the Stow approach is easier to audit. If you maintain several machines with genuinely different needs, it is the wrong tool.
It is also worth noting what this repository does not try to be. It is not a cross-platform dotfiles framework, and it does not attempt to be installable by strangers without reading. The README's insistence on less setup.sh before running it is consistent with that.
Licence, maintenance and what to verify before adopting
The repository is MIT licensed, which permits reuse, modification and redistribution provided the copyright notice and permission notice are retained. That covers the configuration files and scripts in the repository. It does not automatically cover anything the setup installs: LazyVim, Zap, zsh-abbr, Starship, Ghostty, Tmux and the Homebrew formulae all carry their own licences, and the Laptop fork has its own repository and terms. If you copy this configuration into your own project, check the licences of what it pulls in separately. This is a description of the licence text, not legal advice.
Maintenance cost is the more practical question. There are no tagged releases, so updates are commits on master. The setup script is idempotent, which means re-running it after a pull is the expected upgrade path, but Stow conflicts and the ~/.claude folding behavior are the two places where a re-run can surprise you. The Fish removal in PR #135 is the precedent: a component can be dropped between pulls. If you depend on a specific piece, pin your fork to a commit rather than tracking master.
Before adopting, verify the parts the README leaves to you. Check whether ~/.claude exists as a real directory before running setup.sh, since that determines whether Stow links files individually or folds the directory. List what in $HOME, $HOME/.config and $HOME/.local/bin would collide with the repository's targets. Read setup.sh and bubo rather than trusting the description, because the README's account of bubo is truncated and its drift detection logic is not described. And run ~/dotfiles/setup.sh --dry-run first, which is the one command here that costs nothing and tells you the most.
Editorial conclusion
Adopt this if you run macOS and want a Neovim, Zsh and Tmux environment that someone else already debugged, and you are willing to read setup.sh before running it. Do not adopt it if you are on Linux or Windows, or if you want a config you can install without understanding. Verify three things first: whether ~/.claude is a real directory before Stow runs, which files in your $HOME would conflict with the symlink targets, and whether bubo covers the surfaces you actually depend on.
Community notes