ghostbranch doctor
Check your machine, your token and your repo pair in one command, and get the command that fixes each problem.
When something doesn’t work and you’re not sure why, start here. ghostbranch doctor runs six checks, says which ones pass, and for each problem names the command that fixes it.
It doesn’t change your repos, your settings or your records, and it never asks you anything. It reads from GitHub, fetches into its local copy of the repos (as status does), and refreshes the update check’s cache.
Run it
Section titled “Run it”Run it in a clone of your public repo, and it checks that repo’s pair. Anywhere else, name the pair with --repo:
Each line starts with a mark for the check’s status:
A problem with a fix has a fix: line under it. Run that command, then run doctor again. Here the repo isn’t set up on this machine. Its fix adopts the shadow repo another machine made, and Next: also offers to create one: see Set up a repo. Read each --plan, then run it without --plan.
doctor exits with code 0 when nothing fails, warnings included. If any check fails, it exits with code 1 and the error DOCTOR_FAILED, and Next: lists each fix once, in check order.
With no pair to check, outside a clone and without --repo, doctor still checks your tools, token and state, and skips the rest:
Set up a repo shows a run where every check has a pair to look at.
The six checks
Section titled “The six checks”The checks run in this order, and each one reports, whatever the others found.
Git must run, and be version 2.38 or later. The line also names the Ghostbranch version and the commit it was built from. There’s no command to fix an old git: install a newer one and put it on your PATH.
Where the GitHub token comes from, whose it is, and what it may do. With a pair, it also checks whether the token can publish to the public repo, and which permissions each group of commands (init, sync, gate, promote, audit) is missing.
| Problem | Status | Fix |
|---|---|---|
| No token at all | fail | ghostbranch login, or set GHOSTBRANCH_TOKEN |
| The stored login expired, or GitHub refuses it | fail | ghostbranch login |
| The stored login can’t be read | fail | ghostbranch logout, then ghostbranch login |
| The token lacks permissions some commands need | fail | for the app, ghostbranch login, which shows its install page. For your own token, ghostbranch guide, whose token table names each permission; the same table is in Tokens and the keychain |
| The stored login ends within two weeks | warn | ghostbranch login before then |
| The token can’t publish to the public repo | warn | none needed: that’s right for an agent’s token, and a person publishes |
| The run looks like an agent’s, on your stored login | warn | give the agent its own token (see below) |
An app login’s access token lasts a few hours. If it has run out, doctor says so and doesn’t check it: the next command that uses GitHub refreshes it, and doctor writes nothing.
A locked keychain is a fail with no fix command: unlock it and run doctor again. Logging out would delete a login that works once the keychain is unlocked.
When an agent uses your login
Section titled “When an agent uses your login”When a run looks like a coding agent’s, and the token is your stored login, the token line warns:
doctor also prints it as the warning AGENT_ON_PERSON_LOGIN. Your login can publish, so an agent that uses it could too, under a grant. Strict isolation shows how to give the agent a token of its own.
Both repos of the pair: that the token can see them, that the shadow repo is private and not a fork, and that its ghostbranch overlay branch and .ghostbranch.yml are valid. Then whether shadow main has caught up with public main, and whether the files Ghostbranch manages in the shadow repo are current.
| Problem | Status | Fix |
|---|---|---|
| The repo isn’t set up on this machine | fail | ghostbranch init <public> --adopt --plan if its shadow repo exists, else ghostbranch init <public> --plan |
| The shadow repo is missing, or has no overlay branch | fail | ghostbranch init <public> |
| A repo doesn’t exist, or the token can’t see it | fail | give the token access to it |
| The shadow repo is public, or a fork | fail | make it private in its GitHub settings. A fork can’t be: create a private repo and adopt it |
.ghostbranch.yml isn’t valid |
fail | fix it on the overlay branch: see Configuration |
Shadow main is behind public main |
warn | ghostbranch sync --repo <public> |
| The managed overlay files are out of date | warn | ghostbranch upgrade --repo <public> |
| The public repo is private | warn | none: Ghostbranch is made for public repos |
Whether the push guard is installed in the checkout you’re in, and up to date. It’s skipped outside a clone of the public repo.
| Problem | Status | Fix |
|---|---|---|
| The guard isn’t installed | fail | ghostbranch guard install |
| Its hook is from another Ghostbranch version, misses a remote that points at the public repo, or checks another workspace | fail | ghostbranch guard install |
The ghostbranch binary it calls has moved |
warn | ghostbranch guard install |
You set GHOSTBRANCH_HOME in this shell |
warn | set it where git runs too: see Environment variables |
guard install is yours to run. An agent suggests it; it doesn’t run it.
This machine’s records: the workspace, agent grants, waivers, saved plans, and the features of the pair.
| Problem | Status | Fix |
|---|---|---|
| A record was written by a newer Ghostbranch | fail | upgrade Ghostbranch on this machine: see Upgrading |
| The grants file can’t be read | fail | ghostbranch agents revoke --all |
| A feature’s record can’t be read | fail | ghostbranch feature show <feature> to look at it |
| A saved plan can’t be read | warn | none: make a new plan |
| A gate job has been pending for over an hour | warn | ghostbranch wait <job> |
| Other users on the machine can open the state folders (macOS and Linux) | warn | chmod 700 on the folders it names. The next command that changes something makes them private by itself, except a GHOSTBRANCH_HOME you set |
| Other users on the machine can read the records (macOS and Linux) | warn | chmod 600 on the files it names |
GHOSTBRANCH_HOME is outside your user profile (Windows) |
warn | move it under your profile, or the icacls line it gives |
Ghostbranch keeps its state private: its folders are mode 0700 and its files 0600, on macOS and Linux. An install made before that has looser folders. The next Ghostbranch command that changes something makes them private, with one · line per folder, such as Made /home/you/.local/state/ghostbranch/workspaces private (0700). A GHOSTBRANCH_HOME you set yourself is never changed: you chose that folder, so doctor names the chmod 700 for you to run.
doctor itself only reports. It never changes a mode, even with a warning to show. When all is well, the state line ends its state directories are private to you.
On Windows, the default folder is in your profile, which only you can read. A GHOSTBRANCH_HOME outside your profile, such as on another drive, may take that drive’s looser permissions. doctor can’t read them, so it warns while the folder is outside your profile, and gives an icacls line that makes it yours alone.
A summary of ghostbranch audit: repo guards the gate rewrites, secrets the shadow repo lacks, and workflows that run on sync’s pushes. Each warning’s fix is ghostbranch audit --repo <public>, which shows the whole report. It’s skipped when the token can’t read the shadow repo’s CI settings.
The update check line
Section titled “The update check line”The last line says whether Ghostbranch looks for new releases, and what it last found. doctor looks there and then, waiting up to 3 seconds. See Upgrading Ghostbranch for every form of the line, and Updates, and no telemetry for what the look sends.
With –json
Section titled “With –json”ghostbranch doctor --json puts every check in data.checks, in order. Each has an id, a status, the message, the fix (empty when there’s none) and details. The update check is in data.update_check. When a check fails, error.details.failed names the failing checks, and next holds their fixes:
{ "ok": false, "exit_code": 1, "next": [ { "command": ["ghostbranch", "init", "you/other-project", "--adopt", "--plan", "--json"], "why": "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", "needs_human": false, "needs_input": [] }, { "command": ["ghostbranch", "init", "you/other-project", "--plan", "--json"], "why": "no shadow repo yet: create one", "needs_human": false, "needs_input": [] } ], "error": { "code": "DOCTOR_FAILED", "message": "1 check(s) failed: pair", "field": null, "retryable": false, "details": { "failed": ["pair"] } }}The JSON is cut to these four fields; the whole result also has data, warnings and the rest of the envelope. A next entry to log in, log out or install the guard has needs_human: true: those are a person’s to run.
Errors you may see
Section titled “Errors you may see”DOCTOR_FAILEDexit 1ghostbranch doctor found failing checks; error.details.failed lists their IDs.
- one of tools, token, pair, guard, state or ci failed; data.checks[].message says why
Run the fix each failing check names (data.checks[].fix, also listed in next), then run ghostbranch doctor again. Warnings don't fail doctor.
AGENT_ON_PERSON_LOGINwarningThis run looks like an agent or a script, and it is using a person's stored login, which can publish.
Give the agent its own shadow-only token in GHOSTBRANCH_TOKEN (see ghostbranch guide § Tokens). The person's login still needs the console code to publish.
AUTH_MISSINGexit 1No GitHub token was found.
- GHOSTBRANCH_TOKEN, GH_TOKEN and GITHUB_TOKEN are unset, there is no stored login (ghostbranch login), and `gh auth token` gave nothing
A person runs `ghostbranch login`, or sets GHOSTBRANCH_TOKEN. Agents and CI use a token of their own in GHOSTBRANCH_TOKEN; the permissions each command needs are in the token table of `ghostbranch guide`. `gh auth login` also works.
This needs a person.
LOGIN_EXPIREDexit 1The stored GitHub login has expired and can't be refreshed.
- its refresh token is past its six months
- the login was revoked on GitHub, or the Ghostbranch app was uninstalled or its authorization removed
A person runs ghostbranch login again. An agent stops and hands this to the person; an agent with a token of its own sets GHOSTBRANCH_TOKEN, which never uses the stored login.
This needs a person.
LOGIN_UNAVAILABLEexit 1Ghostbranch can't log in here, or can't read the stored login (error.details.reason says which).
- 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
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.
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).
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.