johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
Naming and identity
A repository is identified by its owner's DID and its key. The name is
for people. It can change, and once a repository is deleted or renamed
away, another repository can take the name. This page covers how records
refer to a repository so that they survive this, how renames and transfers
work, and how commits are tied to accounts.
Referring to a repository
Several records name a repository: a pull, issue, comment or state, a
pull's head, and a fork's forkOf. Each holds a repoRef: the at:// URI
of the repo record as it was named when the record was written, plus the
key. Readers match on the URI's owner and the key, never on the name. So
those records follow a rename, and never attach to a later repository that
takes the same name.
A knotless://handle/name remote resolves the handle on every use, from a
cache kept for KNOTLESS_IDENTITY_TTL_SECS. An account that moves to
another PDS is looked up again sooner: once the PDS it was cached as on
answers that it is not there (RepoNotFound, RepoDeactivated,
AccountDeactivated, or "Could not find repo"), as the one it left does,
the cache no longer answers for it, and a command that fails on that
answer drops it from identity.json, so the next one finds the new PDS.
The helper records the DID the
remote first resolved to, in remote.<name>.knotlessdid, and refuses to
push or fetch if the handle later names another account.
Renames
A rename is one applyWrites that:
- creates the
reporecord under the new name; - deletes the old one;
- leaves a
redirectunder the old name; - repoints every redirect already leading to the repository, so an old name is always one hop from the current one;
- deletes a redirect under the new name, if there was one.
A new name another repository has, public or private, is refused before
any of it, with <url> already exists: a rename never writes over a
repository (see storage). Renaming to the name it has is
refused too (<url> is already named <name>).
A lookup by name that finds no repo record reads the redirect under that
name and follows it. Clone URLs, remotes and links made with the old name
keep working, and git is told the new name. A redirect is followed only
within the account, and only to a record with the key the redirect names.
A lookup follows up to four redirects: one is the rule, and the rest let a
read race a rename.
Creating a repository under a name that holds a redirect replaces the
redirect. Deleting a repository deletes its redirects.
On the web, a page asked for by an old name redirects to the same page under the new name. The redirect is temporary, because the old name can be given to another repository. Clones over HTTP follow the old name without a redirect.
Private repositories
A private repository's records stay in the space named for its first name,
its home. A rename creates a space under the new name, with the same
members, holding only a redirect whose space names the home. A member who
opens the repository by its new name finds the redirect there and joins the
home. Sharing and unsharing change every such space, and deleting the
repository deletes them all.
Members' cached credentials are bound to a space's address, so a space's name is never given to another repository. knotless refuses to create a repository under a name where a renamed repository already has a space.
Transfers
Neither account can write to the other's repo, so a repository moves to another account in two steps:
- The owner offers it, by setting
transferon thereporecord to arepoRefin the new account. - The recipient accepts. The repository is copied into their account with
the same key, and with
transferredFromnaming the old place.
Only a public repository can be offered: a private one's pulls and issues
are in its space, which cannot move. An offer to the account it is already
in is refused (<url> is already in that account: rename it instead).
Accepting copies the runners the repository names along with it, and is
refused when nothing is offered (<url> has not been offered to anyone)
or the offer is to another account (<url> is offered to <did>, not to you).
The owner offers it with knotless repo transfer, MCP's repo_transfer, or
the Danger zone of its settings on the pages; the recipient accepts with
knotless repo accept, MCP's repo_accept, or the offer the repository's
pages show them while they are logged in (getRepo's viewer.transfer).
A rename is the same on each: knotless repo rename, repo_rename, or the
pages' settings.
sequenceDiagram
participant A as Old owner
participant B as New owner
A->>A: set transfer on repo record (repoRef in B's account)
B->>A: read the offer
B->>B: copy the repository: same key,<br/>transferredFrom names the old place
Note over A,B: readers follow the move only when both sides say so
A->>A: delete the old copy (optional):<br/>its name and redirects now lead into B
A move holds only when both sides say so:
- A reader following the old name goes to the new place only if the new
reporecord names the old one intransferredFrom. The old place leads there by itstransferoffer, or, once the old copy is deleted, by a redirect into the other account. A redirect to another account is never followed as a rename. - Records about the old repository count for the new one only if the old
owner's offer leads there, or the redirect they leave when they delete
the old copy does. The appview's
getForgethen returns them with the new repository's own. - State and merge-queue records the old owner made before the new
reporecord was created still count as a maintainer's. Later ones do not.
Moves are followed across accounts up to four times. The remote helper
follows a moved or renamed repository and prints the git remote set-url <remote> <new url> to point the remote at it.
When the old copy is deleted, its name and redirects become redirects into the new account.
Commit authors
The web pages show a commit as its author's account, with the account's picture, when they can tie the commit's author address to an account.
An account's emails record lists the addresses its commits are authored
as. Each entry is the hex SHA-256 of the DID, a newline, and the address
trimmed and lowercased. Salting with the DID means the record names no
address, and the same address hashes differently in each account that
claims it.
Nothing proves that an address belongs to the account claiming it. So a page believes a claim only from:
- the repository's owner and collaborators;
- the author of a merged pull, for the commits that pull brought in. Only the owner or a collaborator can merge, so the merge vouches for those commits.
A merge's state record says where the base was before the merge (from),
so from..commit is what it brought in. For a merge recorded without
from, a merge commit brought in its second parent's history less its
first's, and a fast-forward or squash brought in nothing.
The owner and collaborators are matched first, so a pull's author cannot take their addresses. Someone who claims your address gains nothing beyond the commits they got merged.
Commits in other repositories
A commit keeps its ID when its history is pushed somewhere else, as in a fork, a mirror, or a copy pushed as a repository of its own. Where a page's own repository believes no claim for an address in a commit, it asks which accounts other repositories with that commit believe there:
- Exactly one: the page shows that account.
- More than one: the page shows none, rather than pick between them.
- None: the address stays unlinked.
So a copy shows its commits' authors as the original shows them. Someone who copies your commits and claims your address is believed only in their own repository. Everywhere else the claims disagree, and the commits show as no one's rather than as theirs.
What each repository believes comes from a walk of its default branch, kept in the index. The walk runs in the background when a page shows the repository's commits, and goes on from the tip where the last walk stopped. It starts over when the people the repository believes change, or when their claims change, since an address it no longer believes vouches for nothing. Only public repositories the index knows count, and not those taken down. A private repository vouches for nothing outside itself.
A commit knotless makes names who made it as git would where it runs:
- The CLI and MCP over stdio run on a person's own machine, so they commit
as git does there:
user.nameanduser.email, orGIT_AUTHOR_*andGIT_COMMITTER_*, the config of the repository in the directory they run in included. That names the author ofrepo commitwith no--author, and the committer of a merge, squash, branch update or revert, whose author is the merger too but for a squash's. - Where git has no identity it would not guess from the machine's name,
and wherever a server acts for an account (the pages, MCP over HTTP, a
gateway's commit that names no committer), it is the account: its
handle, at
<did>@knotless.invalid, which needs no claim. - A runner is
knotless, at its own account's<did>@knotless.invalid: the committer of the squashes it lands from the merge queue, and both author and committer of what it writes back for a workflow.
So a history made with knotless reads elsewhere, such as a mirror on GitHub, as one made with git: each person's own address on what they wrote and merged, and the runner's on what it landed.
A coding agent has no account to claim its address, so the pages know its
face themselves. An author or co-author at noreply@anthropic.com, the
address Claude Code signs Co-authored-by trailers with, comes with
agent: claude and is drawn with Claude's mark, whoever claims the address.
The first push from a machine that lands a ref claims the address git
authors commits as there, if the account has made no claim yet, and says
so. The cache's emails.json remembers which accounts were looked up, so
it is asked once per machine. After that,
knotless account email add and account email remove change the list, as do MCP's
account_email_add, account_email_remove and the pages' settings (addEmail, removeEmail). An
address taken back stays taken back. Since only hashes are kept, no one
can list an account's addresses, its own pages included: they say how
many it claims, and an address is added or taken back by name.
Profiles
An account's page shows its Bluesky profile. A dev.knotless.profile
record overrides it on knotless field by field: each field set replaces
Bluesky's, and a field set to '' shows Bluesky's again. Readers get the
merged profile from dev.knotless.getProfile?actor=<handle or did>.
A handle is shown for a DID only if the handle resolves back to that DID.