dotfiles
Personal dotfiles, managed with GNU Stow. Each chapter in the sidebar covers one stow package — what the tool is, what’s been changed from the defaults, and the everyday commands worth knowing.
How this book is built
The book is generated from the per-package READMEs in the repo. Every chapter
is a thin wrapper that pulls in the matching README.md via mdBook’s
{{#include}} directive — so the GitHub repo and this site never drift.
If you want install instructions (Debian / macOS), the modern-CLI tour (zoxide, eza, dust, hyperfine, etc.), or the full subdirectory table, those live in the repo README on GitHub.
Conventions
- Every top-level directory in the repo is a stow package mirroring the
layout it should have under
$HOME. For example,zsh/.zshrcis symlinked to~/.zshrcby runningstow zshfrom the repo root. - The same
bootstrap-debian.shscript handles a fresh Debian/Ubuntu box;brew bundlecovers macOS. - Repo-wide conventions for editing live in
CLAUDE.md.
Quick install
git clone https://github.com/jacob-delgado/dotfiles ~/dotfiles
cd ~/dotfiles
# Debian / Ubuntu
./bootstrap-debian.sh
# macOS
brew bundle && stow zsh vim tmux fzf # pick whichever packages you want
atuin
Stow package for ~/.config/atuin/config.toml. Atuin
is a SQLite-backed shell history with full-text search, per-directory
context, and optional E2E-encrypted sync.
Table of contents
Layout
| File | Stows to |
|---|---|
atuin/.config/atuin/config.toml | ~/.config/atuin/config.toml |
Active settings
enter_accept = true # Enter runs the selected line immediately
search_mode = "fuzzy" # fuzzy match (explicit; matches modern default)
filter_mode = "global" # Ctrl+R searches across all sessions/hosts
workspaces = true # treat git repos as a scope for filter_mode = "workspace"
style = "compact" # denser TUI layout
[sync]
records = true # newer record-based sync protocol
The rest of the file is atuin’s commented default template, kept around as a cheat-sheet of tunable options.
enter_accept = true gotcha: the default behavior is Enter inserts
the line for editing, Tab runs it. This config flips them so Enter runs
the selection immediately — faster but easier to misfire on a dangerous
command. Flip back if that bites you.
Why the file is so long
When atuin first generates config.toml it writes the entire schema
with every option commented out. We keep the full file rather than
trimming to the two active lines so the comments stay around as a
cheat-sheet for what’s tunable.
Regenerating
If you change settings via the atuin TUI (some are configurable there), atuin rewrites this file. Diff it against the tracked version:
git -C ~/dotfiles diff atuin/.config/atuin/config.toml
git -C ~/dotfiles commit -am "Update atuin config"
If atuin’s config schema gains new fields in a future release, you may see a flood of diff noise from the new commented entries — review and commit if you want the updated template.
Fresh-machine setup
brew install atuin # in the Brewfile
stow atuin
atuin import auto # one-time: backfill existing zsh history into atuin's DB
Run atuin register if you want sync across machines; otherwise
everything stays local.
zsh/.zshrc initializes atuin via eval "$(atuin init zsh)".
bat
Stow package for ~/.config/bat/config. bat
is a cat clone with syntax highlighting, line numbers, and git change
markers.
Table of contents
Layout
| File | Stows to |
|---|---|
bat/.config/bat/config | ~/.config/bat/config |
Settings
--theme="ansi" # terminal palette (themed by p10k)
--style="numbers,changes,header,grid,header-filesize" # line nums, git markers, file header + size, gutter grid
--pager="less -RFX" # raw colors, quit-if-one-screen, no alt-screen
--theme="ansi" is intentional — it picks up whatever color scheme the
terminal/Dracula gives us instead of locking to a fixed bat theme. Run
bat --list-themes to see alternatives.
BAT_THEME=ansi is also set in zsh/.zshrc as a belt-and-suspenders
default for tools that invoke bat via env (e.g., MANPAGER).
--pager="less -RFX" matches LESS='-RFX' in zsh/.zshrc so the pager
behaves identically whether bat invokes it or LESS is exported.
Syntax mappings
bat recognizes most filenames by extension/shebang, but a few common
files need help:
--map-syntax="Dockerfile.*:Dockerfile" # e.g. Dockerfile.dev
--map-syntax="*.jenkinsfile:Groovy"
--map-syntax="*.tfvars:HCL" # Terraform variable files
--map-syntax=".envrc:Bash" # direnv files
--map-syntax=".ignore:Git Ignore" # ripgrep / fd ignore files
Add more as new conventions show up.
Regenerating the default template
bat ships a fully commented default config:
bat --generate-config-file # writes ~/.config/bat/config if missing
If you want to start over with the full template, delete the symlink and
run the above, then move the new file into bat/.config/bat/config and
re-stow.
Fresh-machine setup
brew install bat # in the Brewfile
stow bat
btop
Stow package for ~/.config/btop/btop.conf. btop is a process / CPU /
memory / network monitor.
Table of contents
Layout
| File | Stows to |
|---|---|
btop/.config/btop/btop.conf | ~/.config/btop/btop.conf |
Settings
color_theme = "dracula" # matches git/, lazygit/, overall palette
theme_background = False # transparent — inherit terminal bg
truecolor = True # 24-bit color (enabled by tmux too)
graph_symbol = "braille" # denser/prettier than block glyphs
vim_keys = True # hjkl navigation
Cycle themes inside btop with p (uppercase P reverses). Press ?
for full keybindings. Other built-in themes worth trying:
gruvbox_dark, gruvbox_dark_v2, gruvbox_material_dark, nord,
tokyo-night, tokyo-storm.
Churn warning
btop rewrites this file every time you change a setting via its TUI (theme cycling, sort order, etc.). Diff before committing:
git -C ~/dotfiles diff btop/.config/btop/btop.conf
Fresh-machine setup
brew install btop # in the Brewfile
stow btop
direnv
Stow package for ~/.config/direnv/direnvrc. Per-user direnv setup —
.envrc files in your projects can call any of the helpers defined
here.
Table of contents
- Layout
- What direnv is
- Helpers defined here
- Example .envrc files
- nix-direnv integration
- Fresh-machine setup
Layout
| File | Stows to |
|---|---|
direnv/.config/direnv/direnvrc | ~/.config/direnv/direnvrc |
What direnv is
direnv hooks into your shell so that whenever you cd into a directory
containing .envrc, it loads that file as environment for the directory
(and unloads on leaving). Activated via the OMZ direnv plugin in
zsh/.zshrc. Trust new .envrc files explicitly with direnv allow.
Helpers defined here
| Helper | Usage in .envrc | What |
|---|---|---|
layout python_venv [python] | layout python_venv python3.12 | create/activate ./.venv/; pin python binary |
layout uv | layout uv | activate ./.venv/ created by uv venv (faster pip-replacement) |
use go <version> | use go 1.21 | use Homebrew’s go@1.21 (run brew install go@1.21 first) |
use node <version> | use node 20 | use Homebrew’s node@20 |
op_env <item> | op_env "GitHub PAT" | inject env vars from a 1Password item (needs op signed in) |
use_flake / use_nix | (provided by nix-direnv when sourced) | activate a nix flake / shell |
Example .envrc files
Python project with uv:
layout uv
export PYTHONDONTWRITEBYTECODE=1
Go service with a version pin:
use go 1.22
export CGO_ENABLED=0
PATH_add bin
Project that needs secrets from 1Password:
op_env "production-postgres" # injects DATABASE_URL, etc.
Then direnv allow once and the env loads automatically when you cd
in.
nix-direnv integration
If nix-direnv is installed (Mac: brew install nix-direnv; Linux:
distro package or via nix itself), the direnvrc sources it
automatically — use_flake / use_nix become available to .envrc
files. Without nix-direnv installed, the source-attempt is silent.
Fresh-machine setup
brew install direnv # in the Brewfile
stow direnv
# OMZ's direnv plugin (in zsh/.zshrc) hooks into the shell on startup.
editorconfig
Stow package for ~/.editorconfig. Vim picks it up via
editorconfig/editorconfig-vim (in vim/.vimrc); most editors / IDEs
support it natively.
Table of contents
Layout
| File | Stows to |
|---|---|
editorconfig/.editorconfig | ~/.editorconfig |
Defaults
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 100
Per-language overrides:
| Pattern | Indent | Notes |
|---|---|---|
*.go | tab | Go convention |
Makefile, *.mk | tab | Make requires tabs |
*.py | 4 spaces | PEP 8 |
*.{rs,c,cpp,h,hpp,java,kt,scala} | 4 spaces | |
*.md | — | trim_trailing_whitespace = false, no max_line_length |
Per-project overrides
This file has root = true so editorconfig stops walking up the
filesystem when it finds it. Any project’s own .editorconfig
overrides this one — drop a more specific file in your project root
when you need different rules.
Fresh-machine setup
stow editorconfig
Vim’s editorconfig-vim plugin (in vim/) reads this automatically.
For other editors: VS Code, JetBrains, Sublime, Neovim, and most modern
editors support .editorconfig natively or via a free plugin.
fzf
Stow package for ~/.fzf.zsh. Sourced automatically by the oh-my-zsh
fzf plugin on shell startup.
Table of contents
- Defaults
- Colors (Dracula)
- fd integration
- Keybinding-specific options
- frg — live ripgrep + fzf
- Fresh-machine setup
Defaults
FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
FZF_TMUX=1
FZF_TMUX=1 makes fzf pop up in a tmux popup pane when invoked inside
tmux — much nicer than taking over the whole terminal.
Colors (Dracula)
FZF_DEFAULT_OPTS also carries the official Dracula
--color palette so the picker matches kitty/tmux/vim:
--color=fg:#f8f8f2,bg:#282a36,hl:#bd93f9
--color=fg+:#f8f8f2,bg+:#44475a,hl+:#bd93f9
--color=info:#ffb86c,prompt:#50fa7b,pointer:#ff79c6
--color=marker:#ff79c6,spinner:#ffb86c,header:#6272a4
Unlike bat/tig (which inherit the terminal’s ANSI palette), fzf’s
colors are pinned to Dracula hex here.
fd integration
If fd is on PATH, replace fzf’s built-in walker with it. fd is
faster than the default find invocation and respects .gitignore:
if command -v fd >/dev/null; then
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'
fi
Guarded so machines without fd still get a working fzf.
Keybinding-specific options
OMZ’s fzf plugin binds:
| Keys | Action | Options set here |
|---|---|---|
Ctrl+T | insert a selected file path into the command line | FZF_CTRL_T_OPTS — bat preview, falling back to cat |
Ctrl+R | history search | FZF_CTRL_R_OPTS — 3-line wrapped preview pane |
Alt+C | cd to a selected directory | FZF_ALT_C_COMMAND — fd-based directory walker (above) |
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --style=numbers --line-range :500 {} 2>/dev/null || cat {}'"
export FZF_CTRL_R_OPTS="--preview 'echo {}' --preview-window down:3:wrap"
frg — live ripgrep + fzf
A function for project-wide content search. Type to refine; rg re-runs on
every keystroke. Enter opens the chosen line in $EDITOR.
frg # start with an empty query, type to search
frg "TODO" # seed the query
Implementation:
frg() {
local rg_prefix='rg --column --line-number --no-heading --color=always --smart-case'
fzf --ansi --disabled --query "${*:-}" \
--bind "start:reload:$rg_prefix {q} || true" \
--bind "change:reload:sleep 0.1; $rg_prefix {q} || true" \
--delimiter : \
--preview 'bat --color=always --style=numbers --highlight-line {2} -- {1}' \
--preview-window 'right,60%,border-left,+{2}+3/3,~3' \
--bind "enter:become(${EDITOR:-vim} +{2} {1})"
}
Also bound to prefix + g in tmux — see tmux/README.md.
Fresh-machine setup
stow fzf # symlinks ~/.fzf.zsh
The OMZ fzf plugin (in zsh/.zshrc’s plugins list) auto-sources the
symlink. No manual source line needed.
gh
Stow package for ~/.config/gh/config.yml only. The GitHub CLI’s
companion file hosts.yml is deliberately not tracked — it carries
OAuth tokens.
Table of contents
Layout
| File | Stows to | Tracked? |
|---|---|---|
gh/.config/gh/config.yml | ~/.config/gh/config.yml | yes |
(~/.config/gh/hosts.yml) | — | no (gitignored) |
Active customization
prefer_editor_prompt: disabled # prompt inline in the terminal, not in $EDITOR
pager: delta # syntax-highlight `gh pr diff` and similar
aliases:
co: pr checkout
prs: pr list --author "@me"
prv: pr view --web
prc: pr create --fill --web
issues: issue list --assignee "@me"
watch: run watch
compare: '!gh repo view --web --branch "$(git branch --show-current)"'
Alias quick-ref:
| Alias | Expands to | Use |
|---|---|---|
gh co <#> | gh pr checkout <#> | check out a PR locally |
gh prs | gh pr list --author "@me" | list my open PRs in this repo |
gh prv | gh pr view --web | open this branch’s PR in browser |
gh prc | gh pr create --fill --web | new PR from commits, open in browser |
gh issues | gh issue list --assignee "@me" | issues assigned to me |
gh watch <run-id> | gh run watch <run-id> | tail a GH Action run |
gh compare | shells out — opens the GH compare view for the current branch |
The leading ! on compare is a shell-out alias; everything after runs
in a subshell.
Extensions
Installed via gh extension install; they live in
~/.local/share/gh/extensions/ (not in this repo). List with
gh extension list, update with gh extension upgrade --all.
| Extension | Command | Use |
|---|---|---|
dlvhdr/gh-dash | gh dash | TUI dashboard of PRs/issues across repos with filters |
seachicken/gh-poi | gh poi | prune local branches whose PRs have been merged |
kyanny/gh-pr-draft | gh pr-draft | toggle the draft state of a PR |
Reinstall on a fresh machine:
gh extension install dlvhdr/gh-dash
gh extension install seachicken/gh-poi
gh extension install kyanny/gh-pr-draft
Why hosts.yml stays out
~/.config/gh/hosts.yml is where gh stores per-host auth state. On
machines using token auth (Linux/CI) it contains the literal token; on
macOS gh typically uses Keychain but the file still holds the user
identity. Either way, it’s per-machine state, not config, so:
gh/.config/gh/hosts.ymlis listed in the repo root.gitignore.- The file lives at
~/.config/gh/hosts.ymlas a regular file (not a symlink) andstow ghleaves it alone.
If you ever see hosts.yml show up in git status, something has gone
wrong — investigate before committing.
Schema churn warning
gh rewrites config.yml when new schema fields are added in a release
(it adds new commented-out entries). Expect occasional noisy diffs after
brew upgrade gh. Either commit the new schema or git checkout to
keep the previous version.
Fresh-machine setup
brew install gh # in the Brewfile
stow gh
gh auth login # populates hosts.yml (which stays untracked)
git
Stow package for ~/.gitconfig.
Table of contents
- Layout
- Local / private overrides
- Delta integration
- Behavior tuning
- Aliases
- Dracula color scheme
- URL aliases
- Companion tools
- Fresh-machine setup
Layout
| File | Stows to |
|---|---|
git/.gitconfig | ~/.gitconfig |
Local / private overrides
The tracked .gitconfig ends with:
[include]
path = ~/.gitconfig.local
Anything machine-specific or sensitive (work email, signing keys, work
remotes, credential helpers) lives in ~/.gitconfig.local, which is
not stowed and not in the repo. Create it by hand per machine.
For example, gh auth setup-git writes a [credential] helper with a
host-specific path (/opt/homebrew/bin/gh on macOS) — that belongs here,
not in the tracked .gitconfig.
Delta integration
git-delta is wired in as the pager
for git diff, show, log, blame:
[core]
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true # n / N jumps between hunks
side-by-side = true # split view (drop for narrow terminals)
line-numbers = true
[merge]
conflictstyle = zdiff3 # better 3-way conflict markers; needs git ≥ 2.35
brew install git-delta provides the binary (already in the Brewfile).
Behavior tuning
[init] defaultBranch = main
[pull] rebase = true; ff = only # rebase on pull, error on divergence
[push] default = current; autoSetupRemote = true; followTags = true
[fetch] prune = true # drop refs to deleted remote branches
[rebase] autosquash = true; autoStash = true # fixup! commits flow; uncommitted work stashes
[diff] algorithm = histogram; colorMoved = default; renames = copies
[rerere] enabled = true # remember conflict resolutions
These set sane defaults for git’s nagging warnings (divergent branches,
missing upstream) and unlock features that aren’t on by default but
should be (rerere, histogram diff, move-block detection).
Aliases
| Alias | Expands to | Use |
|---|---|---|
git st | status -sb | short status with branch line |
git co | checkout | |
git br | branch | |
git cm | commit | |
git ca | commit --amend | |
git cane | commit --amend --no-edit | amend without re-editing the message |
git lg | pretty-graph log | one-line graph with colors and ago-style dates |
git last | log -1 HEAD --stat | what did the last commit touch? |
git staged | diff --cached | review what’s about to be committed |
git unstage | reset HEAD -- | undo git add |
git undo | reset --soft HEAD^ | un-commit (keeping changes) |
git wip | add -A && commit -m 'WIP' | snapshot to step away |
git prune-branches | delete every merged branch (except current / main / master) | safe local cleanup |
git cp | cherry-pick | |
git sw / git swc | switch / switch -c | modern branch switch / create |
git cob | checkout -b | create + switch (older style) |
git aa / git ap | add -A / add -p | stage all / interactively by hunk |
git fa | fetch --all --prune | refresh all remotes, drop stale branches |
git pf | push --force-with-lease | safe force-push (won’t clobber others’ work) |
git rbi | rebase -i --autosquash | interactive rebase, auto-orders fixups |
git rbc / git rba | rebase --continue / --abort | |
git fixup | commit --fixup <sha> | marks a fixup commit for rbi to squash |
git recent | last 10 branches by commit date | |
git root | rev-parse --show-toplevel | print the repo root |
git aliases | list every configured alias |
Dracula color scheme
The [color "*"] sections set a Dracula-inspired palette for branches,
diffs, grep, interactive prompts, and status. Most slots are explicit;
empty values mean “use the default.”
URL aliases
[url "https://github.com/dracula/"]
insteadOf = dracula://
Lets you git clone dracula://vim instead of typing the full URL.
Companion tools
Installed via Brewfile, no extra config required:
| Tool | Use |
|---|---|
git-delta | pager (wired in above) |
git-absorb | git absorb — auto-creates fixup! commits targeting the right earlier commit based on what lines your unstaged changes touch. Pair with git rebase -i --autosquash (which [rebase] autosquash = true runs by default). |
git-branchless | adds porcelain inspired by Mercurial/Sapling: git smartlog, git move, git restack, git undo (cross-operation undo). Power-user; read git help branchless first. |
Fresh-machine setup
stow git
Create ~/.gitconfig.local for any per-machine bits (work email, signing
keys, etc.) that shouldn’t live in the repo. git-delta, git-absorb,
and git-branchless come in via the Brewfile.
gitignore_global
Stow package for ~/.gitignore_global. Wired into git via
[core] excludesfile = ~/.gitignore_global in git/.gitconfig.
Table of contents
Layout
| File | Stows to |
|---|---|
gitignore_global/.gitignore_global | ~/.gitignore_global |
What’s in it
OS, editor, and tool noise that shouldn’t appear in any project:
- macOS:
.DS_Store,._*,.Spotlight-V100,.fseventsd, … - Linux:
.directory,.Trash-*,.nfs* - Windows:
Thumbs.db,Desktop.ini - Vim:
*.swp,*~,.netrwhist - IDEs:
.idea/,.vscode/,*.sublime-workspace,.history/ - ctags / LSP caches:
tags,.ccls-cache/ - direnv:
.direnv/ - pre-commit / lefthook caches
- Generic:
*.bak,*.orig,*.rej
What’s not in it
Per-project artifacts (node_modules/, __pycache__/, dist/,
target/, *.pyc, build dirs, etc.) stay in each project’s own
.gitignore. A global ignore for those would mask bugs in repos that
should be excluding them deliberately.
Fresh-machine setup
stow gitignore_global
git config --global core.excludesfile ~/.gitignore_global # already in git/.gitconfig
hadolint
Stow package for ~/.config/hadolint/config.yaml. hadolint is a
Dockerfile linter. Used by lefthook on pre-commit and by hand.
Table of contents
Layout
| File | Stows to |
|---|---|
hadolint/.config/hadolint/config.yaml | ~/.config/hadolint/config.yaml |
Settings
Ignored rules:
| Code | Rule | Why ignored |
|---|---|---|
DL3008 | apt-get without version pin | unpinned apt is fine in scratch/dev images |
DL3018 | apk add without version pin | same logic for Alpine |
DL3015 | apt-get without --no-install-recommends | stylistic preference |
Trusted registries (skip “use a versioned tag” warnings for these):
docker.io, gcr.io, quay.io, ghcr.io, registry.k8s.io,
public.ecr.aws.
failure-threshold: warning makes hadolint exit non-zero only for
warning/error rules — informational findings don’t fail builds.
Per-project overrides
A repo can drop a .hadolint.yaml at the root; it wins. Per-line
disables also work:
# hadolint ignore=DL3007
FROM nginx:latest
Fresh-machine setup
brew install hadolint # in the Brewfile
stow hadolint
kitty
Stow package for ~/.config/kitty/. Kitty
is the GPU-accelerated terminal — used here on macOS (/Applications/kitty.app,
installed via the kitty.sh installer, not Homebrew).
Table of contents
Layout
| File | Stows to |
|---|---|
kitty/.config/kitty/kitty.conf | ~/.config/kitty/kitty.conf |
kitty/.config/kitty/dracula.conf | ~/.config/kitty/dracula.conf |
kitty/.config/kitty/diff.conf | ~/.config/kitty/diff.conf |
kitty/.config/kitty/ssh.conf | ~/.config/kitty/ssh.conf |
Reload after editing: kitty @ load-config (needs allow_remote_control yes),
or Cmd+Ctrl+, (macOS) / Ctrl+Shift+F5 (Linux).
Main config
kitty.conf pulls in the Dracula palette, sets the font, and tunes a
handful of defaults. Sections:
Font — JetBrainsMono Nerd Font Mono is the patched build with Nerd
Font glyphs (install via the font-jetbrains-mono-nerd-font cask, or
grab from nerdfonts.com).
Quality of life
| Setting | Why |
|---|---|
enable_audio_bell no / visual_bell_duration 0 | Silence the bell entirely |
confirm_os_window_close 0 | Don’t prompt when closing a window with a running process |
scrollback_lines 10000 | Bigger in-memory scrollback |
scrollback_pager_history_size 100 | 100 MB disk-backed scrollback for the pager kitten |
repaint_delay 8 / input_delay 2 / sync_to_monitor yes | Snappier rendering on Apple Silicon |
macOS
| Setting | Why |
|---|---|
macos_option_as_alt left | Left-Option behaves as Alt so ⌥←/⌥→ word-jump works in shells |
macos_titlebar_color background | Title bar adopts the bg color (no white strip) |
macos_quit_when_last_window_closed yes | Quitting the last window quits the app |
Remote control + shell integration
| Setting | Why |
|---|---|
allow_remote_control yes | Unlocks kitty @ ls, kitty @ set-tab-title, the ssh kitten, etc. |
listen_on unix:/tmp/kitty-{kitty_pid} | Per-process control socket |
shell_integration enabled | Click-to-move cursor, prompt marking, jump-by-prompt, etc. |
Visual
| Setting | Why |
|---|---|
window_padding_width 6 | Subtle gutter between text and window edge |
tab_bar_edge top | Tabs on top, away from prompt output |
tab_bar_style powerline + tab_powerline_style slanted | Curved powerline separators (uses JetBrainsMono Nerd Font glyphs) |
Windows, tabs & layouts
kitty has its own multiplexing, independent of tmux. Terminology: an OS
window holds tabs; each tab arranges windows (splits) using a
layout. Every kitty shortcut uses cmd (macOS) or ctrl+shift
(kitty_mod, e.g. on Linux) — a different namespace from the tmux C-a
prefix, so the two never collide. Inside a tmux session, cmd+… drives kitty
and C-a … drives tmux; they stack happily.
Keys below are macOS (the daily driver). On Linux cmd is inert and kitty’s
ctrl+shift defaults apply — see the kitty docs.
Built-in defaults
| Action | Key |
|---|---|
| New tab | ⌘T |
| Next / prev tab | ⌘] / ⌘[ (or ⌃Tab / ⇧⌃Tab) |
| Go to tab 1–10 | ⌘1 … ⌘9, ⌘0 |
| Close tab | ctrl+shift+q |
| New OS window | ⌘N |
| Next / prev split | ctrl+shift+] / ctrl+shift+[ |
| Close split | ⌘W |
| Next layout | ctrl+shift+l |
| Fullscreen toggle | ⌘↩ |
Custom maps (this config) — added to kitty.conf, macOS only. They
repurpose the default ⌘D/⌘⇧D (new window) into iTerm-style directional
splits:
| Key | Action | vim / tmux analog |
|---|---|---|
⌘D / ⌘⇧D | split right / down (launch --location=vsplit/hsplit) | tmux `prefix |
⌘⌃H ⌘⌃J ⌘⌃K ⌘⌃L | focus split left / down / up / right | vim ctrl-w hjkl, tmux prefix hjkl |
⌘⌃⇧H ⌘⌃⇧J ⌘⌃⇧K ⌘⌃⇧L | move / swap split | vim ctrl-w HJKL |
⌘⌃Z | zoom the active split (toggle stack) | tmux prefix z |
⌘R | resize mode → then arrows or hjkl, Enter/Esc | tmux prefix HJKL |
(In kitty.conf the focus/move directions are left/right/top/bottom —
kitty does not accept up/down.) These are safe alongside tmux because cmd
never reaches the shell. The only chord that would clash — ctrl+a (the tmux
prefix) — is deliberately never mapped in kitty.
Layouts — enabled_layouts splits,stack,tall,fat,grid, cycled with
ctrl+shift+l:
- splits — arbitrary horizontal/vertical splits; the default, and the only
layout where the
⌘D/⌘⇧Ddirection applies. - stack — one split maximized (zoom); flip back with
ctrl+shift+l. - tall / fat / grid — auto-tiled arrangements for quick even layouts.
In non-splits layouts, --location is ignored and a new window is just
auto-placed by that layout.
ssh kitten
kitten ssh is kitty’s smarter ssh: it ships kitty’s terminfo and shell
integration to the remote, so colors, prompt marking, and the clipboard work
there even when kitty isn’t installed on the remote — and it can copy
selected dotfiles over. It only works when your local terminal is kitty
(allow_remote_control yes is already set above).
kitten ssh user@host # older form: kitty +kitten ssh user@host
kssh user@host # the alias in zsh/.zshrc (kitty-guarded)
kssh is preferred over aliasing ssh itself — it’s explicit, and ssh keeps
working normally outside kitty (where kitten ssh can’t run anyway).
Per-host behavior lives in ssh.conf (tracked in this package, stowed to
~/.config/kitty/ssh.conf). It is not ~/.ssh/config — OpenSSH still owns
hostnames, ports and keys; this only controls what kitten ssh carries over.
The shipped starter enables remote shell integration and copies a few
dependency-free configs (.vimrc, .editorconfig, .gitconfig), with a
commented per-host block for heavier setups (nvim/zsh/tmux):
hostname *
shell_integration enabled
copy .vimrc
copy .editorconfig
copy .gitconfig
env EDITOR=vim
Caveat: wait for the shell prompt before typing on a fresh connection — early keystrokes during the bootstrap handshake can be dropped.
Dracula theme
dracula.conf is the upstream palette from
https://draculatheme.com/kitty, unmodified. It sets:
- Foreground/background and the full ANSI palette (
color0–color15) - Cursor, selection, URL underline
- Tab bar foreground/background (active + inactive)
- Split/window border colors
- Mark colors (
kitty’s scrollback marks feature)
Matches the rest of the dotfiles’ Dracula-ish palette ([[../tig/README.md]], [[../lazygit/README.md]]).
Diff colors
diff.conf styles kitty +kitten diff (kitty’s built-in side-by-side
diff viewer) with the Dracula palette — green for added, red for removed,
purple hunk separators, cyan search, yellow selection.
Invoke with kitty +kitten diff old new or alias it.
Fresh-machine setup
# kitty itself: NOT in Brewfile — installed standalone on macOS via
# curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin
# On Debian: apt install kitty (or use the same installer).
stow kitty
lazygit
Stow package for ~/.config/lazygit/config.yml.
Table of contents
Layout
| File | Stows to |
|---|---|
lazygit/.config/lazygit/config.yml | ~/.config/lazygit/config.yml |
Find the live config dir on any machine with lazygit --config-dir.
Theme
The official Dracula theme (copied
verbatim from upstream), matching git/.gitconfig:
| Element | Color |
|---|---|
| activeBorderColor | #FF79C6 bold (pink) |
| inactiveBorderColor | #BD93F9 (purple) |
| searchingActiveBorderColor | #8BE9FD bold (cyan) |
| selectedLineBgColor | #6272A4 (comment) |
| optionsTextColor | #6272A4 (comment) |
| unstagedChangesColor | #FF5555 (red) |
| defaultFgColor | #F8F8F2 (foreground) |
Plus cherry-pick / marked-base-commit colors — see the file for the full upstream block.
Other settings
gui:
nerdFontsVersion: "3" # use NF v3 glyphs (font-hack-nerd-font in Brewfile)
showRandomTip: false # silence the splash tips
showCommandLog: false # hide the bottom command-log panel
showFileTree: true # tree view in the files panel
experimentalShowBranchHeads: true # show branch heads in the commit log
git:
paging:
colorArg: always
pager: delta --paging=never # diff pane uses delta, matching `git diff` in the terminal
os:
editPreset: "vim" # `e` opens files in vim
The delta pager means lazygit’s diff pane renders with the same
side-by-side, syntax-highlighted view you see from git diff. Both
read settings from git/.gitconfig’s [delta] block.
gh integration (custom commands)
| Key | Context | Action |
|---|---|---|
O | global | gh pr view --web — open this branch’s PR in browser |
C | global | gh pr create --fill --web — create PR for current branch |
V | commits | gh pr view --web {SHA} — open PR for selected commit |
L | global | gh pr list --author "@me" — list my PRs (terminal output) |
X | global | gh poi — prune branches whose PRs are merged (terminal output) |
{{.SelectedLocalCommit.Sha}} is lazygit’s template syntax for the
selected entry in the commits panel.
Schema reference
Full options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md
Keep this file lean — lazygit can write to it when settings change via its UI, which creates noisy diffs when you only meant to tweak one thing.
Fresh-machine setup
brew install lazygit # in the Brewfile
stow lazygit
lefthook
Per-project git-hooks manager template. Lefthook has no global config
— this directory ships a lefthook.yml you can copy or extends: into
new repos.
Table of contents
- Layout
- Two ways to use it
- What the template runs
- Adding repo-specific hooks
- Skipping a hook
- Fresh-machine setup
Layout
| File | Purpose |
|---|---|
lefthook/lefthook.yml | Go-dev baseline template (no symlink) |
This package is not stowed — lefthook.yml is meant to live inside
your project repos, not in $HOME.
Two ways to use it
1. Copy into a new repo
cp ~/dotfiles/lefthook/lefthook.yml ./lefthook.yml
lefthook install
lefthook install writes the .git/hooks/* shims that invoke lefthook.
You only need to do this once per clone.
2. Extend it from your project’s lefthook.yml
# project/lefthook.yml
extends: ~/dotfiles/lefthook/lefthook.yml
pre-commit:
commands:
# project-specific addition
api-spec-check:
glob: "openapi.yaml"
run: spectral lint {staged_files}
Then lefthook install and additions stack on top of the baseline.
What the template runs
pre-commit (parallel):
| Hook | Glob | What |
|---|---|---|
gofmt | *.go | fail if any file isn’t gofmt’d |
goimports | *.go | fail if imports aren’t sorted |
golangci-lint | *.go | run lint, only new findings since HEAD |
govet | *.go | go vet ./... |
shellcheck | *.{sh,bash} | lint staged shell |
shfmt | *.{sh,bash} | format-diff shell (2-space, switch-case indent) |
yamllint | *.{yml,yaml} | YAML lint |
hadolint | Dockerfile* | Dockerfile lint |
commit-msg: reject empty subjects.
pre-push (parallel):
| Hook | Glob | What |
|---|---|---|
go-test | *.go | go test ./... |
go-build | *.go | go build ./... |
All the linters / formatters are in the Brewfile.
Adding repo-specific hooks
The most common pattern: extends: the baseline, then add or override.
Anything you add lives in the project repo (versioned with the code).
Skipping a hook
LEFTHOOK_EXCLUDE=golangci-lint git commit -m "wip"
# or skip all hooks (use sparingly):
git commit --no-verify
Fresh-machine setup
brew install lefthook # in the Brewfile
# In each repo where you want hooks:
lefthook install
mise
mise manages language runtimes and CLI tools with per-project version pinning. This package stows one file:
| Source | Symlinks to |
|---|---|
mise/.config/mise/config.toml | ~/.config/mise/config.toml (mise’s global config) |
What it’s for
The global [tools] list is the primary tool manager on Linux. There is no
Homebrew on the Debian VMs (see the repo CLAUDE.md and bootstrap-debian.sh),
so mise install from $HOME is what actually installs bat, eza,
ripgrep, go-task, the Node/Go runtimes, etc. On macOS the runtimes and CLIs
still come from the Brewfile, but the repo’s lint tools are installed via mise
there too — bootstrap-macos.sh stows this package and mise installs just
those (see the lint-tooling note below). The [settings] block silences mise’s
“missing tool” nag for the brew-provided entries on macOS.
zsh/.zshrc runs mise activate zsh before the tool-integration blocks
(zoxide, atuin, eza, task, …) so mise-managed binaries are on PATH when those
command -v checks run.
Why these tools (and not others)
- Runtimes —
node(LTS) andgo(latest). Python is left to Debian’s systempython3; uncomment the pin inconfig.tomlif you want mise to own it. - CLIs — the modern replacements the shell config references. Each is gated
behind
command -vinzsh/.zshrc, so adding/removing one here never breaks a machine that lacks it. fzfanddirenvare deliberately NOT here. Their oh-my-zsh plugins load duringsource $ZSH/oh-my-zsh.sh, which runs beforemise activate. A mise-onlyfzf/direnvwouldn’t be onPATHyet, so those two come from apt (Linux) / brew (macOS) instead.- Repo lint tooling — the linters
lefthookand CI run live here so one file tracks them and they match CI:shellcheck,shfmt,taplo,actionlint,editorconfig-checker,lefthook, plusnpm:markdownlint-cli2(needs node) andpipx:yamllint(needs python). These come from mise on macOS too — notablyeditorconfig-checker, whose aqua build ships theeccommand lefthook/CI call (Homebrew’s shipseditorconfig-checker, noec).mdformatis the one exception: itsgfm/tablesplugins needpipx inject, which mise’s pipx backend can’t express, sotask miseinstall-or-upgrades it viapipx.
How it’s used
mise install # install everything in the global config (run from anywhere)
mise upgrade # upgrade installed tools to the latest matching the config
mise outdated # preview which tools have newer versions (nothing changes)
mise ls # show installed tools + versions
mise doctor # diagnose activation / shim problems
mise use -g go@latest # add/pin another global tool (edits this config)
mise install only fetches what’s missing; mise upgrade re-resolves the
latest/lts pins and moves them forward. The repo root’s Taskfile.yml has
a task mise target that runs mise upgrade and also install-or-upgrades
mdformat via pipx (see the bullet above); task update refreshes the vim
and nvim plugins too.
The headless devbox flow does this automatically: it installs mise from
apt, and after the dotfiles are stowed it runs mise install in $HOME with
MISE_YES=1.
nvim
Stow package for ~/.config/nvim/. A from-scratch Lua config
(lazy.nvim + native LSP + Treesitter),
not a port of ~/.vimrc. The older vim/ package stays as-is and remains the
default $EDITOR; this exists to migrate to Neovim incrementally without
losing the muscle memory baked into the Vim config.
Table of contents
- Layout
- Plugin map (Vim → Neovim)
- What Neovim made redundant
- LSP, completion, linting
- Leader-key cheat sheet
- Text objects (Treesitter)
- Go workflow
- Settings that differ from the .vimrc
- Runtime directories
- Fresh-machine setup
Leader is , (vim.g.mapleader = ','), set in init.lua before any plugin
loads.
Layout
nvim/.config/nvim/
├── init.lua entry: leader, then require the config modules
└── lua/
├── config/
│ ├── lazy.lua bootstrap lazy.nvim, import lua/plugins/
│ ├── options.lua vim.opt settings ported from .vimrc
│ ├── keymaps.lua non-plugin maps (leader, visual-line j/k, …)
│ ├── autocmds.lua trailing-WS, cursor restore, text tw=80
│ └── commands.lua :W (SudoWrite alias), :DiffOrig
└── plugins/ one spec file per concern; lazy imports them all
├── colorscheme.lua dracula.nvim
├── editing.lua tpope stack, autopairs, tmux-navigator
├── ui.lua lualine, nvim-tree, aerial, undotree
├── finder.lua fzf-lua
├── git.lua fugitive, gitsigns
├── treesitter.lua nvim-treesitter
├── completion.lua blink.cmp + friendly-snippets
├── lsp.lua mason, lspconfig, nvim-lint
└── lang.lua go.nvim, rust.vim
init.lua is Neovim’s $MYVIMRC, so ,ev / ,sv edit / source it.
Plugin map (Vim → Neovim)
Kept as-is — these run natively and have no better replacement:
vim-surround, vim-repeat, vim-sleuth, vim-eunuch,
vim-projectionist, vim-dispatch, vim-fugitive, splitjoin.vim,
vim-tmux-navigator, undotree, rust.vim.
Replaced with a Lua-native equivalent:
| Vim plugin | Neovim replacement | Notes |
|---|---|---|
vim-airline | lualine.nvim | theme auto follows Dracula |
nerdtree | nvim-tree.lua | ,nt / ,nf unchanged |
tagbar | aerial.nvim | ,tt; LSP/Treesitter-backed, no ctags |
fzf + fzf.vim | fzf-lua | same ,f ,b ,a ,* ,h maps |
vim-gitgutter | gitsigns.nvim | ]c/[c hunks, ,gg toggle |
auto-pairs | nvim-autopairs | integrates with blink.cmp |
ultisnips + vim-snippets | blink.cmp + friendly-snippets | snippets via the completion menu |
ale | nvim-lint + LSP diagnostics | pylint / shellcheck / yamllint |
vim-go | go.nvim + gopls | commands + maps preserved (see below) |
dracula/vim | dracula.nvim | Treesitter- & LSP-aware |
What Neovim made redundant
Dropped entirely — the capability is now built in:
| Dropped | Replaced by (built-in) |
|---|---|
vim-commentary | gc / gcc commenting (Neovim 0.10+) |
vim-unimpaired | ]q [q, ]b [b, ]<Space>, ]d [d default maps (0.11+); ]c [c via gitsigns |
editorconfig-vim | native .editorconfig support (Neovim 0.9+) |
syntax on + g:go_highlight_* | Treesitter highlighting |
nocompatible, filetype … on, backspace, incsearch, wildmenu, hidden, autoindent, ttyfast | Neovim defaults |
LSP, completion, linting
- Servers (auto-installed by
mason-lspconfig):gopls(via go.nvim),rust_analyzer,pyright,lua_ls,bashls,yamlls. Enabled with the nativevim.lsp.enable();:Masonmanages the binaries. - Built-in LSP maps (Neovim defaults, on attach):
Khover,grrreferences,grnrename,gracode action,griimplementation,[d/]ddiagnostics. Added on top:gddefinition,,erename,,cacode action. - Completion:
blink.cmp—<C-y>accept,<C-n>/<C-p>select,<C-space>toggle docs. Snippets come fromfriendly-snippets. - Format on save: Go uses go.nvim’s
goimports; every other server that advertises formatting (rust_analyzer,lua_ls) runsvim.lsp.buf.format()onBufWritePre(built-in, no plugin). Servers that can’t format (pyright,bashls,yamlls) are simply skipped. - Linting:
nvim-lintruns the same linters ALE did —pylint(Python),shellcheck(sh),yamllint(yaml). The CLI tools must be onPATH.
Leader-key cheat sheet
(Leader = ,. Maps shared with the Vim config keep the same keys.)
| Keys | Action |
|---|---|
,<space> | clear search highlight |
,, | switch to last file |
,ev / ,sv | edit / source init.lua |
,ln / ,nw / ,rn | toggle number / wrap / relativenumber |
,sc / ,rt | toggle spell / retab |
,tn / ,tc | new / close tab |
,bd / ,bda | delete current / all buffers |
,dc / ,do | close diff / :DiffOrig |
,url | open URL under cursor (system browser) |
,u | toggle undotree |
,tt | toggle aerial symbol outline |
,nt / ,nf | toggle / reveal nvim-tree |
,f / <C-p> | fzf-lua files |
,b / ,be | fzf-lua buffers |
,a | fzf-lua live grep |
,* | fzf-lua grep word under cursor |
,h | fzf-lua recent files |
,gg | toggle gitsigns line highlight |
,hs / ,hu / ,hp | stage / reset / preview git hunk |
,q | close quickfix |
]q / [q | next / prev quickfix (built-in) |
]c / [c | next / prev git hunk |
Non-leader maps carried over verbatim from the .vimrc: jj→Esc, visual-line
j/k, <M-j>/<M-k> move line/selection, < / > keep selection,
<Tab>→%, recentering n/N/*/#, very-magic /, Q→gq,
<C-h/j/k/l> window+tmux navigation. See the vim README for the
rationale behind each.
Text objects (Treesitter)
From nvim-treesitter-textobjects (no built-in equivalent). Use in operator-
or visual-pending position, e.g. daf deletes a whole function, vif selects
its body, cia changes an argument:
| Key | Selects / moves to |
|---|---|
af / if | a function / inside a function |
ac / ic | a class / inside a class |
aa / ia | a parameter / inside a parameter |
]m / [m | next / prev function start |
]] / [[ | next / prev class start |
Go workflow
go.nvim owns gopls and reproduces the vim-go commands. Maps active only in
.go buffers:
| Keys | Action |
|---|---|
,b | :GoBuild |
,t / ,r | test / run |
,c | :GoCoverage toggle |
,ga | :GoAlt (code ↔ test) |
,gl / ,gd | lint / doc |
,i / ,s | hover (type/doc) / implements |
,ds / ,dv / ,dt | definition in split / vsplit / tab |
,e | rename (LSP, global map) |
go.nvim drives gopls (already on $PATH if you used vim-go — it installs to
~/go/bin). The extra tools it shells out to (gotests, iferr, …) come from
:GoInstallBinaries — run it from inside a Go buffer, since go.nvim only
loads for Go filetypes; nvim "+GoInstallBinaries" on an empty buffer fails
with “not an editor command”. goimports runs on save.
Settings that differ from the .vimrc
Mostly faithful; the deliberate changes:
| Change | Reason |
|---|---|
cindent dropped | Treesitter handles indentation per-filetype |
signcolumn=yes added | keep git/diagnostic signs from shifting text |
runtime dirs under ~/.local/share/nvim | Neovim’s stdpath('data'), not ~/.vim |
viminfo → shada | Neovim’s default shada is sensible; not overridden |
statusline/laststatus/ruler unset | lualine owns the statusline |
Everything else — colorcolumn 80+120 formula, clipboard, undofile,
textwidth=200 (80 for text), ignorecase+smartcase, gdefault,
trailing-whitespace highlight + strip-on-save for go/py/sh — is ported directly.
Runtime directories
init.lua/options.lua create these on startup (Neovim won’t):
| Dir | Contents |
|---|---|
~/.local/share/nvim/undo | persistent undo |
~/.local/share/nvim/backup | backup files |
~/.local/share/nvim/swap | swap files |
~/.local/share/nvim/lazy | lazy.nvim-managed plugins |
~/.local/share/nvim/mason | LSP servers / tools |
~/.config/nvim/lazy-lock.json pins plugin commits — commit it after a
:Lazy sync so installs are reproducible.
Fresh-machine setup
stow nvim # symlinks ~/.config/nvim
nvim --headless "+Lazy! sync" +qa # install plugins
Then, for the optional Go tools, open any .go file and run
:GoInstallBinaries (gopls itself is already covered if vim-go installed it).
Treesitter compiles parsers on first run (needs a C compiler — clang on macOS,
build-essential on Debian). :checkhealth flags anything missing.
p10k
Stow package for ~/.p10k.zsh — the configured
powerlevel10k prompt.
Table of contents
Why this is in the repo
The theme itself ships with sensible defaults, but the prompt I actually
use is a customized version produced by running p10k configure. Without
this file, a fresh machine gets the wizard the first time zsh starts —
which is fine, but I prefer the deployed prompt to match what I’m used
to.
The file is ~1700 lines and mostly auto-generated; treat it as a
snapshot, not as hand-tuned config. Edits are fine, but re-running
p10k configure overwrites them.
How it’s loaded
zsh/.zshrc ends with:
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
Plus the instant-prompt preamble at the top of .zshrc (must stay close
to the top to be effective):
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
The instant-prompt cache file is regenerated automatically as you use the shell; it isn’t in this repo.
Instant-prompt mode
POWERLEVEL9K_INSTANT_PROMPT is set to quiet rather than the stock
verbose. In a real terminal, init is silent and instant prompt works
perfectly. But a non-tty shell — like the one Claude Code uses to snapshot
the environment — can’t give p10k’s gitstatus job control or let
atuin/zoxide bind zle widgets, so those steps print errors. With
verbose, p10k then emits a console-output warning on every such launch.
quiet suppresses that spurious warning and has no effect on real
interactive sessions (which produce no output, so the prompt never
“jumps”). ⚠️ Like the Dracula colors below, re-running p10k configure
resets this to verbose — re-apply it.
Colors (Dracula)
This is a rainbow-style config with the official
Dracula color values merged in
(colors only — segments, layout, and options are mine). The Dracula
theme is itself rainbow-based and mostly uses ANSI slots 0–7, so it
relies on the terminal palette being Dracula (kitty’s dracula.conf)
rather than hardcoding hex; only ~11 segment colors are overridden
(os_icon, dir text, time, status, prompt char, multiline gap).
⚠️ Re-running p10k configure overwrites these. After regenerating,
re-merge the Dracula values — pull files/.p10k.zsh from
dracula/powerlevel10k and
copy across only the *_FOREGROUND/*_BACKGROUND/*_COLOR values, or
re-run the merge script used originally.
Regenerating
If you change the prompt with p10k configure, the wizard writes the new
config to ~/.p10k.zsh — which is symlinked here. Commit the resulting
diff (then re-apply the Dracula colors per the section above):
p10k configure # interactive wizard
git -C ~/dotfiles diff p10k/.p10k.zsh
git -C ~/dotfiles commit -am "Regenerate p10k prompt"
Fresh-machine setup
stow p10k # symlinks ~/.p10k.zsh
Theme itself (cloned as an OMZ custom theme) is installed by
bootstrap-debian.sh:
git clone --depth=1 https://github.com/romkatv/powerlevel10k \
"${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k"
You’ll want a Nerd Font installed in your terminal for the prompt’s
glyphs to render. The Brewfile pulls in font-hack-nerd-font.
ripgrep
Stow package for ~/.config/ripgrep/config. The config file is only
read if RIPGREP_CONFIG_PATH points at it — rg does not look for it
by default.
Table of contents
Layout
| File | Stows to |
|---|---|
ripgrep/.config/ripgrep/config | ~/.config/ripgrep/config |
Required env var
zsh/.zshrc sets:
export RIPGREP_CONFIG_PATH="$HOME/.config/ripgrep/config"
Without that variable, rg ignores the config file completely.
Settings
--smart-case # case-insensitive unless the query has uppercase
--hidden # search dotfiles (still respects .gitignore)
--max-columns=200 # cap matched-line width
--max-columns-preview # show a truncation message on long lines
Glob excludes (additional to whatever .gitignore says):
--glob=!.git/*
--glob=!node_modules/*
--glob=!.venv/*
--glob=!__pycache__/*
--glob=!dist/*
--glob=!build/*
--glob=!target/* # Rust + JVM build dirs
--glob=!vendor/* # Go vendored deps
--glob=!*.min.js
--glob=!*.min.css
--glob=!*.map # source maps
Custom filetype aliases — rg --type <alias> "pattern":
--type-add=tf:*.{tf,tfvars}
--type-add=k8s:*.{yaml,yml}
--type-add=helm:*.{yaml,yml,tpl}
--type-add=proto:*.proto
--hidden makes rg walk into .config/, .local/, etc. — the
--glob exclusions then prune the noisy ones.
Anything you override on the CLI wins over the config file.
Colors (Dracula)
The official Dracula palette, so rg
output matches fzf/kitty/etc:
--colors=path:fg:0xbd,0x93,0xf9 # purple paths
--colors=line:fg:0x50,0xfa,0x7b # green line numbers
--colors=column:fg:0x50,0xfa,0x7b # green column numbers
--colors=match:fg:0xff,0x55,0x55 # red match highlights
Verifying it’s loaded
rg foo --debug 2>&1 | head -5
The first line of debug output names the config file path being read.
Companion: ripgrep-all (rga)
brew install ripgrep-all provides rga, a wrapper that runs rg
across non-text files by extracting their content first: PDFs, ebooks
(EPUB, MOBI), Office docs, archives (zip, tar, 7z), and even SQLite
databases:
rga "encryption" ~/Documents/papers # grep across PDFs
rga "TODO" project.zip # grep inside an archive without extracting
rga reads its own config dir (~/.config/ripgrep-all/) — it does
not share this ripgrep/config file. Run rga --help to set up
adapter-specific options.
Fresh-machine setup
brew install ripgrep ripgrep-all # both in the Brewfile
stow ripgrep
# RIPGREP_CONFIG_PATH is exported by zsh/.zshrc already
shellcheck
Stow package for ~/.shellcheckrc. shellcheck is the de-facto shell
script linter. Used by lefthook on pre-commit, ALE in vim, and by
hand.
Table of contents
Layout
| File | Stows to |
|---|---|
shellcheck/.shellcheckrc | ~/.shellcheckrc |
What’s enabled / disabled
enable=all
shell=bash
external-sources=true
disable=SC2034 # variable appears unused (false positive in sourced libs)
disable=SC1090 # can't follow non-constant source
disable=SC1091 # can't find source file
The enable=all turns on shellcheck’s optional checks (literal numeric
comparison, etc.) — more signal at the cost of slightly more verbose
output. The three disabled codes are the ones that fire most often on
legitimate code (utility scripts that source siblings, library files
that define helpers their callers use, etc.).
Per-project overrides
Each rule can be re-enabled in a single file via a comment:
#!/usr/bin/env bash
# shellcheck enable=SC2034
Or per-line:
foo=bar # shellcheck disable=SC2034
A repo can also drop its own .shellcheckrc at the root; it wins.
Fresh-machine setup
brew install shellcheck # in the Brewfile
stow shellcheck
task
Stow package for Taskwarrior and
taskwarrior-tui. Taskwarrior is
a console todo manager; taskwarrior-tui is a terminal UI on top of it. The TUI
has no config file of its own — it reads everything from Taskwarrior’s
taskrc, so both tools are configured from one place here.
Table of contents
- Layout
- Config file resolution
- Dracula theme
- Sensible defaults
- taskwarrior-tui settings
- Things you might add
- Verifying
- Fresh-machine setup
Layout
| File | Stows to |
|---|---|
task/.config/task/taskrc | ~/.config/task/taskrc |
task/.config/task/dracula.theme | ~/.config/task/dracula.theme |
task/.config/task/shortcuts/*.sh | ~/.config/task/shortcuts/*.sh |
Task data (the TaskChampion SQLite DB) lives at ~/.local/share/task via
data.location — XDG, matching the rest of the repo. It is not tracked
here (it’s your actual todo data).
Config file resolution
Taskwarrior reads the first that exists, in order:
rc:<path>on the command line$TASKRC~/.taskrc(classic location)~/.config/task/taskrc(XDG) ← what this package uses
Because there is no ~/.taskrc, the XDG path wins. include dracula.theme in
taskrc is resolved relative to the taskrc file, so the two files must stay
side by side.
Dracula theme
dracula.theme uses named ANSI colours (color1/red, color4/blue, …)
and palette indices, not hex. Taskwarrior maps those to the terminal’s 16-colour
palette, which kitty themes with Dracula (kitty/dracula.conf). So the colours
render as true Dracula and stay correct on any Dracula-themed terminal —
including the Debian box. This is the same --theme=ansi trick bat uses.
The one gotcha worth remembering: in Dracula’s ANSI map, color4 (“blue”) is
purple #bd93f9 and color5 (“magenta”) is pink #ff79c6. The theme
leans on those for headers, projects, and recurring tasks.
Taskwarrior colours can’t take hex directly (only names / colorN / rgbRGB /
grayN), which is the other reason the named-ANSI approach is the right call.
Sensible defaults
Set in taskrc:
| Setting | Value | Why |
|---|---|---|
data.location | ~/.local/share/task | XDG, consistent with the repo |
news.version | 3.4.2 | skip the interactive “task news” prompt on first run |
weekstart | monday | week starts Monday in reports/calendar |
due | 4 | colour tasks as “due” within 4 days (default is 7) |
nag | (empty) | silence the “you have more urgent tasks” scold |
confirmation | on | still prompt before bulk/destructive edits |
dateformat* | Y-M-D | ISO-8601 dates on input, reports, info, annotations |
taskwarrior-tui settings
All under the uda.taskwarrior-tui.* namespace in taskrc:
- Behaviour — opens on the
nextreport (status:pending), shows the task detail pane (show-info), wraps top/bottom (looping), jumps to a task after adding it, pre-fills metadata when modifying, prompts before delete but not before done. - Cursor / marks —
•selection indicator (bold),✔for multi-selected rows. - Quick-tag —
ttoggles the+nexttag for fast prioritising. - Dracula chrome — context bar, navbar, calendar title/today, command-line
highlight, and the selection row all use the same named-ANSI palette as
dracula.theme(e.g. active context = black oncolor4/purple).
taskwarrior-tui’s movement keys are already vim-style out of the box
(j/k, g/G, / to filter, : for a command), so no key remapping is
needed. Run it with taskwarrior-tui, press ? for the full key list.
Number-key shortcuts
Press 1–3 in the TUI to run a one-key action on the selected (or
multi-marked) task(s). Each is a small script in shortcuts/, invoked with the
task UUID(s) as arguments (rc.confirmation=off etc. so it’s a single keypress):
| Key | Script | Action |
|---|---|---|
1 | snooze.sh | wait:tomorrow — hide until tomorrow |
2 | today.sh | due:today — pull into today |
3 | bump.sh | priority:H +next — escalate |
Add more by dropping an executable in shortcuts/ and pointing
uda.taskwarrior-tui.shortcuts.N at it (keys 1–9). ~ is expanded; the
script must be chmod +x with a shebang.
Shell access — TUI only, no Taskwarrior CLI aliases
Taskwarrior is driven here only through its TUI. zsh/.zshrc defines a
single Taskwarrior alias, tt=taskwarrior-tui; there are intentionally no
Taskwarrior t/ta/tl/… CLI aliases. (The task alias below is go-task,
not Taskwarrior.)
The reason is a name clash: task is an overloaded command on this machine —
it’s both Taskwarrior and go-task (the Taskfile.yml
runner, installed at $GOBIN/task). The bare task command is bound to
go-task with an explicit alias in zsh/.zshrc:
alias task="${GOBIN:-$HOME/go/bin}/task"
Without it, $(brew --prefix)/bin precedes $GOBIN in $PATH, so task would
resolve to Taskwarrior unless a project prepends $GOBIN (e.g. via direnv) —
fragile and directory-dependent. The alias makes it deterministic. There’s no
per-directory dispatch to set up: go-task itself walks up parent directories to
the nearest Taskfile.{yml,yaml}, so task <target> works anywhere inside a
project tree (and task -g runs $HOME/{T,t}askfile.{yml,yaml}).
zsh/.zshrc also loads go-task’s own completion for task (cached at
$XDG_CACHE_HOME/gotask-completion.zsh). That deliberately overrides
Taskwarrior’s _task (linked in Homebrew’s site-functions, which auto-binds
to task) — otherwise task <TAB> would fire Taskwarrior’s helpers
(task _zshids, …) against go-task and error.
The OMZ taskwarrior plugin is also dropped from the plugin list: its stale
2022 _task errors on current zsh (_task_attributes:zregexparse: invalid regex : local, from an uninitialized word array), and we don’t want
Taskwarrior CLI completion bound to task anyway.
Note: the alias only affects this interactive shell. taskwarrior-tui is a separate process that does its own
$PATHlookup fortask(it has no flag to point at a specific binary), so it never sees the alias and keeps talking to Taskwarrior — which wins in$PATHby default. The one caveat: launchttfrom a directory that prepends$GOBIN(e.g. a direnv’d Taskfile project) and the TUI would shell out to go-task instead. Start it from elsewhere, or frontttwith a Taskwarrior-first$PATHif that ever bites.
Things you might add
Personal enough that they’re left out by default:
# Contexts — scope every command to a project/tag set, toggle with `task context`
task context define work +work or project:work
task context define home -work
task context work # activate; task context none to clear
# A leaner custom report
task config report.next.columns id,start.age,priority,project,due.relative,description.count
task config report.next.labels ID,Active,P,Project,Due,Description
# Sync (TaskChampion server / taskwarrior.org) — then set
# uda.taskwarrior-tui.background_process=sync to auto-sync from the TUI.
Verifying
task rc:task/.config/task/taskrc rc.data.location=/tmp/taskcheck show >/dev/null # parses config + theme
Colours and the TUI need a real Dracula-themed terminal to eyeball —
taskwarrior-tui can’t be checked headlessly.
Fresh-machine setup
brew install taskwarrior-tui # in the Brewfile; pulls in task/Taskwarrior
stow task
tig
Stow package for ~/.tigrc. tig is the text-mode interface for Git —
the read/navigate companion to lazygit’s interactive UI.
Table of contents
Layout
| File | Stows to |
|---|---|
tig/.tigrc | ~/.tigrc |
Display options
set git-colors = no # don't double-style on top of git's color.* output
set diff-options = -m --first-parent # render merges as patches; follow first parent only
set show-changes = yes # staged/unstaged appear as faux commits in main view
set blame-options = -C -C -C # `tig blame` follows copies across files
set wrap-lines = yes # wrap long lines instead of clipping
show-changes = yes is especially nice: in the main commit view, the
top entries show your current HEAD state with unstaged + staged
changes — no need to bounce back to git status.
Color scheme
Dracula-ish, using ANSI color slots (named colors) rather than hex.
Each terminal’s Dracula theme maps the ANSI palette consistently
(magenta → pink, blue → comment-purple, etc.), so the same .tigrc
renders right on macOS iTerm/Terminal.app and Linux URxvt/Wezterm
without per-platform tweaks.
| Element | Color |
|---|---|
| Selected line (cursor) | bold magenta (pink) |
| Search results | black on yellow |
| Line numbers | blue (comment-purple), subtle |
| Active pane title | bold magenta on black |
| Inactive pane title | blue on black |
| diff +/- bodies | green / red |
| diff +/- highlight | inverse green / red |
| Commit headers | bold yellow |
| Index lines | cyan |
| Hunk markers | magenta |
| Author column | cyan |
| Date column | blue (comment-purple) |
| HEAD marker | bold magenta |
| Tags | bold yellow |
| Remote refs | green |
Trailers (Reported-by:, Signed-off-by:) | green |
Key bindings
| Key | Action |
|---|---|
Y | Copy the selected commit SHA to the system clipboard (pbcopy / xclip / wl-copy, whichever is available) |
All of tig’s built-in bindings (Enter view commit, R refresh, q
quit, ? help, etc.) remain unchanged.
Fresh-machine setup
brew install tig # in the Brewfile
stow tig
tmux
Stow package for ~/.tmux.conf. Installs via stow tmux from the dotfiles
root. TPM auto-bootstraps on first run, so a fresh machine just needs tmux
itself.
Table of contents
Quick reference
Prefix is Ctrl+A (not the tmux default Ctrl+B). Everything below
assumes you press the prefix first.
Sessions, windows, panes
| Keys | Action |
|---|---|
prefix + a | Toggle to last window |
prefix + c | New window |
prefix + , | Rename window |
prefix + s | Session picker (built-in) |
prefix + F | fzf picker for sessions / windows / panes (tmux-fzf) |
prefix + g | Ripgrep+fzf popup over the current pane’s directory (runs frg; opens result in $EDITOR) |
prefix + d | Detach session |
prefix + r | Reload ~/.tmux.conf |
Splits and navigation (tmux-pain-control)
| Keys | Action |
|---|---|
| `prefix + | ` |
prefix + - | Split pane vertically (top/bottom) |
prefix + h/j/k/l | Move focus left/down/up/right |
prefix + H/J/K/L | Resize pane (repeatable) |
prefix + < / > | Swap window with previous/next |
Copy / paste (vi mode + tmux-yank)
| Keys | Action |
|---|---|
prefix + [ | Enter copy mode |
v | Begin visual selection |
y | Copy selection to system clipboard (pbcopy / xclip / wl-copy) |
q | Exit copy mode |
prefix + ] | Paste |
Session persistence (tmux-resurrect + tmux-continuum)
| Keys | Action |
|---|---|
prefix + Ctrl-s | Save current session manually |
prefix + Ctrl-r | Restore last saved session |
Continuum also auto-saves every 15 minutes and auto-restores the last
session whenever tmux starts (@continuum-restore 'on').
Misc
| Keys | Action |
|---|---|
prefix + Ctrl-s (this config) | Toggle synchronize-panes for the current window |
| Mouse | Enabled — click to focus panes, drag to resize, scroll to scrollback |
SYNC shows in the right status bar while pane sync is active.
Plugins
Managed by TPM. Install or update with
prefix + I (capital i) inside tmux.
| Plugin | Purpose |
|---|---|
tmux-sensible | Saner defaults (history-limit, escape-time, focus-events, etc.) |
tmux-pain-control | ` |
tmux-yank | Copy from tmux to the OS clipboard |
tmux-resurrect | Save / restore tmux sessions |
tmux-continuum | Auto-save every 15 min, auto-restore on tmux start |
tmux-fzf | prefix + F opens an fzf picker for sessions/windows/panes/keybindings |
dracula/tmux | Status-bar theme |
Optional plugins commented out in the config (uncomment + prefix + I):
tmux-logging, tmux-sessionist, tmux-menus, tmux-sidebar.
Fresh-machine setup
# tmux must be installed (Brewfile handles macOS; apt install tmux on Debian)
stow tmux # from the dotfiles root
tmux # TPM auto-clones and installs plugins on first launch
vim
Stow package for ~/.vimrc and ~/.vim/. Vim 8+ with vim-plug managing
the plugin set.
Table of contents
- Plugins
- Leader-key cheat sheet
- Other mappings
- Non-default settings
- Window/pane navigation across vim and tmux
- Ripgrep integration
- Go workflow
- Runtime directories
- Fresh-machine setup
Leader is , (let mapleader=',').
Plugins
Managed by vim-plug. Install/update
with :PlugInstall / :PlugUpdate. Clean orphans with :PlugClean!.
Editing essentials (tpope stack)
| Plugin | Use |
|---|---|
vim-commentary | gcc toggles a line, gc{motion} toggles a range |
vim-surround | cs'" → change 'foo' to "foo"; ys{motion}{char}; ds{char} |
vim-repeat | makes the above two .-repeatable |
vim-sleuth | auto-detect indent settings from the file |
vim-fugitive | :Git, :Gdiffsplit, :Gblame, :Git log |
vim-dispatch | async build/test runner |
vim-projectionist | project-aware :A-style file switching |
vim-unimpaired | bracket-pair mappings: ]q/[q quickfix, ]b/[b buffers, ]c/[c (shared with gitgutter), ]<Space> add blank line, etc. |
vim-eunuch | shell-command Ex wrappers: :Rename, :Move, :Delete, :SudoWrite, :Chmod |
UI
| Plugin | Use |
|---|---|
vim-airline | statusline with mode/branch/lint |
bufexplorer | <leader>be/bt/bs/bv — list/toggle/split buffers |
preservim/nerdtree | <leader>nt toggle, <leader>nf reveal current file |
tagbar | <leader>tt outline of symbols (needs ctags) |
undotree | <leader>u visualize undo branches (paired with undofile) |
Fuzzy finder
| Plugin | Use |
|---|---|
junegunn/fzf + junegunn/fzf.vim | :Files, :Rg, :Buffers, :GFiles?, :History |
Editor integrations
| Plugin | Use |
|---|---|
editorconfig-vim | honor .editorconfig files |
vim-tmux-navigator | <C-h/j/k/l> jumps across vim splits and tmux panes |
vim-gitgutter | gutter diff markers; ]c/[c next/prev hunk |
splitjoin.vim | gS/gJ split/join one-liners ↔ blocks |
auto-pairs | auto-close brackets/quotes |
ultisnips | snippet engine |
vim-snippets | the snippet library ultisnips expands (engine ships none on its own) |
Linting
| Plugin | Use |
|---|---|
dense-analysis/ale | async linting (pylint, shellcheck, yamllint). Go is owned by vim-go. |
Languages
| Plugin | Use |
|---|---|
fatih/vim-go | Go IDE-ish features. See Go workflow. |
rust-lang/rust.vim | Rust filetype + rustfmt |
Colors
dracula is the scheme for all filetypes, including Go.
Leader-key cheat sheet
(Leader = ,)
| Keys | Action |
|---|---|
,<space> | clear search highlight |
,, | switch to last file |
,ev / ,sv | edit / source $MYVIMRC |
,ln / ,nw / ,rn | toggle number / wrap / relativenumber |
,sc | toggle spell check |
,tn / ,tc | new / close tab |
,bd / ,bda | close current buffer / close all buffers |
,dc / ,do | close diff / :DiffOrig (diff against unsaved) |
,rt | retab |
,url | open URL under cursor (OS default browser) |
,gg | toggle gitgutter line highlights |
,tt | toggle tagbar |
,nt / ,nf | toggle / reveal NERDTree |
,u | toggle undotree |
,f / <C-p> | fzf :Files |
,b | fzf :Buffers |
,a | fzf :Rg (prompt for pattern) |
,* | fzf :Rg word under cursor |
,h | fzf :History |
,q | close quickfix |
<C-n> / <C-m> | :cnext / :cprevious |
Other mappings
Non-leader bindings worth knowing:
| Keys | Action |
|---|---|
jj (insert) | <Esc> — escape without leaving home row |
<Down> / <Up> (insert) | move by visual line (gj/gk), so wrapped lines work intuitively |
j / k (normal) | move by visual line (gj/gk) |
<M-j> / <M-k> (normal + visual) | move the current line / selection down / up |
< / > (visual) | indent left / right and keep the selection (gv re-select) |
<tab> (normal + visual) | jump to matching bracket (rebinds %) |
n / N / * / # | re-center the screen on the match (Nzz) |
/ | starts a “very magic” search (/\v — most punctuation is regex metachar) |
Q | gq — reformat with motion (the default Q ex mode is rarely useful) |
Custom Ex commands:
| Command | Action |
|---|---|
:W / :SudoWrite | sudo-save the current buffer (:W is an alias for vim-eunuch’s :SudoWrite) |
:DiffOrig | vertical diff between the buffer and the on-disk file (,do triggers it) |
Non-default settings
| Setting | Reason |
|---|---|
hidden | allow buffer switching with unsaved changes |
clipboard=unnamed,unnamedplus | system clipboard on Mac (unnamed) and Linux (unnamedplus) |
termguicolors | true color; pairs with tmux terminal-overrides ",xterm-256color:Tc" |
splitbelow, splitright | new splits open in more intuitive positions |
completeopt=menu,menuone,noselect | dropdown without auto-selecting first match |
mouse=a | mouse on |
undofile, undolevels=500 | persistent undo across sessions |
backup, backupdir=~/.vim/backup | keep backup files in a known place |
directory=~/.vim/tmp, undodir=~/.vim/undo | move swap/undo out of the working dir |
colorcolumn="80," + range(120,999) | line marker at 80, solid color from 120+ |
textwidth=200 | global default; overridden to 80 for FileType text |
ignorecase + smartcase | case-insensitive unless query has uppercase |
tags=tags;/ | walk up looking for ctags tags file |
Trailing whitespace is highlighted in red and stripped on save for
*.go/*.py/*.sh. The motion/search rebindings that pair with
these settings (visual-line j/k, recentering n/*, <tab> for
bracket matching) are listed under Other mappings.
Window/pane navigation across vim and tmux
<C-h/j/k/l> is bound in both. With vim-tmux-navigator and tmux’s
matching keybindings, hitting <C-l> from a rightmost vim split jumps
into the tmux pane to the right. Same in reverse.
Ripgrep integration
-
:Rg foo— interactive fzf picker over rg results (fromfzf.vim). -
,*—:Rgfor the word under the cursor. -
:grep foo— batch search; populates the quickfix list. Uses rg via:set grepprg=rg\ --vimgrep\ --smart-case\ --hidden set grepformat=%f:%l:%c:%m -
For “live rg + fzf, open at line in editor” from outside vim, see the
frgfunction infzf/README.md.
Go workflow
vim-go is configured with:
g:go_fmt_command="goimports"— format + organize imports on saveg:go_metalinter_autosave=1— run linter on saveg:go_auto_sameids=1,g:go_auto_type_info=1— highlight same identifiers, show types- Many
g:go_highlight_*=1flags for richer syntax
Go-specific leader maps (active only in .go files):
| Keys | Action |
|---|---|
,b | :GoBuild or :GoTestCompile based on filename |
,c | toggle coverage |
,t | test |
,r | run |
,e | rename (gopls) |
,i | info under cursor |
,s | implements |
,ds/,dt/,dv | def in split/tab/vsplit |
,gd/,gv/,gb | doc / doc-vertical / doc-browser |
,gl | metalinter |
,ga | :GoAlternate (jump between code and test file) |
Runtime directories
The repo includes empty .gitignore files in these dirs to keep them
present in stow targets without committing the runtime data:
| Dir | What goes there |
|---|---|
~/.vim/backup | *~ backup files (gitignored repo-wide) |
~/.vim/tmp | swap files |
~/.vim/undo | persistent undo blobs |
~/.vim/plugged | vim-plug-managed plugins (not in repo) |
Fresh-machine setup
stow vim # symlinks ~/.vimrc and ~/.vim/
vim +"PlugInstall --sync" +qall
For headless (CI/script) install, vim-plug needs a real pty — run inside
a temporary tmux session (see CLAUDE.md at the repo root).
yamllint
Stow package for ~/.config/yamllint/config. yamllint is a YAML
linter — invoked by lefthook on pre-commit, by ALE in vim, and by
hand.
Table of contents
Layout
| File | Stows to |
|---|---|
yamllint/.config/yamllint/config | ~/.config/yamllint/config |
yamllint only reads this file when run without -c <path> and when
the project has no .yamllint/.yamllint.yml/.yamllint.yaml of its
own.
Rule tweaks
Starting from extends: default:
| Rule | Change | Why |
|---|---|---|
line-length: 120 (warning) | bump from 80, demote to warning | nobody wraps YAML at 80 |
document-start | disable | --- is rarely useful in single-doc files |
truthy.check-keys: false | don’t flag on: keys | GitHub Actions uses on: as a top-level key |
comments.min-spaces-from-content: 1 | down from 2 | the default is overly strict |
comments-indentation | disable | mis-indented comments are common in real YAML and rarely matter |
indentation.indent-sequences: consistent | don’t require nested under key | matches both common styles |
braces / brackets max-spaces-inside: 1 | allow { k: v } | flow style is fine |
Per-project overrides
Drop a .yamllint in your repo root — it wins. Lefthook’s pre-commit
hook (template at lefthook/lefthook.yml) will use whichever yamllint
finds first.
Fresh-machine setup
brew install yamllint # in the Brewfile
stow yamllint
zsh
Stow package for ~/.zshrc. Oh My Zsh + powerlevel10k + a curated plugin set.
Table of contents
- Theme: powerlevel10k
- Plugins
- Completion
- PATH
- History
- Shell behavior
- Environment
- Tool integrations
- OMZ behavior
- What’s loaded from elsewhere
Theme: powerlevel10k
ZSH_THEME="powerlevel10k/powerlevel10k". The instant-prompt preamble is at
the top of .zshrc and must stay there. Configured prompt lives in
~/.p10k.zsh (see p10k/ stow package); .zshrc sources it if present.
Plugins
aliases colored-man-pages command-not-found cp dirhistory direnv docker
extract fzf gitfast golang helm kind kube-ps1 kubectl skaffold sudo
tig tmux
The OMZ
taskwarriorplugin was deliberately not included: its stale 2022_taskcompletion errors on current zsh (_task_attributes:zregexparse: invalid regex). Taskwarrior is used only via its TUI (tt=taskwarrior-tui) — there are notask/tCLI aliases, and the baretaskcommand is go-task, whose completion.zshrcloads instead. Seetask/README.md.
Highlights of non-obvious ones:
| Plugin | Why |
|---|---|
gitfast | Replaces git’s built-in completion with the official upstream Git completion — faster on big repos. |
command-not-found | Suggests how to install missing commands (apt install foo / brew install foo). |
extract | x file.{zip,tar.gz,7z,...} — one command for any archive. |
sudo | Esc Esc prepends sudo to the current line (or recalls the previous one with sudo). |
dirhistory | Alt+←/→ walks back/forward through cd history. Pairs with AUTO_PUSHD. |
golang | Adds Go aliases (g, gob, goc, gor, …) and completion. |
kube-ps1 | Current k8s context/namespace in the prompt. |
Custom plugins (conditionally loaded)
Loaded only if the directory exists under ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/.
| Plugin | Effect |
|---|---|
you-should-use | Nags when you type the long form of an alias. |
zsh-autosuggestions | Greyed-out command completion from history; right-arrow to accept. |
fzf-tab | Replaces zsh’s default completion menu with an fzf picker. Must load last (after every plugin that registers completions). |
Install on a fresh machine:
ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
git clone --depth=1 https://github.com/Aloxaf/fzf-tab "$ZSH_CUSTOM/plugins/fzf-tab"
git clone --depth=1 https://github.com/MichaelAquilina/zsh-you-should-use "$ZSH_CUSTOM/plugins/you-should-use"
The bootstrap-debian.sh script does this automatically.
Completion
Before OMZ runs compinit, the config prepends Homebrew completion dirs
to FPATH so brew-installed tools (gh, lazygit, yq, stern, etc.)
get proper tab-completion:
FPATH="$(brew --prefix)/share/zsh-completions:$(brew --prefix)/share/zsh/site-functions:$FPATH"
ZSH_DISABLE_COMPFIX=true
ZSH_DISABLE_COMPFIX=true skips OMZ’s complaint about Homebrew’s
group-writable share/ directory (which is intentional on Homebrew, not
fixable without fighting brew).
The zsh-completions brew formula provides extra completion definitions
beyond what individual tools ship.
zstyle polish (after OMZ’s compinit):
zstyle ':completion:*' menu select # arrow-key menu nav
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' # case-insensitive
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" # colored menu
zstyle ':completion:*' rehash true # pick up new $PATH binaries
PATH
export PATH=$PATH:$HOME/bin
export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN
export PATH=$HOME/.local/bin:$PATH # pipx / user-installed binaries
export PATH=$PATH:/usr/local/go/bin
GEM_HOME is set only if ruby is on PATH (guarded).
On macOS, Homebrew’s GNU coreutils/findutils/sed/tar/which/grep/binutils get prepended to PATH so GNU semantics win over the BSD defaults:
if [[ "$OSTYPE" == darwin* ]]; then
for gnu_tool in binutils coreutils findutils gnu-sed gnu-tar gnu-which grep; do
gnu_bin="/opt/homebrew/opt/$gnu_tool/libexec/gnubin"
[[ "$gnu_tool" == "binutils" ]] && gnu_bin="/opt/homebrew/opt/$gnu_tool/bin"
[[ -d "$gnu_bin" ]] && export PATH="$gnu_bin:$PATH"
done
fi
History
HISTFILE = ~/.zsh_history
HISTSIZE = 500000 # in-memory
SAVEHIST = 500000 # on disk
Setopts beyond OMZ defaults:
HIST_FCNTL_LOCK,HIST_FIND_NO_DUPS,HIST_IGNORE_ALL_DUPS,HIST_LEX_WORDS,HIST_NO_FUNCTIONS,HIST_NO_STORE,HIST_REDUCE_BLANKS,HIST_SAVE_NO_DUPSHISTORY_IGNORE="(ls|cd|pwd|exit)"— never store these
OMZ’s defaults provide SHARE_HISTORY, EXTENDED_HISTORY,
HIST_EXPIRE_DUPS_FIRST, HIST_IGNORE_SPACE, HIST_VERIFY — re-affirmed
in the config for clarity.
atuin replaces Ctrl+R with a SQLite-backed TUI on
top of this history.
Shell behavior
EXTENDED_GLOB # ^pat (negate), ~pat (exclude), **/* (recursive)
NO_BEEP # silence the bell
INTERACTIVE_COMMENTS # # comments allowed in interactive lines
AUTO_CD # `dirname` alone cd's there
AUTO_PUSHD # every cd pushes the old dir onto the stack
PUSHD_IGNORE_DUPS
PUSHD_SILENT
COMPLETE_IN_WORD # complete from cursor position, not only end
ALWAYS_TO_END # cursor goes to end after completion
Environment
EDITOR = vim
LANG = en_US.UTF-8
LESS = -RFX # raw colors, quit-if-one-screen, no alt-screen
LESSHISTFILE = - # don't write ~/.lesshst
MANPAGER = sh -c 'col -bx | bat -l man -p' # syntax-highlighted man pages
BAT_THEME = ansi # bat respects terminal palette
RIPGREP_CONFIG_PATH = $HOME/.config/ripgrep/config # rg won't read its config without this
Try man ssh after a fresh shell — colorized.
Tool integrations
| Tool | Wiring |
|---|---|
| zoxide | eval "$(zoxide init zsh)" (guarded on command -v) |
| atuin | eval "$(atuin init zsh)" (guarded; replaces Ctrl+R) |
| eza | when installed: ls is a function (falls back to real ls for short flag bundles with t, e.g. ls -ltrh), plus ll/la/lt/ltrh aliases |
| git | curated shell shortcuts: gst ga gaa gc gca gco gcb gd gds gp gpf glg (names match the OMZ git plugin, which isn’t enabled — gitfast gives completion only) |
| kitty | kt/kw rename + kgo <title> jump-by-name + ktabs listing (uses jq) when installed |
| fzf | OMZ fzf plugin auto-sources ~/.fzf.zsh (see fzf/) |
| zsh-syntax-highlighting | sourced at the end; tries Linux, Apple Silicon brew, Intel brew paths in order |
OMZ behavior
zstyle ':omz:update' mode reminder # prompt when updates are available
zstyle ':omz:update' frequency 14 # check every 2 weeks
DISABLE_UNTRACKED_FILES_DIRTY=true # faster git status in big repos
COMPLETION_WAITING_DOTS=true
What’s loaded from elsewhere
~/.fzf.zsh(auto-sourced by OMZfzfplugin) — fzf options + thefrgripgrep+fzf function. Seefzf/README.md.~/.p10k.zsh— prompt configuration. Seep10k/README.md.