Skip to content
GhostbranchdocsGitHub

Common problems

The errors people hit most, what each one means, and how to fix it.

Every error Ghostbranch prints ends with its code in brackets, such as [LOCKED]. ghostbranch explain <code> says what the code means and what to do, with no token and no network. These are the codes people run into most.

If yours isn’t here, run ghostbranch doctor: it checks your whole setup and names each fix. Error and warning codes lists every code.

PAIR_NOT_FOUND: this repo isn’t set up here

Section titled “PAIR_NOT_FOUND: this repo isn’t set up here”

You named a repo, or ran a command in its clone, and it isn’t set up on this machine. It happens on a second machine, or with a different GHOSTBRANCH_HOME. Next: offers both ways to set it up:

Terminal
ghostbranch status --repo you/project
error: no repo "you/project" in workspace "default"; run ghostbranch init first [PAIR_NOT_FOUND]
Next:
ghostbranch init you/project --adopt --plan --json
its shadow repo already exists (set up on another machine, or in another GHOSTBRANCH_HOME): adopt it — this only registers the pair here and checks the shadow repo. If the shadow repo has a non-default name, adopt it with --shadow <owner/name> instead
ghostbranch init you/project --plan --json
no shadow repo yet: create one
  • The shadow repo already exists, because you set it up on another machine: adopt it with init --adopt. This is almost always the one.
  • There’s no shadow repo yet: init without --adopt creates one.
  • A typo in --repo: check the name. error.details.public names the repo it looked for.

Read the plan each --plan prints, then run the same command without --plan.

Terminal
ghostbranch explain PAIR_NOT_FOUND
PAIR_NOT_FOUND (error, exit 2): This repo isn't set up on this machine (in this workspace and GHOSTBRANCH_HOME); error.details.public names it.
cause: it was set up on another machine, or under another GHOSTBRANCH_HOME or workspace
cause: it was never set up
cause: a typo in --repo
what to do: When its shadow repo already exists, run ghostbranch init <public> --adopt --plan (it only registers the pair here and checks the shadow repo); otherwise ghostbranch init <public> --plan creates one. next lists both, adopt first.

Ghostbranch works on one pair of repos at a time, and can’t tell which one you mean. Outside a clone of the public repo, with more than one pair set up on this machine, you see:

Terminal
ghostbranch status
error: several repos in workspace "default"; pass --repo [PAIR_AMBIGUOUS]

With --json, error.details.candidates lists the pairs set up here. Run the command inside a clone of the public repo, or name the repo:

Terminal
ghostbranch status --repo you/project

The same code comes in a folder that is a git checkout, but not of a GitHub repo. A repo that isn’t set up on this machine at all is PAIR_NOT_FOUND instead.

Terminal
ghostbranch explain PAIR_AMBIGUOUS
PAIR_AMBIGUOUS (error, exit 2): Ghostbranch cannot tell which repo pair to use.
what to do: Pass --repo owner/name, or --repo . inside a checkout of the public repo.

The gate opens a pull request in the shadow repo and waits for its CI. If no check appears within 10 minutes, it stops with exit code 3. The message has this form (here with the values of a real run):

Terminal
the message's format, with a real run's values:
error: no checks appeared on shadow PR #3 within 10m0s; check that Actions is enabled in you/project-shadow, or no workflow triggers on pull_request [NO_CI]

error.details.reason says which case it is:

  • no_workflow: the code the gate tested has no workflow under .github/workflows at all, so nothing could start, and the message says so. Add CI to the public repo. If your CI is an outside app rather than GitHub Actions, install that app on the shadow repo too.
  • actions_disabled_or_untriggered: there are workflows, but none ran. Either Actions is turned off in the shadow repo (turn it on in its settings, under Actions), or no workflow runs on pull_request: add that trigger to one.
  • No reason: the gate couldn’t read its candidate, so it can’t tell which. Check both.

Then gate again. If your CI is just slow to start, raise gate.ci_appear_timeout in .ghostbranch.yml.

Terminal
ghostbranch explain NO_CI
NO_CI (error, exit 3): No CI checks appeared on the gate candidate (error.details.reason).
cause: no_workflow: the candidate has no workflow under .github/workflows, and no CI app posts checks
cause: actions_disabled_or_untriggered: Actions is disabled in the shadow repo, or no workflow triggers on pull_request
cause: no reason: the gate candidate couldn't be read, so which of the two holds is unknown
what to do: For no_workflow, add CI to the public repo, or, if CI is an external app, install it on the shadow repo. Otherwise enable Actions in the shadow repo or add a pull_request trigger. Then gate again.

TOKEN_PERMISSION_MISSING: the token can’t do this

Section titled “TOKEN_PERMISSION_MISSING: the token can’t do this”

GitHub refused something because the token lacks a permission. The error names the permission, the repo and the command:

Terminal
the message's format, with sample values:
error: the token lacks Workflows: write on you/project-shadow, which ghostbranch gate needs; a person grants it, and ghostbranch doctor lists every missing permission [TOKEN_PERMISSION_MISSING]

Run ghostbranch doctor: its token check lists everything the token lacks, for each group of commands, so you can fix them all at once. Then grant them:

  • With the Ghostbranch app, check that the app’s installation includes both repos. ghostbranch login prints its install page.
  • With a fine-grained token, add the permissions in the token’s settings on GitHub. Tokens and the keychain lists what each command needs.

Grant only what doctor names. An agent’s token in particular should stay unable to publish.

Terminal
ghostbranch explain TOKEN_PERMISSION_MISSING
TOKEN_PERMISSION_MISSING (error, exit 1): The token in use lacks a GitHub permission this command needs: error.details.permission on error.details.repo.
cause: a fine-grained token or the Ghostbranch app wasn't granted that permission, for example Workflows: write to change .github/workflows files
cause: the repo isn't in the token's or the app installation's repository access
cause: a classic or OAuth token lacks the scope named (OAuth scope: ...)
what to do: Run ghostbranch doctor: it lists every permission the token lacks, for each command. A person grants error.details.permission on error.details.repo (in the fine-grained token's settings, or the Ghostbranch app installation's repository access), then runs the command in error.details.command again. Never ask for wider access than doctor names.
a person must act

Ghostbranch runs one command that changes things at a time on each pair, on each machine. When another one holds the pair, you see which one:

Terminal
the message's format, with a real run's values:
ghostbranch sync
error: another ghostbranch operation (gate, pid 13096, since 2026-10-13T07:58:14Z) holds the lock /home/you/.local/state/ghostbranch/workspaces/default/locks/you__project.lock [LOCKED]
Next:
ghostbranch sync --repo you/project
another ghostbranch command (gate, pid 13096, since 2026-10-13T07:58:14Z) holds the lock; wait a few seconds for it to end, then run this again

A gate in another terminal held the lock. That run was on Windows, so the lock file’s path is shown as it would be on Linux. With --json, error.details.held_by has the same three parts: the command, its process ID and when it started.

LOCKED is retryable (error.retryable is true): wait a few seconds, then run the same command again, as next says. The other command may be in another terminal, or one a coding agent is running. When the lock was busy as a gate job tried to record its result, next is wait on that job instead.

A gate holds the lock while it prepares and pushes its candidate, and again for a moment when CI finishes, to record the result. While it waits on CI, other commands run as usual.

The lock frees itself when that command ends, even if it crashed or you closed its window. There’s no lock file to delete.

Terminal
ghostbranch explain LOCKED
LOCKED (error, exit 1): Another ghostbranch operation on this pair is running on this machine (error.details.held_by names it).
what to do: Wait a few seconds and run the command again (error.retryable is true; next repeats it, or waits on error.details.job). The lock frees itself when that process ends, even if it crashed; there is nothing to delete.

Public main has moved on, and the shadow repo hasn’t caught up. feature start refuses to start a feature on old code; other commands only warn. Run ghostbranch sync, then start the feature again. Sync shows the message and when to sync.

Terminal
ghostbranch explain SHADOW_BEHIND
SHADOW_BEHIND (error, exit 4): Shadow main is behind public main, so feature start refused to start a feature from it; nothing was created.
what to do: Run ghostbranch sync --repo <public>, then start the feature again.
SHADOW_BEHIND (warning): Shadow main is behind public main.
what to do: Run ghostbranch sync --repo <public> (fast-forward only). The gate is unaffected: it always rebases on public main.

Something moved between promote --plan and apply: public main, the feature, the overlay branch or the gate. apply published nothing. Make a new plan with the command in Next:, and apply that one. When the plan is stale shows a real run.

Terminal
ghostbranch explain PLAN_STALE
PLAN_STALE (error, exit 6): The repos changed since the plan was made.
what to do: Make a new plan with ghostbranch promote <feature> --plan and apply that.

apply was given a plan ID, but there’s no plan file with that ID on this machine. apply looks in the shadow repo to tell you why, and error.details.reason says what it found:

  • other_machine: the plan was made on another machine, or under another GHOSTBRANCH_HOME. Plan files stay where they were made. Apply it there, or make a new plan here. See Plans stay where they were made.
  • unknown: neither this machine nor the shadow repo knows it. It’s usually a typo in the ID:
Terminal
ghostbranch apply 20990101T000000Z-abcdef
error: no plan 20990101T000000Z-abcdef on this machine or in you/project-shadow [PLAN_NOT_FOUND]
Next:
ghostbranch status --repo you/project
see the features, then plan the one you meant again with promote <feature> --plan

An ID that isn’t in the plan ID format at all is BAD_FLAG, not this.

Terminal
ghostbranch explain PLAN_NOT_FOUND
PLAN_NOT_FOUND (error, exit 2): No 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).
cause: 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
cause: 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.

NO_OPEN_PR: feature pull needs an open pull request

Section titled “NO_OPEN_PR: feature pull needs an open pull request”

feature pull brings commits made on GitHub into your feature. It needs the feature’s open pull request. When there isn’t one, it stops, and error.details.reason says why. Here the feature was never published:

Terminal
ghostbranch feature pull add-search
error: feature pull needs a feature's open public PR, and add-search has none: it was never published [NO_OPEN_PR]
Next:
ghostbranch promote add-search --plan --repo you/project
nothing of add-search is public yet; publish it first

The reasons, and what next offers for each:

  • not_published: publish it first, with promote --plan.
  • merged: its pull request merged, and sync closes the feature.
  • closed or branch_gone: its pull request was closed without merging, or its public branch is gone. A person decides whether to promote it again, as a new pull request, or close the feature.

feature archive needs an open pull request too, and stops the same way.

Terminal
ghostbranch explain NO_OPEN_PR
NO_OPEN_PR (error, exit 2): feature pull and feature archive need a feature's open public PR, and this feature (error.details.feature) has none (error.details.reason; error.details.public_pr once it was published).
cause: not_published: it was never published
cause: merged: its PR merged
cause: closed: its PR was closed without merging
cause: branch_gone: its public branch is gone
what to do: Follow next: for not_published, promote <feature> --plan publishes it; for merged, sync closes the feature; for closed or branch_gone, a person decides whether to promote again (a new PR) or close the feature.

FEATURE_ARCHIVED: the feature was handed off

Section titled “FEATURE_ARCHIVED: the feature was handed off”

After publishing, apply archives the feature unless you chose Keep. Its pull request is then the feature’s home, so the commands that work through the shadow repo stop with FEATURE_ARCHIVED: promote, gate, feature pull and waivers add. So do feature start --from and feature restack --onto, because nothing can be built on an archived feature: start from main, or restack onto main, instead.

To change its pull request, push to the pull request’s branch. next gives the steps, with the branch from error.details.branch:

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

Commit on origin/add-search, never on the old shadow branch: the pre-push guard refuses its private commits. Promote and approval explains Archive and Keep.

Terminal
ghostbranch explain FEATURE_ARCHIVED
FEATURE_ARCHIVED (error, exit 2): This 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).
cause: apply archived it after publishing (Archive is apply's default)
cause: 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.

UPGRADE_NEEDED: a newer Ghostbranch wrote this

Section titled “UPGRADE_NEEDED: a newer Ghostbranch wrote this”

A record or the settings were written by a newer version of Ghostbranch than the one you’re running. It happens most when you work on two machines and upgraded only one. Upgrade this machine’s copy: see Upgrading.

When error.details.unknown_key is set, it’s a key in .ghostbranch.yml this version doesn’t know. If no newer version wrote it, it’s a typo, or a section left empty: fix it on the overlay branch (see Keys Ghostbranch doesn’t know). Don’t edit records to make an older version accept them.

Terminal
ghostbranch explain UPGRADE_NEEDED
UPGRADE_NEEDED (error, exit 1): Something 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.

Publishing asks you to type a code at a console, and ghostbranch login needs one too. Git Bash’s window (mintty) connects programs through pipes, not a Windows console, so Ghostbranch finds no console there and stops:

Git Bash
the message's format, with a sample plan ID:
ghostbranch apply 20261013T101500Z-3f9a2c
error: publishing needs a person to type a code at a console, or an agent grant (ghostbranch agents allow); plan 20261013T101500Z-3f9a2c is saved [APPROVAL_REQUIRED]

error.details.reason is no_console. Nothing was published, and the plan is still saved. Run the same command in PowerShell, Command Prompt or Windows Terminal, and type the code there. ghostbranch login stops the same way in Git Bash, with LOGIN_UNAVAILABLE: log in from one of those too, or pipe a token to ghostbranch login --with-token, as the error says.

You can keep using Git Bash for git and for the commands that don’t ask you anything.

Terminal
ghostbranch explain APPROVAL_REQUIRED
APPROVAL_REQUIRED (error, exit 5): This needs a person: a code typed at a console. For promote and apply, an agent grant also works.
cause: no_console: no terminal a person can type into
cause: not_confirmed, timeout, too_many_attempts: the person did not confirm
cause: 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.
a person must act
PAIR_NOT_FOUNDexit 2This repo isn't set up on this machine (in this workspace and GHOSTBRANCH_HOME); error.details.public names it.
Causes
  • it was set up on another machine, or under another GHOSTBRANCH_HOME or workspace
  • it was never set up
  • a typo in --repo
What to do

When its shadow repo already exists, run ghostbranch init <public> --adopt --plan (it only registers the pair here and checks the shadow repo); otherwise ghostbranch init <public> --plan creates one. next lists both, adopt first.

PAIR_AMBIGUOUSexit 2Ghostbranch cannot tell which repo pair to use.
What to do

Pass --repo owner/name, or --repo . inside a checkout of the public repo.

NO_CIexit 3No CI checks appeared on the gate candidate (error.details.reason).
Causes
  • no_workflow: the candidate has no workflow under .github/workflows, and no CI app posts checks
  • actions_disabled_or_untriggered: Actions is disabled in the shadow repo, or no workflow triggers on pull_request
  • no reason: the gate candidate couldn't be read, so which of the two holds is unknown
What to do

For no_workflow, add CI to the public repo, or, if CI is an external app, install it on the shadow repo. Otherwise enable Actions in the shadow repo or add a pull_request trigger. Then gate again.

TOKEN_PERMISSION_MISSINGexit 1The token in use lacks a GitHub permission this command needs: error.details.permission on error.details.repo.
Causes
  • a fine-grained token or the Ghostbranch app wasn't granted that permission, for example Workflows: write to change .github/workflows files
  • the repo isn't in the token's or the app installation's repository access
  • a classic or OAuth token lacks the scope named (OAuth scope: ...)
What to do

Run ghostbranch doctor: it lists every permission the token lacks, for each command. A person grants error.details.permission on error.details.repo (in the fine-grained token's settings, or the Ghostbranch app installation's repository access), then runs the command in error.details.command again. Never ask for wider access than doctor names.

This needs a person.

LOCKEDexit 1Another ghostbranch operation on this pair is running on this machine (error.details.held_by names it).
What to do

Wait a few seconds and run the command again (error.retryable is true; next repeats it, or waits on error.details.job). The lock frees itself when that process ends, even if it crashed; there is nothing to delete.

SHADOW_BEHINDexit 4Shadow main is behind public main, so feature start refused to start a feature from it; nothing was created.
What to do

Run ghostbranch sync --repo <public>, then start the feature again.

SHADOW_BEHINDwarningShadow main is behind public main.
What to do

Run ghostbranch sync --repo <public> (fast-forward only). The gate is unaffected: it always rebases on public main.

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.

NO_OPEN_PRexit 2feature pull and feature archive need a feature's open public PR, and this feature (error.details.feature) has none (error.details.reason; error.details.public_pr once it was published).
Causes
  • not_published: it was never published
  • merged: its PR merged
  • closed: its PR was closed without merging
  • branch_gone: its public branch is gone
What to do

Follow next: for not_published, promote <feature> --plan publishes it; for merged, sync closes the feature; for closed or branch_gone, a person decides whether to promote again (a new PR) or close the feature.

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.

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.

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.

LOGIN_UNAVAILABLEexit 1Ghostbranch can't log in here, or can't read the stored login (error.details.reason says which).
Causes
  • no_console: no terminal a person can use for the device flow; ghostbranch login --with-token reads a pasted token from stdin instead
  • no_keychain: this machine has no usable OS keychain (for example Linux without a Secret Service), and Ghostbranch never stores a token in a plain file
  • no_client_id: this build of Ghostbranch has no GitHub App client ID
  • denied, expired_code: the person denied the request on GitHub, or the code expired before they approved it
  • corrupt: the stored login can't be read
  • keychain_error: the OS keychain exists but refused (for example, it is locked)
  • refused: GitHub refused the login, for example because the app has device flow turned off
  • refresh_failed: GitHub refused to refresh the stored login for a reason other than its expiry
What to do

Read error.details.reason. For no_console or no_keychain, set GHOSTBRANCH_TOKEN to a token instead; agents and CI always do. For no_client_id, use ghostbranch login --with-token or GHOSTBRANCH_TOKEN. For denied or expired_code, run ghostbranch login again and approve the code on GitHub. For corrupt, a person runs ghostbranch logout, then ghostbranch login. For keychain_error, unlock the keychain or set GHOSTBRANCH_TOKEN. For refused or refresh_failed, run ghostbranch login again.

This needs a person.