Skip to content
GhostbranchdocsGitHub

The repo pair and the shadow

How your public repo and its private shadow repo fit together, and what moves between them.

Ghostbranch never keeps your private work in the public repo, not even for a moment. It all lives in a second repo, and only a gated publish carries it across. The examples use a public repo called you/project.

  • The public repo is your repo, exactly as it is today. Ghostbranch only ever writes to it through promote, which opens a pull request.
  • The shadow repo is a private repo that ghostbranch init creates next to it, called you/project-shadow unless you name it. It holds:
    • a copy of public main, which sync keeps up to date;
    • your feature branches;
    • a gate branch for each gated feature (ghostbranch-gate/<feature>), with a draft pull request where CI runs;
    • the overlay branch, ghostbranch, which is the shadow repo’s default branch;
    • a small record for each feature: its parent, its last gate and its public pull request.

Your checkout is a clone of the public repo with two remotes: origin for the public repo and shadow for the shadow repo. The first feature start prints the line that adds the shadow remote.

From To How
Public main Shadow main ghostbranch sync, fast-forward only
Your checkout A feature branch in the shadow repo git push shadow <feature>
A feature branch A gate branch and a draft PR in the shadow repo ghostbranch gate
A gated feature A pull request on the public repo ghostbranch promote, then a person’s apply
A merged pull request Shadow main, and the feature closes ghostbranch sync again

Promote is the only arrow that points at the public repo. Nothing else Ghostbranch does writes there, and the push guard stops git push from sending feature work there by mistake.

The ghostbranch branch holds what belongs to the pair and never to the public repo:

  • .ghostbranch.yml, the pair’s settings;
  • .ghostbranch/, for your own internal notes and docs;
  • .github/workflows/ghostbranch-*.yml, private CI workflows, such as the optional scheduled sync.

When a feature is gated, these files are added on top of it, so CI sees them. None of them is ever promoted. This is a real .ghostbranch.yml, as init writes it:

.ghostbranch.yml
# Ghostbranch configuration. It lives only on this shadow repo's
# "ghostbranch" branch and is never promoted.
version: 1
public: you/project
shadow: you/project-shadow
promote:
target: pr
# preserve publishes each feature commit; squash publishes one commit;
# curated regroups, reorders and rewords commits from a --curate file.
default_mode: preserve
# noreply publishes every commit as your GitHub noreply identity; keep
# publishes the original authors; map uses author_map.
author_email: noreply
leak_scan:
strip: [".ghostbranch/**"]
markers:
begin: BEGIN-INTERNAL
end: END-INTERNAL
block_patterns: []
large_file: 5MB
secrets: true
sync:
# Scheduled sync is off: run ghostbranch sync yourself (commands warn when
# shadow main falls behind public). To opt in, set a cron such as
# "23 */6 * * *" (about 120 Actions minutes a month), then run
# ghostbranch upgrade.
schedule: "off"
branches: [main]
ci:
rewrite_repo_guards: rewrite
gate:
ci_appear_timeout: 10m
ci_timeout: 60m

Each section has its own page: promote in History modes, leak_scan and ci in The gate, sync in Sync, and the gate timeouts in Long CI.

To change a setting, edit the file on the ghostbranch branch and push that branch to the shadow repo. A key Ghostbranch doesn’t know, even under sync: or ci:, is an error, so a typo can’t be silently ignored.

ghostbranch status shows the pair’s health on one line, then every feature:

Terminal
ghostbranch status
you/project -> you/project-shadow: shadow main up to date, overlay current, scheduled sync off
NAME STATE DRIFT GATE PR
add-search gate_passed clean passed -
└─ search-filters gate_passed clean passed -
└─ search-ui draft clean - -
try-dark-mode abandoned clean - -
  • The first line says whether shadow main has fallen behind public main (run sync), whether the overlay’s managed files need an upgrade, and whether scheduled sync is on.
  • The tree shows stacked features: search-filters is built on add-search, and search-ui on search-filters.
  • DRIFT says whether the feature still merges cleanly onto public main.
  • GATE is the feature’s last gate, and PR its public pull request, if it has one.

The states you’ll see most:

State Means
draft Not gated yet.
in_review Gated, but the gate is still running, failed, or is out of date.
gate_passed The last gate passed and still matches the feature. Ready to promote.
published Its pull request is open on the public repo.
pr_behind The pull request is open, but the feature has commits it doesn’t have yet. See Updating an open PR.
archived Published and handed off: its pull request takes fixes directly, and its shadow branch is archived. See After publishing.
merged The pull request merged. The next sync closes the feature.
parent_stale A stacked feature whose parent moved since it was started or restacked. See Restack.
public_pr_needs_update Its open pull request still carries the commits of a merged one below it. See After the merge.
abandoned Parked with feature abandon.

A few more need a person: merged_moved and pr_closed (see Sync), branch_gone, and unreadable, a feature record Ghostbranch can’t read.

ghostbranch feature list prints the same table without the first line, and leaves out abandoned features unless you add --all. ghostbranch feature show <name> prints everything about one feature:

Terminal
ghostbranch feature show search-filters
Feature search-filters: gate_passed
branch: search-filters at 93e0baa3fc86, based on 231b0f357d2b
stack: on add-search; branches on it: search-ui
drift: clean
gate: checks passed, leak scan clean, scanned as preserve, fresh, at 2026-10-12T16:30:12Z
public PR: none
Commits since public main (21):
4ba9a1113723 Add the search box (You <you@example.com>)
src/components/SearchBox.astro
the other 20 commits and the ledger entries left out

All three only read. They push nothing and write nothing to the ledger, so run them as often as you like. Add --json for the same data in a form a script can read.

OVERLAY_OUTDATEDwarningA Ghostbranch-managed file on the overlay branch is missing, out of date, or edited by hand.
What to do

Run ghostbranch upgrade --plan to see the change, then ghostbranch upgrade.

UPGRADE_NEEDEDexit 1Something this pair uses was written by a newer Ghostbranch: a record (error.details.record: feature, plan, grants or waivers) or the overlay config (error.details.unknown_key, a key this binary doesn't know).
What to do

Upgrade ghostbranch on this machine to the version that wrote it, or newer (the other machine is probably newer). Don't edit the record or the overlay config to make an older binary accept it. If the key in error.details.unknown_key is a typo, a person fixes it in .ghostbranch.yml on the overlay branch.

NOT_A_PAIR_CHECKOUTexit 2This directory is not a checkout of a registered public repo.
What to do

Run the command inside a clone of the public repo, after ghostbranch init.

SHADOW_NOT_PRIVATEexit 1The shadow repo is public or a fork, so Ghostbranch refuses to touch it.
Causes
  • someone changed the shadow repo's visibility
What to do

A person makes the shadow repo private (and not a fork) on GitHub.

This needs a person.