Personal dotfiles for macOS, managed as a bare git repository with the work tree at $HOME.
Run this on a fresh Mac to bootstrap everything:
curl https://raw.githubusercontent.com/betterwithranch/dotfiles/main/scripts/init.sh | zshThis single command will:
- Enable Touch ID for
sudo - Install Xcode Command Line Tools
- Clone the dotfiles bare repo to
~/.dotfiles - Install Oh My Zsh
- Install TPM and tmux plugins
- Install Homebrew and all packages from the Brewfile
- Link Alfred workflows
- Install Claude Code
- Install language runtimes via asdf
- Configure pam-reattach (Touch ID in tmux)
- Apply macOS system preferences and menu bar configuration
- Set up Finder sidebar favorites
- Install Neovim plugins
- Launch and configure Hammerspoon, Alfred, Ghostty, and Karabiner-Elements
- Grant macOS permissions (Accessibility, Input Monitoring, etc.)
- Bootstrap workspace automation
- Inject secrets from 1Password CLI
- Generate an SSH key and register it with GitHub
The repo uses a bare git repository pattern. A config alias replaces git for dotfiles operations:
# The alias (defined in .zshrc)
alias config='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
# Track a new file
config add ~/.some-config-file
# Commit and push
config commit -m "add some-config-file"
config push
# Check status (untracked files are hidden by default)
config status
# Use lazygit for the dotfiles repo
lazycfg- Oh My Zsh with the
robbyrusselltheme - Plugins:
aws,asdf,direnv,docker,docker-compose,git,pipenv,rails,ruby - Vi mode (
set -o vi) with vi keybindings in readline (.inputrc) and editline (.editrc) - Deduped, shared history across sessions
- fzf shell integration (Ctrl+R, Ctrl+T, Alt+C) with Solarized Dark colors
- zoxide (
z) for smart directory jumping - Yazi wrapper function (
y) that syncs the working directory on exit - Auto-attach to tmux when Ghostty opens
- Ghostty terminal with Solarized Dark Higher Contrast theme and Hack Nerd Font
- tmux with
C-uprefix, vi copy mode, mouse support, and hjkl pane navigation - TPM plugins:
- tmux-resurrect + tmux-continuum for session persistence (auto-save every 5 min, auto-restore on start)
- tmux-colors-solarized for consistent theming
- tmuxinator (
mux) for project layouts
- LazyVim distribution with catppuccin-frappe colorscheme
- Leader key:
, - Plugins for Rails, Treesitter, Diffview, Yazi, auto-session, render-markdown, and more
- Copilot and CodeCompanion integrations
- conform.nvim for formatting, Solargraph for Ruby LSP
- Legacy Vim config (
.vimrc,.vim/) also included
- git-delta as pager for both
gitandgh nvimdifffor diffs, Diffview for mergeszdiff3conflict style,histogramdiff algorithmrerereenabled for remembering conflict resolutions- Auto-prune on fetch, rebase on pull with auto-stash
- Branches sorted by most recent commit
- Push defaults:
autoSetupRemote, push tocurrent - SSH protocol enforced for GitHub URLs
- Local overrides via
~/.gitconfig.local(gitignored) - Global gitignore (
.config/git/ignore):.DS_Store,.env*,.idea/,.vscode/,*.log,.direnv/, swap files
Installed via Homebrew (.Brewfile):
| Tool | Description |
|---|---|
| bat | cat replacement with syntax highlighting (aliased: cat) |
| eza | ls replacement with icons and git status (aliased: ls, ll, la, tree) |
| fzf | Fuzzy finder for files, history, and more |
| ripgrep | Fast recursive grep (smart-case, searches hidden files) |
| fd | Fast find replacement |
| zoxide | Smarter cd that learns your habits |
| yazi | Terminal file manager |
| lazygit | Terminal UI for git |
| git-delta | Beautiful git diffs |
| btop | System resource monitor |
| httpie | User-friendly HTTP client |
| tldr | Simplified man pages |
| jq | JSON processor |
| gh | GitHub CLI |
| direnv | Per-directory environment variables (auto-loads .env files) |
asdf manages runtime versions for Ruby, Node.js, Python, and Terraform. See .tool-versions for current versions.
Karabiner-Elements remaps Caps Lock into a Hyper key (ctrl+alt+cmd+shift) when held, and Escape when tapped alone. This powers all workspace switching hotkeys. Config lives at ~/.config/karabiner/karabiner.json.
Hammerspoon provides a full workspace management system. Config lives at ~/.config/hammerspoon/.
Each workspace maps to a macOS Space and has an associated Chrome profile and tmux session:
| Workspace | Hotkey | Space | Chrome Profile | tmux Session |
|---|---|---|---|---|
| Personal | Hyper+P |
1 | craig@theisraels.net | personal |
| Sanctum | Hyper+S |
2 | craig@hellosanctum.com | sanctum |
| SendCarrot | Hyper+C |
3 | craig@sendcarrot.com | sendcarrot |
| MergeFreeze | Hyper+M |
4 | hello@mergefreeze.com | mergefreeze |
Additional hotkeys:
| Hotkey | Action |
|---|---|
Ctrl+Tab |
Next workspace |
Ctrl+Shift+Tab |
Previous workspace |
Hyper+R |
Reload Hammerspoon config |
Features:
- Menu bar indicator shows the current workspace name and icon, persists position across restarts
- Auto-focus brings the frontmost window on the target space into focus after switching
- App bootstrapping ensures Ghostty (with the correct tmux session) and Chrome (with the correct profile) are running when switching to a workspace
- Chrome profile detection uses
lsofto reliably check which profiles are active - Space refresh re-discovers macOS space IDs every 60 seconds (macOS renumbers them occasionally)
Workspace configs are individual Lua files in ~/.config/hammerspoon/workspaces/.
An Alfred workflow provides a searchable workspace picker. Type ws in Alfred to list all workspaces and switch to one. The workflow calls into Hammerspoon via hs -c, sharing the same switching logic as the hotkeys.
Workflow files live at ~/.alfred/workflows/workspace-switcher/ and are symlinked into Alfred's preferences.
The scripts/mac-defaults.sh and scripts/menubar.sh scripts configure:
- Finder: Show hidden files, show all extensions, list view by default, no extension change warnings
- Dock: Only show running apps, auto-hide, don't rearrange Spaces
- Menu bar: Show Battery, WiFi, Clock (day + AM/PM); hide Sound, Focus, Screen Mirroring, Now Playing, Siri
- System: Disable smart quotes/dashes, expand save dialogs, auto light/dark mode, silence system sounds
- Storage: Don't create
.DS_Storeon network or USB volumes - Hot corners: Bottom-right locks screen
- Claude Code with global instructions (
.claude.md), custom skills, hooks, and MCP plugins - Codex (
.codex/) - Copilot via Neovim
- Ollama for local models
scripts/networking.sh sets DNS to Cloudflare (1.1.1.1 / 1.0.0.1) on all Wi-Fi, Ethernet, and USB interfaces.
.gemrc: Skip documentation on gem install.rspec: Color output.irbrc: Auto-enable ActiveRecord logging in Rails console, auto-start Pry.pryrc: Debugger aliases (c/s/n/f), ActiveRecord logging, repeat-last-command on Enter
Secrets are managed with 1Password CLI using template files. The scripts/secrets.sh script finds all .tpl files and uses op inject to populate the real config files with values from your 1Password vault.
-
Create a template file alongside the real config file, with a
.tplextension:# ~/.aws/credentials.tpl [default] aws_access_key_id = {{ op://Personal/AWS/access_key_id }} aws_secret_access_key = {{ op://Personal/AWS/secret_access_key }} -
The
op://reference format isop://Vault/Item/Field. To find the right values:op vault list op item list --vault Personal op item get "AWS" --vault Personal -
Track the template in your dotfiles (it contains no real secrets):
config add ~/.aws/credentials.tpl -
Run the secrets script to inject values:
source ~/scripts/secrets.shThis also runs automatically during
init.sh.
Project-specific files (secrets, local config, editor settings) are stored in ~/.project-templates/ and copied into ~/dev/ using the create_project_files script.
The directory structure under ~/.project-templates/<project>/ mirrors the project layout:
~/.project-templates/sanctum/otter/
.env.tpl # template: secrets injected via op
.claude/settings.local.json # raw file: copied as-is
settings/local.py # raw file: copied as-is
.tplfiles are processed withop inject-- use{{ op://Vault/Item/Field }}for secrets- All other files are copied directly
-
Create a template directory matching your project path:
mkdir -p ~/.project-templates/sanctum/otter -
Add files. For secrets, use a
.tplextension:# ~/.project-templates/sanctum/otter/.env.tpl DATABASE_URL={{ op://Development/Otter/database_url }} STRIPE_SECRET_KEY={{ op://Development/Otter/stripe_secret_key }} RAILS_ENV=development PORT=3000
For non-secret files, just place them directly:
cp ~/dev/sanctum/otter/.claude/settings.local.json \ ~/.project-templates/sanctum/otter/.claude/settings.local.json -
Track in your dotfiles:
config add ~/.project-templates/sanctum/otter/ -
Populate the project:
create_project_files sanctum/otter
- System secret templates (e.g.
~/.aws/credentials.tpl) live next to the real config file and are injected byscripts/secrets.sh - Project files live in
~/.project-templates/<project-path>/and are populated bycreate_project_files - Only
.tplfiles inside hidden directories under$HOME(max depth 3) are discovered automatically bysecrets.sh - Never track real files containing secrets in source control
| Alias | Command | Description |
|---|---|---|
config |
git --git-dir=$HOME/.dotfiles/ ... |
Manage dotfiles repo |
vim |
nvim |
Neovim |
cat |
bat |
Syntax-highlighted cat |
ls |
eza |
Modern ls |
ll |
eza -l |
Long listing |
la |
eza -la |
Long listing with hidden files |
tree |
eza --tree |
Tree view |
lg |
lazygit |
Git TUI |
lazycfg |
lazygit for dotfiles | Manage dotfiles in lazygit |
s |
bundle exec rspec spec |
Run RSpec tests |
rspec |
bundle exec rspec |
Run RSpec |
dbm |
bundle exec rails db:migrate |
Rails migrations |
mux |
tmuxinator |
Tmux session manager |
dcw |
docker compose watch |
Docker Compose watch mode |
brewdump |
brew bundle dump ... |
Update Brewfile |
To pull the latest dotfiles and re-run setup on a machine that's already configured:
source ~/scripts/refresh.shThis fetches and runs init.sh from the remote, which handles pulling updates, reinstalling packages, and re-injecting secrets.