Skip to content

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.

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.

A rename is one applyWrites that:

  1. creates the repo record under the new name;
  2. deletes the old one;
  3. leaves a redirect under the old name;
  4. repoints every redirect already leading to the repository, so an old name is always one hop from the current one;
  5. 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.

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.

Neither account can write to the other’s repo, so a repository moves to another account in two steps:

  1. The owner offers it, by setting transfer on the repo record to a repoRef in the new account.
  2. The recipient accepts. The repository is copied into their account with the same key, and with transferredFrom naming 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 transfer, MCP’s transfer_repo, or the Danger zone of its settings on the pages; the recipient accepts with knotless accept, MCP’s accept_transfer, 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 rename, rename_repo, or the pages’ settings.

set transfer on repo record (repoRef in B's account)read the offercopy the repository: same key,transferredFrom names the old placedelete the old copy (optional):its name and redirects now lead into Breaders follow the move only when both sides say soOld ownerNew ownerOld ownerNew owner

A move holds only when both sides say so:

  • A reader following the old name goes to the new place only if the new repo record names the old one in transferredFrom. The old place leads there by its transfer offer, 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 getForge then returns them with the new repository’s own.
  • State and merge-queue records the old owner made before the new repo record 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.

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.

A commit knotless makes for an account that names no author is authored as <did>@knotless.invalid, which needs no claim.

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 email add and remove change the list, as do MCP’s set_email 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.

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.