# 0007 — Personal access tokens over HTTP Basic, with a uniform 401

**Status:** accepted · **Date:** 2026-08-28

## Context

Milestone 4a serves `git clone` for public repositories to anonymous callers.
Everything else the git transport should eventually do — pushing, and cloning a private
repository — needs to know who is asking.
[0001](0001-git-over-http-not-ssh.md) already chose the credential: personal access
tokens over HTTP Basic. What it did not settle is how they are stored, what they are
allowed to do, and — the one with a real cost either way — what an unauthenticated
request is told.

That last question is forced by the client. **Git only sends credentials after a 401.**
Milestone 4a answers 404 for a private repository, matching `view_repo`'s rule that an
invisible repository is absent rather than forbidden. Keep that, and an authenticated
private clone is impossible: the client is told the repository does not exist and never
offers a credential.

## Decision

**Tokens are SHA-256 hashed, carry no scope, and any git request that is not
anonymously readable answers 401 — whether or not the repository exists.**

- **Hashing: SHA-256, with a display prefix stored alongside.** Mirrors
  `SessionTokenHash`. A token is 256 bits from the OS random source, so there is no
  dictionary for a slow hash to defend against, and this credential is verified on every
  request of a clone — several per `git clone`. Argon2 is the right answer for a
  human-chosen password and the wrong one here. The first eight characters are kept in
  the clear so a management UI can name a token it can no longer show.
- **No scopes.** A token acts as the user who issued it. Personal-first means one
  person's own credential; narrowing it is a change to make when something needs it,
  behind an unchanged port.
- **A uniform 401.** Any git path the caller may not read anonymously answers `401` with
  `WWW-Authenticate: Basic`, including repositories that do not exist. Nothing in the
  response distinguishes "private" from "absent".

## Alternatives considered

- **401 only for repositories that exist**, 404 otherwise. No spurious credential prompt
  on a typo. Rejected because the pair of responses is itself the leak: probing tells an
  attacker which private repository names exist, which is the thing private visibility is
  protecting. The prompt-on-typo cost is a nuisance; the leak is a defect.
- **Keep 404 and require credentials up front.** Leaks nothing, and git never prompts —
  a private clone works only after configuring a credential helper. Rejected as hostile
  to the ordinary case, and it makes the documented instructions longer than the feature.
- **Argon2, reusing the existing `PasswordHasher` port.** One credential mechanism
  instead of two. Rejected on cost: deliberately slow hashing on every request of every
  clone, defending against a dictionary attack that cannot exist against 256 random bits.
- **Read-only versus read-write scopes now.** Genuinely useful — a CI token that can
  clone but not push. Rejected as a second authorization axis crossing the one that
  already exists, designed against no requirement. Cheap to add later.

## Consequences

- **A typo'd clone URL prompts for a password before reporting `not found`.** The
  accepted cost of the uniform 401, and the same behaviour GitHub has.
- **The 404 rule now has an exception, and it is transport-shaped.** `view_repo` still
  answers "absent" for a page; the git routes answer 401. Two rules for one question, so
  the reason lives here rather than being rediscovered as an inconsistency.
- **A lost token cannot be recovered**, only replaced. That is the point of storing a
  hash, and the UI has to show the token exactly once and say so.
- **Revocation is a delete.** A flag would mean every read has to remember to check it.
- **Tokens authenticate; they do not authorize.** `serve_git` remains the one place that
  decides what an actor may do, so a token widens who the actor is and changes nothing
  about the rules.
- **Reversible where it matters:** scopes can be added behind `TokenRepository`
  unchanged. The 401 rule is the part that is expensive to revisit, because it is
  observable behaviour that clients and instructions come to depend on.
