My complete dev environment for a fresh macOS or Linux machine. Shell, editor, multiplexer, prompt, and supporting CLI tools — installed and configured with one command.
The repo splits by role, not by OS. OS is not what varies across this fleet: the two Macs are ~95% identical to each other, the four Linux nodes are ~95% identical to each other, and what actually differs is whether a human is sitting in front of the machine.
base/ every machine zsh git tmux starship nvim claude
workstation/ human in front aerospace sketchybar skhd ghostty clangd Brewfile macos
server/ headless overrides only (deliberately near-empty)
services/ opt-in jobs lifeos wnba
hosts/ per machine type studio.zsh macbook.zsh layers.conf
lib/ the apply engine engine reloads render packages agents
manifest.conf which file goes where
install.sh bootstrap a machine, then apply
apply.sh put config in place; idempotent, safe to re-run
sync.sh pull/push + apply, every 30 min via launchd
Layers compose. A machine gets the union of everything listed for its type
in hosts/layers.conf — the Mac Studio is base workstation services, because
it is both the daily driver and the host of the lifeos server, three WNBA sync
jobs and MLX inference.
manifest.conf is the single source of truth. Adding a config file is a new
row, not new code:
# layer mode source dest reload
base link base/zsh/zshrc ~/.zshrc -
workstation copy workstation/aerospace/aerospace.toml ~/.config/aerospace/aerospace.toml aerospace
| Mode | Behavior | Why |
|---|---|---|
link |
symlink | the default — edits on either side stay in sync |
copy |
copy, gated on the source hash moving | aerospace.toml is tracked and rewritten at runtime by set-main-display.sh; a symlink would aim those writes at the git repo |
seed |
copy only if absent | displays.env holds this machine's display IDs |
render |
generate from a template | clangd needs the installed GCC's paths |
tree |
recursive | claude/rules, claude/skills |
agent |
install + reload a launchd plist | |
exec |
executable symlink onto PATH | newproj, yt-transcribe |
git clone https://github.com/ostepan8/dotfiles.git ~/dotfiles
cd ~/dotfiles
echo studio > ~/.dotfiles-host # or macbook, gpu1, fedora... see hosts/layers.conf
./install.sh~/.dotfiles-host holds a machine type, not a device id, and lives outside the
repo so it never syncs. A machine with no marker gets base only — a safe
default, so a new server node comes up with a working shell and nothing GUI is
ever attempted on a headless box.
Both install.sh and apply.sh are idempotent and safe to re-run. Existing
files are backed up once to <name>.backup before being replaced.
make dry-run # show what would change on this machine
make apply # apply
make test # secret scan + engine verificationapply.sh copies; it does not remove. Deleting a skill or a rule set from the
repo therefore leaves it live on every machine forever — which is how a roku
skill whose every path was dead survived months after the tool it pointed at
was gone, still being loaded and still being believed.
--prune removes tree entries the source no longer has. Always look first:
./apply.sh --prune --dry-run # list what would go
./apply.sh --prune # asks per entry
PRUNE_YES=1 ./apply.sh --prune # no prompts, for a list you have already readIt is deliberately hard to fire by accident, because the list is not always what you expect:
- Top level only. Destinations legitimately hold files the source does not
— state written at runtime (
govee/scripts/keychain.py,__pycache__), thebulbs.txtaseedrow places inside a tree, macOS droppings. A full-depth prune deletes all of it. A top-level entry is one whole unit — one skill, one rule language — and its absence from the source is a real deletion. - Entries another manifest row owns are kept, and say so.
- Dotfiles are skipped.
- It confirms every deletion, and refuses entirely when stdin is not a
terminal unless
PRUNE_YES=1.sync.shrunsapply.shwith no flags from launchd every 30 minutes, so the scheduled path can never delete — but a hook or a script invoking apply.sh wrongly must not be able to either. ~/.claude-personal/skillsalso holds entries the plugin manager owns, which dotfiles has never heard of and which look exactly like a deleted skill. They are plugin-managed and recreate themselves, but read the list before answeringy.
base/nvim/lazy-lock.json is committed — the lockfile, never the plugin
sources. :Lazy restore pins any machine to the same set. Without it, every
machine resolved plugin versions independently on first launch and drifted.
Nothing machine-specific enters this repo — it is public. The pattern is a
tracked *.template plus an untracked real file:
~/.config/nephos/env— fleet addresses and endpoints~/.config/aerospace/displays.env— this machine's display IDshosts/<type>.zsh— config shared by all machines of one type
make check scans every tracked file for credentials, the personal domain, the
tailnet and RFC1918 addresses before a push, and make hooks installs it as a
pre-push hook. Every pattern carries a sample it must match, so a rule that
silently stops matching fails the run instead of reporting "clean".
| Tool | Purpose |
|---|---|
| neovim | Editor |
| tmux | Terminal multiplexer |
| ghostty | Terminal emulator (replaces Terminal.app; default handler for .sh/.command/.tool/.zsh/.bash) |
| skhd | Global hotkey daemon (Opt+Space → open Ghostty) |
| aerospace | Tiling window manager (keyboard-driven workspaces + window arrangement) |
| sketchybar | Custom top bar with workspace indicators + system stats |
| duti | Set default app handlers for file types (used to make Ghostty the default terminal) |
| dockutil | Manage Dock items from the command line |
| starship | Shell prompt |
| antidote | zsh plugin manager |
| fzf | Fuzzy finder (Ctrl+R, Ctrl+T, Alt+C) |
| zoxide | Smart cd replacement (z <fragment>) |
| atuin | Shell history database (fuzzy Ctrl+R, synced, timestamped) |
| fd | Fast file search |
| ripgrep | Fast text search |
| bat | cat with syntax highlighting |
| eza | Modern ls replacement |
| jq | JSON parser |
| lazygit | Git TUI |
| git-delta | Better git diffs |
| gh | GitHub CLI |
| uv | Python package and tool runner (runs the Blender MCP server) |
| blender | 3D creation suite with the Blender MCP add-on preinstalled |
| govee | Secure Govee lamp control through local LAN and cloud APIs |
base/zsh/zshrc is fully shared and identical on every machine. For config that should only apply to one type of machine (e.g. a local model path that only exists on the Mac Studio), add it to hosts/macbook.zsh or hosts/studio.zsh instead of the shared file — both are still committed and synced normally.
Each machine picks which one to load via a one-line marker file that lives outside the repo (so it's never synced):
echo studio > ~/.dotfiles-host # on the Mac Studio
echo macbook > ~/.dotfiles-host # on the MacBookThe marker holds a type, not a unique device id — a second Mac Studio just gets the same echo studio > ~/.dotfiles-host and immediately inherits every studio-only setting, since hosts/studio.zsh syncs like any other file in the repo. Add more types (e.g. hosts/work-laptop.zsh) the same way — no changes to zshrc, apply.sh, or install.sh needed.
nvm and conda are stubbed and only loaded on first use. Shell starts in ~120ms instead of ~800ms. Force-load anytime with load_nvm or load_conda.
50k entries, shared across all open terminals in real time, deduplicated. Prefix a command with a space to keep it out of history (use for secrets).
- zsh-autosuggestions — type and see a gray-ghost suggestion from history.
→accepts. - fzf-tab —
<Tab>opens a fuzzy-searchable picker for completions. - zsh-syntax-highlighting — colors commands as you type (red = typo).
- zsh-history-substring-search — type a fragment, then
↑/↓cycles through every matching history entry.
| Key | Action |
|---|---|
Ctrl+R |
Fuzzy history search |
Ctrl+T |
Fuzzy file picker (inserts path) |
Alt+C |
Fuzzy cd |
z <fragment> jumps to the most-visited directory matching the fragment. zi opens an interactive picker. Learns as you use regular cd.
| Alias | Expands to |
|---|---|
ll / la |
ls -lh / ls -lAh |
.. / ... / .... |
cd .. up 1/2/3 levels |
gs / gd / gco / gl |
git status / diff / checkout / pretty log |
reload |
source ~/.zshrc |
ccd / ccrd |
claude --dangerously-skip-permissions (new / resume) |
newproj / np |
New project: asks stack / GitHub repo / Claude profile / first task, then makes the dir + git + optional GitHub repo + tmux session + Claude + attached Ghostty window (newproj <name>; -y to skip the questions, --help for flags) |
nts / tns |
tmux new-session -s <name> |
ta [name] |
tmux attach (tab-completes live session names) |
tn |
Show the active Tailscale tailnet |
tn - |
Toggle to the other tailnet profile |
tn ls / tn <name> |
List profiles / switch to the one matching <name> (friendly names come from $TAILNET_ALIASES in the untracked ~/.zshrc.local) |
| Key | Action |
|---|---|
Opt+Space |
Open new Ghostty window (launches Ghostty if dead) |
Opt+` |
Toggle Ghostty drop-down quick terminal (only while Ghostty is running) |
Opt+B |
Chrome — personal profile (new window on current workspace) |
Opt+W |
Chrome — work profile (new window on current workspace) |
Opt+S |
Slack |
Opt+M |
Spotify |
Opt+E |
New Finder window |
| Key | Action |
|---|---|
Alt+1..9 |
Jump to workspace 1..9 |
Alt+Shift+1..9 |
Move current window to workspace N |
Alt+H/J/K/L |
Focus window left/down/up/right |
Alt+Shift+H/J/K/L |
Move window in that direction |
Alt+- / Alt+= |
Shrink / grow focused window |
Alt+Shift+= |
Balance window sizes |
Alt+/ |
Toggle tile orientation (horizontal/vertical) |
Alt+, |
Accordion stack layout |
Alt+F |
Fullscreen focused window |
Alt+Shift+Space |
Toggle float/tile on focused window |
Alt+Tab |
Jump to previous workspace (last-used) |
Alt+Shift+C |
Reload Aerospace config |
Conflicts with the tmux config:
- tmux
Alt+1-9(window switch) → shadowed by Aerospace. Use tmux'sprefix + Ninstead. - tmux
Alt+h/j/k/l(pane switch) → shadowed by Aerospace. UseCtrl+h/j/k/l(vim-tmux-navigator) instead.
After installing, grant Accessibility permission (System Settings → Privacy & Security → Accessibility) to:
skhd(at/opt/homebrew/bin/skhd)AeroSpace(at/Applications/AeroSpace.app)
If using 2+ external monitors with Aerospace + sketchybar, arrange all externals to the LEFT of the main laptop display in System Settings → Displays. Externals positioned to the right of main tend to cause flickering / Spotify-type glitches during workspace switches. This is a macOS coordinate-system quirk (negative X coords work more reliably than positive ones for tiling WMs).
install.sh sets Ghostty as the default app for .sh/.command/.tool/.zsh/.bash files automatically (via duti). To also make Ghostty the external terminal in editors:
Cursor / VS Code — add to settings.json:
"terminal.external.osxExec": "Ghostty.app",
"terminal.explorerKind": "external"IntelliJ IDEA / JetBrains — Preferences → Tools → Terminal → set Shell path to /Applications/Ghostty.app/Contents/MacOS/ghostty (affects internal terminal). For "Open in Terminal" external calls, create an External Tool pointing at Ghostty.
Dock — remove Terminal.app, add Ghostty: dockutil --remove Terminal; dockutil --add /Applications/Ghostty.app.
| Key | Action |
|---|---|
| Alt+1-9 | Jump to window N |
| Shift+Left/Right | Prev/next window |
| Ctrl+a then Tab | Last window |
| Alt+h/j/k/l | Switch panes |
| Ctrl+a then | | Split horizontal |
| Ctrl+a then - | Split vertical |
| Ctrl+a then r | Reload config |
| Key | Action |
|---|---|
| Space+f | Find files |
| Space+g | Live grep |
| Space+e | Toggle file tree |
| Space+b | List buffers |
| gd | Go to definition |
| K | Hover docs |
| Space+rn | Rename symbol |
| Space+ca | Code actions |