# 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 `.** 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 ` 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. Both copies also glob one directory level deeper, matching `~/.config/dot/commands//.fish`, so a subcommand's companion file (e.g. a Python helper) can live alongside it in its own directory. `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//`, 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/.fish`, sources it, and calls `_dot_`. A subcommand needing a companion file can instead live nested one level deeper, as `~/.config/dot/commands//.fish` — both layouts dispatch identically. 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/.fish` defining a `_dot_` function. 2. Confirm `dot ` 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_`'s first positional argument before `argparse`, and call a `_dot__usage` function that prints usage and every flag. If `_dot_` 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__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" ` 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. - `~/.claude/` and this project's own `.claude/` (e.g. `~/.config/dot/.claude/`) are two different directories that both happen to exist. Project-relative paths referenced in specs, task breakdowns, or other project docs — like `.claude/spec/.md` or `.claude/tasks/-.md` — are relative to this project directory (`~/.config/dot/.claude/...`), not to `$HOME/.claude/`. Writing to `$HOME/.claude/tasks/` instead of `~/.config/dot/.claude/tasks/` silently lands files in Claude Code's own global config dir instead of the project. ## 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`](../../.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.