Problem
All ~20 symlinked config files live in the repo root, mixed in with docs, scripts, and meta files (README.md, MIGRATION.md, BrewFile, skills, etc.). Every new tool adds another root file and a hand-maintained line in the LINKS manifest in setup_sim_links.zsh. The root gets noisier over time and there are two sources of truth for "where does this file go" (its location + its manifest entry).
Research: standard approaches
- Grouped
config/<tool>/ dir, keep manifest — cleaner root, but the manifest still grows with every tool. Half the win.
- Home-mirror layout (GNU Stow convention) ✅ chosen — files live under
home/ at paths mirroring their real destination (home/.config/starship.toml → ~/.config/starship.toml). The symlink script derives destinations from paths; no manifest for the normal case. This is the classic, widely-recommended dotfiles pattern.
- Adopt stow/chezmoi/dotbot — rejected. Big migration, new tooling; our custom script's per-file backup behavior is nicer than stow's, and stow's directory folding can surprise (e.g. the
television/ directory link). Repo + skills already handle per-host config and installs.
Agreed approach
- New top-level
home/ directory mirroring $HOME:
home/.zshenv
home/.zshrc
home/.gitconfig
home/.gitignore_global
home/.claude/settings.json
home/.config/starship.toml
home/.config/atuin/config.toml
home/.config/atuin/themes/catppuccin-mocha-mauve.toml
home/.config/karabiner/karabiner.json
home/.config/television/ (directory link — see note below)
home/.config/ghostty/config
home/.config/bat/config
home/.config/hunk/config.toml
home/.warp/keybindings.yaml
home/.warp/themes/catppuccin_mocha.yml
home/.zsh/catppuccin_mocha-zsh-syntax-highlighting.zsh
home/Brewfile (or keep at root — see Decisions)
- Rewrite
setup_sim_links.zsh: walk home/ (files, plus an explicit list of directory-linked paths like television), compute dest = $HOME/<relative path>, keep the existing backup-to-.bak behavior.
- Small
OVERRIDES map for the irregular cases — the escape hatch that makes pure convention workable:
lazygit_config.yml → $(lazygit -cd)/config.yml (destination computed at runtime)
- anything we decide to keep at repo root (see below)
- Non-symlinked repo files (
README.md, CLAUDE.md, MIGRATION.md, docs/, scripts/, hosts/, macos_defaults.sh, skills) stay where they are.
Decisions to confirm during implementation
BrewFile + mise_config.toml: recommend keeping at repo root with override entries, since they're the install ledger that CLAUDE.md and all three skills reference constantly; moving them churns many references for little gain.
- Root cleanup candidates spotted while auditing:
favicon.ico/favicon.png, droid_test/, raycast.rayconfig (not in LINKS), Brewfile.lock.json — decide keep/move/delete as part of this work (or via /cleanup-report).
Migration plan
- Branch off master.
git mv each symlinked file/dir to its home/ path (preserves history).
- Rewrite
setup_sim_links.zsh (convention walker + OVERRIDES + directory-link list). Keep the same UX: idempotent, .bak backups, SKIP on missing source.
- Run it. Verify every link:
for f in ~/.zshrc ~/.gitconfig ...; do readlink $f; done — all should point into home/. Old links get replaced automatically since the script backs up existing dests.
- Open a new shell; smoke-test zsh, starship, atuin, git, lazygit, bat.
- Grep the repo for old paths (
grep -rn "zshrc\b\|starship.toml" --include='*.md' --include='*.zsh' .) and fix references in CLAUDE.md, MIGRATION.md, README.md, docs/conventions/shell-config.md.
- Update skills (below), commit, PR.
Skill updates required
.claude/skills/install-app/SKILL.md — replace the "add file to repo root + add LINKS entry" instructions (~lines 61–64) with: "place the config at home/<its real path relative to $HOME> and re-run ./setup_sim_links.zsh; only touch OVERRIDES for dynamic destinations." Update the zsh-syntax-highlighting path example.
.claude/skills/uninstall-app/SKILL.md — the grep sweep (line 19) and step 2 (~lines 48–49): instead of "remove the LINKS entry", it's "delete the file from home/ (and any OVERRIDES entry), remove the dangling symlink in $HOME."
.claude/skills/cleanup-report/SKILL.md — dangling-symlink audit (~lines 72–74): derive expected destinations from the home/ tree instead of parsing LINKS.
CLAUDE.md — update the symlink-management sentence to name the new convention.
Definition of done
- Repo root contains no tool config files; all symlinked configs live under
home/.
setup_sim_links.zsh needs no edits when adding a normally-located config — drop the file in home/ and re-run.
- All existing symlinks resolve to the new locations; shell + key tools verified working.
- All three skills, CLAUDE.md, and docs reference the new structure; no stale mentions of the old root paths or
LINKS array.
Problem
All ~20 symlinked config files live in the repo root, mixed in with docs, scripts, and meta files (
README.md,MIGRATION.md,BrewFile, skills, etc.). Every new tool adds another root file and a hand-maintained line in theLINKSmanifest insetup_sim_links.zsh. The root gets noisier over time and there are two sources of truth for "where does this file go" (its location + its manifest entry).Research: standard approaches
config/<tool>/dir, keep manifest — cleaner root, but the manifest still grows with every tool. Half the win.home/at paths mirroring their real destination (home/.config/starship.toml→~/.config/starship.toml). The symlink script derives destinations from paths; no manifest for the normal case. This is the classic, widely-recommended dotfiles pattern.television/directory link). Repo + skills already handle per-host config and installs.Agreed approach
home/directory mirroring$HOME:setup_sim_links.zsh: walkhome/(files, plus an explicit list of directory-linked paths liketelevision), computedest = $HOME/<relative path>, keep the existing backup-to-.bakbehavior.OVERRIDESmap for the irregular cases — the escape hatch that makes pure convention workable:lazygit_config.yml→$(lazygit -cd)/config.yml(destination computed at runtime)README.md,CLAUDE.md,MIGRATION.md,docs/,scripts/,hosts/,macos_defaults.sh, skills) stay where they are.Decisions to confirm during implementation
BrewFile+mise_config.toml: recommend keeping at repo root with override entries, since they're the install ledger that CLAUDE.md and all three skills reference constantly; moving them churns many references for little gain.favicon.ico/favicon.png,droid_test/,raycast.rayconfig(not in LINKS),Brewfile.lock.json— decide keep/move/delete as part of this work (or via/cleanup-report).Migration plan
git mveach symlinked file/dir to itshome/path (preserves history).setup_sim_links.zsh(convention walker +OVERRIDES+ directory-link list). Keep the same UX: idempotent,.bakbackups,SKIPon missing source.for f in ~/.zshrc ~/.gitconfig ...; do readlink $f; done— all should point intohome/. Old links get replaced automatically since the script backs up existing dests.grep -rn "zshrc\b\|starship.toml" --include='*.md' --include='*.zsh' .) and fix references inCLAUDE.md,MIGRATION.md,README.md,docs/conventions/shell-config.md.Skill updates required
.claude/skills/install-app/SKILL.md— replace the "add file to repo root + addLINKSentry" instructions (~lines 61–64) with: "place the config athome/<its real path relative to $HOME>and re-run./setup_sim_links.zsh; only touchOVERRIDESfor dynamic destinations." Update the zsh-syntax-highlighting path example..claude/skills/uninstall-app/SKILL.md— the grep sweep (line 19) and step 2 (~lines 48–49): instead of "remove theLINKSentry", it's "delete the file fromhome/(and anyOVERRIDESentry), remove the dangling symlink in$HOME.".claude/skills/cleanup-report/SKILL.md— dangling-symlink audit (~lines 72–74): derive expected destinations from thehome/tree instead of parsingLINKS.CLAUDE.md— update the symlink-management sentence to name the new convention.Definition of done
home/.setup_sim_links.zshneeds no edits when adding a normally-located config — drop the file inhome/and re-run.LINKSarray.