Appearance
Mirror Remote — The Second Git Host
Where every managed repo publishes its second copy, and how that target is declared rather than hand-configured (#1747). (Back to reference index.)
A single GitHub remote makes every git operation a single point of failure. On 2026-08-17, Git Operations were degraded for roughly four hours; for that window no clone could fetch and no commit could be published anywhere. The mirror remote closes that gap with the cheapest possible layer: a second bare repo on a fleet machine, reachable over Tailscale, wired into every clone from declared state.
It touches no forge API — this is pure git plumbing, which is why it works when the forge does not.
Per the Prime Directive, the mirror target lives in exactly one place (mirrors.json), reconciliation is idempotent, and every outcome is reported rather than assumed.
Declared state
mirrors.json in BOB_SOURCE owns git publishing topology — and only that. Which projects exist is still owned by projects.json; which machines exist by machines.json. Schema: schemas/mirrors.schema.json, validated by make check-schemas.
json
{
"_meta": { "version": 1 },
"defaults": {
"host": "farm-01",
"user": "farm",
"root": "~/git-mirrors",
"remote_name": "mirror",
"enabled": true
},
"projects": [
{ "name": "vidz", "enabled": false, "reason": "large media working tree" }
]
}defaults is what every active project in projects.json inherits. The projects array is exceptions only — never restate a project that already matches the defaults, or the manifest becomes a second project registry that can drift from the first.
| Field | Meaning |
|---|---|
host | machines.json id of the mirror host. Resolved to its Tailscale name for SSH and for the URL. |
user | SSH user on that host. Omit to let ~/.ssh/config decide. |
root | Directory holding the bare repos. Absolute, or ~/-prefixed for the SSH user's home. |
remote_name | The local git remote name wired into every clone. Conventionally mirror — never origin. |
enabled | false excludes the project. Always pair with reason. |
The mirror URL is derived, never declared: <user>@<tailscale-name>:<root>/<project>.git. Renaming a host in machines.json therefore cannot leave a stale URL behind in a second file.
Commands
The entrypoint is bob-mirror (engine: ~/.claude/scripts/fleet/mirror.js). With no project argument and no --all, it operates on the project containing the current directory.
bash
bob-mirror resolve # effective config for this repo
bob-mirror reconcile --apply # create the bare repo + wire the remote
bob-mirror publish --apply # push to BOTH origin and the mirror
bob-mirror verify # prove the mirror is a usable copy
bob-mirror status --all # fleet-wide condition| Command | What it does |
|---|---|
resolve | Prints the effective config (defaults + override) and the derived URL. Read-only. |
reconcile | Ensures the bare repo exists on the mirror host and the local remote points at the declared URL. |
publish | Pushes the checked-out branch (plus tags) to origin and the mirror. |
verify | Clones the mirror into a throwaway directory and proves it reproduces origin. Read-only. |
status | Reports declared / wired / reachable / in-sync per repo. Read-only. |
| Option | Effect |
|---|---|
--all | Operate on every project with an enabled mirror. |
--apply | Actually mutate. reconcile and publish are dry-run without it. |
--local | reconcile: wire the local remote only, never contacting the mirror host. |
--ref BRANCH | publish / verify: use this branch instead of the checked-out one. |
--path DIR | Operate on this working copy instead of the one in projects.json. |
--json | Machine-readable output. |
--no-probe | status: report declared state without touching the network. |
Exit codes: 0 healthy · 1 operational failure (unreachable, push rejected, mirror stale) · 2 declaration error (bad manifest, unknown project or host).
Safety model
- Dry-run by default.
reconcileandpublishprint exactly what they would do and change nothing until--applyis passed. - Idempotent. An existing bare repo is left alone; an already-correct remote is reported, not rewritten. Running
reconcile --applytwice is a no-op. - A retarget is never silent. If a clone's mirror remote points somewhere other than the declared URL,
reconcilenames what it replaced. A quietly repointed remote is how a repo ends up publishing where nobody declared. - Neither push hides the other.
publishattemptsoriginand the mirror independently and reports both. A mirror failure is never masked by a healthy origin, and — the case this capability exists for — a healthy mirror push still happens when origin is unreachable, so the commit is not stranded. - Skips are not failures. A project that is not checked out on this machine, or is not a git repo, is reported as a visible skip with its reason. It does not poison a
--allexit code, and it is never silently dropped.
Verification: status vs. verify
The two read-only commands answer different questions, and the difference matters during an outage.
status compares the refs the mirror advertises against the local tip. It is one ls-remote per repo — cheap enough to run for every project in a fleet snapshot — but it never asks the mirror for a single object. A bare repo whose objects were truncated would still read as in sync.
verify actually clones the mirror into a throwaway directory, checks out the branch, and compares that clone's tip against origin's — not against the local working copy, which may be ahead of or behind what was published. It then confirms the clone has a real working tree and passes a connectivity fsck. That is the question worth answering: could this copy stand in for origin?
$ bob-mirror verify --ref master
[ok] bigbrain (master) — mirror clone reproduces origin at 48804d2, 1372 filesA local commit you have not pushed does not fail verification — the reference point is origin, so verify measures the two published copies against each other, not your desk.
Fresh clones
A new clone materializes its mirror remote from declared state; there is no manual git remote add step. cdi does this during init:
bash
bob-mirror reconcile <project> --local --apply --path <clone-dir>--local wires the local remote and deliberately does not contact the mirror host, so cdi cannot hang on a sleeping farm machine or an offline laptop. Creating the bare repo remains an explicit bob-mirror reconcile --apply. --path is what makes this correct for a clone that lives somewhere other than the path recorded in projects.json.
Reading a status report
MIRROR STATUS
PROJECT WIRED REACH SYNC LOCAL MIRROR
-------- ----- ----- ----- ------- -------
bigbrain yes yes yes d059d2f d059d2f
seebod no - - 17cfe6e -
! seebod: not reconciled — run: bob-mirror reconcile --apply
- drminizer: not a git repo| Column | Meaning |
|---|---|
WIRED | The local remote exists and matches the declared URL. |
REACH | The mirror answered an ls-remote. - means it was not probed (never reconciled). |
SYNC | The mirror's tip equals the local branch tip. STALE means committed work exists on only one host. |
LOCAL / MIRROR | The two tips being compared. |
An unreconciled repo is reported as not reconciled rather than unreachable — the network is not at fault, and the fix is named in the message.
Fleet reporting
Mirror condition is carried in the fleet snapshot (scripts/fleet/audit.js), so the question "how much of my committed work exists on only one host?" is answerable for the whole fleet without visiting each machine.
"fleet" here means BoB's machine layer —
machines.jsonplusscripts/fleet/*— not the Fleet project (~/projects/fleet), the host-environment layer beneath BoB that composes these scripts rather than replacing them (vision:~/projects/fleet/docs/vision.md). See the glossary.
Each project entry gains a mirror block (declared, wired, reachable, in_sync, both SHAs), and summary.mirror rolls them up. audit.js view renders it:
Mirror (second git host; UNMIRRORED = commits on one host only)
MACHINE DECLARED WIRED UNMIRRORED UNREACHABLE
macbook-pro-16 26 1/26 25! 0
FLEET 26 1/26 25! 0UNMIRRORED is the number to read: repos whose commits live on exactly one host, and would therefore be unreachable during a forge outage. It covers three cases — never reconciled, stale, and unreachable — because all three mean the same thing for recovery.
Buckets are kept distinct rather than collapsed:
- skipped — not cloned on this machine, or not a git repo. No commits to strand, so not a gap.
- opted out —
enabled: falsein the manifest. Reported with itsreason, so a deliberate exclusion stays visible instead of vanishing. - unknown (
?N) — wired but not probed this run. Not proven safe.
The probe cost is one ls-remote per wired repo; a clone that was never reconciled short-circuits before any network call, so an unmirrored fleet costs nothing to audit. Set BOB_FLEET_MIRROR_PROBE=0 to report declared state only, which moves wired repos into the unknown bucket rather than pretending they are in sync.
Recovering when github.com is down
The mirror is a complete, fetchable copy, so it can stand in for origin:
bash
# Keep committing and publishing while origin is unreachable
bob-mirror publish --apply # origin fails loudly, the mirror succeeds
# Work from the mirror on another machine
git clone farm@farm-01:~/git-mirrors/bigbrain.git
# When origin returns, publish again — both remotes converge
bob-mirror publish --applyRelated
- Fleet audit — mirror condition is carried in the fleet snapshot alongside provision and readiness state.
- Prime Directive — why the target is declared once and reconciled, rather than configured per clone.