Skip to content
GhostbranchdocsGitHub

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 in a clone of your public repo, and it checks that repo’s pair. Anywhere else, name the pair with --repo:

Terminal
ghostbranch doctor --repo you/other-project
✓ tools git 2.43.0; ghostbranch v0.0.0-20261014135732-03a0aa5dc864, commit 03a0aa5dc864
✓ token the fine-grained token from GHOSTBRANCH_TOKEN works, as you; permissions not checked: no pair (run doctor in a checkout of the public repo, or pass --repo)
✗ pair you/other-project isn't set up in workspace "default": ghostbranch init you/other-project --adopt --plan if its shadow repo exists, else ghostbranch init you/other-project --plan
fix: ghostbranch init you/other-project --adopt --plan
– guard no pair, so no public repo to guard (see pair)
✓ state workspace "default"; the records are readable, none is newer than this binary, and no gate job has been pending for over an hour (features not checked: no readable pair); its state directories are private to you
– ci no readable pair to audit (see pair)
update check: off (development build)
error: 1 check(s) failed: pair [DOCTOR_FAILED]
Next:
ghostbranch init you/other-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/other-project --plan --json
no shadow repo yet: create one

Each line starts with a mark for the check’s status:

The marks
✓ ok nothing wrong
! warn worth fixing, but nothing is stopped
✗ fail something a command needs is missing or broken
– skipped the check couldn't run, and the line says why

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:

Terminal
ghostbranch doctor
tools and token as above, then:
– pair not in a checkout of a registered public repo; pass --repo <owner/repo> to check a pair
– guard no pair, so no public repo to guard (see pair)
state and ci as above; the exit code is 0

Set up a repo shows a run where every check has a pair to look at.

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 a run looks like a coding agent’s, and the token is your stored login, the token line warns:

Terminal
! token the app token from login works, as you; this looks like an agent's session (GHOSTBRANCH_ACTOR names an agent, a coding agent's marker variable is set, or no person is at a console), and it is using the person's stored login, which can publish; give the agent its own token, reaching only the shadow repo, in GHOSTBRANCH_TOKEN (see "Tokens" in ghostbranch guide)

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

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:

ghostbranch doctor --repo you/other-project --json (part)
{
"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.

DOCTOR_FAILEDexit 1ghostbranch doctor found failing checks; error.details.failed lists their IDs.
Causes
  • one of tools, token, pair, guard, state or ci failed; data.checks[].message says why
What to do

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.
What to do

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.
Causes
  • GHOSTBRANCH_TOKEN, GH_TOKEN and GITHUB_TOKEN are unset, there is no stored login (ghostbranch login), and `gh auth token` gave nothing
What to do

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.
Causes
  • its refresh token is past its six months
  • the login was revoked on GitHub, or the Ghostbranch app was uninstalled or its authorization removed
What to do

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

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.