Files
dotfiles/.config/dot/CLAUDE.md
2026-07-05 12:05:02 -04:00

7.5 KiB

Dotfiles

This machine's dotfiles are a bare git repo at ~/.dotfiles, checked out with $HOME as its work-tree. The dot fish function wraps that invocation (git --git-dir=~/.dotfiles --work-tree=$HOME $argv, declared with --wraps=git), so every git subcommand works through it: dot status, dot add, dot commit, dot push, etc.

This directory (~/.config/dot) holds the dot CLI's custom subcommands, tests, and package lists, but the repo tracks files across $HOME — fish config, git identity, the dot function itself, and more. To see everything tracked, run dot ls-tree -r --name-only HEAD from $HOME (paths are shown relative to cwd, so running it from elsewhere silently truncates the list).

For an agent driving this through separate tool calls: cd ~ in one call does not reliably carry over to the next, since each call may reset to the project's working directory. Always cd "$HOME" and run the ls-tree (or any other cwd-sensitive dot/git command) in that same call — e.g. cd "$HOME" && dot ls-tree -r --name-only HEAD — rather than trusting a prior cd to have stuck. Getting this wrong silently narrows the listing to whatever the leftover cwd happens to be, which reads as "this file isn't tracked" when it actually is.

Always add by explicit path

status.showUntrackedFiles=no is set locally (see dot init below), and .gitignore only excludes .dotfiles itself plus OS/editor cruft — it is not a whitelist. That means virtually everything under $HOME reads as untracked, and git status deliberately hides all of it.

Always run dot add <specific-path>. Never dot add -A, dot add ., or any wildcard add — that would try to stage the entire home directory (caches, secrets, everything).

Stage automatically after changes. Once a tracked file is edited, run dot add <specific-path> for it right away rather than waiting to be asked — one explicit path per changed file, still never a wildcard. This does not extend to dot commit or dot push, which still require an explicit request.

The dot CLI

Architecture

dot is defined in one file: ~/.config/fish/functions/dot.fish. It holds three functions:

  • dot (--wraps=git) — dispatches init, help, and any file found under ~/.config/dot/commands/, otherwise forwards everything to git --git-dir=~/.dotfiles --work-tree=$HOME $argv (full passthrough).
  • __dot_init — the bootstrap logic, inlined in the same file rather than autoloaded separately, because it's the one subcommand that must work before the dotfiles repo has ever been cloned onto a machine.
  • __dot_help — prints usage: the built-in commands plus whatever is currently found under ~/.config/dot/commands/, generated by globbing that directory rather than a hardcoded list, so it can't drift from reality.

__dot_help's glob over ~/.config/dot/commands/*.fish is duplicated in ~/.config/fish/completions/dot.fish's __dot_custom_subcommands rather than shared: fish only autoloads a function from a file named after that function, so a helper defined inside dot.fish would be undefined if tab-completion ran before dot had ever been sourced in the session. Keep both copies in sync when the listing logic changes.

dot init:

  • refuses to run if ~/.dotfiles already exists (no re-init support)
  • clones the bare repo from --url (default: the hardcoded Gitea remote) — if the clone fails, it errors out; it never falls back to git init
  • backs up any pre-existing file that checkout would clobber into ~/.dotfiles-backup/<timestamp>/, then retries the checkout
  • explicitly sets status.showUntrackedFiles=no after cloning — this is a local-only git setting, so a fresh git clone never carries it over

Adding a subcommand

Beyond init, dot looks for ~/.config/dot/commands/<name>.fish, sources it, and calls _dot_<name>. These files are deliberately kept out of ~/.config/fish/functions/ (fish's autoload path) so they never become independently invokable top-level commands or clutter tab-completion outside of dot itself.

  1. Create ~/.config/dot/commands/<name>.fish defining a _dot_<name> function.
  2. Confirm dot <name> dispatches to it. No other wiring is needed — ~/.config/fish/completions/dot.fish and __dot_help both discover new command files by globbing that directory, and --wraps=git still covers raw git subcommands.
  3. Implement a help subcommand: check for help as _dot_<name>'s first positional argument before argparse, and call a _dot_<name>_usage function that prints usage and every flag. If _dot_<name> itself dispatches to nested subcommands, apply this same check-then-dispatch pattern at that level too — there's no central --help handling in dot.fish to lean on; each level is responsible for its own. _dot_<name>_usage should print its text as a single multi-line echo "..." string (fish preserves literal newlines inside double quotes) rather than one echo per line.
  4. Add a row to ~/.github/README.md's command table for it — one row per distinct use case, with paths written relative to $HOME (~/.config/dot/...), not relative to the README's own location.
  5. Add a case to ~/.config/dot/tests/dot.fish covering it, including its help output, and run fishtape ~/.config/dot/tests/dot.fish until it passes.

Testing

Tests live at ~/.config/dot/tests/dot.fish, run with fishtape ~/.config/dot/tests/dot.fish. Fishtape is installed via Fisher (fisher install jorgebucaran/fishtape) and tracked in ~/.config/fish/fish_plugins — a real, restorable dependency for developing dot, but never required just to use it.

  • Each scenario overrides $HOME (set -gx HOME (mktemp -d)) before calling dot, so tests never touch the real ~/.dotfiles.
  • Build a throwaway bare "remote" fixture with git init --bare plus a seeded commit, and explicitly set its HEAD (git --git-dir=$remote symbolic-ref HEAD refs/heads/main). Pushing with git push origin HEAD:main does not update the bare repo's HEAD symref — skip this and a clone of the fixture can end up "on a branch yet to be born."
  • Don't use .gitconfig as a fake pre-existing "conflict" file in a fixture — git parses $HOME/.gitconfig as its own global config on every invocation, and garbage content there spams "key does not contain a section" errors that drown out the real assertion. Use a harmless file like .bashrc instead.
  • @test "description" <expr> <op> <expected> mirrors fish's test builtin (-eq, -ne, =, -e, -f, -d, -n, -z); -a/-o combinators aren't supported.

Gotchas

  • ~/.claude/ (Claude Code's own config: skills, agents, commands, etc.) is a plain directory, not a separate git repo of its own — plain git commands run from inside it report "not a git repository". It's tracked the same way as everything else under $HOME: through the dot bare repo. Use dot add/dot status on paths under ~/.claude/, not a git invocation scoped to that directory, and don't assume an unrelated repo (e.g. a separate skills-source checkout elsewhere) is the tracked copy just because it also holds a copy of the same files.

Keybindings

Whenever a keybind is added, changed, or removed in any config on this machine (tmux, KDE, neovim, fish, whatever), add or update its row in ~/.github/keybindings.md in the same change. That file is the single reference for every keybind across tools — it drifts the moment a bind changes somewhere without a matching edit there.