Skip to content
GhostbranchdocsGitHub

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.

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.

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.

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.

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:

  1. 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 sync to cancel the runs it starts.
  2. Set it as GHOSTBRANCH_TOKEN only in the agent’s environment. With it set, Ghostbranch never opens your keychain.
  3. Make sure the agent’s sandbox can’t read your keychain or the GitHub CLI’s login. Otherwise gh auth token hands it your own token, which can publish.
  4. Check it from the agent’s environment: ghostbranch agents status --json shows "token_can_publish": "false", and ghostbranch doctor says 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.

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.

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.

TOKEN_CANNOT_PUBLISHexit 1The token in use cannot push to the public repo.
Causes
  • the token is scoped to the shadow repo only (the strict-isolation setup)
  • the token lacks Contents: write on the public repo
What to do

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