Skip to content
GhostbranchdocsGitHub

A feature in detail

What each step of the feature loop prints, from sync to the merge, and what to do when feature start only fetches, CI is long, apply asks Archive or Keep, or a review needs a fix.

This page expands Your first feature: what each step prints, and what to do when a step doesn’t go the plain way. The examples use a public repo called you/project and a feature called add-search. The concept pages go further: The gate, Promote and approval, and Merging and closing.

Terminal
ghostbranch sync
Shadow main is up to date with public main (815a84b30f4a).

Sync only ever fast-forwards the shadow repo. It never writes to the public repo. Sync has the rest.

Run it in your clone of the public repo:

Terminal
ghostbranch feature start add-search
Started feature add-search from shadow main 815a84b30f4a.
Added remote shadow (https://github.com/you/project-shadow.git).
Switched to add-search (from shadow main 815a84b30f4a). Commit here, then: git push shadow add-search
Next:
ghostbranch gate add-search --repo you/project
feature start switched this checkout to add-search, tracking shadow/add-search; commit there, push it with `git push shadow add-search` (the shadow repo, never origin), then gate it

This creates the branch in the shadow repo. Then it gets your checkout ready:

  1. It adds a remote called shadow for the shadow repo, the first time. If a remote already reaches the shadow repo, it uses that one.
  2. It fetches the shadow repo.
  3. It switches your checkout to the new branch, tracking shadow/add-search.

Every result ends with Next: the command to run after it, and why.

feature start never switches when that could lose work or surprise you. In these cases it fetches, leaves your checkout where it is, and says why and what to do:

  • Uncommitted or untracked files. Commit or stash them first (untracked files too, with git stash --include-untracked), then git switch add-search.
  • A detached HEAD with commits no branch has. Keep them on a branch first, with git switch -c <branch-name>, then git switch add-search.
  • A local branch called add-search already exists. Ghostbranch leaves it alone. Compare it with shadow/add-search, and switch to the one you mean to work on.
  • A remote called shadow that points somewhere else. Ghostbranch never rewrites a remote. It prints the commands to check the branch out instead.
  • The fetch failed. The branch exists in the shadow repo either way. If git says the repository was not found, git may be signed in to GitHub as an account that can’t see the shadow repo: Ghostbranch’s own login doesn’t cover git. The message names a git remote set-url command that fixes it for that remote only.

For example, with uncommitted changes:

Terminal
ghostbranch feature start add-search
Started feature add-search from shadow main 815a84b30f4a.
Fetched shadow, but this checkout has uncommitted changes, so it stays on its branch.
To work on add-search: commit or stash first, then `git switch add-search`. Commit there, then: git push shadow add-search
then Next

To create the branch without touching your checkout, add --no-checkout. It prints the commands to run instead. In a checkout that already has the shadow remote, that looks like this:

Terminal
ghostbranch feature start add-search --no-checkout
Started feature add-search from shadow main 815a84b30f4a.
Check it out with: git fetch shadow && git switch add-search
then Next

Without the remote, the line adds it first. Outside a clone of the public repo, feature start leaves things alone in the same way, and the line clones the shadow repo instead.

The gate rebases the feature onto public main, scans exactly what would be published for leaks, and runs your CI on the result, in a draft pull request in the shadow repo. Nothing goes public.

ghostbranch gate add-search waits for CI and prints the result. CI can take a while, so you can also start it and come back:

Terminal
ghostbranch gate add-search --detach
Gate for add-search: CI running on candidate de15b3e1f274 (shadow PR #7); job gate/add-search/de15b3e1f274.
1 commit(s), scanned as preserve
Next:
ghostbranch wait gate/add-search/de15b3e1f274 --repo you/project
CI is running on the gate candidate; wait finishes the job and records the result (with --json it streams progress)

The leak scan has already passed by now: the gate scans before it starts CI. wait finishes the job when CI is done. You can run it from either of your machines, and a job that has already finished answers at once:

Terminal
ghostbranch wait gate/add-search/de15b3e1f274
Gate for add-search: checks passed, leak scan clean.
1 commit(s), scanned as preserve
candidate de15b3e1f274 on shadow PR #7
Next:
ghostbranch promote add-search --mode preserve --plan --repo you/project
the gate passed; plan the publish

The gate explains the rebase, the leak scan and CI parity, and Long CI explains jobs and wait.

promote --plan builds the public commits on top of public main and saves a plan of exactly what will go public: every commit, file and author, and the pull request’s title and text. It publishes nothing yet.

Terminal
ghostbranch promote add-search --plan --title "Add search to the docs"
Plan 20261012T162037Z-b870aa saved to /home/you/.local/state/ghostbranch/workspaces/default/plans/20261012T162037Z-b870aa.json.
preserve: 1 public commit(s)
Publish it with: ghostbranch apply 20261012T162037Z-b870aa
Next:
ghostbranch apply 20261012T162037Z-b870aa (a person must run this)
publish the plan: a person runs this in their own terminal and types the code it shows (or allows agents first with ghostbranch agents allow)

The default history mode, preserve, publishes each of your commits as it is, messages included. With more than one commit, give the pull request a title with --title. To publish one squashed commit instead, use --mode squash -m "<message>".

Make a plan and History modes have the rest.

apply publishes the plan, if nothing has moved since you made it. It shows what will go public on your own terminal, asks what happens to the feature afterwards, and shows a one-time code. Type the code back to confirm:

Terminal
ghostbranch apply 20261014T142810Z-e6a232
a summary of the commits, files and pull request that will go public
After publishing, this feature:
❯ Archive — the private branch is archived; make fixes or changes to this PR by pushing to it directly
Keep — the private branch stays; make fixes or changes to this PR through gate and promote, as today
Press Enter for Archive, or type k for Keep:
To confirm, type this code and press Enter: F9ARDF
Leave it empty, or type n, to cancel.
> f9ardf
progress lines, then:
Published add-search as PR #35: https://github.com/you/project/pull/35
Archived add-search's shadow branch at refs/ghostbranch/archive/add-search: fix PR #35 by pushing to add-search.
Next:
ghostbranch sync
a maintainer reviews and merges PR #35 on GitHub; then sync closes the feature
then the three git steps for a fix, below

This run is a later one than the plan above, so its plan ID differs.

The first time you publish, the question just above the code matters most:

  • Archive (press Enter) hands the pull request off. The feature’s private branch is archived in the shadow repo, so nothing is lost. From now on you fix the pull request by pushing straight to its branch, with plain git: no gate, no plan, no code.
  • Keep (type k) keeps the feature private. Fixes go through the shadow branch, gate and promote, as before.

Pick Archive unless you have a reason to keep the work private, such as more private changes still to come. Promote and approval has the details, and ghostbranch feature archive add-search hands a kept feature off later.

If anything moved after the plan was made (public main, the feature, or its gate), apply refuses with exit code 6 and publishes nothing. Make a new plan and apply that one.

The pull request is an ordinary one on your public repo, and it’s reviewed and merged as usual. Merge it with a merge commit or a rebase, not “Squash and merge”, or the commits preserve published are squashed after all.

To answer a review before the merge, push the fix to the pull request’s branch. With Archive:

Terminal window
git fetch origin add-search
git switch --detach origin/add-search
# commit your fix here
git push origin HEAD:add-search

Start from origin/add-search, not your old add-search branch: that one holds the private history, and the push guard refuses it. If you chose Keep, commit on add-search, push to the shadow repo, then gate and promote again: see Updating an open PR.

After the merge, run ghostbranch sync again. It brings the merge into the shadow repo and closes the feature: if nothing changed since you published it, sync deletes what’s left of the feature in the shadow repo, its archive included, and says so.

Terminal
ghostbranch sync
Shadow main fast-forwarded ec0c2b31243a -> b924a62f7732.
cancelled 1 push-triggered workflow run(s) in the shadow repo
closed merged feature add-search (PR #35)
this run also closed two other features, left out

Merging and closing covers merging a stack, closing a feature yourself, and parking one.

To try the loop without opening a pull request, stop after the plan (step 5) and abandon the feature. Nothing is deleted, and nothing is published:

Terminal
ghostbranch feature abandon add-search
Abandoned add-search (its branch is kept; undo with --undo).

ghostbranch feature list hides an abandoned feature, and --undo brings it back. A plan you never apply does nothing. The gate’s draft pull request in the shadow repo stays open; close it on GitHub if you like.