A reproducible developer environment for macOS and Ubuntu/Debian Linux, set up by a single command. One repo, two OSes, same shell + git config + tooling on both.
Note
The installer is non-destructive. Anything it would overwrite is moved into a timestamped backup directory first — it never rms a real file.
- Quick start
- What you get
- Philosophy
- Day-to-day:
dotcommands - Customization
- How it works
- Post-install steps
- FAQ
git clone https://github.com/philipdowner/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
script/bootstrapThe bootstrap script will:
- Prompt for your git author name + email (written to a git-ignored
~/.gitconfig.local). - Symlink every
*.symlinkfile into$HOME(with timestamped backups of anything pre-existing). - Detect your OS and run the right installer chain (
bin/dot install).
That's it. Re-run script/bootstrap or dot update any time — both are idempotent.
Tip
If you want to customize without losing the ability to pull upstream changes, fork the repo first and clone your fork.
- zsh as the default login shell, with oh-my-zsh (default
robbyrusselltheme on Linux, powerlevel10k on macOS) - Topic-based config: drop a
*.zshfile in any subdirectory and it gets sourced ~/.localrcescape hatch for per-machine secrets and tweaks (see Customization)- Auto-deduped
$PATH, fast history search, sensible defaults
- Pre-configured
.gitconfigwith git-delta as the pager (side-by-side diffs, syntax highlighting) - PhpStorm wired up as
core.editor,diff.tool, andmerge.tool - A
~/.gitconfig.localfile for your name/email and any per-machine overrides - Helpful aliases (
promote,wtf, etc.) — seegit/gitconfig.symlink
git,gh(GitHub CLI),curl,wget,tree,tldr,jq,ripgrep,fd,shellcheck,zsh-syntax-highlighting- Node.js + npm from apt (Linux) or brew (macOS) — for global CLIs only; project Node lives in Docker
- Claude Code (
@anthropic-ai/claude-code) via npm - Docker Engine + Compose v2 (Linux: official apt repo; macOS: Docker Desktop)
- 1Password CLI (
op) with the SSH agent integration
| App | macOS | Linux |
|---|---|---|
| PhpStorm (default IDE) | Toolbox | Toolbox |
| 1Password + 1Password CLI | brew | apt (official repo, not snap) |
| Google Chrome | brew | apt (official repo) |
| Slack | brew | snap |
| Zoom | brew | vendor .deb |
| Spotify | brew | apt (official repo) |
| Obsidian | brew | snap |
| Postman | brew | snap |
| Docker | Desktop | Engine |
| iTerm2 | brew | — (use GNOME Terminal) |
| Rectangle / Kap / QuickLook plugins | brew | — (macOS-only) |
Important
Don't install 1Password from snap. The snap build is sandboxed and breaks the SSH agent socket. The repo's 1password/install.sh uses the official apt repo and detects an existing snap install to warn you.
This repo gives you a stable, reproducible base shell + tooling environment, nothing more. The opinions are:
- Language runtimes live in Docker. PHP, MySQL, Postgres, Python, Ruby — none of them are installed system-wide. Each project ships its own container. Node + npm are the only host runtimes, and only because global CLIs need them.
- One IDE. PhpStorm is the canonical editor. Git uses it for diffs, merges, and commit messages.
- macOS and Linux at parity where it matters. The same
.zshrc, the same.gitconfig, the samedot updateworkflow. Differences are isolated to per-OS topic installers. - Opt-in, not opt-out. New apps are explicit additions (
Aptfile,Brewfile, or a<topic>/install.sh). No surprise installs.
After the first install, bin/dot is the maintenance entry point:
dot # same as `dot install`
dot install # re-run all installers (idempotent)
dot update # git pull, install, then upgrade brew/apt + oh-my-zsh
dot edit # open ~/.dotfiles in PhpStorm
dot help # show usagedot update is the one to run weekly: it git pull --rebase --autostash's the repo, re-runs every topic installer (so new tools you add show up automatically), then upgrades system packages.
Anything you don't want committed to a public dotfiles repo — work credentials, API tokens, machine-specific $PATH entries, hostname-specific aliases — goes in ~/.localrc. It's sourced at the very top of ~/.zshrc, before any topic config.
See zsh/localrc.example for the canonical patterns:
# Pull secrets from 1Password at shell-start time, never touching disk
export GITHUB_TOKEN="$(op read 'op://Private/GitHub/token')"
# Hostname-specific tweaks
case "$(hostname -s)" in
work-laptop) export AWS_PROFILE=work ;;
home-desktop) export AWS_PROFILE=personal ;;
esacThe 1password/functions.zsh file also exposes two helpers (gated on op being on $PATH):
op_export GITHUB_TOKEN "op://Private/GitHub/token" # silently no-ops if not signed in
with-op ~/.config/op/work.env -- terraform plan # `op run` wrapperThe 1password/install.sh topic enables the SSH agent. You can use the same key to sign your git commits without ever exporting it. Add this to ~/.gitconfig.local:
[user]
signingkey = ssh-ed25519 AAAAC3Nza...your public key...
[gpg]
format = ssh
[gpg "ssh"]
program = "/Applications/1Password.app/Contents/MacOS/op-ssh-sign" # macOS
# program = "/opt/1Password/op-ssh-sign" # Linux
[commit]
gpgsign = true
[tag]
gpgsign = trueGitHub will mark your commits as Verified with no extra agents and no on-disk private key.
If your shell starts to feel slow:
ZSH_PROFILE=1 zsh -ic exitThis loads zsh/zprof and prints a per-function timing report at the end of zshrc. Use the lazy_load helper in system/lazy.zsh to defer slow initializers (nvm, pyenv, direnv, etc.) until first use.
Everything is organized by topic — one directory per tool. To add a new topic, just mkdir node and drop files in:
| Filename | What it does |
|---|---|
topic/path.zsh |
Sourced first. Set $PATH and friends here. |
topic/*.zsh |
Sourced in the middle. Aliases, functions, env vars, prompts. |
topic/completion.zsh |
Sourced last, after compinit. Use for completion definitions. |
topic/install.sh |
Run by script/install (and dot install). Idempotent installer for the tool. |
topic/*.symlink |
Symlinked into $HOME as ~/.<basename> by script/bootstrap. |
bin/* |
Anything in bin/ is added to $PATH. |
Per-OS installers should self-skip with a [ "$(uname -s)" = "Linux" ] || exit 0 (or equivalent) at the top so they're safe to include in the global install loop on the wrong OS.
script/bootstrap
├── prompt for git name/email → ~/.gitconfig.local
├── symlink */*.symlink → ~ (timestamped backups for anything pre-existing)
└── bin/dot install
├── macOS: macos/set-defaults.sh, homebrew/install.sh, brew update
├── Linux: linux/set-defaults.sh, linux/install.sh (apt + Aptfile)
└── script/install # runs every topic */install.sh + Brewfile on macOS
- CLI tool available in your distro's repos: add it to
linux/Aptfileand/orBrewfile, thendot install. - Anything else (vendor apt repo, snap, .deb, tarball): create a new topic dir with an
install.shthat follows the pattern inchrome/,1password/,docker/, etc.
A few things the installer can't fully automate.
- PhpStorm: Install via JetBrains Toolbox. In Toolbox settings, enable "Generate shell scripts" so the
phpstormlauncher ends up on your$PATH. (Or in PhpStorm: Tools → Create Command-line Launcher.) - iTerm2 theme: iTerm2 → Settings → Profiles and pick
Dotfiles Default. - Open new apps once: macOS will prompt for security confirmation the first time you launch any non-App-Store app — this is expected.
- PhpStorm: Install via JetBrains Toolbox. Enable "Generate shell scripts" in Toolbox settings (target
~/.local/bin). Thephpstorm/install.shtopic will detect it and symlink the launcher if Toolbox didn't. - Log out and back in after the first install so:
- your shell switches to zsh, and
- your user picks up
dockergroup membership
- GNOME Terminal font: Preferences → your profile → Custom font. Pick whichever monospace font you prefer.
- 1Password SSH agent: Open 1Password → Settings → Developer → Use the SSH agent. The
1password/env.zshfile will exportSSH_AUTH_SOCKfor you on next shell start.
Yes. Anything pre-existing at a symlink target is moved to ~/.dotfiles-backup/<timestamp>/<original/path> before being replaced. Restore with cp -a ~/.dotfiles-backup/<timestamp>/. /.
See What you get. The Linux installers default to a lean set of base tools — you can extend by editing linux/Aptfile. The macOS Brewfile is broader because brew handles GUI apps too.
Several installers (apt, system defaults, chsh) need root. They're prompted via sudo; no characters appear as you type — just hit enter.
Yes — that's exactly what dot install and dot update do. Every topic installer is idempotent and skips work that's already done.
- For secrets and per-machine env vars, use
~/.localrc(see Customization). - For git author info and any local git config, edit
~/.gitconfig.local. - For bigger changes — adding/removing topics, changing the Brewfile or Aptfile — fork the repo so you can pull upstream changes later.
Open an issue on the repo.