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.
Make a plan
Section titled “Make a plan”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.
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:
--titleis needed when more than one commit is published. With one commit, the title is that commit’s subject.--bodysets the description. By default it’s the commit’s body, or one line per commit.--branchnames the public branch. By default it’s the feature’s name.
The plan file
Section titled “The plan file”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:
{ "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.
Apply it
Section titled “Apply 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:
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 console code
Section titled “The console code”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.
When the plan is stale
Section titled “When the plan is stale”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:
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.
After publishing: Archive or Keep
Section titled “After publishing: Archive or Keep”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:
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 becomesarchived. 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,
gateandpromote, as in Updating an open PR.
To fix an archived feature’s pull request, start from the public branch, never the old shadow branch:
git fetch origin add-searchgit switch --detach origin/add-search# commit your fix heregit push origin HEAD:add-searchThe 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:
Nothing can be built on an archived feature either, so feature start --from and feature restack --onto refuse it too:
When the pull request merges, sync closes the feature and deletes its archive.
Without a console
Section titled “Without a console”--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.
Archive later
Section titled “Archive later”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:
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.
Grants: letting an agent publish
Section titled “Grants: letting an agent publish”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:
ghostbranch agents allow --for 6hMaking 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 statusshows the grant and how long it has left.ghostbranch agents revokeends 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.
The pull request
Section titled “The pull request”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.
Errors you may see
Section titled “Errors you may see”APPROVAL_REQUIREDexit 5This needs a person: a code typed at a console. For promote and apply, an agent grant also works.
- 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
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.
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).
- 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)
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.
Pass the input named in error.field, for example --title or -m for promote.
NOTHING_TO_PUBLISHexit 1Nothing would be published (error.details.reason).
- 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)
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.
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.
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.
- the token is scoped to the shadow repo only (the strict-isolation setup)
- the token lacks Contents: write on the public repo
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).
- apply archived it after publishing (Archive is apply's default)
- ghostbranch feature archive archived it
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).
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.