@ 261016.2raw
current

lord

Keep a repo’s private, unversioned files without committing them.

Every repo accumulates files that belong to it but not in it: .env, .claude/, editor state, local overrides. Delete the checkout and they are gone. lord moves them into a store keyed by the repo’s origin URL and symlinks them back into the worktree. A fresh clone recovers them with one command.

How it works

A .lord manifest in the repo root lists one literal path per line:

# private files, never committed
.env
.claude/
notes.md

Running lord with no arguments converges the repo to that manifest:

The manifest itself is managed the same way: it lives in the store and appears in the worktree as a symlink. A repo with no manifest anywhere gets one initialized, copied from $LORD_CONFIG/skel if that file exists.

With nothing to do it prints nothing and exits 0. The only destructive operations are the adoption rename, replacing an identical copy with its link and moving a renamed repo’s store.

One caveat: a repo that sets core.hooksPath, husky for example, never runs hooks from .git/hooks, so the guards are silent there. Run lord by hand in those repos, call it from the custom hooks or let lord all cover them once they are registered.

Usage

usage: lord [-n] [dir]
       lord all [-n]
       lord list
       lord prune [-n]
       lord eject [-n] [dir]
       lord id [dir]
       lord -h | -V

With no command lord converges one repo. The commands:

The options:

A command is the first word. To name a directory called list, write ./list. -- ends the options.

dir defaults to the current directory and may be anywhere inside the repo. The commands that act on the registry take no dir.

$ printf '.env\n' > .lord
$ lord
adopt .lord
adopt .env
$ ls -l .env
lrwxrwxrwx 1 t t 54 .env -> /home/t/.local/share/lord/github.com/acme/app/.env

Store

Files live under $LORD_DIR, default ~/.local/share/lord, keyed by the origin URL as <host>/<owner>/<repo>. Both ssh and https remotes normalize to the same key, so reclones and remote rewrites find the same store. A repo needs an origin remote; it need not exist on the host yet.

The store is plain files. lord is a tool for one machine: it keeps no machine identity in the store and never merges two copies of it. Back the store up with the tools you already use.

A renamed repo keeps its store, see Renames and transfers.

Stash and notes

.git/lord is a private directory for the repo, kept in the store as .lord-stash and linked into the git dir. Git neither tracks nor pushes anything there, and it outlives the checkout like any listed file. In a linked worktree it sits in the common git dir. A real directory found at .git/lord is adopted.

lord keeps the repo’s git notes in the stash as notes.bundle, every ref under refs/notes/. Each run compares them ref by ref:

$ git notes add -m 'reviewed'
$ lord
save refs/notes/commits

Notes trouble fails a run by hand. In a run from lord’s hooks it only warns, so diverged notes never block a commit.

A lock in the stash keeps two runs on one store from losing each other’s saves. A run waits five seconds for it, then fails with E16. A lock left by a killed run stays until removed by hand.

A pre-push hook refuses every push of notes: a ref under refs/notes/ on either side of a refspec, or an object a notes ref contains, as when a notes commit is pushed by its id. A mirror push, a copied push refspec or a renamed destination cannot publish them. Deleting a remote notes ref passes. git push --no-verify skips the guard.

The guard goes first in the pre-push hook. It reads the refs on stdin and hands them on, so the hook’s own code, git lfs pre-push for example, reads them as git sent them.

A repo that sets core.hooksPath never runs the guard. Nor does one whose pre-push hook is not a shell script, since lord cannot put sh in it. lord leaves such a repo’s notes alone in both directions and warns when there are any.

Notes are keyed by commit, and an amend or rebase makes new commits. Set git config --global notes.rewriteRef refs/notes/commits and git moves each note to the new commit; the post-rewrite hook then saves the result.

Renames and transfers

Each store holds .lord-id, lord’s own record, never linked into the worktree:

url git@github.com:acme/app.git
root 5f7e675...
id github github.com 1283757050

root is the repo’s oldest root commit. id is the hosting provider’s own repo id, which survives renames and transfers. lord looks it up once, when it writes the record, so hook runs stay offline. A failed lookup records id none; delete the file to retry.

When the store for the origin URL is missing, lord looks for the store this repo used before:

So a rename is one remote change and one run:

$ git remote set-url origin git@github.com:acme/app2.git
$ lord
move github.com/acme/app -> github.com/acme/app2
relink .lord
relink .env

A fresh clone under the new name finds the store by id alone. The move leaves a symlink at the old key, so clones still on the old URL, which the host keeps redirecting, share the store. If a different repo later answers at the old URL, told apart by its root commit, lord drops that forward and gives it a store of its own. A store moved by hand works too: links that point at a missing path in the store are re-pointed.

lord id prints the URL, key, root commit and the id as looked up now, next to the recorded one.

Providers

provider hosts id token
github github.com id GITHUB_TOKEN, GH_TOKEN, else gh auth token
gitlab gitlab.com project id GITLAB_TOKEN
forgejo, gitea codeberg.org id GITEA_TOKEN, FORGEJO_TOKEN
bitbucket bitbucket.org uuid BITBUCKET_TOKEN
bitbucket-dc none built in id BITBUCKET_TOKEN
azure dev.azure.com, ssh.dev.azure.com id AZURE_DEVOPS_PAT
sourcehut git.sr.ht rid SRHT_TOKEN, required even for public repos

Public repos need no token, except on SourceHut. Tokens reach curl through its config on stdin, never the command line. Self-hosted instances go in $LORD_CONFIG/hosts, one <host> <provider> [api-base] per line:

git.example.org gitlab
code.example.org forgejo
github.example.com github https://github.example.com/api/v3

Without curl, or for an unknown host, there is no id and only the link rule applies. LORD_ID_CMD replaces the lookup entirely: a command that gets the origin URL as $1 and prints <provider> <host> <id>, or nothing.

Checkouts

Every real run records its checkout in a registry, one line per work tree: the UTC time of the last run, then the path. A linked git worktree counts as its own checkout. -n and id record nothing and eject removes the entry. The registry lives in $LORD_STATE, never in the store, because paths belong to one machine.

$ lord list
drift  1d   ~/src/api
ok     5m   ~/src/app
dead   9w   ~/src/old

On a terminal the rows are sorted by path and $HOME shows as ~. The second column is the age of the last run: now, 12m, 5h, 6d, 9w or 2y. In a pipe or a $(...) the output is for scripts instead: three tab separated fields in registry order, newest first, with the UTC time in ISO form and the absolute path last.

$ lord list | cat
ok	2026-10-13T14:11:55Z	/home/t/src/app

The state column reads:

list only reads. lord all runs lord in every live checkout and prints the path followed by the output of each one that had something to say. A failure in one checkout never stops the rest. The exit status is the first nonzero status of any checkout. A dead path prints as skip <path>. A path that holds a repo without a manifest counts as dead, so all never initializes one. all -n reports the same and changes nothing.

all never edits the registry. lord prune drops the dead paths and prints a drop <path> line for each. prune -n only prints them.

The registry is a convenience. If it cannot be written, the run goes on without it and prints nothing.

Manifest rules

Paths are relative to the repo root and taken literally, no globs. Blank lines and # comments are ignored. A trailing slash is stripped, so directories may be listed either way. Absolute paths and paths containing .. are rejected. A path listed before it exists is excluded at once and adopted on the run after it appears.

Conflicts

lord never overwrites data. A path present in both worktree and store with different contents, a symlink pointing somewhere unexpected or a listed path tracked in git each produce an error naming the path, and the run exits nonzero. Orphaned store entries and unlisted symlinks into the store produce warnings only.

Errors print as E<code>: message with errno-flavored codes:

code meaning
1 listed path is tracked in git
2 no origin remote, or store file missing
16 notes bundle locked by another run
17 worktree and store conflict, notes that diverged from the stash, or several stores with this repo’s id
21 manifest is not a regular file
22 invalid flag, manifest entry or directory, or a manifest listing .lord-id or .lord-stash or a path inside them

Environment

variable default
LORD_DIR $XDG_DATA_HOME/lord, ie ~/.local/share/lord
LORD_CONFIG $XDG_CONFIG_HOME/lord, ie ~/.config/lord
LORD_STATE $XDG_STATE_HOME/lord, ie ~/.local/state/lord
LORD_ID_CMD unset: the built-in provider lookups
LORD_HOOK unset; lord’s hooks set it, and notes trouble then warns instead of failing

$LORD_CONFIG/skel seeds the manifest for repos that have none. $LORD_CONFIG/hosts names the provider of self-hosted instances. $LORD_STATE/checkouts is the registry.

Install

make install

Installs to ~/.local/bin/lord. Set PREFIX to install elsewhere. lord is a single POSIX sh script; it needs git, awk and standard utilities, plus curl for id lookups.

Test

make check

Runs the end-to-end suite: 317 checks across adoption, fresh clones, conflicts, hooks, eject, dry runs, renames, store moves, the registry, the stash and notes. Id lookups run offline through LORD_ID_CMD.

License

MIT