A Peek Inside My Dotfiles
Freek Van der Herten recently published a tour of his dotfiles, so I opened mine to see what similarities and differences I would find. There are many similarities but the organizing principle is different.
My dotfiles live at ~/_dot/. At a high level:
~/_dot/
├── app-settings/ # Configuration for Ghostty, Micro, Yazi, Zellij, VS Code, and coding agents
├── dot-common/ # Shell aliases and functions used from .zshrc
├── dot-files/ # Traditional files such as zshrc, gitconfig, vimrc, and editorconfig
├── agent-library/ # Shared instructions, skills, and agent definitions
├── install.sh # Connects files in this repo to their expected locations
└── install-brew.sh # Installs the command-line tools used by the configuration
This is not every setting. These are the pieces that have earned their place.
The Installer
The setup script symlinks files from the repo to the locations where each tool expects to find them:
for f in zshrc vimrc editorconfig gitignore prettierrc; do
ln -sf "$HOME/_dot/dot-files/$f" "$HOME/.$f"
done
Not copies. Editing ~/.zshrc edits the version-controlled source. There is no sync step and no stale copy to remember.
Application configs only get linked when the application exists:
command -v ghostty >/dev/null 2>&1 && \
mkdir -p "$HOME/.config/ghostty" && \
ln -sf "$HOME/_dot/app-settings/ghostty/config" "$HOME/.config/ghostty/config"
This makes the installer safe to run at any point during machine setup. Missing applications get skipped. Installed applications pick up the configuration on the next run. No ordering dependency, no separate first-time path.
The Prompt
No oh-my-zsh. No Starship. Seven lines of built-in Zsh:
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats ' %F{red}%b%f'
setopt PROMPT_SUBST
PROMPT='%F{green}%~%f${vcs_info_msg_0_}
%(?.%F{blue}.%F{red})%(!.#.$)%f '
The first line shows the current path in green and the Git branch in red. The second line contains the prompt character. It is blue after a successful command and red after a failure. That is all I want from a prompt. I do not need execution time, Kubernetes context, or a row of icons.
auto_cd lets me enter a directory path without typing cd. Incremental history is written as commands run rather than waiting for the shell to exit.
Shell Tricks
Most aliases only save keystrokes. The interesting ones change how commands compose.
The Trailing Space
alias sudo='sudo '
alias which='which '
alias source='source '
A trailing space tells Zsh to expand aliases in the next word too. Without it, sudo c some-file sends the literal string c. With the space, Zsh resolves c to bat first. Same trick lets which report what an alias resolves to and source expand one in its argument.
One ls, Several Views
alias ls="eza --group-directories-first -s type --icons --hyperlink --octal-permissions --ignore-glob='.DS_Store'"
alias l='ls -a'
alias ll='ls -la'
alias lt='ls --tree --level=2'
alias tree='ls --tree --level=5'
Every variant chains through the base alias. Icons, hyperlinks, permission octals, and .DS_Store suppression remain consistent regardless of which shortcut I use.
Yazi With Directory Handoff
A child process cannot change the working directory of the shell that launched it. The y function works around this:
function y() {
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd
command yazi "$@" --cwd-file="$tmp"
IFS= read -r -d '' cwd < "$tmp"
[ "$cwd" != "$PWD" ] && [ -d "$cwd" ] && builtin cd -- "$cwd"
command rm -f -- "$tmp"
}
Yazi writes its final location to a temp file on exit. The wrapper reads it and performs the cd in the parent shell. Navigate visually, press q, land in the directory you selected.
That function carries state across a process boundary. It changes the workflow instead of shortening the spelling.
Git
Diffs Through Delta
[core]
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
side-by-side = true
line-numbers = true
navigate = true
[merge]
conflictstyle = diff3
[help]
autocorrect = 20
Delta handles every diff surface: git diff, git show, interactive staging. Side-by-side with line numbers makes changes scannable rather than something to mentally reassemble. navigate = true means n/N jump between diff sections. diff3 includes the common ancestor during merge conflicts, which is often the missing fact when both sides look reasonable.
autocorrect = 20 gives me two seconds to cancel when Git guesses a mistyped command.
Multi-Remote Sync
I push to both GitHub and GitLab. gpa pushes the current branch to every configured remote:
function git_push_all() {
local branch=$(git symbolic-ref --short HEAD 2>/dev/null)
local remote
for remote in $(git remote); do
echo "» pushing to $remote/$branch"
git push "$remote" "$branch"
done
}
gsync does the full round trip: fetch all remotes, find whichever is furthest ahead of local, stash dirty state, pull from the leader, restore the stash, then push to every remote that is now behind. One command resolves any sync gap regardless of which machine last pushed.
Terminal Tools
Ghostty
Mostly typography: Geist Mono at 15pt, warm dark background (#1b1a18), 30% extra cell height, transparent titlebar. The behavioral setting:
keybind = global:cmd+backquote=toggle_quick_terminal
quick-terminal-position = top
quick-terminal-size = 55%,85%
Cmd+backtick drops a terminal from the top of the screen from any application. Large enough for real work without becoming another window to manage.
Micro
Not vim. Not neovim. A terminal editor where Ctrl+S saves, Ctrl+C copies, and Ctrl+Z undoes.
{
"diffgutter": true,
"mkparents": true,
"rmtrailingws": true,
"saveundo": true,
"tabstospaces": true
}
mkparents creates the parent directory on save if it does not exist. saveundo persists undo history across sessions. For quick edits in the terminal, I do not want a modal editor.
Zellij in Locked Mode
default_mode "locked"
theme "kanagawa"
copy_command "pbcopy"
default_mode "locked" is the reason I use Zellij over tmux. Locked mode passes every keystroke through to the running program. Ctrl+G toggles between locked mode and the normal mode where Zellij shortcuts work.
A multiplexer should not make me think about whether a key belongs to the shell, the editor, or the multiplexer itself.
Yazi Layout
[mgr]
ratio = [0, 2, 5]
Yazi normally shows three columns: parent, current, and preview. Setting the parent ratio to 0 removes it entirely and gives that space to the preview. Files open in Micro by default.
Coding Agent Configuration
The newest part of the repo. Claude Code, Codex, and OpenCode have different configuration formats, but the operating rules that describe how I work should exist in one place:
ln -sf "$HOME/_dot/agent-library/GLOBAL-AGENTS.md" "$HOME/.claude/CLAUDE.md"
ln -sf "$HOME/_dot/agent-library/GLOBAL-AGENTS.md" "$HOME/.codex/AGENTS.md"
One markdown file, two symlinks. Change a rule once, every agent picks it up.
Skills follow the same pattern. Each skill lives once under agent-library/skills/, then gets symlinked into the directories Claude, Codex, and OpenCode read. There is no manual copying and no drift between three slightly different instruction files.
Claude Code also gets a custom status line: a Bash script that renders model, effort level, context-window usage, session cost, working directory, and Git branch with dirty-state dots (green for staged, orange for modified, pink for untracked). Those are the pieces of state that change how I use the tool, so they belong in the prompt.
Session Cleanup
Each tool stores its session history differently, so clai puts cleanup behind one interface:
clai # dry run
clai -f # remove all inactive sessions
clai -f "keep" # retain sessions with "keep" in the name
clai -d "tmp-" # remove only matching sessions
clai -o claude,codex -f # limit to selected providers
Dry run is the default. Deletion requires -f, active sessions are skipped, and names can be retained or selected by pattern. The provider-specific storage remains an implementation detail inside the function.
This is what dotfiles look like once coding agents become ordinary development tools. Prompt files, skills, permissions, and session hygiene belong beside shell and editor configuration because they share the same failure mode: something configured locally that disappears during the next machine setup.
The Package List
The terminal utilities, REPLs, and fonts that support the configuration above:
# utilities (terminal)
brew install bat eza fd fzf gh glab git git-lfs git-delta git-svn lazygit pre-commit
brew install 7zip cmake curl httpie wget ripgrep ast-grep jq yq
brew install zoxide zsh-autosuggestions zsh-syntax-highlighting
brew install btop gdu micro yazi zellij
brew install ffmpeg imagemagick poppler resvg tdf yt-dlp
# REPLs, package managers, fonts
brew install psysh ptpython
brew install composer mise uv
brew install font-geist-mono font-hasklig font-jetbrains-mono
The modern replacements (bat, eza, fd, ripgrep, delta, zoxide) sit alongside ast-grep for structural code search, gdu for visual disk usage, and slides and tdf for terminal-based presentations. psysh and ptpython are enhanced REPLs for PHP and Python. mise manages runtime versions. uv handles Python packaging.
Installing any of these takes one brew install. The parts worth version-controlling are the mechanisms between them: a wrapper that carries directory state out of a child process, one instruction file reaching three coding agents, a locked multiplexer that never fights for a keystroke, and a cleanup function that knows how four tools store their sessions differently. Those are what install.sh rebuilds.