Warning: If you want to give these dotfiles a try, you should first fork this repository, review the code, and remove things you don’t want or need. Don’t blindly use my settings unless you know what that entails. Use at your own risk!
You can clone the repository wherever you want (I like to keep it in ~/.dotfiles). The modern bootstrapper is TypeScript run by Bun; ./bs fetches a pinned Bun with bun-strap, so nothing has to be installed globally. It then installs dependencies, creates symlinks, and backs up existing files.
git clone https://github.com/murrayju/dotfiles.git ~/.dotfiles && cd ~/.dotfiles && ./bsTo update, just run the bootstrap script again
cd ~/.dotfiles && ./bsThe new bootstrap system will:
- Download a pinned Bun into
download/bun/using bun-strap (no global Node.js, Bun, or package manager needed) - Create symlinks from the repository to your home directory
- Back up any existing files that differ from the repository versions
- Install Oh My Zsh, Powerlevel10k theme, and Tmux Plugin Manager
- Install Homebrew and the formulae in
Brewfile(macOS and Linux) - Install mise and the tools in
dotfiles/.config/mise/config.toml - Install pi if it is not already installed
- Set up the modern Neovim configuration
On Linux the bootstrap does not install distro packages. Do that once, separately (it needs sudo):
cd ~/.dotfiles && ./bs aptThe pi installer is normally interactive; the bootstrap runs it without a controlling terminal, which makes it take its default action and install noninteractively. Re-run it on its own with ./bs pi. The shell config puts ~/.pi/agent/bin on $PATH, so a freshly installed pi is usable after reload.
To install these dotfiles without Git:
cd; curl -#L https://github.com/murrayju/dotfiles/tarball/master | tar -xzv --strip-components 1 --exclude={README.md,macos.sh,LICENSE-MIT.txt} && ./bsTo update later on, just run that command again.
If ~/.path exists, it will be sourced along with the other files, before any feature testing (such as detecting which version of ls is being used) takes place.
Here’s an example ~/.path file that adds /usr/local/bin to the $PATH:
export PATH="/usr/local/bin:$PATH"If ~/.extra exists, it will be sourced along with the other files. You can use this to add a few custom commands without the need to fork this entire repository, or to add commands you don’t want to commit to a public repository.
My ~/.extra looks something like this:
# Git credentials
# Not in the repository, to prevent people from accidentally committing under my name
GIT_AUTHOR_NAME="Justin Murray"
GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
git config --file ~/.gitconfig.local user.name "$GIT_AUTHOR_NAME"
GIT_AUTHOR_EMAIL="murrayju@addpcs.com"
GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
git config --file ~/.gitconfig.local user.email "$GIT_AUTHOR_EMAIL"You could also use ~/.extra to override settings, functions and aliases from my dotfiles repository. It’s probably better to fork this repository instead, though.
~/.gitconfig is a symlink into this repository, so git config --global changes the tracked file. Put machine-specific git settings (for example, a work email) in ~/.gitconfig.local instead. ~/.gitconfig includes it last, so its values override:
git config --file ~/.gitconfig.local user.email "me@work.example"When setting up a new Mac, you may want to set some sensible macOS defaults:
./macos.shThe bootstrap uses three package managers:
- Homebrew (
Brewfile): system tools, libraries, fonts, and apps. The file is a Ruby DSL withif OS.mac?/if OS.linux?blocks, sobrew bundle --file Brewfileis valid on either platform. macOS gets the GNU userland and casks; Linux gets only what Homebrew is genuinely best at (fonts), because anything with system integration has to come from the distro. - apt (
packages/apt.txt,./bs apt): Debian/Ubuntu packages (gnupg, openssh-client, bash-completion, bubblewrap, …). Linux-only and opt-in, because it needs sudo. - mise (
dotfiles/.config/mise/config.toml): language runtimes and developer CLIs (Node, Go, Neovim, ripgrep, starship, and more). Projects can pin their own versions with amise.tomlor.tool-versionsfile. - pi (
targets/pi.ts,./bs pi): installed with https://pi.dev/install.sh when it is missing. Not a package manager, but it is the one tool that installs itself outside of them.
Separately from those, the bootstrap's own tooling (build-strap, fs-extra, typescript, prettier) is installed by bun into node_modules from package.json and bun.lock. Bun itself is not a prerequisite: ./bun is a bun-strap copy that downloads the pinned version into download/bun/. The shell config adds a bun function that prefers a project-local ./bun and otherwise falls back to the globally installed one.
Path order matters. On macOS, Homebrew is prepended to $PATH so its modern GNU tools win. On Linux it is appended, because Homebrew's bin directory also contains duplicates of gpg, ssh, python3, curl, sed, grep, dbus-* and friends — and those must keep resolving to the distro's copies so they talk to the system's agents and services (most notably Ubuntu's systemd-managed gpg-agent, which breaks commit signing when a different gpg version talks to it). Tools you do want from Homebrew on Linux should be symlinked into ~/.local/bin, which is ahead of everything. mise activates last, so mise tools beat both.
For the same reason, the bootstrap appends the Linux Homebrew prefixes to its own process $PATH before it runs the installer (putLinuxBrewOnProcessPath in targets/bootstrap.ts). The installer only prints its "/home/linuxbrew/.linuxbrew/bin is not in your PATH" warning — and points at the Next steps snippet — when the prefix is missing from $PATH at install time, and following that snippet on Linux is exactly what breaks the ordering above. Seeding it also means build-strap finds brew by itself afterwards instead of appending eval "$(brew shellenv)" to the (symlinked) .zshrc.
Because the Linux branch of the Brewfile no longer installs anything that duplicates distro tools, previously installed formulae stay behind as dead weight (they no longer shadow anything, since Homebrew is appended to $PATH):
# Check for dependents first; only remove what nothing else needs.
brew uses --installed gnupg openssh screen php grep findutils coreutils gnu-sed
brew uninstall gnupg openssh screen php grep findutils coreutils gnu-sed
# Let the system gpg-agent take over again (note: /usr/bin/gpgconf).
/usr/bin/gpgconf --kill gpg-agent
/usr/bin/gpg --list-secret-keys # then test a signed commitFinally, make sure brew shellenv is not left in ~/.extra, ~/.profile, or ~/.bashrc. On Linux that re-prepends Homebrew and undoes the ordering above — which is exactly what makes gpg and ssh talk to the wrong agents.
Shell config is layered. Each shared file is followed by a per-OS companion that $OSTYPE-detects at source time:
| Shared | macOS | Linux |
|---|---|---|
.exports |
.exports_darwin |
.exports_linux |
.aliases |
.aliases_darwin |
.aliases_linux |
.functions |
.functions_darwin |
.functions_linux |
So an alias that only works on macOS (Finder, defaults, pbcopy, Spotlight) belongs in .aliases_darwin, and one that only works on Linux (bwrap, ip, loginctl) belongs in .aliases_linux / .functions_linux. .extra is sourced last and always wins. New OS-specific files are picked up automatically by the bootstrap symlinks; no bootstrap change is needed.
Suggestions/improvements welcome!
| Mathias Bynens |
- @ptb and his macOS Setup repository
- Ben Alman and his dotfiles repository
- Cătălin Mariș and his dotfiles repository
- Gianni Chiappetta for sharing his amazing collection of dotfiles
- Jan Moesen and his ancient
.bash_profile+ shiny tilde repository - Lauri ‘Lri’ Ranta for sharing loads of hidden preferences
- Matijs Brinkhuis and his dotfiles repository
- Nicolas Gallagher and his dotfiles repository
- Sindre Sorhus
- Tom Ryder and his dotfiles repository
- Kevin Suttle and his dotfiles repository and macOS-Defaults project, which aims to provide better documentation for
~/.macos - Haralan Dobrev
- Anyone who contributed a patch or made a helpful suggestion
