Linking rules

Linking rules

homebot link merges private/ + autolink/, common/ + this OS, and makes
symlinks in $HOME. Four rules cover everything.

TL;DR
  1. Top-level names gain a dot: config → ~/.config. Deeper names are literal.
  2. A folder only one layer provides becomes one symlink.
  3. (name) means this folder must be a real folder, never a link.
  4. Every folder above a (name) folder is marked too (or built in).

1. Names

In autolink/common/ In $HOME Why
zshenv ~/.zshenv top level gains a dot
config/starship.toml ~/.config/starship.toml only the top part changes
config/(zsh)/.zshrc ~/.config/zsh/.zshrc deeper names are literal, dots included
Library/Application Support/X ~/Library/Application Support/X Library is the one name that keeps no dot
.gitkeep, .notes.md ignored dot-names at the top of a layer are never linked
dot-config error old layout: rename it to config

A folder that comes from one layer of autolink/, isn’t marked and isn’t
built in, becomes a single symlink. New files the app writes there land in
the repo, which is usually what you want for config folders.

autolink/common/config/nvim/         ~/.config/nvim  ->  ~/.home/autolink/common/config/nvim

When two layers both provide a folder, it can’t be one link, so it becomes
a real folder holding per-file links:

autolink/common/config/wezterm/wezterm.lua      ~/.config/wezterm/          (real folder)
autolink/linux/config/wezterm/platform.lua        ├── wezterm.lua  -> common/...
                                                  └── platform.lua -> linux/...

private/ never folds: secrets are always linked file by file.

3. (name): must stay a real folder

Wrap a folder’s name in () when the app also writes files there you don’t
want in the repo (sessions, caches, tokens). Only the files you put in the
repo get linked.

autolink/common/(claude)/statusline-command.sh
  →  ~/.claude/                      real folder, Claude Code writes freely
       statusline-command.sh  ->  ~/.home/autolink/common/(claude)/statusline-command.sh

Nest them as deep as you need:

(claude)/settings.json              real ~/.claude, linked settings.json
(claude)/(sessions)/x               real ~/.claude/sessions, linked x
(claude)/projects/                  ~/.claude/projects is one link
(a)/b/(c)/d                         ✗  b is plain but holds (c)
(a)/(b)/(c)/d                       ✓

Built-ins: always real, no () needed

These are never links, however you spell them (config or (config) both work):

.config · .local · .local/bin · .local/share · .local/state · .cache ·
.ssh · .gnupg · Library · Library/Application Support · Library/Preferences

4. The chain rule

A plain folder may become a link. A link can’t contain a folder that must not
be a link. So every folder above a (name) folder must be marked or built in.

Repo path Valid?
config/(zsh)/conf.d/x.zsh ✓ config is built in
(claude)/(sessions)/x ✓
a/(b)/c ✗ rename a to (a)
(a)/b/(c)/d ✗ rename b to (b)

Layout errors

Anything that breaks a rule is a layout error. That part of the repo isn’t
linked (links already there stay put), homebot link exits 1, and the inbox
shows it as urgent. Messages tell you the exact rename.

Mistake Message
Plain folder holds a (…) rename "a" to "(a)" in every layer: it holds …, which must not be a link, so it can't be one either
Same folder, two spellings spelled "(zsh)" in common but "zsh" in linux; use one spelling everywhere
Same file in two layers provided by both common and linux; keep it in one place
() on a file () marks folders only: rename "(x)" to "x"
(), ((a)), (a)) wrap the name in one pair of (), like "(a)"
(.foo) at the top top-level names get their dot from homebot; rename it to "(foo)"
dot-config at the top "dot-config" is the old layout: rename it to "config"

Conflicts: something real is in the way

A conflict is your machine’s fault, not the repo’s: a real file, a real
folder, or a foreign symlink sits where a link should go. homebot never
overwrites
anything.

homebot link --dry-run    # show what would happen
homebot link --backup     # rename blockers to *.homebot-backup-<time>, then link
homebot cleanup           # later: compare each backup with the link, then decide

“Moved aside” means renamed in place: ~/.zshenv becomes
~/.zshenv.homebot-backup-20261012-054107 and the link takes its name.
Nothing is deleted. To keep something from a file before it’s replaced,
homebot pack it first.

Cleaning up backups

The inbox counts the backups next to links. homebot cleanup goes through
them one at a time. With fzf installed, each one opens a menu with the diff
in a preview pane: red lines are only in the backup, green lines only in what
is linked now. Without fzf, the diff is printed and a letter picks.

Choice What happens
delete The backup goes; the link stays as it is.
keep Nothing changes; it’s asked about again next time.
use The backup’s content is copied into the repo file the link points at, then the backup goes. homebot save commits it. Only offered for files in the repo.
merge Take some of the backup’s lines into the repo file. Only offered for files in the repo.

merge opens lazygit when it is installed, else git add -p, on a scratch
copy: the repo file as committed, the backup as the change. In lazygit, press
Enter on the file; the backup’s lines are green here (the preview’s colors are
the other way round). Space takes the line under the cursor, shift+arrows
select several, d drops them, q finishes. In git add -p, y takes a
block, n skips it, s splits it smaller, e edits it. What you took is
written to the repo file (homebot save commits it), and the same backup
comes back with what is left, usually to delete it.

Esc (or q) stops; the rest stay untouched. Without a terminal, and with
--list, it only lists them. A backup that was a link to somewhere else is
moved aside as a link, and deleting it removes only the link.

Pruning

Links are recorded per machine ($XDG_STATE_HOME/homebot). Remove a file from
the repo and the next homebot link (or the hourly helper) removes its
dangling link. Links homebot didn’t make are never touched. homebot unlink
removes every link it made; the files stay in the repo.

Adding things: homebot pack

Don’t move files into the repo by hand; pack gets the spelling right.

homebot pack ~/.config/zed                  # autolink/common/config/zed
homebot pack ~/.config/zed --layer linux    # autolink/linux/config/zed
homebot pack ~/.netrc --private             # private/common/netrc (local) + netrc.age (committed)

It moves the item, links it back, and:

  • reuses the spelling an ancestor already has in any layer ((zsh) stays (zsh)),
  • marks new ancestors (…) so the parent stays real (~/.claude/settings.json → (claude)/settings.json),
  • refuses non-dot top-level names (except Library), names already wrapped in (), things already in the repo, and new marked folders under an existing plain one (with the rename to do first).