Skip to content
GhostbranchdocsGitHub

Promote and approval

How a gated feature becomes a pull request on your public repo, and why a person has to say yes.

Publishing is split in two on purpose. promote --plan works out exactly what would go public and saves it. apply publishes that plan, and only after a person says yes. Between the two, nothing can change without the plan noticing.

ghostbranch promote <feature> --plan reuses the feature’s green gate, or runs a new one if it’s missing or out of date. Then it builds the public commits on top of public main, ending on exactly the files the gate’s CI tested. It scans every commit’s files, message and author, the branch name and the pull request’s text, and saves the plan.

Terminal
ghostbranch promote add-search --title "Add search to the docs" --plan
Plan 20261012T164001Z-e15904 saved to /home/you/.local/state/ghostbranch/workspaces/default/plans/20261012T164001Z-e15904.json.
preserve: 2 public commit(s)
Publish it with: ghostbranch apply 20261012T164001Z-e15904
Next:
ghostbranch apply 20261012T164001Z-e15904 --yes
a person allowed agents to publish here; publish the plan

Here a person had allowed agents to publish, so next offers apply … --yes. Without that, it’s apply <id> (a person must run this).

The pull request’s text:

  • --title is needed when more than one commit is published. With one commit, the title is that commit’s subject.
  • --body sets the description. By default it’s the commit’s body, or one line per commit.
  • --branch names the public branch. By default it’s the feature’s name.

A plan is a JSON file on your machine. It records what will be published and what it was built from. Part of the one above:

20261012T164001Z-e15904.json (part)
{
"feature": "add-search",
"mode": "preserve",
"fingerprint": {
"public_main": "815a84b30f4a5526f386c64407a987d05e493932",
"feature_head": "09278daf2c364e1770327ce8df39e1e8d47f3e73",
"overlay": "dca70aa8d22be5d31da00e544c600aef86c1370a",
"gate_candidate": "74adada8d4b1ac89c7b8ef33065d355828eff3f5"
},
"publish": {
"target_branch": "add-search",
"pr": {
"title": "Add search to the docs",
"body": "- Add the search box\n- Add search results"
},
"commits": [
{
"commit": "37501135710d4246e88952e7ed6e965f02782c48",
"sources": ["893f3408d9cad3ce18bbd77685e8d2f5a6b0f6c5"],
"author": { "name": "you", "email": "12345+you@users.noreply.github.com" },
"subject": "Add the search box"
}
]
}
}

The fingerprint is what apply checks. Each public commit names its sources, the feature commits it was built from. It stays on the machine that made it, and on macOS and Linux only your user can read it.

ghostbranch apply <plan> takes the plan’s ID, or the path to its file. First it checks that nothing in the fingerprint has moved. Then it shows a summary of everything that will go public, and asks for a one-time code:

Terminal
ghostbranch apply 20261012T164001Z-e15904
About to publish to you/project:
branch add-search
history preserve, 2 commit(s) on 815a84b30f4a
37501135710d Add the search box (you <12345+you@users.noreply.github.com>)
57608c155d63 Add search results (you <12345+you@users.noreply.github.com>)
PR title Add search to the docs
PR body
- Add the search box
- Add search results
files 2 changed
src/components/SearchBox.astro
src/pages/search.astro
Plan 20261012T164001Z-e15904
To confirm, type this code and press Enter: K7QZM4
Leave it empty, or type n, to cancel.
> K7QZM4
Published add-search as PR #23: https://github.com/you/project/pull/23

The summary and the prompt above are the format Ghostbranch prints, filled in with the plan’s values and a sample code, not a captured run: a coding agent can’t be shown the code. The last line is what a real apply printed when it opened a pull request, with neutral names.

Read the summary before you type the code: it’s what the world will see. Any warning from the scan, such as WORKFLOW_CHANGED, is listed at the end.

ghostbranch promote <feature> without --plan does both steps in one go: it makes the plan, then asks for the code.

The summary is followed by one question, just above the code: what happens to the feature after it’s published. After publishing below explains it.

The code is shown on your terminal, and only a person at that terminal can type it back. It’s there so a coding agent can’t publish on its own: an agent that runs apply stops with APPROVAL_REQUIRED (exit 5), and hands the command to you.

If public main, the feature, the overlay branch or the gate’s candidate has moved since the plan was made, apply publishes nothing and exits with code 6:

Terminal
ghostbranch apply 20261012T173149Z-7dacc1
error: the repos changed since plan 20261012T173149Z-7dacc1 was made (feature_head); make a new plan with ghostbranch promote [PLAN_STALE]
Next:
ghostbranch promote add-search --mode preserve --plan --repo you/project
the repos moved since the plan was made; make a new plan

Here a commit was pushed to the feature after the plan was made, so feature_head moved. (An agent ran this apply with --yes, under a grant.) error.details.moved names what moved. Make a new plan with the next command and apply that one. If you typed a --title or --body for the old plan, next carries it over. A plan you never apply does nothing, so old ones are harmless.

If an apply is interrupted after it started pushing, run the same apply again: it finishes the publish. Don’t run promote again, which builds new commits: it refuses with BRANCH_EXISTS and points you back to that apply.

Once a pull request is public, keeping its feature private has done its job. So just above the code, apply asks what happens to the feature next:

Terminal
ghostbranch apply 20261014T142810Z-e6a232
two progress lines, then:
About to publish to you/project:
branch add-search
history preserve, 1 commit(s) on ec0c2b31243a
148ad21aec65 Add the search box (you <12345+you@users.noreply.github.com>)
PR title Add search to the docs
PR body (empty)
files 1 changed
src/components/SearchBox.astro
Plan 20261014T142810Z-e6a232
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
· commit 148ad21aec65 → you/project:add-search
✓ Published add-search to you/project (branch add-search) 2s
✓ Archived add-search at refs/ghostbranch/archive/add-search 2s
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
git fetch origin add-search
feature add-search is handed off: fix or change PR #35 on its branch add-search. In your checkout, fetch it from the public repo (origin is its remote there)
git switch --detach origin/add-search
work on the PR's branch as GitHub has it, never on the feature's old shadow branch: the guard refuses its private commits
git push origin HEAD:add-search
after committing the fix, push it to the PR; no gate, plan or apply

The person pressed Enter at the question, for Archive, the default, then typed the code. Type k instead for Keep. The commit’s subject, the title and the file are made neutral above; everything else is as the run printed it.

  • Archive hands the pull request off. The feature’s shadow branch moves to refs/ghostbranch/archive/<name> in the shadow repo, so nothing is lost, and its gate branch and draft pull request go. The feature’s state becomes archived. From then on, the pull request is the feature’s home: fix it or change it by pushing straight to its branch, with no gate, no leak scan, no plan and no code. What you push is public at once, so check it first.
  • Keep leaves everything as it was. Fixes go through the shadow branch, gate and promote, as in Updating an open PR.

To fix an archived feature’s pull request, start from the public branch, never the old shadow branch:

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

The push guard lets that push through, and still refuses the archive’s private commits.

After the hand-off, promote, gate, feature pull and waivers add stop with FEATURE_ARCHIVED, and next gives the push steps instead:

Terminal
ghostbranch gate add-search
error: feature add-search was handed off to the public repo: its shadow branch is archived at refs/ghostbranch/archive/add-search; fix or change PR #35 by pushing to its branch add-search, and ghostbranch sync closes the feature when it merges [FEATURE_ARCHIVED]
Next:
git fetch origin add-search
feature add-search is handed off: fix or change PR #35 on its branch add-search. In your checkout, fetch it from the public repo (origin is its remote there)
git switch --detach origin/add-search
work on the PR's branch as GitHub has it, never on the feature's old shadow branch: the guard refuses its private commits
git push origin HEAD:add-search
after committing the fix, push it to the PR; no gate, plan or apply
ghostbranch sync --repo you/project
when PR #35 merges, sync closes add-search and deletes its archive

Nothing can be built on an archived feature either, so feature start --from and feature restack --onto refuse it too:

Terminal
ghostbranch feature start search-ui --from add-search
error: feature add-search was handed off to the public repo: its shadow branch is archived at refs/ghostbranch/archive/add-search; fix or change PR #35 by pushing to its branch add-search, and ghostbranch sync closes the feature when it merges; nothing can be stacked on a handed-off feature: start from main, or wait until PR #35 merges and run ghostbranch sync [FEATURE_ARCHIVED]
Next:
ghostbranch feature start search-ui --repo you/project
nothing can be stacked on handed-off feature add-search: start from main instead

When the pull request merges, sync closes the feature and deletes its archive.

--keep on apply or promote chooses Keep up front. With --yes, under an agent grant, or with --non-interactive, nothing is asked: apply archives unless the command has --keep. data.after_publish in the result says what happened to each feature: archived, kept or incomplete.

A stack gets one choice for all its levels.

While another feature is built on this one, apply shows Archive as unavailable, says why, and makes Keep the default, because that feature still needs this one’s branch. Publish the two together, restack the other one onto main, or close it if it’s abandoned. Then hand the feature off later:

Terminal
ghostbranch feature archive add-search

ghostbranch feature archive <name> does what Archive does, for a feature whose pull request is open. Add --plan to see what it would change first. The same command finishes an archive that stopped partway: then apply warns ARCHIVE_INCOMPLETE, the publish still stands, and next gives this command. Every step is safe to run again.

If you’d rather not type a code for every publish, you can let agents on your machine publish to one repo for a while:

Terminal window
ghostbranch agents allow --for 6h

Making the grant asks for a code too, so an agent can’t grant itself. The choices are 1h, 6h (the default), 1d, 7d, 15d, 30d and 90d, or --until-revoked. Until it ends, an agent may run apply <id> --yes. Every publish still needs a green gate and a clean leak scan.

  • ghostbranch agents status shows the grant and how long it has left.
  • ghostbranch agents revoke ends it now.
  • A grant lives on the machine that made it, and covers only publishing: closing a feature or removing a pair still needs your code.

Working with a coding agent has the setup, including the extra allow rule Claude Code needs.

apply opens an ordinary pull request on your public repo, from a branch named after the feature into main. It’s reviewed and merged like any other.

--keep on apply or promote keeps the feature private after publishing, without asking (see above).

Merge it with a merge commit or a rebase. “Squash and merge” turns the commits preserve published into one, after all: see History modes. When it has merged, run ghostbranch sync, which closes the feature.

To change an open pull request after a review, push to its branch if you archived the feature, or commit to the feature and promote again if you kept it: see Updating an open PR.

APPROVAL_REQUIREDexit 5This needs a person: a code typed at a console. For promote and apply, an agent grant also works.
Causes
  • no_console: no terminal a person can type into
  • not_confirmed, timeout, too_many_attempts: the person did not confirm
  • grant_needs_yes: a grant exists, but --yes was not passed
What to do

Stop and give the human the command in next. It needs a person at a console; for promote and apply, an agent grant made with ghostbranch agents allow also works (feature close and remove ignore grants). Never fake a terminal, answer the code, edit grants.json or run ghostbranch agents allow yourself.

This needs a person.

PLAN_STALEexit 6The repos changed since the plan was made.
What to do

Make a new plan with ghostbranch promote <feature> --plan and apply that.

PLAN_NOT_FOUNDexit 2No plan file with this ID (error.details.plan) is on this machine (error.details.reason; error.details.public names the pair looked in, when one was found).
Causes
  • other_machine: the plan was made on another machine, or in another workspace or GHOSTBRANCH_HOME with the same shadow repo; the shadow repo has its ref, but plan files stay where they were made
  • unknown: no such plan here or in the shadow repo (a typo, or a plan for another shadow repo)
What to do

For other_machine, apply it on the machine that made it, or make a new plan here: next gives promote <error.details.feature> --plan when one feature's gate matches the plan. Otherwise run ghostbranch status and promote the feature you meant again.

MISSING_INPUTexit 2Required input is missing.
What to do

Pass the input named in error.field, for example --title or -m for promote.

NOTHING_TO_PUBLISHexit 1Nothing would be published (error.details.reason).
Causes
  • no_commits: the feature has no commits of its own yet
  • all_stripped: every commit is empty, or the commits cancel out, once overlay paths and internal sections are stripped (error.details.dropped_empty)
  • an update of an open PR that already holds every change (no reason)
What to do

For no_commits, commit on the feature branch, push it to the shadow repo, then gate. Otherwise there is nothing to do: the feature has no public changes.

BRANCH_EXISTSexit 4The name is taken: by a shadow branch, by a public branch Ghostbranch didn't publish for this feature, or by the branch of this feature's closed PR.
What to do

With error.details.plan_id the branch holds an earlier, interrupted publish of this feature: resume it with ghostbranch apply <plan_id> rather than promoting under another name. Otherwise use another feature name, or promote with --branch <other-name>. A feature whose own PR is open is never refused with this: promote updates that PR.

INTERRUPTEDexit 1The command was interrupted (Ctrl-C) before it finished.
What to do

With error.details.plan_id the plan is saved and the publish may have started: resume it with ghostbranch apply <plan_id> (never promote again; a person runs it unless an agent grant exists). With error.details.job the gate's CI is still running in the shadow repo: run ghostbranch wait <job>. With neither, nothing is pending: run the command again.

TOKEN_CANNOT_PUBLISHexit 1The token in use cannot push to the public repo.
Causes
  • the token is scoped to the shadow repo only (the strict-isolation setup)
  • the token lacks Contents: write on the public repo
What to do

A person publishes from their own terminal with a token that can write to the public repo (`ghostbranch apply <plan>`).

This needs a person.

FEATURE_ARCHIVEDexit 2This feature was handed off to the public repo (archived): its shadow branch is archived, so promote, gate, feature pull and waivers add no longer work on it, and feature start --from and feature restack --onto can't build on it (error.details.feature, error.details.public_pr, error.details.branch; error.details.pr_state says whether the PR is still open, merged or closed).
Causes
  • apply archived it after publishing (Archive is apply's default)
  • ghostbranch feature archive archived it
What to do

When feature start --from or feature restack --onto refused it (error.field from or onto), start from main or restack onto main instead, after ghostbranch sync if pr_state is merged. Otherwise, fix or change its PR by pushing to the PR's branch: git fetch origin <error.details.branch>, commit on origin/<error.details.branch> (never on the old shadow branch: the guard refuses its private commits), then git push origin HEAD:<error.details.branch>. When the PR merges, ghostbranch sync closes the feature; once it merged (pr_state merged) run sync, and once it was closed without merging (pr_state closed) a person closes the feature at the console. next gives these steps.

ARCHIVE_INCOMPLETEwarningThe publish succeeded, but handing a feature off (archiving its shadow branch) stopped partway (details.feature; details.step is push, gate_pr or record; details.code is the step's error).
What to do

Nothing is undone: the PR is public. Run ghostbranch feature archive <details.feature>: every step is safe to run again, and it archives the branch's head as it is then, so nothing is lost. Until it finishes, don't push to the PR's branch: the guard may still refuse it.