Configuration files for bash, zsh, vim, git, and more. Compatible with
macOS and Arch Linux (Omarchy).
Installation requires bash, curl, and git.
git clone git@github.com:sidwood/dotfiles.git
cd dotfiles
./install.shThe install script presents an interactive menu where you can select which components to install. The available options vary by platform:
Select installations (↑/↓/k/j navigate, Space toggle, Enter confirm):
> [x] Install Homebrew packages and applications.
[x] Apply macOS system defaults.
[x] Symlink dotfile packages with GNU Stow.
[x] Set up mise with default runtimes.
[x] Install global npm packages.
[x] Install vim plugins.
Select installations (↑/↓/k/j navigate, Space toggle, Enter confirm):
> [x] Install Omarchy packages and standalone tools.
[x] Symlink dotfile packages with GNU Stow.
[x] Set up mise with default runtimes.
[x] Install global npm packages.
[x] Install vim plugins.
Use arrow keys or j/k to navigate, space to toggle options, and enter to
confirm.
On Omarchy, most packages are installed with pacman or AUR helpers. Heroku CLI
uses Heroku's official standalone installer so it can track upstream CLI
releases without waiting on AUR package updates.
The dotfiles are organized as GNU Stow
packages. Each top-level directory is a package that gets symlinked to $HOME.
dotfiles/
├── agents/ # Global agent memory shared by every AI harness
├── bash/ # Bash config (macOS only, skipped on Omarchy)
├── bin/ # Custom executables on PATH via ~/.local/bin
├── ghostty/ # Ghostty terminal config
├── git/ # Git config and global ignore
├── htop/ # htop process viewer config
├── iterm2/ # iTerm2 terminal config (macOS)
├── misc/ # Miscellaneous dotfiles (.editorconfig, .agignore, etc.)
├── mutt/ # Mutt email client config
├── npm/ # NPM configuration
├── nvim/ # Neovim config (Lua with lazy.nvim)
├── profile/ # Shared environment variables (.profile)
├── ruby/ # Ruby/Bundler/IRB config
├── shell/ # Unified shell config (aliases, functions, init)
├── ssh/ # SSH config (1Password agent)
├── tmux/ # Tmux config
├── vim/ # Vim config and plugins (vim-plug)
├── yazi/ # Yazi file manager config
└── zsh/ # Zsh config with zinit and powerlevel10k
The shell/ package provides cross-platform configuration that works in both
bash and zsh:
~/.config/shell/aliases- Common aliases~/.config/shell/functions- Utility functions~/.config/shell/init- Tool integrations (mise, zoxide, fzf)
cd /path/to/dotfiles
./uninstall.shThe uninstall script presents a similar interactive menu:
Select uninstallations (↑/↓/k/j navigate, Space toggle, Enter confirm):
> [x] Uninstall Homebrew packages and applications.
[x] Reset macOS system defaults.
[x] Remove dotfile package symlinks with GNU Stow.
[x] Uninstall vim plugins.
Select uninstallations (↑/↓/k/j navigate, Space toggle, Enter confirm):
> [x] Uninstall Omarchy packages and standalone tools.
[x] Remove dotfile package symlinks with GNU Stow.
[x] Uninstall vim plugins.
An alternative to git worktree, following Avdi Grimm's
You probably don't need git worktrees.
Worktrees bake absolute paths into config files, which breaks containers and
makes them awkward to move. A plain local clone has none of those problems, and
git hardlinks the object files when cloning locally — so a second copy of a repo
is near-instant and costs almost no disk.
The bin/ package wraps the bookkeeping in five commands:
gbca ~/code/myapp feature-x # clone ~/code/myapp -> ~/code/myapp.feature-x
gbc # list branch clones, with status and ahead/behind
gbc --pr # same, plus each branch's PR state from gh
gbcd # fzf-pick a branch clone and cd into it
gbcs # re-sync ignored local files from the source
gbcr ~/code/myapp.feature-x # remove one clone, with safety checks
gbcp --dry-run # show which clones have landed and can go
gbcp # remove the clones whose PRs merged or closedEach has a long form as a git subcommand — git bc-add, git bc-list,
git bc-sync-extras, git bc-rm, git bc-prune. Use -h for usage: git
intercepts --help on a subcommand and looks for a man page instead.
gbca clones from a local seed, repoints origin at the real remote so nothing
depends on the seed, and copies across the gitignored files a fresh clone needs
(.env and variants, .envrc, Rails credentials keys,
docker-compose.override.yml, Procfile.local, mise local config). Rebuildable
trees like node_modules are never copied.
The seed's own copy of a branch always wins, unpushed commits included. If the
seed has the branch but is parked elsewhere, gbca checks it out in the seed
before cloning and restores the previous branch afterwards; a seed with
uncommitted changes is refused rather than disturbed. This matters because a
local clone only materialises the source's current branch — the rest arrive as
remote-tracking refs, which the first fetch --prune deletes whenever the server
has never seen them. Branches that exist only on the server are tracked as
normal, and a name that exists nowhere starts a new branch off the seed's HEAD.
A clone is marked as a branch clone by a bc.source entry in its local git
config. Base clones lack it, so gbcr and gbcp will not touch them. gbcr
also refuses a clone with uncommitted work, one holding commits that exist
nowhere else, or the one the current shell is sitting in — --force overrides
the first three, nothing overrides the last.
"Nowhere else" counts the seed, not just the remote. A branch cloned before it
was pushed sits above every remote ref while risking nothing, since the seed
still holds every commit; refusing there would mean reaching for --force on
routine cleanup, which is how the one clone that mattered eventually gets
deleted. Commits absent from both the remote and the seed still block removal,
and a seed that has since been moved or deleted falls back to comparing against
the remote alone.
Three per-repo config keys, all set on the base clone:
bc.source- written bygbca; marks a directory as a branch clonebc.postadd- command run in a new clone once the branch is checked outbc.extras- extra file patterns forgbcsto copy (repeatable)
git -C ~/code/myapp config bc.postadd 'mise install && npm ci'
git -C ~/code/myapp config --add bc.extras 'config/local.yml'Two layers, both plain markdown so any agent harness can read them:
AGENTS.mdin this repo (symlinked asCLAUDE.md) — conventions for working on the dotfiles themselves. Only loads when an agent works in this repo.agents/.config/agents/AGENTS.md→~/.config/agents/AGENTS.md— global memory, loaded in every session in every repository. Machine-wide tooling likegit bc-*is documented here, since a project-scoped file cannot advertise something installed to$HOME.
Every harness reads a different filename in a different directory. Rather than
keeping a copy per tool, install.sh symlinks each one at the single canonical
file:
~/.config/agents/AGENTS.md # canonical, the only file to edit
├── ~/.claude/CLAUDE.md # Claude Code
├── ~/.codex/AGENTS.md # Codex
├── ~/.grok/AGENTS.md # Grok
├── ~/.config/opencode/AGENTS.md # OpenCode
├── ~/.cursor/rules/global-agent-memory.mdc # Cursor
├── ~/.gemini/GEMINI.md # Gemini CLI
└── ~/.pi/agent/AGENTS.md # Pi
Add a harness by adding one <command>|<path> line to the harnesses table in
link_agent_memory(). Links are only created for harnesses that are actually
installed, so $HOME does not collect config directories for tools you do not
have. Re-running is a no-op, and any pre-existing real file is moved to .bak
before being replaced.
Every path was verified against the tool itself rather than assumed — Codex from
strings in its binary, Grok from its shipped docs/user-guide, OpenCode from its
config root, Pi from its agentDir default, Cursor and Kimi from their docs and
issue tracker. Two are worth knowing about:
- Cursor does not read a home-level
AGENTS.md. Its global rules are.mdcfiles in~/.cursor/rules/, which is where the link points. Cursor loads a rule in every chat only when it carriesalwaysApply: truefrontmatter, and the canonical file deliberately has none — frontmatter would be dead weight in every other harness's context window. If Cursor does not pick the rule up automatically, enable it in Cursor's own rules settings. - Kimi is deliberately absent. It only reads
AGENTS.mdfrom the working directory; global support is an open, unimplemented request (kimi-cli#2152). Linking~/.kimi/AGENTS.mdtoday would create a file nothing reads;link_agent_memory()records the line to add when it ships.
Keep the canonical file short and free of meta-commentary. Every agent loads it into context on every session, so it should carry only what an agent needs to act on — not notes about the file itself, which belong here instead. Anything that should not enter git history does not belong in it at all.
OpenSSH is installed by the package steps (Brewfile on macOS, openssh on
Omarchy). The SSH daemon is not enabled automatically on either platform.
sudo systemctl enable --now sshd
sudo ufw allow ssh # only if you need inbound SSH through the firewallTo disable later:
sudo systemctl disable --now sshd
sudo ufw delete allow sshPrefer Apple’s built-in Remote Login (system sshd), not Homebrew’s openssh
daemon:
- GUI: System Settings → General → Sharing → Remote Login
- CLI:
sudo systemsetup -setremotelogin on
Check status with sudo systemsetup -getremotelogin or
sudo launchctl print system/com.openssh.sshd. Turn it off with
sudo systemsetup -setremotelogin off.
Omarchy ships with a default LazyVim configuration
in ~/.config/nvim. This repo includes its own neovim config (custom lazy.nvim
setup in the nvim/ stow package), which conflicts with those files.
During installation, install.sh automatically backs up the Omarchy neovim
config before stowing:
~/.config/nvim → ~/.config/nvim.omarchy-backup
To restore the original Omarchy neovim config:
cd /path/to/dotfiles
stow -D -t "$HOME" nvim # remove dotfile symlinks
rm -rf ~/.config/nvim # clean up any leftover files
mv ~/.config/nvim.omarchy-backup ~/.config/nvimSome projects require secrets stored in 1Password (e.g., private GitHub Package
registries). The profile/.profile exports environment variables using
1Password secret references.
To inject secrets when running npm commands, use op run:
op run -- npm installThis replaces secret references (e.g., op://Personal/GitHub Registry Token/token)
with their actual values for the duration of that command.
This repo includes a template at shell/.config/shell/local.env.tpl for
machine-local API keys using 1Password secret references.
During ./install.sh, the script resolves that template with op run and
generates:
~/.config/shell/local.env
The generated file is sourced by shell/.config/shell/init, overwritten on
each install run, permissioned to 600, and ignored by git.