Personal dotfiles for macOS and Ubuntu/Debian, managed with chezmoi.
Source of truth: github.com/abkolan/dotfiles. Everything below is verified against the
files in this repository — if a command here disagrees with the source, the source wins and
the docs are a bug.
- What's Included
- Before You Start
- Install
- The Setup Prompts
- What Bootstrap Actually Does
- Verify Your Install
- Daily Workflow
- Shell Helpers
- Scripts
- Git Configuration
- Per-Tool Documentation
- Platform Notes and Known Limitations
- Troubleshooting
- Adding New Tools
Shell: ZSH with Zinit plugin management, Powerlevel10k prompt, turbo-mode lazy loading, fzf-tab completion, Atuin history.
Editor: Neovim (NvChad v2.5 base) with LSP, Telescope, FZF-Lua, Harpoon.
Git: delta pager, difftastic structural diffs, nvimdiff mergetool, ~30 aliases, per-account identity switching.
Terminals: Ghostty (with custom cursor shaders) and Kitty.
CLI tools: fd, fzf, ripgrep, bat, lsd, eza, zoxide, broot, lazygit, btop, zellij, atuin, direnv, hyperfine, gh.
macOS extras: LinearMouse, dark-mode/dock/audio toggle scripts.
Package lists are declared in Brewfile (macOS) and Aptfile
(Ubuntu/Debian). See Platform Notes — the two
lists are not at parity.
| Requirement | Notes |
|---|---|
| macOS or Ubuntu/Debian | Other Linux distros are not handled by the bootstrap scripts |
git and curl |
Pre-installed on macOS; installed by bootstrap on Ubuntu |
sudo access |
Needed for apt, for chsh, and to append to /etc/shells |
| A Nerd Font | Installed automatically on macOS; manual on Linux (see below) |
chezmoi 2.40.0 or newer is required — this is enforced by .chezmoiversion.
The bootstrap one-liner installs a current chezmoi for you.
This is a personal dotfiles repo, and some parts are wired to the owner's identity:
dot_gitconfig.tmpl routes commit identity through gh-abkolan / gh-abhilast SSH host
aliases, and .zshenv hardcodes a 1Password SSH agent socket. You can install it as-is and
it will work, but you will be prompted for email addresses for two GitHub accounts that
aren't yours.
If you want this as a starting point for your own setup:
# Fork on GitHub, then:
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply <your-github-username>Then read Git Configuration and adjust the identity rules.
# Fresh Mac — installs chezmoi, then Homebrew, then everything else:
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply abkolan
# If you already have Homebrew:
brew install chezmoi && chezmoi init --apply abkolansh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply abkolanRead the Ubuntu caveats before you rely on this — the apt package list is much smaller than the Homebrew one.
chezmoi init clones this repo to chezmoi's source directory, which by default is:
~/.local/share/chezmoi
That is the directory you edit. chezmoi apply copies rendered files from there into
$HOME. Use chezmoi cd to get there. If you want it somewhere else, pass
--source ~/path/of/your/choice to chezmoi init.
If you'd rather look before you leap:
chezmoi init abkolan # clone only, don't apply
chezmoi diff # show exactly what would change in $HOME
chezmoi apply # commit to itOn first init, chezmoi asks five questions (defined in
.chezmoi.toml.tmpl). Answers are saved to ~/.config/chezmoi/chezmoi.toml
and never committed.
| Prompt | Used for | If you don't care |
|---|---|---|
Git email address |
Default user.email in ~/.gitconfig |
Your everyday email |
Git user name |
Default user.name in ~/.gitconfig |
Your name |
Workspace path (default: ~/repos) |
Sets $WORKSPACE, which the wt worktree helper and pj project switcher use as their root |
Enter ~/repos |
Git email for the abkolan GitHub account |
Fills ~/.gitconfig-abkolan, used for repos whose remote matches the gh-abkolan SSH alias or plain git@github.com |
Repeat your main email |
Git email for the abhilast GitHub account |
Fills ~/.gitconfig-abhilast, used for repos whose remote matches the gh-abhilast SSH alias |
Repeat your main email |
The last two exist for multi-account Git identity switching. There is no default — chezmoi will wait for input, so a fully unattended install needs the values passed up front:
chezmoi init --apply abkolan \
--promptString email=you@example.com \
--promptString name="Your Name" \
--promptString workspacePath=~/repos \
--promptString abkolanEmail=you@example.com \
--promptString abhilastEmail=you@example.comTo change an answer later, edit ~/.config/chezmoi/chezmoi.toml and run chezmoi apply.
chezmoi runs the run_ scripts at the repo root in filename order:
| Order | Script | macOS | Linux |
|---|---|---|---|
| 1 | run_once_before_01-install-packages.sh.tmpl |
Installs Homebrew if missing | apt-get install curl git zsh build-essential |
| 2 | run_onchange_before_02-install-packages.sh.tmpl |
brew bundle from Brewfile |
apt-get install from Aptfile, plus snap install zellij if snap exists |
| 3 | (config files applied here) | — | — |
| 4 | run_once_after_10-neovim-plugins.sh |
nvim --headless "+Lazy! sync" +qa |
same |
| 5 | run_once_after_11-set-default-shell.sh.tmpl |
chsh to Homebrew zsh |
chsh to system zsh |
Notes on behaviour that surprises people:
- Step 2 is
run_onchange, keyed on a hash ofBrewfile/Aptfile. Edit either file and the nextchezmoi applyre-runs package installation automatically. - Steps 4 and 5 never fail the install. If Neovim isn't on PATH, or
chshneeds an interactive password it can't get, they print aWARNING:with the exact command to run by hand and exit 0. Read the bootstrap output. - Step 5 may prompt for your password twice: once for
sudo tee -a /etc/shells, once forchsh. - Zinit is not installed by these scripts.
~/.zshrcclones it on your first interactive shell, then downloads plugins. Your first new terminal will be slow and print install messages. That is expected; the second one is fast.
After bootstrap finishes, open a new terminal (or exec zsh) so the new shell config loads.
chezmoi doctor # environment health check
chezmoi verify # every managed file matches the source
chezmoi diff # should print nothing if there's no drift
zsh -n ~/.zshenv ~/.zshrc # syntax-check shell config
hyperfine 'zsh -i -c exit' # startup benchmark (target: <200ms)chezmoi doctor reporting warnings about optional tools is normal. Errors are not.
chezmoi edit ~/.zshrc # edit the source file behind ~/.zshrc
chezmoi diff # preview what would change
chezmoi apply # apply changes to $HOME
chezmoi cd # cd into the source directory
chezmoi update # git pull + apply (sync a second machine)
chezmoi managed # list every file chezmoi controlsEdit files in the chezmoi source directory, never in $HOME — chezmoi apply overwrites
$HOME copies. ~/.zshenv even carries a comment saying so.
To pull your changes back into git:
chezmoi cd
git add . && git commit -m "feat: ..." && git pushDefined in dot_zsh_aliases and dot_zsh_functions:
| Alias | Runs | Purpose |
|---|---|---|
pj |
project |
Fuzzy project switcher — fd+fzf over git repos in $WORKSPACE, ~/.local/share/chezmoi, ~/Projects |
wtn <branch> |
wtnew |
Create a git worktree for <branch> from the current repo |
wtl |
wtls |
List and jump between worktrees |
wtr <target> |
wtrm |
Remove a worktree |
prn |
prnow |
Push current branch and open a PR via gh |
dot_zsh_aliases and dot_zsh_functions hold many more. The full annotated reference —
aliases, functions, keybindings, completions, load order, performance — is in
ZSH.md.
Helper scripts live in scripts/.
These are not copied into $HOME. .chezmoiignore excludes scripts/, and instead
~/.zshenv appends chezmoi's source directory to PATH:
[[ -d "{{ .chezmoi.sourceDir }}/scripts" ]] && PATH="$PATH:{{ .chezmoi.sourceDir }}/scripts"The path is baked in at chezmoi apply time. If you relocate the chezmoi source directory,
re-run chezmoi apply to refresh it.
| Script | Purpose |
|---|---|
wt |
Git worktree manager (see below) |
benchmark.sh |
Shell startup benchmark (hyperfine, falls back to /usr/bin/time) |
profile-zsh-startup.sh |
Detailed zsh startup profiling via zprof |
ghostty-theme-switcher.sh |
List/apply Ghostty themes by rewriting ~/.config/ghostty/config |
mac/*.sh |
macOS toggles: dark mode, dock position/visibility, audio source |
wt new <repo> <name> [--base <remote-branch>] [--branch <branch-name>]
wt ls <repo>
wt rm <repo> <name>
wt prune <repo>
wt path <repo> <name>
wt helpLayout and defaults, straight from scripts/wt:
- Root directory:
$WT_ROOT, falling back to$WORKSPACE, falling back to~/repos.$WORKSPACEcomes from theworkspacePathsetup prompt. - Main repo checkout is expected at
$ROOT/<repo>. - New worktrees are created at
$ROOT/<repo>-wt-<name>. - Default base branch:
$WT_REMOTE_BASE, defaulting toorigin/main.
# With WORKSPACE=~/repos, this creates ~/repos/myapp-wt-hero-fix from origin/main
wt new myapp hero-fixdot_gitconfig.tmpl renders to ~/.gitconfig. It fills user.email / user.name from your
setup prompts and picks a delta light/dark theme by reading macOS AppleInterfaceStyle at
apply time.
Rather than switching identities by directory, this config keys off the remote URL:
| Remote URL matches | Identity file |
|---|---|
gh-abkolan:*/** |
~/.gitconfig-abkolan |
git@github.com:*/** (the default) |
~/.gitconfig-abkolan |
gh-abhilast:*/** |
~/.gitconfig-abhilast |
gh-abkolan and gh-abhilast are SSH host aliases, which must be defined in your
~/.ssh/config. That file is not managed by this repo, so the aliases won't exist on a
fresh machine until you add them:
Host gh-abkolan
HostName github.com
User git
IdentityFile ~/.ssh/id_abkolan
Host gh-abhilast
HostName github.com
User git
IdentityFile ~/.ssh/id_abhilastThen clone with git clone gh-abhilast:org/repo.git to get that identity. Without the SSH
config, plain git@github.com remotes still work and fall through to the abkolan identity.
If you forked this repo, replace these three includeIf blocks in dot_gitconfig.tmpl with
your own accounts, and update the matching prompts in .chezmoi.toml.tmpl.
| Alias | Command | Purpose |
|---|---|---|
st |
status -sb |
Short status |
ll |
log --oneline --graph --decorate -15 |
Compact log |
lg |
log --graph --pretty=<format> |
Decorated log with author/relative date |
co / br |
checkout / branch |
Switch branches / branch management |
cm / cam |
commit -m / commit -am |
Quick commit / commit all |
unstage / last |
reset HEAD -- / log -1 HEAD |
Unstage / show last commit |
d / dc |
diff / diff --cached |
View changes (through delta) |
dt / dtc |
difftool / difftool --cached |
Structural diff (through difftastic) |
s / sp / sl |
stash / stash pop / stash list |
Stash shortcuts |
f / fo |
fetch / fetch origin |
Fetch |
p / pl |
push / pull |
Push / pull |
rb / rbi / rbc / rba |
rebase variants |
Rebase, interactive, continue, abort |
brd / brD |
branch -d / branch -D |
Delete branch, safe / forced |
cleanup |
merged-branch cleanup | Delete branches merged into main/master/develop |
find |
log --grep |
Search commit messages |
- delta — pager for all git output. Side-by-side, line numbers,
navigateon. Light/dark chosen at apply time from macOS appearance; always dark on Linux. Re-runchezmoi applyafter switching your system theme. - difftastic — structural, AST-aware diff via
git dt/git dtc. - nvimdiff — 3-way mergetool for conflicts, with
diff3conflict style.
Other defaults worth knowing: pull.rebase = true, rebase.autoStash = true,
init.defaultBranch = main, diff.algorithm = histogram.
| Tool | Source | Docs |
|---|---|---|
| ZSH | dot_zshrc, dot_zsh_* |
ZSH.md — aliases, functions, load order, performance |
| Neovim | dot_config/nvim/ |
README — keybindings, plugins, LSP |
| Ghostty | dot_config/ghostty/ |
README — themes, shaders, splits |
| Kitty | dot_config/kitty/ |
README — shortcuts, themes, diff |
| Git | dot_gitconfig.tmpl |
above — aliases, delta, difftastic |
| Atuin | dot_config/atuin/ |
README — shell history sync |
| Broot | dot_config/broot/ |
README — tree navigation, verbs |
| Btop | dot_config/btop/ |
README — resource monitoring |
| Lazygit | dot_config/lazygit/ |
README — git TUI, diff pipeline |
| Zellij | dot_config/zellij/ |
README — multiplexer, keybindings |
| LSD | dot_config/lsd/ |
README — modern ls |
| LinearMouse | dot_config/linearmouse/ |
README — pointer/scroll tuning |
| Scripts | scripts/ |
README — utility scripts |
Per-tool READMEs are listed in .chezmoiignore, so they stay in the repo and are never
copied into ~/.config/.
These are real constraints in the current source, not aspirations.
dot_zshenv.tmpl statically exports HOMEBREW_PREFIX=/opt/homebrew and friends rather than
calling brew shellenv, deliberately, to save ~15ms per shell. On an Intel Mac, where
Homebrew lives in /usr/local, those exported variables will be wrong. Homebrew binaries
still resolve (/usr/local/bin is on the default PATH), but anything reading
$HOMEBREW_PREFIX breaks. Intel users should edit the prefix block in dot_zshenv.tmpl.
The bootstrap and chsh scripts do handle both prefixes correctly.
Aptfile installs only: direnv zsh fd-find fzf ripgrep bat eza tree neovim git jq curl wget btop, plus zellij via snap.
Relative to macOS, an apt install does not get you: lsd, zoxide, broot, lazygit,
git-delta, difftastic, atuin, gh, hyperfine, chezmoi (from the package manager),
fnm, bun, or any of the LSP servers. Configs for those tools are still deployed, and the
shell is written to degrade gracefully — .zshrc guards each integration with
command -v — but aliases that call a missing binary will fail until you install it.
Also note on Debian/Ubuntu: fd-find installs the binary as fdfind, and bat as batcat.
Anything expecting fd or bat needs symlinks:
mkdir -p ~/.local/bin
ln -sf "$(which fdfind)" ~/.local/bin/fd
ln -sf "$(which batcat)" ~/.local/bin/batThis matters because FZF_DEFAULT_COMMAND in .zshrc calls fd directly.
Nerd Font installation is macOS-only (cask "font-jetbrains-mono-nerd-font"). On Linux,
install a Nerd Font manually or the Powerlevel10k prompt and lsd icons will render as
tofu boxes.
dot_zshenv.tmplsetsSSH_AUTH_SOCK=~/.1password/agent.sockunconditionally. Without 1Password's SSH agent, unset or override this or SSH auth will fail.dot_gitconfig.tmplpointscredential.helperat/usr/local/share/gcm-core/git-credential-manager, which is the Intel-Homebrew path.- The macOS scripts under
scripts/mac/usedefaults/osascriptand are macOS-only.
Prompt shows boxes/garbage instead of icons. You need a Nerd Font installed and selected in your terminal. macOS gets JetBrains Mono Nerd Font from the Brewfile; set it as your terminal font. On Linux, install one manually.
First shell is slow and prints install messages. Zinit is bootstrapping. Let it finish;
subsequent shells are fast. Benchmark with hyperfine 'zsh -i -c exit'.
chezmoi apply says my shell config is modified. You probably edited ~/.zshrc directly.
Move the change into the source (chezmoi edit ~/.zshrc), or capture it with
chezmoi add ~/.zshrc. Use chezmoi diff to see the discrepancy first.
Default shell didn't change. Step 5 exits 0 on failure by design. Run it by hand:
echo /opt/homebrew/bin/zsh | sudo tee -a /etc/shells
chsh -s /opt/homebrew/bin/zsh(Use $(which zsh) on Linux.) Log out and back in for it to take effect.
Neovim plugins are missing. The sync step is best-effort. Run it manually:
nvim --headless "+Lazy! sync" +qafd: command not found on Ubuntu. See the fdfind/batcat symlinks above.
Git is committing as the wrong identity. Check git config user.email inside the repo.
Identity is chosen by remote URL — see Per-account identity.
Starting over. chezmoi doesn't uninstall. To reset the shell layer specifically:
rm -rf ~/.local/share/zinit ~/.cache/zinit ~/.zcompdumpThen open a new terminal to let Zinit reinstall.
chezmoi cd
# 1. Declare the package
# - macOS: add a brew/cask line to Brewfile
# - Ubuntu: add the apt package name to Aptfile
# 2. Add its config with chezmoi naming
# dot_foo -> ~/.foo
# dot_config/foo/ -> ~/.config/foo/
# *.tmpl suffix -> rendered as a Go template
# executable_bar -> ~/bar with the executable bit set
chezmoi add ~/.config/foo/config.toml
# 3. Apply (this also re-runs package install, since Brewfile/Aptfile changed)
chezmoi apply
# 4. Verify, then commit
chezmoi verify
git add . && git commit -m "feat: add foo" && git pushConventions for contributions are in AGENTS.md.