Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

dotfiles

Personal dotfiles optimized for macOS and zsh. Features modern shell tools, comprehensive git configuration, and a customized development environment.

Installation

curl -fsSL https://mise.run | sh
git clone https://github.com/ericboehs/dotfiles ~/Code/github.com/ericboehs/dotfiles
cd~/Code/github.com/ericboehs/dotfiles && mise trust && mise bootstrap

Install mise from mise.run, not Homebrew. mise bootstraps itself, so it is deliberately absent from [bootstrap.packages] — whichever copy you install by hand is the one that runs. Homebrew's build disables mise self-update and lags upstream, and because it lands in /opt/homebrew/bin instead of ~/.local/bin it papers over a real bug: mise's rubygems plugin shells out to mise reshim after installing any gem with executables, so mason's gem-backed packages fail on a mise.run machine unless that directory is on PATH.

Setup is declared in mise.toml rather than scripted, so it converges — re-running only changes what has drifted. Requires mise 2026.8.4 or newer for the per-package os filters; older versions say so and stop. Useful variations:

mise bootstrap -n # preview every change, touch nothing
mise bootstrap --only dotfiles # just the $HOME symlinks
mise bootstrap --skip macos-defaults # leave system preferences alone
mise bootstrap dotfiles status # what's linked, and what has drifted

Dotfile linking is all-or-nothing: if $HOME already has real files where symlinks belong, mise names them and refuses the whole step. --force-dotfiles overrides that, but it replaces those files rather than merging them — on a machine where Homebrew or rbenv had written their own .zprofile, that content is gone. Move anything you want to keep aside first.

One caveat on convergence: the tools step runs before the bootstrap task, and a single failed download there — mise resolves runtime versions from GitHub's releases API, so a GitHub incident is enough — aborts the run before the task ever starts. The output still reads like a finished bootstrap. If neovim and tmux look unwarmed, re-run; it picks up where it left off.

Configure git with your personal information:

cp ~/.gitconfig.private.example ~/.gitconfig.private
$EDITOR~/.gitconfig.private

Core Components

Shell (zsh)

  • Prompt: Starship - Fast, customizable prompt
  • Syntax highlighting: fast-syntax-highlighting
  • Autosuggestions: zsh-autosuggestions
  • Smart cd: zoxide - Directory jumper that learns your habits
  • Abbreviations: Custom expansion system (see .zsh/abbreviations.zsh)
    • Type abbreviation + space/enter to expand
    • Extensive git shortcuts (e.g., gcogit checkout, gsgit status)
    • GitHub CLI helpers for PRs and workflow runs
  • Notifications: Auto-notify for long-running commands

Editor (Neovim)

  • Configuration: LazyVim
  • Location: .config/nvim/
  • Custom plugins for CSV, Markdown, Tailwind, and GitHub integration

Terminal Multiplexer (tmux)

  • Prefix: Ctrl-B (default)
  • Plugins: tmux-yank, tmux-copycat
  • Features:
    • Vi-mode copy/paste
    • Mouse support
    • Activity and bell monitoring
    • Custom status line with zoom indicator
    • Auto-renumber windows

Version Management and Setup

  • Tool: mise (replaces asdf)
  • Manages Node.js, Ruby, Python, and other language runtimes
  • Also drives machine setup — packages, $HOME symlinks, macOS defaults and git checkouts are all declared in mise.toml and applied with mise bootstrap
  • bin/dotfiles-link-check verifies the result: every path the [dotfiles] table manages, plus the links the bootstrap tasks make themselves, is still a symlink pointing where it should. Writing over a link rather than through it (jq … > tmp && mv tmp ~/.gitconfig) works until the next bootstrap renames the file to .bak and relinks, at which point the change is gone and its cause is days old. The pre-dotfiles hook runs the audit before that rename, and it takes a path prefix to scope it: dotfiles-link-check ~/.pi
  • It checks the reverse too, which is the quieter failure: config tracked here that no [dotfiles] entry links. That works perfectly on the machine where it was set up by hand and is simply absent on the next one, and nothing about the working machine says so — the first run found three, including a Hammerspoon config that had been tracked for two years and linked by hand. .dotfiles-unmanaged lists what is unlinked on purpose (the README, the test suite, per-host settings) with the reason.

Pi coding agent

  • Pi itself and its extension packages are version-pinned through mise.
  • Stable configuration and local extensions live in .pi-agent/. Settings are per host (settings.<hostname>.json) because pi rewrites them at runtime; bootstrap:pi links the right one and seeds new machines from settings.default.json.
  • bootstrap:pi installs and verifies the pinned package set without tracking credentials, sessions, caches, or downloaded package contents.
  • bin/pi-pin moves a pinned package to a new commit in every profile at once and then reconciles the clone pi actually loads. A pin is two pieces of state — the sha in settings.<host>.json and the checkout under ~/.pi/agent/git — and pi only reconciles the second on pi update --extensions, so a bumped pin otherwise keeps running the old code until somebody notices. pi-pin with no arguments lists both shas side by side and exits non-zero on a stale clone.
  • bootstrap:pi then runs bin/pi-bundle, which bundles pi's ~200-module Node build into one file and points the pi bin at bin/pi-launch. Worth ~115ms per launch (716ms → 602ms to first frame here, 738ms → 616ms on Linux), plus another 32ms from PI_BUNDLE_NO_BEDROCK=1, which drops the AWS SDK that neither machine authenticates. The launcher falls back to the stock entrypoint if the bundle is missing or older than the package, so an upgrade costs speed rather than a working pi; pi-bundle --off reverts and PI_NO_BUNDLE=1 skips it for one launch.

Fuzzy Finder

  • Tool: fzf
  • Keybindings:
    • Ctrl-R: Command history search (with regex support)
    • Ctrl-T: File search
  • Enhanced with preview windows and custom options

Git

  • Pager: Delta - Syntax-highlighted diffs
  • Features:
    • GPG signing enabled
    • Conditional includes for different organizations
    • GitHub CLI credential helpers
    • Verbose commits
    • Rebase by default for pulls

Utilities

Enhanced replacements for common commands:

  • lslsd (modern ls with icons and colors)
  • cdzoxide (smart directory jumping)

Bin Scripts

Collection of utility scripts in bin/ including:

  • Claude Code helpers: claude-man, claude-notify, claude-resume, claude-watcher
  • GitHub CLI extensions: gh-pm, gh-reruns, gh-reviews-by-user, gh-labeler, ghb
  • Tmux utilities: toggle_notes_pane, monitor_tmux_pane, notes
  • Development tools: refresh_safari, colors, true-colors, utcdate
  • Pi: pi-bundle (faster startup), pi-launch, pi-pin (bump a pinned package everywhere and reconcile its clone), pi-ext-check (typecheck + test extensions), pi-ext-prepush (pre-push hook running that check on the pushed sha)
  • Setup integrity: dotfiles-link-check (every managed path is still the symlink bootstrap made), pi-profile-check (that, plus packages a local extension has replaced)
  • Throwaway macOS VMs: vm (see below)

Throwaway macOS VMs

bin/vm wraps tart to give you disposable macOS guests on Apple Silicon — useful for testing this bootstrap against a genuinely clean machine.

brew install cirruslabs/cli/tart
vm new # clone a fresh VM named "clean"
vm up # boot it in a GUI window (-d to detach)
vm ssh # shell in as admin, no password
vm reset # wipe it and re-clone — back to pristine, ~3 seconds
vm bootstrap # clone these dotfiles into it and run `mise bootstrap`
vm ls / vm ip / vm down / vm rm / vm seed

vm bootstrap is the point of the whole thing: it installs mise, clones this repo and converges it, so a cold run proves the bootstrap works on a machine that has never seen it. --fresh resets the VM first, --ref <branch> picks the branch, and anything after -- is passed through to mise bootstrap:

vm bootstrap --fresh --ref my-branch # cold run, ~4 minutes
vm bootstrap -- --only dotfiles # just the symlinks

It clones from origin, not your working tree, so uncommitted work is invisible to it — push the branch first. --force-dotfiles is the default here, because the base image ships its own ~/.gitconfig and ~/.zprofile and the all-or-nothing dotfiles step would otherwise abort every run.

Every command takes an optional VM name, so vm new sandbox && vm up sandbox runs a second one alongside. Defaults come from VM_CPU, VM_MEM, VM_DISK, VM_NAME, VM_DISPLAY, VM_SSH_KEY, VM_BASE_OCI, VM_REPO_URL, and VM_REPO_PATH.

Two golden images sit behind this and are never booted for day-to-day work. sequoia-base is the pulled upstream image; sequoia-base-keyed is a clone of it with your public key appended to authorized_keys, built once by vm seed. new and reset clone from the keyed image, which is why a reset VM is both instant and still passwordless — pushing a key per-VM would mean re-injecting it after every reset. Re-run vm seed after changing VM_SSH_KEY.

Clones are APFS copy-on-write, so a 28GB VM costs almost no disk until it diverges. Treat these as disposable rather than something to repair.

Worth knowing, both imposed by Apple's Virtualization.framework: at most two macOS guests may run at once, and guests cannot sign in to iCloud or the App Store. Anything needing an Apple ID has to be tested on real hardware.

Keybindings

Zsh

  • Esc: Enter vi command mode
  • Ctrl-Y: Copy current command to clipboard
  • Ctrl-R: Fuzzy search command history
  • Ctrl-Alt-L: Clear screen (zsh built-in; prompt only — see Ctrl-Shift-L for anything else)

Tmux

  • Ctrl-h/j/k/l: Navigate between vim and tmux panes
  • Ctrl-Shift-L: Clear screen and scrollback
  • Alt-h/l: Previous/next window
  • Alt-j/k: Previous/next session
  • Prefix+C: Open pi in a new window
  • Prefix+r: Resume the pi/claude session that just quit in this pane

Directory Structure

.
├── .config/nvim/ # Neovim configuration
├── .zsh/ # Zsh configuration modules
│ ├── abbreviations.zsh
│ ├── keybindings.zsh
│ ├── history.zsh
│ └── fzf.zsh
├── bin/ # Utility scripts
├── .gitconfig # Git configuration
├── .tmux.conf # Tmux configuration
├── .zshrc # Zsh initialization
└── mise.toml # Declarative machine setup (`mise bootstrap`)

License

MIT

About

The curated tool configurations which gives me my super powers.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages