Skip to content
GhostbranchdocsGitHub

Your first feature

Build a feature in private, gate it, and publish it to your public repo as a pull request.

It takes seven steps, from sync to the merge, and each one is a single command. The examples use a public repo called you/project and a feature called add-search.

  • Ghostbranch is installed and you’ve logged in.
  • You ran ghostbranch init in a clone of your public repo (Set up a repo).
  • The repo has at least one GitHub Actions workflow. The gate needs CI to run.

Bring the shadow repo up to date with the public repo first, because new features start from shadow main:

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.

Terminal
ghostbranch feature start add-search
Started feature add-search from shadow main 815a84b30f4a.
Check it out with: git fetch shadow && git switch add-search
Next:
ghostbranch gate add-search --repo you/project
check out with `git fetch shadow && git switch add-search`, commit on add-search, push it with `git push shadow add-search` (the shadow repo, never origin), then gate it

This creates the branch in the shadow repo, not in your checkout. Every result ends with Next: the command to run after it, and why.

The “Check it out with” line is the exact command for your checkout. The first time, your checkout has no remote for the shadow repo yet, so the line adds one too:

Terminal
git remote add shadow https://github.com/you/project-shadow.git && git fetch shadow && git switch add-search

After that, it’s the shorter form shown above.

Run the “Check it out with” line exactly as printed, work on the branch as usual, and push it to the shadow repo:

Terminal window
# run the "Check it out with" line from feature start
# edit, test, commit
git push shadow add-search

Push to shadow, never to origin. If you push feature work to the public repo by mistake, the push guard refuses it and nothing is sent.

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

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>".

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 step 5’s, 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

To try the loop without opening a pull request, stop after 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.