Tokens and the keychain
How the GitHub App login works and where it's kept, which permissions each command needs, and how to keep an agent from publishing.
Ghostbranch needs a GitHub token to work with your repos. You can log in with its GitHub App, or give it a token of your own. Log in shows the commands. This page is about what each kind of token can do, where it’s kept, and how to give a coding agent less.
The GitHub App login
Section titled “The GitHub App login”ghostbranch login uses GitHub’s device login: Ghostbranch shows a code, you enter it on GitHub’s own page and approve. Ghostbranch never sees your password. It gets back a token that acts as you, but only through the Ghostbranch app.
When you install the app, you pick the repos it may use: your public repo and its shadow repo. The token reaches those repos only, and on them it can do only what both you and the app may do. The app asks for these repository permissions:
| Permission | What Ghostbranch uses it for |
|---|---|
| Contents: write | Push branches to the shadow repo, and publish to the public repo |
| Pull requests: write | Open the gate’s pull request in the shadow repo, and the public pull request |
| Workflows: write | Push branches that change files in .github/workflows |
| Administration: write | Create the shadow repo and set it up, in init |
| Actions: write | Cancel the workflow runs that sync’s pushes start |
| Commit statuses: read | Read the CI results on the gate’s pull request |
| Metadata: read | Read the repos at all; every GitHub app has it |
It doesn’t ask for Secrets, so ghostbranch audit can’t compare your repos’ secret names with an app login. Use a token with Secrets: read for that.
The token lasts a few hours. When it’s about to run out, Ghostbranch refreshes it, and the login keeps refreshing for about six months. ghostbranch doctor warns two weeks before the login ends; then you run ghostbranch login again.
Logging out deletes the stored login, but the app keeps its access at GitHub until you revoke it there. ghostbranch logout prints the page where you do that.
Where the login is kept
Section titled “Where the login is kept”ghostbranch login, and ghostbranch login --with-token for a token of your own, store the token in your operating system’s keychain: the macOS Keychain, Windows Credential Manager, or a Secret Service keyring on Linux, such as GNOME Keyring. It’s one entry, named ghostbranch, for the account github.com.
There’s no fallback to a file. On a machine with no keychain, such as a server with no desktop session, login stops with LOGIN_UNAVAILABLE. Set GHOSTBRANCH_TOKEN there instead.
If the keychain is locked, or the stored login can’t be read, Ghostbranch stops and says so. It never falls back quietly to another token, so you always know which one is in use. ghostbranch auth status shows it, and Which token wins gives the order Ghostbranch looks in.
Which permissions a token needs
Section titled “Which permissions a token needs”Any token can read a public repo. So for a public repo, you grant permissions on the shadow repo only, and add the public repo just for the person who publishes.
With a fine-grained token, these are the permissions per command:
| Command | Permissions |
|---|---|
init, creating the shadow repo |
Administration: write on all repositories of the owner, because a token can’t name a repo that doesn’t exist yet. Plus the init --adopt set. |
init --adopt, with an empty private repo you made first |
Shadow: Administration, Contents and Workflows, read and write |
sync, upgrade |
Shadow: Contents and Workflows, write. Actions: write is optional: it lets sync cancel the runs its pushes start. |
feature start, gate |
Shadow: Contents, Pull requests and Workflows, write. Actions and Commit statuses, read. |
promote, apply |
Public: Contents and Pull requests, write. These are yours, never an agent’s. |
audit |
Contents, read. Secrets: read is optional, to compare secret names. |
ghostbranch doctor checks the token against this table, and lists anything missing for each group of commands. A command that hits a missing permission stops with TOKEN_PERMISSION_MISSING, naming the permission and the repo.
A classic token, or the GitHub CLI’s own token, works too. It needs the repo scope, because the shadow repo is private, and the workflow scope, and it reaches every repo you can. A fine-grained token can be held to the repos and permissions above, so prefer one.
Strict isolation
Section titled “Strict isolation”The console code, agent grants and the push guard stop mistakes. They don’t stop a determined agent running as your user: it could fake a terminal, edit Ghostbranch’s files, or push with git directly. The real boundary is the token. To make sure only you can publish:
- Make a fine-grained token for the shadow repo only, with Contents, Pull requests and Workflows read and write, and Actions and Commit statuses read. Add Actions write if you want
syncto cancel the runs it starts. - Set it as
GHOSTBRANCH_TOKENonly in the agent’s environment. With it set, Ghostbranch never opens your keychain. - Make sure the agent’s sandbox can’t read your keychain or the GitHub CLI’s login. Otherwise
gh auth tokenhands it your own token, which can publish. - Check it from the agent’s environment:
ghostbranch agents status --jsonshows"token_can_publish": "false", andghostbranch doctorsays the token can’t publish to the public repo.
Then you publish, by running ghostbranch apply <plan> in your own terminal, with your own login. Letting agents publish has the same steps from the agent’s side.
Errors you may see
Section titled “Errors you may see”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.
TOKEN_PERMISSION_MISSINGexit 1The token in use lacks a GitHub permission this command needs: error.details.permission on error.details.repo.
- 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: ...)
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.
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.
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.