Skip to content

Repos (encrypted git remotes)

run402 repos is your source, encrypted before it leaves the machine — a Git remote whose contents are encrypted client-side and stored as a chain of signed, admitted heads (wire protocol r402s/v0). One noun, seventeen verbs (repo singular resolves identically — say what gh repo already taught you). run402 gitvault <verb> answers a structured COMMAND_MOVED (naming its repos successor) or, for push/reconcile (no behavioral successor), COMMAND_REMOVED. r.gitvault is unchanged in the SDK — gitvault stays the protocol/infrastructure name; repos is what you type.

What Run402 claims about it, and how strong each claim is. These are the entire approved claims vocabulary — three separate promises with three separate strengths:

  1. Run402 cannot decrypt your gitvault or repository history. Deployment artifacts remain a disclosed plaintext custody boundary. Cryptographic, against Run402 itself: source payload and repository-history content are ciphertext-only; the substrate retains only enumerated plaintext metadata and holds zero vault keys. The deploy lane is separate — the platform custodially holds the plaintext artifacts of every deploy, and says so.
  2. Activation requires vault admission by default; an explicit, audited override can bypass it. An operational platform invariant, not a cryptographic one — the platform is the party enforcing it.
  3. Retention is an operational promise of the platform, not a cryptographic guarantee against it (the host controls timestamps and bytes).

Naming law: a verb is either exactly a gh repo verb (create/list/view/rename/delete), exactly a git verb meaning what it means in git (gc, fsck), or a plain-English verb for an operation with no analog (snapshot, policy, mirror, access, recover) — never a git verb reused for a different meaning. That rule is why the offline-restore verb is recover, not restore: git restore already means something else.

Tiered help — three or four verbs loud, the rest occasional or maintenance:

Common: repos create [name], repos view, repos list
Then git: git push, git clone run402::<org>/<repo>
Occasional: repos snapshot, repos mirror, repos recover, repos recovery-bundle
Handoff: repos handoff, repos resume
Invite: repos invite, repos join
Lifecycle: repos rename, repos delete
Maintenance: repos fsck, repos gc, repos access, repos policy
Verb What it does
create [name] Provision a project (or, with --project <id>, ADOPT an existing one), ALLOCATE its vault (mints key material and, on first allocation, a one-shot recovery receipt), and scaffold the remote — origin when free, run402 when taken. [name] is inferred from an existing git remote’s basename or the directory name when unambiguous — never a prompt; if the directory and an existing remote disagree, or nothing usable can be derived, this is a structured error naming exactly one next_action. The response’s next_action is the exact git push to run. Nothing is deployed, ever, unless you separately choose to — a vault-only project is a first-class shape, not a half-configured one. --byo <s3://bucket/prefix> allocates the vault with storage_profile: "byo" — payload ciphertext lives in that bucket, which you own (see “BYO primary bucket” below) — taking the same --profile <name>/--ambient/--region/--endpoint credential flags mirror uses; the destination is probed create-only BEFORE allocation and a probe refusal (GITVAULT_BYO_BUCKET_PROBE_FAILED, naming the failing property) means no vault half-exists.
view [--human] Side-effect-free: what this machine and the control plane each believe — allocation, policy, the local keystore, the authenticated and materialized pins, the mirror summary when one is configured, and where the keystore lives. NEVER materializes refs or advances a local pin — refs reports {known:false, reason:"not_materialized"} with a verify_refs next_action pointing at fsck, which is the one place materialization happens now. Carries the same advisory compact_advised next_action a publish does when this checkout’s learned checkpoint coverage has fallen far enough behind, and echoes the vault’s warnings[] verbatim — including the progressive terminal_loss_risk warning, which trips on generations, source bytes, or age since genesis and never clears (nothing can detect a second principal able to open the vault, so the terminal-loss statement itself never changes — only how prominently it is repeated). Also echoes the standing vault_unmirrored finding (gitvault-mirror-default) until a first mirror write or sync succeeds — informational, never blocking, computed client-side only. Multi-writer vaults (gitvault-multi-writer rev 47, see below) additionally carry writer_set (the chain-verified roster) and pending_writers[] (eligible candidates not yet admitted) — --human prints one roster line per writer plus a repos access sync hint when candidates are waiting, alongside the rest of its short summary.
list [--human] The organization’s vault-bearing repos, via the bulk GET /gitvault/v1/vaults?org_id= read when the gateway has it (one round trip) — gracefully falls back to the older per-project walk when that route 404s. Not every project in the org; ones with no vault are omitted. --human renders a compact roster (address, generation, bytes, policy) instead of JSON.
rename <new_name> Claim or rename the repo’s per-org-unique, address-form name — the <name> half of run402::<org-slug>/<name>. Address with --repo <repo_id> or --project <id> (not both).
delete Deletes a REPO-ONLY project: database, functions, subdomains, mailbox, and secrets must all be absent. When any is materialized, this REFUSES with PROJECT_HAS_NON_REPO_RESOURCES, enumerating refused_resources and pointing at run402 projects delete — the verb whose name says what it destroys. --force overrides ONLY the separate vault-history confirmation (the repo holds admitted generations) — it never overrides the non-repo-infra refusal. Success enumerates deleted_resources.
snapshot [--allow-dirty] [--manifest-out <path>] Capture the working tree and publish it. Not gated on a deploy — a vault-only repo snapshots for months without one. Against a project with no vault yet, this ALLOCATES one inline before publishing. Push-to-creates through a slug-form remote (run402::<org-slug>/<name>) the same way git push does. --dry-run previews the real local pipeline without publishing. A dirty tree (any modified/staged tracked path, or any untracked-not-ignored path) refuses by defaultSNAPSHOT_DIRTY_TREE, before any object is created, with details.modified/details.untracked naming every path and next_actions pointing at committing or --allow-dirty. --dry-run surfaces the same refusal (a preview that hid it would lie). --allow-dirty captures the tree as-is; the result discloses exactly what got swept in via modified_captured/untracked_captured (printed to stderr too) — even the override never captures silently. The same rule applies to the deploy-lane capture (deploy apply against a gitvault_policy: required project) — see --allow-dirty there. Both --dry-run and a real snapshot print a SUMMARY by default — files_total/files_changed/files_new counts, total/delta bytes, and up to 200 changed_paths (changed_more names any overflow) — never the full captured-file inventory (which can run to thousands of entries on a real repo). --manifest-out <path> writes the complete, untouched inventory to a private 0600 file and names it in the result’s manifest_path; -v/--verbose inlines the full inventory directly in the JSON, composing with (not replacing) its usual stderr stats line. Every publish result carries checkpoint_staleness{generations_since_checkpoint, advised}, computed locally from the coverage THIS checkout has learned (unknown coverage reads as {0, false}, silence rather than a guess). When advised is true the result gains a compact_advised next_action pointing at run402 repos gc, because a cold clone re-verifies every generation since the last checkpoint one at a time. It is advisory only and never gates a push. A vault holding no branch heads (snapshot-only) additionally carries a restore_snapshot_ref next_action with the exact git fetch/git checkout to run — a plain git clone of such a vault reports an empty repository with no hint the snapshots exist.
mirror [<destination>] ONE flag-driven verb for the client-side, customer-owned ciphertext mirror — Run402 never holds a credential to it, and it is the recommended default, not a power-user extra (see “Mirroring — the recommended default” below). No argument: READ the configured destination plus a keyless freshness check. <destination>: configure (idempotent upsert). --off: remove the config only — never touches the mirror’s own bytes. --backfill: copy every object the mirror is missing (also what completes the FIRST copy and clears the vault_unmirrored finding). Exactly one of these per call. State also renders inside view; integrity inside fsck --mirror.
fsck [--human] Walks the head chain AND materializes the ref map, advancing BOTH local trust pins — reported explicitly as local_state_changed + pin_before + pin_after, never implied. --no-write is a genuine audit mode: the same real walk and decrypt, persisting neither pin. --budget <n> caps heads walked per call. --mirror additionally runs the keyless mirror integrity probe — proves the mirror’s VALIDITY, never its FRESHNESS. On a storage_profile: "byo" vault it ALSO HEAD-checks every object the vault’s own signed chain names against your destination bucket, automatically, no flag — adjudicated against run402’s chain (what SHOULD exist): a confirmed absence fails fsck with GITVAULT_BYO_OBJECT_MISSING naming each missing {key, object_kind} (capped list + count), while a machine with no local BYO credentials reports an explicit byo_presence: {verified: false, not_checked_reason} — named as unchecked, never a silent pass, never a hard failure. A managed vault is byte-identical to before (no byo_presence key at all). --human renders a short summary instead of JSON.
gc [--force-headroom] git gc’s own two halves — checkpoint publication and prune planning — in one verb, not described as “exactly git gc”: the deletion ceremony is stricter. Plans and checkpoints by default; nothing is deleted until --submit --intent-core <path> --verifier-receipt <path> supplies both receipts the two-phase protocol requires. The plan’s submit next_action carries destructive: true / requires_approval: true / safe_to_auto_execute: false as additive fields. Storage headroom: compaction transiently holds BOTH the new checkpoint and the not-yet-pruned history — roughly source_bytes, counted against the org’s POOLED tier storage — so this preflights before building or uploading anything and refuses GITVAULT_COMPACT_INSUFFICIENT_HEADROOM (naming pool_used_bytes, pool_limit_bytes, projected_transient_bytes, and the override) when the projection will not fit. Every run carries a headroom block with those figures, fitting or not, on both the planning and --submit halves. --force-headroom proceeds anyway; a preflight that cannot be ANSWERED (tier status unreadable) proceeds after one stderr note rather than blocking maintenance on an advisory read. The platform’s own storage-quota enforcement stays authoritative in every branch — this only ever refuses earlier and more legibly, never admits more.
access [--human] [--no-reconcile] READ-ONLY of the roster (it lists AGENT members’ keystore keys too, and this_keystore reports YOUR keystore for every principal type — enrolled, publish_state (active/pending/absent/rotation_required), covered_on_this_vault — never null for a real keystore). One side effect, reported never hidden: on a key-holder’s FIRST ordinary gitvault operation in a process (view/list/access included) the SDK wraps every pending desired recipient (reconcile_recipients on the handle); --no-reconcile defers that and says so with the pending count; fsck, --no-write and recover never wrap. The org’s directory of encryption-key-holding members, which of the vault’s current envelope-recipient fingerprints are covered, per-recipient envelope_state (converged/pending/pending_removal, from the gateway’s desired-recipient-state substrate), and (best-effort, this machine only) each principal’s local TOFU pin. stale_access names removed members whose access was NOT actually revoked yet — pending_removal is honest bookkeeping, not enforcement. States an honest remaining gap rather than inventing one: history_scope (which epochs each recipient can read) has no substrate to report — gitvault protocol v0 pins a single fixed epoch, so there is no per-epoch scope to report. member_custody: when a control-plane session is cached (operator login --loopback), the read also carries YOUR OWN wrapper custody — key identity, custody scheme, each wrapper’s kind (webauthn_prf/recovery_code) and state (pending/active) — with stderr warnings for a passkey-only key (no offline recovery path) or an unfinished pending enrollment; without a session the block is honestly { available: false, reason, hint }, never silently missing and never misleadingly answered as the agent principal. An enrolled teammate’s key envelope is wrapped AUTOMATICALLY — no manual step — by the next git push or repos snapshot any key-holding client runs (best-effort, non-blocking; there is no manual reconcile verb). access repair (a general re-key clearing stale_access / a first-ever migration; needs --recipient-state-version/--recipient-revocation-version), access revoke-key <principal_id> (self-contained, no flags), and access declare-exposure are the explicit owner+step-up rotation entry points. --human (read form only) renders a compact roster instead of JSON.
access repin --principal <principal_id> --fingerprint <ek_fingerprint> A KEY-HOLDER explicitly accepts a recipient’s CHANGED key (gitvault-agent-envelopes). The session-start wrap refuses pinned_key_mismatch and never bypasses it — not even after an owner’s org member revoke-key, because from this machine an owner re-key and a platform substitution look identical. Name the new fingerprint (read it back with the recipient — public data); refuses a fingerprint the org directory does not currently serve. Records the D197 re-pin receipt (best-effort) and moves the local pin; the next ordinary operation wraps.
access sync [--human] Multi-writer (gitvault-multi-writer rev 47, see below): on-demand reconcile of pending_writers[] — admits every eligible candidate (active org membership at role developer+, a published signing key, not yet a writer) via a real add_writer_key{"writer"} head per candidate. Requires THIS machine’s own key to already be an active writer — checked locally first, no network call otherwise; eligible: false reports “no authority here”, distinct from an empty admitted: [] (“nothing pending”). skipped[] names candidates whose signing key could not be resolved, with why. This is the explicit, on-demand form of the SAME reconcile every ordinary repos operation already runs automatically when this machine is itself an active writer. --human renders a short summary instead of JSON.
policy <required|grandfathered> Set the activation policy. Owner + step-up, audited. grandfathered needs --reason; returning to required does not. Allocating a repo never sets this.
recover <source> --out <dir> [--human] r402s-recover: rebuild a BARE recovery repository (no working files) straight from a mirrored prefix, with NO SERVER INVOLVED — the offline disaster path. Kept its name (design D10): restore was considered and rejected because git restore already means something else. The result’s layout is "bare" and its next_actions print the exact git clone <out_dir> <out_dir>-worktree to run for a working tree — recover itself never checks files out. Normal retrieval is plain git clone run402::<org>/<repo> — there is no repos clone verb. Cloning needs a Run402 principal on this machine — a wallet with an allowance and a keystore holding an envelope for this vault — this is encrypted git, not a shareable link. A fresh clone installs local refs/r402/retain/<oid> refs for every retained deploy-capture tip no branch reaches, so git fsck is silent — git for-each-ref refs/r402/ lists what is retained. Clones made by a client older than this one (or a checkout whose ref write degraded) may still show dangling commits under git fsck; harmless, not corruption — one run402 repos fsck run installs the missing refs. A retained ref locally pins that history against git gc until the vault prunes the capture, at which point the next fetch retracts it. --human renders a short summary instead of JSON. Human-member path (gitvault-recovery-custody): a member under wrapper custody (no keystore) recovers with --bundle <file> (their exported r402s-member-recovery-bundle/v1 — omit it to use the mirror’s own member-recovery-bundles/ sidecar) + the source recovery code (--code, or prompted with hidden input so it never lands in shell history) + --receipt <pin.json> (the vault’s one-shot recovery receipt — the trust anchor; key material never substitutes for it; without any pin the result is honestly labeled unauthenticated_salvage). --rp-id <host> overrides the seal-time ceremony host bound into the wrapper context (default: the bundle’s own rp_id, then console.run402.com). A raw WebAuthn PRF output is NOT a supported input — the no-server path for a human is the recovery code; a code with no exported bundle refuses as RECOVERY_BUNDLE_MISSING (a server-side wrapper row that was never exported is not offline backup). A bundle-decrypted result names it in member_recovery (which sidecar, which wrapper, which rp_id). fsck --mirror reports any member-recovery-bundles/ sidecars as UNVERIFIED availability hints in member_recovery_bundles[].
recovery-bundle [--out <file> | --out -] Export YOUR member recovery bundle (GET /agent/v1/source-access/recovery-bundle, format r402s-member-recovery-bundle/v1): key identity + every ACTIVE wrapper ciphertext — the file recover --bundle opens with the source recovery code, kept SEPARATELY. A server-side wrapper row alone is NOT offline backup; this export is what makes the code work offline, and the gateway stamps it as recovery-posture evidence. Writes run402-source-recovery-bundle-<fingerprint>.json (0600) in the cwd unless --out says otherwise (--out - prints only); the full JSON always goes to stdout. Principal-scoped, not repo-scoped — one bundle covers every vault you can read, so there is no --project, and auth is your control-plane (human) session (run402 operator login --loopback first; without one the request answers for the active WALLET’s agent principal, normally no wrappers, with a stderr note saying so). To make the bundle travel WITH a mirror, copy it to member-recovery-bundles/<name>.json under the mirrored prefix — recover finds it there automatically. Enrollment/activation/revocation/code replacement are browser ceremonies at console.run402.com/account → Source access; requires an active source-access key (409 NO_ACTIVE_ENCRYPTION_KEY otherwise). No MCP tool (same human-session rule as operator).
handoff [--note-file <path>] [--role <role>] [--ttl <seconds>] [--include-sensitive <glob>]... [--list] [--revoke <handoff_id>] Capture the ACTUAL working tree — staged, unstaged, AND untracked, the same shape real git stash push -u produces (a synthetic 3-parent commit: tracked-worktree-state, an index-tree parent, a parentless untracked-tree parent) — push it into the vault as a retention root (no ref; it never appears in git log/branch listings), and mint a single-use bearer key, kgh1_<69 chars>, printed to stdout alone — nothing else this verb prints, in any mode, ever carries it. Mints at the caller’s OWN org role by default (--role overrides, never escalates); the key is real authority until claimed or its TTL expires (default 3600s, 60–86400 range) — the mint result’s warnings[] says so verbatim and the CLI echoes it to stderr before the key. The Handoff Note (kygit.handoff-note.v1summary required; completed/in_progress/failing/tried/next_steps/decisions/open_questions/commands) comes from --note-file <path> or piped stdin (a non-TTY stdin with nothing on it is a BAD_USAGE refusal, not a hang). Sensitive untracked paths (.env, .env.*, *.pem, *.key, *.p12, *.pfx, *.jks, *.keystore, id_rsa*, id_ed25519*, id_ecdsa*, .npmrc, .netrc, .pypirc, .git-credentials, *.tfstate*, *credentials*.json, .aws/**, .ssh/**, .gnupg/**, *.secret, secrets.* — 22 globs) are excluded from capture by default; repeatable --include-sensitive <glob> re-admits one explicitly, by name, never silently. --list reads outstanding handoffs instead of minting; --revoke <handoff_id> kills one before it’s claimed. No MCP tool — mints a bearer secret.
resume <kgh1_…> | --key-stdin [--to <dir>] [--no-init] Claim a Handoff Key, clone a fresh checkout, and reapply the checkpoint with git stash apply --index — bit-for-bit what the sender’s working tree looked like, staged/unstaged/untracked distinction included. One-shot: a claim from the SAME principal that already claimed it is a safe, explicit dedup (deduplicated: true, the original envelope reused — never a re-mint); any other claim after the first fails. --to <dir> picks the target directory (default: derived from the vault address or repo id). On a wallet with no active tier, resume first folds the same cold-start chain create does — allowance → faucet → one x402 prototype payment, each step announced on stderr — so a resumed agent arrives as a paid-up run402 wallet of its own; --no-init opts out. The claim itself needs no tier, so a faucet throttle or payment refusal is reported (cold_start.error in --json, plus a renew_tier next action) and never blocks the resume. Prints the Handoff Note as Markdown by default (--json for the raw envelope) followed by the restore location and every next_actions command — always git push origin main, plus, when the sender minted the key at owner, a remove_member reminder that the previous agent is still an org owner and names removing it as the (destructive, approval-required) follow-up if that environment is gone for good. No MCP tool — mutates org membership.
invite [--room <key>] [--note-file <path>] [--role <role>] [--ttl <seconds>] [--include-sensitive <glob>]... [--list] [--revoke <invite_id>] The second claim kind (kygit-invite): capture the checkpoint exactly like handoff — the inviter’s own worktree, index, branch, refs, and access are all untouched — register the INVITER’s own presence in a coordination room (the project’s default room, or --room <key> for a named org room), mint a single-use bearer key, kgi1_<69 chars>, printed to stdout alone, and post ONE room message naming the checkpoint and the invite id — never the key. A Handoff passes the work on and the sender stops; an Invite grows the team while the sender keeps working. Mints at developer by default (--role overrides, never wider than the minter’s own); the key is real authority until claimed or its TTL expires (default 3600s, same 60–86400 range as handoff) — the mint result’s warnings[] says so verbatim. A presence-registration or room-message failure is reported (inviter_presence/room_fact in the result) and NEVER voids the mint — the key is still printed and the invite remains claimable. The Invite Note (kygit.invite-note.v1 — same shape as the Handoff Note) comes from --note-file <path> or piped stdin, scanned for bare secrets before the commit is written (no override flag). --list/--revoke <invite_id> read/revoke instead of minting. No MCP tool — mints a bearer secret.
join <kgi1_…> | --key-stdin [--to <dir>] [--no-init] Claim an Invite Key on ANY machine: folds the SAME cold-start chain resume does (allowance → faucet → one x402 prototype payment, announced; --no-init opts out; a chain failure is reported and never blocks the claim), clones a fresh checkout, and reapplies the checkpoint with git stash apply --index exactly like resume. Additionally pins the invite’s OWN room locally (r402.room — a named room, or the project id when the mint omitted one), adds .run402/ to .git/info/exclude (never .gitignore, which is part of the captured tree), registers THIS session’s own presence, and posts ONE arrival message. Prints the Invite Note as Markdown by default (--json for the raw envelope), then the restore location, the inviter’s name/labels/liveness, who else is live in the room, and the last few messages — so your first message needs no lookup. next_actions name run402 messages wait (block until the other side speaks) and an addressed run402 messages send "…" --to <name>. No MCP tool — mutates org membership and writes a working tree.

Every verb above also accepts -v/--verbose (one stderr summary line of this call’s request stats — round trips, wire time, bytes — coexists with --human) and always carries a stats block in its JSON result. See Observability below.

After rewriting history (rebase/amend), push with git push --force — NOT --force-with-lease. The lease cannot cross git’s remote-helper boundary (the helper receives a non-force update and refuses it as non-fast-forward), while plain --force through this remote already carries force-with-lease safety: every forced update is compare-and-swapped against the freshly read remote state server-side, so a concurrent push is never silently overwritten. The non-fast-forward refusal itself names this in its next_actions.

Address resolution is offline after first contact. The first successful resolution of a run402::… remote pins the vault id plus its project/org ids in the checkout’s local git config; every later git push/fetch on that checkout resolves with zero extra round trips. If the pinned vault is ever deleted and the project re-allocated a new one, the next verb heals itself: it clears the pin, re-resolves once, and retries.

Terminal window
run402 repos create # name inferred from cwd/remote
run402 repos create my-notes --org org_1a2b3c # explicit name, new project
run402 repos create --project prj_xyz # adopt an existing project (absorbs `gitvault init`)
run402 repos create --nested --project prj_xyz # the app lives inside another repo: make it its own nested repo + remote
git push -u origin HEAD # the printed next_action, verbatim
run402 repos view --human
run402 repos list --org org_1a2b3c
run402 repos rename my-notes --project prj_xyz
run402 repos delete --project prj_xyz --force # only once you've read what --force would lose
run402 repos snapshot --dry-run
run402 repos snapshot --dry-run --manifest-out /tmp/snapshot-plan.json
run402 repos mirror s3://acme-vault-mirror --profile acme
run402 repos mirror --backfill
run402 repos fsck --mirror
run402 repos gc
run402 repos access
run402 repos recover s3://acme-vault-mirror --out ./restored
run402 repos handoff --note-file handoff.json # mints kgh1_… — the key is the ONLY thing on stdout
run402 repos resume kgh1_… # on the other machine: claim, clone, restore, print the note
run402 repos invite --note-file invite.json # mints kgi1_… — the key is the ONLY thing on stdout
run402 repos join kgi1_… # on the other machine: pay in, claim, clone, restore, print the note
run402 messages wait # then: block until the other agent speaks (or the timeout elapses)

A working tree inside another repository (a monorepo app) still gets its vault allocated, but the remote scaffold is skipped (remote.status: "skipped", enclosing toplevel named) and the response carries a create_nested_repo next_action instead of push_repo; re-run with --nested to make the directory its own repository with the encrypted remote — the enclosing repository only gains one local .git/info/exclude line and is otherwise untouched. The inferred [name] for a monorepo app comes from the app directory, never from the enclosing repository’s origin.

resume and join end with the checkout’s first git command in mind: when the remote helper the checkout’s origin scheme needs (git-remote-kygit for kygit::, git-remote-run402 for run402::) is not on PATH — an npx -y @kychee/kygit run keeps it inside the npx cache, where git cannot see it — the result carries an install_remote_helper next action naming npm i -g @kychee/kygit run402; nothing else in the claim is affected.

Agents read each other’s vaults with no human step (gitvault-agent-envelopes). Custody follows membership, not principal type: every active human or agent member of the owning org is an envelope recipient. The CLI keystore’s own X25519 key is enrolled AUTOMATICALLY on a keystore’s first gitvault operation (repos …, git clone/push) — published as custody_scheme: keystore_v1 and possession-proven in the same invocation; there is no enrollment verb. Rotation is never automatic: if this keystore’s key differs from the principal’s published key, the operation fails GITVAULT_KEY_ROTATION_REQUIRED — restore the keystore backup, or have an org owner revoke the stale key (DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key, owner + step-up), after which the next operation enrolls afresh; a principal that is the sole member of every org it belongs to may republish with replace_current. Cold open: a member on a fresh keystore that has been wrapped opens a vault straight from its own envelope (git clone run402::<org>/<repo> just works); the handle’s restored block says how much was verified — trust: platform_attested + continuity: first_seen|pinned + independently_verified: false is the control plane’s signed allocation record vouching for genesis, NOT end-to-end authentication (a creator-held recovery receipt is the receipt tier). Not yet wrapped → GITVAULT_ENVELOPE_PENDING naming the key-holders and the next actions: poll repos access; any key-holder’s next ordinary gitvault operation wraps you.

MCP. Three read-only tools carry the SAME one-noun renaming: repos_view, repos_list_heads, repos_fsck. No mutating verb has an MCP tool, and none will — mutating verbs are CLI-only by design (immutable generations with no undo, the one-shot recovery receipt, the once-returned maintenance lease token, destructive gc --submit, owner+step-up policy, handoff/invite’s single-use bearer keys, resume/join’s org-membership mutations).

Wallet selection, remote naming, id-pinning, named addressing, lazy allocation, the vault-only track, dry-run preview, --human rendering, the deploy-time capture lane, gc’s two-phase submit ceremony, expiry, terminal loss, the backup reminder, and mirroring to your own storage are all covered in the sections below.

Cost. There is no separate repos price. A repo’s bytes count against the same organization-pooled storage_bytes budget your projects already share, charged once per unique object, with a 4 KiB per-object accounting floor and a 1 MiB per-vault minimum.

One thing to know before you rely on this: a vault has a writer set, not a single key. Every admitted member or handoff recipient opens the vault and pushes under its OWN keystore key (repos access lists them; a member whose key is not yet admitted gets GITVAULT_WRITER_NOT_ADMITTED until any live writer’s next operation admits it). A vault whose only admitted principal is this keystore is exactly as safe as this keystore: back up the keystore directory the moment you have something in it worth keeping, or admit a second principal.

Terminal loss (protocol §0). In V0-A, whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship. view prints the full statement verbatim on stderr and carries it in its JSON — read it before you rely on this. The vault protects source history from host-side loss while a principal keystore survives.

Mirroring — the recommended default (gitvault-mirror-default). The customer-owned mirror is not a power-user feature: the recommended shape is three copies: your working clone, the platform’s replicated vault, and a mirror in storage you own. Until a first mirror write or sync succeeds, doctor and repos view carry the standing vault_unmirrored finding — named, informational, never blocking, computed entirely client-side (the gateway never learns whether a mirror exists), and cleared by the first successful mirror write or sync (run402 repos mirror <destination> to configure, run402 repos mirror --backfill to complete the first copy; every later snapshot dual-pushes automatically). repos create prints the same one-liner beside the recovery receipt — the two things worth doing in the first minute. The two honesty limits ride every mirror status verbatim: this recovery proves validity, never freshness — a mirror (or the vault itself) can only tell you the newest generation it happens to hold, never that a newer one does not exist elsewhere; and a mirror without the principal keystore (or an equivalent key) recovers nothing — mirroring ciphertext does not create a second key, and the V0 terminal-loss sentence is unchanged.

BYO primary bucket (gitvault-byo-primary-bucket). run402 repos create --byo <s3://bucket/prefix> allocates the vault with storage_profile: "byo": payload ciphertext (WAL packs, checkpoint packs/manifests/claim sets, ref state, retention roots, key envelopes) is written by YOUR client straight to a bucket YOU own — for a BYO vault, your source ciphertext never touches our infrastructure — not even encrypted. The gateway stores only the destination ADDRESS (never credential material of any kind); the credential is resolved locally at use time, exactly like mirror’s, and no request to run402 ever carries it. run402 still holds and serves the authoritative signed chain — heads, admission records, rotation/prune/maintenance records — for a BYO vault exactly as for a managed one, so admission, rotation, and deploy-lane activation gating are byte-identical regardless of profile; your client additionally dual-writes a non-authoritative copy of that chain into your bucket on every push, in admission order, so the bucket alone is a complete repos recover source. Storage is honest about what it can verify: a BYO finalize is client-attested — your client declares the manifest, writes create-only, and submits a per-object hash/size attestation — and the resulting receipts on payload kinds say storage_verification: "client_attested" (a managed vault’s receipts say "gateway", byte-identical to before); an attestation that disagrees with the declared manifest is refused GITVAULT_BYO_ATTESTATION_MISMATCH (422, gateway-raised) before any receipt is minted. Three more codes are CLIENT-raised, against your own bucket, which run402 has no credential to observe: GITVAULT_BYO_BUCKET_PROBE_FAILED (allocation-time probe, fail-closed — no vault half-exists), GITVAULT_BYO_BUCKET_WRITE_REFUSED (a payload write failed — the primary is never advisory, so the push fails), and GITVAULT_BYO_OBJECT_MISSING (a reader or fsck found a chain-referenced object absent from your bucket, adjudicated against run402’s own signed chain). Degraded reads: on a network-class gateway failure (never a 4xx), git fetch/clone fall back to the vault’s own BYO destination — or, on a managed vault, a configured mirror, same code path — marked degraded: true with source provenance and one stderr line; it proves validity, never freshness, local trust pins advance only as far as the copy chain-verifies, and writes still require the gateway in every storage model. Billing: BYO payload bytes never enter the org-pooled storage_bytes accounting; the gateway-held chain bills as ordinary storage, and abuse caps stay platform-enforced. Copy-count honesty, said plainly: run402 holds no payload copy of a BYO vault — only the small signed chain; your primary bucket is the sole copy of your source until you add a second customer-held location. The vault_unmirrored finding applies to BYO vaults exactly as to managed ones, with the BYO remedy: add a second customer-held location: ‘run402 repos mirror ’ works unchanged against a different destination — for a BYO vault this is your only additional copy, since run402 holds no payload copy of its own. storage_profile is chosen at allocation only in v1 — nothing flips it on an existing vault; promote-mirror-to-primary is a tracked follow-up.

Handoff / resume — pass a working tree to another agent (kygit-handoff). handoff and resume are a session PASS, not a backup: a working tree, dirty state and all, moved from one agent/machine to another with a single-use bearer key and no shared keystore, no shared allowance, no server-side principal to configure ahead of time. handoff captures whatever is actually checked out right now (not the last snapshot) into the same synthetic-commit shape git stash push -u produces, pushes it as a vault retention root, and mints kgh1_<69 chars> — HKDF-derived from a fresh handoff_id and a random master secret, deriving an auth_secret (the gateway sees only its SHA-256 hash) and a wrap_key that seals the vault’s live epoch key directly to the recipient in a small AEAD envelope, a deliberately different, simpler format than an r402s/v0 vault object. The key is printed to stdout and NOWHERE else — not in --json, not on stderr, not in any log line this CLI writes. resume claims it once (a same-principal retry is a safe, explicit dedup — never a re-mint; a different principal’s retry after the first claim fails), clones fresh, and reapplies the exact checkpoint with git stash apply --index. The claim is a REAL org-membership grant at the sender’s role by default — treat the key exactly like a credential, because until claimed or expired, it is one. A resumed agent is also a NEW run402 wallet: on a machine with no active tier, resume folds the cold-start chain (allowance → faucet → one x402 prototype payment) before the claim, exactly as create does, and --no-init opts out — the claim never waits on it. On a multi-writer vault (gitvault-multi-writer rev 47, see below), claiming also makes the recipient a WRITER — their own key signs future pushes, not the sender’s — and resume prints writer: <outcome> (generation N) to stderr reporting it; handoff prints the same fact before minting the key.

Invite / join — bring a second agent into the exact work, dirty tree included (kygit-invite). The second claim kind beside handoff/resume, sharing the same crypto/keystore/restore machinery — a Handoff passes the work on and the sender stops; an Invite grows the team while the sender keeps working. invite captures the checkpoint exactly like handoff and mints kgi1_<69 chars> — HKDF-derived from a fresh invite_id and a random master secret, domain-separated from a Handoff Key by kind (kygit/invite/… info strings, so an invite secret never verifies as a handoff hash or the reverse), sealed in its own KGI1 envelope frame (distinct from both r402s/v0’s object frame and the handoff bridge’s KGH1). Before minting, invite registers the INVITER’s own presence in a coordination room — the project’s default room, or --room <key> for a named org room — so the row can carry it; after minting it posts ONE room message naming the checkpoint (never the key), never blocking the mint on a presence or fact-post failure. join claims it (a same-principal replay dedups safely), clones fresh, reapplies the checkpoint with git stash apply --index, pins the invite’s OWN room locally (r402.room), registers this session’s own presence, posts ONE arrival message, and reports the inviter (name, labels, liveness), who else is live, the catch-up cursor, and the last few messages — so the first reply needs no lookup. A joined agent is also a NEW run402 wallet: on a machine with no active tier, join folds the SAME cold-start chain resume does before the claim. A kgh1_… key handed to join, or a kgi1_… key handed to resume, is refused BY NAME before any network call. From there, run402 messages wait is the agent’s ear.

The kygit:: remote scheme (design D8) — same vault, a different spelling. Set RUN402_REMOTE_SCHEME=kygit and every remote this CLI scaffolds or prints — create’s origin, view, rendered next_actions commands — comes out kygit::<org>/<name> instead of run402::<org>/<name>. parseGitvaultRemoteUrl accepts either prefix into the identical address; neither client ever rewrites a remote the other one wrote. This is what the @kychee/kygit package sets before every run402 repos … call it runs, and it ships its own git remote helper, git-remote-kygit, alongside its kygit binary — npm i -g @kychee/kygit alone is a complete install for kygit:: remotes.

Multi-writer vaults (gitvault-multi-writer rev 47). A vault’s writer set is chain state, not a fixed genesis-creator key — every read/push/rotation re-derives it fresh from the signed chain. Two doors admit a new writer: an ALREADY-active writer can admit any eligible org member (active membership at role developer+, a published signing key) with no grant/acceptance ceremony — the admitting head’s own signature IS the authorization; repos handoff/repos resume is the other door — claiming a Handoff Key makes the recipient a writer too, not just a checkout pass (see “Handoff / resume” above). A key that is ever removed can never be re-added — burned, permanently.

repos view lists the chain-verified roster (writer_set.writers[], each with its admission generation and which door admitted it) and any pending_writers[] — eligible candidates not yet admitted; --human renders one line per writer plus a repos access sync hint when candidates are waiting. run402 repos access sync is the explicit, on-demand form of the SAME reconcile every ordinary repos operation (view, push, snapshot, deploy, …) already runs automatically whenever this machine’s own key is itself an active writer — it distinguishes “I have no authority here” (eligible: false) from “nothing is pending” (an empty admitted: []), and names any candidate it could not admit in skipped[] with why. run402 doctor’s gitvault check reports this machine’s own standing as writer: active|pending|not_admitted|read_only_vault, each with its own remedy in gaps[]. run402 org member add runs the SAME reconcile across every vault the gateway names in its response — best-effort, warning rather than refusing when the caller isn’t a writer anywhere yet.

Pushing (or snapshot/deploy/any other head-signing operation) from a machine that is not an active writer refuses LOCALLY, before any network call — GITVAULT_WRITER_NOT_ADMITTED, with a request_writer_sync next action pointing at the fix: any current writer’s next gitvault operation admits pending writers automatically, or run repos access sync yourself once you are one. Removing a writer rides the SAME rotation that would otherwise change the vault’s epoch key (a rekey and a removal are one atomic head — a removed writer can never decrypt the epoch it was cut from); removing the vault’s LAST writer is refused unless explicitly acknowledged, entering a read-only terminal — the vault keeps serving reads, but nothing can push until a new writer is admitted through a recovery path.