My personal environment, managed with chezmoi. On a fresh
machine chezmoi apply links every config, runs the one-time installs, generates
machine-local bits, and installs public packages in one command on macOS, Linux,
and Windows. The private secrets companion has a separate bootstrap.
Do not add personal customization when a default or widely adopted standard already solves the problem. Prefer, in order:
- built-in/default behavior
- official or widely adopted integration/plugin
- minimal configuration
- custom scripts/keymaps only when the above are insufficient
Custom configuration should have a concrete reason to exist. Avoid duplicating a tool's defaults, introducing new keybindings without a clear benefit, or keeping integration layers that are no longer used.
The previous shell /
bootstrap.shversion (symlink-based) is preserved at thepre-chezmoigit tag — restore withgit checkout pre-chezmoi.
macOS / Linux
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
if [ -x /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
brew install chezmoi
git clone <repo> ~/dotfiles
# one-time: generate ~/.config/chezmoi/chezmoi.toml — prompts once for your
# git name/email and derives sourceDir from where you cloned (~ is expanded)
chezmoi init --source ~/dotfiles
chezmoi applyWindows (PowerShell) — scaffolded, verify on a real machine
# Enable Windows Developer Mode first (required for the Neovim symlink).
winget install Git.Git twpayne.chezmoi
# clone, then `chezmoi init --source <path>` (prompts, writes chezmoi.toml), then:
chezmoi apply- Setup walkthrough — including secret handling (SSH keys,
~/.secrets, identity): SETUP.md - Tool and plugin review: RECOMMENDATIONS.md
The chezmoi source lives in chezmoi/ (a home-mirror tree). chezmoi apply
turns it into your home directory:
| chezmoi source | becomes | kind |
|---|---|---|
dot_zshrc, dot_gitconfig, dot_tmux.conf, … |
~/.zshrc, ~/.gitconfig, … |
managed file (copy) |
private_dot_ssh/ |
~/.ssh/ (0700) |
managed files; keys stay per-machine |
dot_config/symlink_nvim.tmpl |
~/.config/nvim → nvim/lazy |
symlink |
symlink_dot_hammerspoon.tmpl |
~/.hammerspoon → hammerspoon/ |
symlink |
dot_claude/ |
selected files in ~/.claude/ |
managed files |
run_once_* / run_onchange_* |
packages, TPM plugins, and mise runtimes | scripts |
Managed vs symlink: big, live-edited config directories (nvim/,
hammerspoon/) stay as their own folders and are symlinked. Claude's runtime
directory stays local while chezmoi manages only its public config files. Everything
else is a managed file; edit with chezmoi edit --apply ~/.zshrc.
| Folder | What |
|---|---|
chezmoi/ |
chezmoi source — managed dotfiles, OS-branched via .chezmoiignore.tmpl |
nvim/ |
Neovim — lazy/ LazyVim → ~/.config/nvim |
hammerspoon/ |
macOS automation — symlinked to ~/.hammerspoon |
packages/ |
package manifests — Brewfile (macOS/Linux), casks (macOS), Scoop (Windows) |
scripts/ |
tooling: key generation, workspace identity, and Windows installers |
vim/ |
legacy Vim config snapshots (not linked; kept for reference) |
chezmoi/.chezmoiignore.tmpl branches by OS. Linux skips macOS-only AeroSpace,
Hammerspoon, and casks. Windows uses AppData for Neovim, installs shared CLI tools
with Scoop, and manages Git Bash startup files alongside a PowerShell 7 all-hosts
profile. Terminal appearance remains host-specific. Windows is scaffolded and
must still be verified on a real machine after the migration.
Nothing private lives in this repo. chezmoi apply installs only the public config.
The private secrets companion (real SSH hosts, nvim DB connections, ~/.pgpass) owns
its own bootstrap — clone it and run its ./setup.sh; see that repo's README for the
command. Public clones simply skip it. See SETUP.md.
Comments inside the config files are in Korean, but the configs themselves are language-agnostic — the setup works the same regardless.
- Use this when you want to check whether
dot_tmux.conf.localhas drifted from upstream OMT defaults.
git diff --no-index --color=always \
<(curl -fsSL 'https://raw.githubusercontent.com/gpakosz/.tmux/master/.tmux.conf.local') \
chezmoi/dot_tmux.conf.local- If you only want to compare the currently applied file with upstream:
git diff --no-index --color=always \
<(curl -fsSL 'https://raw.githubusercontent.com/gpakosz/.tmux/master/.tmux.conf.local') \
~/.tmux.conf.local-
No output means the files match.
-
git diff --no-indexexits with status1when differences are found, which is expected and does not indicate command failure. -
If you want to force
deltaeven when your shell decides to suppress pager output (eg. inside pipes), use:
git diff --no-index --color=always \
<(curl -fsSL 'https://raw.githubusercontent.com/gpakosz/.tmux/master/.tmux.conf.local') \
chezmoi/dot_tmux.conf.local \
| delta- If you want a plain change summary only:
import urllib.request
import difflib
upstream_url = 'https://raw.githubusercontent.com/gpakosz/.tmux/master/.tmux.conf.local'
up = urllib.request.urlopen(upstream_url).read().decode().splitlines()
with open('chezmoi/dot_tmux.conf.local', 'r', encoding='utf-8') as f:
local = f.read().splitlines()
diff_lines = list(difflib.unified_diff(up, local, lineterm=''))
print('added', sum(1 for l in diff_lines if l.startswith('+') and not l.startswith('+++')))
print('removed', sum(1 for l in diff_lines if l.startswith('-') and not l.startswith('---')))