Stow aborts on anything already in $HOME, and setup.sh pre-creates ~/.claude so it cannot fold your Claude state into the repo
:round_pushpin: My dotfiles for macOS using Neovim, Zsh, and Ghostty + Tmux
At a glance
- What is it?
- These are macOS dotfiles installed with GNU Stow through a single setup.sh, on top of a fork of thoughtbot's Laptop. The two mechanisms worth understanding before you run anything are the Stow conflict, which aborts when $HOME already holds a file of the same name, and the pre-created ~/.claude and ~/.claude/skills directories, which exist to stop Stow from collapsing a directory into one symlink and dragging Claude Code's runtime state into the repository.
- Who is it for?
- These dotfiles suit a macOS developer setting up a new machine who wants a working LazyVim, Starship, tmux and Claude Code setup in one pass and is prepared to read three scripts before running them. They do not suit someone on Linux, on Hyprland or on Arch looking for a portable config, and they do not suit a machine where the existing $HOME contents are precious, because the Stow layer will refuse to run until the collisions are resolved by hand.
- 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 2 days 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 October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Stow aborts on any file already sitting in $HOME
The whole install is one symlink farm. `setup.sh` uses GNU Stow to link every config file in the repository into your `$HOME` directory, and Stow's rule is unforgiving: if `$HOME` already contains a file or directory with the same name as the one being linked, the run stops with an error.
That is not a hypothetical on a machine that has been through the Laptop step, because the documented example is a `~/.zshrc` left behind by installing Laptop. The script tries to detect and back up such files ahead of Stow, which handles the obvious cases, and the README is explicit that this is not a substitute for looking yourself. The three places to check are `$HOME` itself, `$HOME/.config` and `$HOME/.local/bin`.
The recovery path is short because the script is idempotent. Resolve the conflicts, run `setup.sh` again, and it is safe to run as many times as you need:
git clone https://github.com/joshukraine/dotfiles.git ~/dotfiles
less ~/dotfiles/setup.sh
~/dotfiles/setup.sh --help
~/dotfiles/setup.sh --dry-run
~/dotfiles/setup.shThe `--dry-run` flag is the one to reach for first. It previews the changes without applying them, which on a Stow based setup is the difference between seeing a list of links and finding out that a directory you needed has become a symlink.
Two directories exist so Stow cannot swallow ~/.claude
There is a specific workaround in the script for a specific failure, and it is the most interesting thing in the repository.
On a fresh machine, `setup.sh` pre-creates `~/.claude` and `~/.claude/skills` as real directories before Stow runs. The reason is given directly: so that Stow links the Claude config files individually instead of folding a whole directory into a single symlink. If `~/.claude` were absent when Stow got there, and the repository ships a `claude/` package, Stow would be free to symlink the entire directory, and Claude Code's runtime state, along with its synced account skills, would end up living inside the repository and showing up in `git status`.
Two real directories defeat that. Stow sees a directory already at the target and links file by file inside it, so the things Claude Code writes at runtime have somewhere else to go.
The README also handles the case where you forked before this behaviour existed: if Claude runtime files appear in `git status`, there are two named troubleshooting sections, one for `~/.claude` folding and one for `~/.claude/skills` folding. The repository root carries a `.stow-local-ignore` alongside a `.claude/` and a `claude/` directory, which is the trio that this mechanism is built out of.
The same reasoning applies to the other packages: a `claude/` directory that Stow links file by file is not the same as a whole tree of config committed and symlinked wholesale.
The Laptop fork's whole diff is three changes
Before the dotfiles, there is a machine bootstrap, and it is a fork of thoughtbot's Laptop with a very small patch. The prerequisite list is what Laptop installs: Git, Homebrew including coreutils, asdf, Ruby, Node.js, Zsh, Neovim and Starship. The README's claim is that all of it, and more, comes from the fork.
Two files are downloaded, and they come from two different repositories. The `mac` script comes from the Laptop fork, and `.laptop.local` comes from this repository, which is where the author's own customisations live:
curl --remote-name https://raw.githubusercontent.com/joshukraine/laptop/main/mac
curl --remote-name https://raw.githubusercontent.com/joshukraine/dotfiles/master/laptop/.laptop.localBoth are supposed to be read before they run, with `less mac` and `less .laptop.local`, and then executed with the output captured:
sh mac 2>&1 | tee ~/laptop.logThe `tee` is the useful part, since it leaves a log of a script that installs a lot of things unattended, and the script is described as idempotent, so running it again is a way to converge rather than a way to break something.
The three changes to the upstream are worth knowing because they explain the shape of the resulting machine: asdf is installed via git rather than through Homebrew, the Heroku related code is commented out, and unused Homebrew taps and formulae are commented out. That is a fork edited to remove what its owner does not use, not to add anything.
Zap will replace your .zshrc unless you pass --keep
Zsh deserves its own section because of one instruction that is flagged as important in the documentation. Zap is the plugin manager, described as a minimal zsh plugin manager that does what you expect, and the warning after the install command is specific: after copying and pasting it, add the `--keep` flag, because without it Zap replaces your existing `.zshrc` file.
That matters more here than in a normal setup, because the `.zshrc` in question is managed by Stow. A plugin manager that overwrites a symlinked config file is not a cosmetic problem, it is a config file whose source of truth is a repository, being replaced by a generated one.
The Zsh path itself forks by hardware, and it is worth getting right before anything else. macOS ships a `/bin/zsh`, and the dotfiles want the Homebrew build instead, at `/opt/homebrew/bin/zsh` on Apple Silicon and `/usr/local/bin/zsh` on Intel. If `which zsh` does not return one of those, `brew install zsh` is the first step. Then the path goes into `/etc/shells`, which is the file macOS uses to decide what a login shell may be:
echo /opt/homebrew/bin/zsh | sudo tee -a /etc/shells
echo /usr/local/bin/zsh | sudo tee -a /etc/shells
echo $(which zsh) | sudo tee -a /etc/shellsThe first two are the per architecture forms and the third is the universal one, and the README shows all three rather than making you pick. The default shell is then set with `chsh -s $(which zsh)`, and the terminal is restarted afterwards, which is the step people skip and then wonder why the prompt has not changed.
Abbreviations, and four git functions that guess your branch
Two shell features are worth pulling out of the Zsh package because they are the parts a reader would otherwise only find by reading the config.
The first is zsh-abbr, which brings abbreviations to Zsh. The README is explicit about where the idea came from: it is one of the best things picked up from using the Fish shell, and the linked argument is for abbreviations over aliases. The difference matters in practice because an abbreviation expands in place while an alias is textual substitution, so an abbreviation can wrap a command with flags or a pipe and still leave the rest of the line working.
The abbreviation file is `zsh/.config/zsh-abbr/abbreviations.zsh`, and there are two ways to change it: edit the file directly, or use the `abbr add` and `abbr remove` commands in the shell. Keeping it as a plain file in the repository is what makes it Stow managed and therefore portable.
The second is a set of four git functions that detect your main branch instead of hardcoding it. `gpum` pushes the current branch to origin with upstream tracking. `grbm` rebases onto main or master. `gcom` checks out main or master. `gbrm` removes branches already merged into main or master. All four work with either branch name automatically, which is the whole point, and each is one function instead of a long line with a conditional in it.
Fish support was removed in PR 135, with a commit to recover it from
The README carries a note that answers a question a visitor will otherwise have to guess at: this project used to support the Fish shell alongside Zsh, and it does not any more. Fish support was removed in PR #135, commit f158de9.
The useful part of the note is the recovery path rather than the history. If you were using the Fish configuration, the PR is the place to see what changed, and to recover code for your own setup. That is a more useful answer than a removal notice with no pointer, because a dotfiles repository is a thing people fork and a removed shell configuration is code somebody still wants.
The broader pattern is worth noticing for anyone forking this repository. Config that is dropped gets a commit hash and a pull request, and config that is kept for your own machine goes into a `*.local` file rather than into the shared package. The post-install checklist names the two: `~/.gitconfig.local` and `~/.laptop.local`, for personal data that should not be committed. The `laptop/` directory in the tree is where `.laptop.local` lives, which is the same file the bootstrap step downloads and then asks you to review.
The shell itself is Zsh with Starship as the prompt, and both are on the prerequisite list, so the prompt is installed by the bootstrap rather than by the dotfiles.
ghostty/ and kitty/ both ship, though only one is in the highlights
The top level of the repository is a directory per package, and reading it is the fastest way to see what is actually managed. There is `nvim/`, `zsh/`, `starship/`, `tmux/`, `lazygit/`, `git/`, `ghostty/`, `kitty/`, `asdf/`, `node/`, `ruby/`, `brew/`, `claude/`, `markdown/`, `rc/`, `shared/`, `spell/`, `terminfo/` and `yamllint/`, plus `bin/`, `scripts/`, `tests/`, `tmp/`, `laptop/`, `docs` free tooling like `nerd-font-smoke-test.sh`, and the linting and editor configuration at the root.
The interesting entry is `kitty/`. The highlights list names Ghostty and Tmux as the terminal stack, and Ghostty has its own directory, but a `kitty/` directory sits there too, so the repository carries a second terminal emulator's configuration that the highlights do not mention. Whether that is a leftover or a supported fallback is not something the visible text answers, and it is a good example of why reading the tree beats reading the feature list.
Two more root files explain a lot of the setup's behaviour. `.stow-local-ignore` is what Stow consults per package, and `nerd-font-smoke-test.sh` is a script whose name says what it is: a check that the Nerd Font glyphs the prompt and the status line depend on actually render, which is the failure mode nobody diagnoses from a screenshot.
There is also a staleness reporting command, `bubo`, which reports what in the environment has drifted across every surface the setup script touches. That is the tool for the question a dotfiles repository raises after six months, namely which of the settings on this machine came from the repository and which came from somewhere else.
The last steps are a checklist, not a script
After `brew bundle install`, the remaining work is a list of checkboxes, and three of the six cannot be done by a script at all.
The first is launching Neovim and running `:checkhealth`, which is Neovim's own diagnostic command, and resolving whatever errors and warnings it reports. Plugins are expected to install automatically on that first launch, so a clean health check after a first run is the signal that the LazyVim layer came up. The second is filling in the `*.local` files with personal data, with `~/.gitconfig.local` and `~/.laptop.local` named as the examples, which is the separation that lets one repository serve two people without either committing the other's name and email.
The third is secrets, and both items are optional: the 1Password CLI for managing secrets, and 1Password SSH key management. The repository stores no credentials, which is consistent with everything else about the layout.
The fourth is tmux plugins, installed with `<prefix> + I` through tpm, so the plugin installation is a keypress inside a running session rather than a package manager call.
The fifth is the Brewfile, which is reviewed with `less ~/Brewfile` before `brew bundle install` runs, which is the same read before you run discipline applied to the package list. The Homebrew bundle covers whatever the prerequisites and the bootstrap did not, and the README's instruction is to make your own adjustments to it first.
Editorial conclusion
These dotfiles suit a macOS developer setting up a new machine who wants a working LazyVim, Starship, tmux and Claude Code setup in one pass and is prepared to read three scripts before running them. They do not suit someone on Linux, on Hyprland or on Arch looking for a portable config, and they do not suit a machine where the existing $HOME contents are precious, because the Stow layer will refuse to run until the collisions are resolved by hand. Before installing, run the dry run, inventory $HOME, $HOME/.config and $HOME/.local/bin for names the repo will claim, read the downloaded mac and .laptop.local scripts rather than piping them straight to sh, and add --keep to any Zap install command.
Frequently asked questions
What is a dotfile?
In this repository a dotfile is a configuration file that setup.sh symlinks out of the repo and into your $HOME using GNU Stow, one package directory at a time. The tree is a directory per tool, so nvim/, zsh/, tmux/ and the rest each hold the files that get linked into place.
How do I install dotfiles from this repository?
Clone the repo to ~/dotfiles, read setup.sh, check its options with --help, preview with --dry-run, then run it. It needs macOS with Git, Homebrew with coreutils, asdf, Ruby, Node.js, Zsh, Neovim and Starship, and the script is idempotent so you can resolve Stow conflicts and run it again.
Why does the setup script fail with a Stow error?
Because GNU Stow aborts when $HOME already contains a file or directory with the same name as the one it is linking, a ~/.zshrc left over from installing Laptop being the documented example. The script tries to detect and back those up first, and the README asks you to check $HOME, $HOME/.config and $HOME/.local/bin by hand.
Why does joshukraine/dotfiles create ~/.claude before running Stow?
So Stow links the Claude config files individually instead of folding the whole directory into one symlink, which would put Claude Code's runtime state and synced account skills inside the repository. The README links two troubleshooting sections for forks that predate this behaviour, one for ~/.claude folding and one for ~/.claude/skills folding.
What does the Zap install warning in these dotfiles mean?
Zap's install command replaces your existing .zshrc unless you add the --keep flag. Since the .zshrc is managed by Stow from the repository, running it without --keep would overwrite a file whose source of truth is version controlled.
What shell features do these macOS dotfiles add?
Zsh with zsh-abbr for abbreviations managed in zsh/.config/zsh-abbr/abbreviations.zsh, Starship as the prompt, and four git functions that detect the main branch: gpum, grbm, gcom and gbrm. All four work with either a main or a master branch name.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/joshukraine-dotfiles)