Skip to content
GhostbranchdocsGitHub

Letting agents publish

Give agents a time-limited grant to publish one repo from one machine, see what it covers, and set up the stricter option where only you can publish.

By default an agent stops at the publish and hands it to you. If you’d rather it finished the job on its own for a while, you can give it a grant: your permission, made at your terminal, for agents to publish one repo from one machine. This page covers how a grant works, what it leaves unchanged, and the stricter setup in which an agent can’t publish at all.

promote and apply ask the same questions before anything goes public, and the first rule that fits decides:

  1. There’s a grant for this repo on this machine, and the command has --yes: it publishes, with no code.
  2. Someone is at a terminal: it shows what would go public and asks for a one-time code. That holds even with a grant, if --yes wasn’t passed.
  3. There’s a grant, but no --yes, and no terminal: it stops with APPROVAL_REQUIRED and the reason grant_needs_yes. next gives the same command with --yes.
  4. Otherwise: it stops with APPROVAL_REQUIRED, and next hands the command to a person.

Two things never count. --yes on its own never skips the code. And GHOSTBRANCH_ACTOR is never read: calling yourself human: approves nothing.

A person runs agents allow at a terminal and types the code it shows. An agent can’t make a grant for itself: without a terminal, the command stops with APPROVAL_REQUIRED. Working with a coding agent shows the whole exchange. --plan previews the grant without making it:

Terminal
ghostbranch agents allow --repo you/project --for 7d --plan
Dry run; nothing was changed. This would:
- allow agents to publish to you/project until 2026-10-20T04:25:01Z
ghostbranch agents allow --repo you/project --until-revoked --plan
Dry run; nothing was changed. This would:
- allow agents to publish to you/project until revoked
  • How long: --for takes 1h, 6h, 1d, 7d, 15d, 30d or 90d. Without it, a grant lasts 6 hours. --until-revoked never expires, and you can’t pass it with --for. Any other length is refused:

    Terminal
    ghostbranch agents allow --repo you/project --for 2d --plan
    error: --for must be 1h, 6h, 1d, 7d, 15d, 30d or 90d, or pass --until-revoked; got "2d" [BAD_FLAG]
  • One repo pair, one machine. The grant lives in a file on the machine where you made it. Your other machine needs its own.

  • One grant per pair. A new grant for the same pair replaces the old one, and the ledger records it as grant_replaced. --plan doesn’t mention a grant it would replace, so look at agents status first.

Pick the shortest length that covers the work. A grant for an afternoon’s session is safer than one that’s still in force next month.

ghostbranch agents status lists the grants on this machine, and checks whether the token in use could push to the public repo:

Terminal
ghostbranch agents status --repo you/project
on stderr:
warning: 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)
on stdout:
you/project: agents may publish for 167h55m29s more
The app token from login can publish to you/project: true

That was an agent’s session, using a person’s login, a few minutes after the person made a 7-day grant. The time left is in hours, minutes and seconds. An expired grant shows as expired, and one with no end as agents may publish, until revoked. With --json:

agents status --repo you/project --json (data)
{
"pair": "you/project",
"grants": [
{
"id": "g-fd7237",
"public": "you/project",
"shadow": "you/project-shadow",
"created_at": "2026-10-13T04:17:35Z",
"expires_at": "2026-10-20T04:17:35Z",
"created_by": "human:you",
"expired": false,
"remaining_seconds": 604528
}
],
"token_can_publish": "true",
"token_kind": "app",
"token": { "source": "login", "http_status": 200 }
}
  • expires_at and remaining_seconds are null for a grant that runs until revoked.
  • token_can_publish is a string: "true", "false", or "unknown" when GitHub didn’t say.
  • token_kind is app (from ghostbranch login), fine-grained, classic, oauth or unknown. token.source says where the token came from, such as login or GHOSTBRANCH_TOKEN.

Revoking needs no code: anyone can take power away. --plan shows what it would do:

Terminal
ghostbranch agents revoke --repo you/project --plan
Dry run; nothing was changed. This would:
- revoke the agent grant for you/project

Without --plan it prints Agents may no longer publish to you/project., or There was no agent grant to revoke. if there wasn’t one. Those two lines are the binary’s own text, not a capture. --all revokes every grant in this workspace, and --all --repo you/project every grant for that repo.

A grant replaces the code, and nothing else:

  • Every publish still needs a green gate and a clean leak scan. apply checks that the repos haven’t moved since the plan, and scans exactly what it would publish, before it asks for any approval.
  • It covers promote and apply only. Closing a feature that would lose work, removing a repo pair, and waiving a leak finding still need a person’s code. A grant doesn’t help there, so an agent shouldn’t ask you for one to get past them.
  • It doesn’t give the agent’s token any power it lacks. If the token can’t push to the public repo, the publish fails with TOKEN_CANNOT_PUBLISH, grant or not.
  • It leaves a record. The apply result and the ledger say the publish was approved by a grant, with the grant’s ID and who made it.

At a console, apply asks a person whether to archive the feature after publishing or keep it (see After publishing: Archive or Keep). With --yes or --non-interactive nothing is asked: apply archives every feature it published, unless the command has --keep. A stack gets one choice for all its levels.

The result says what happened to each feature, in data.after_publish:

apply --yes --json (data, part)
"after_publish": { "add-search": "archived" }

Each value is archived, kept or incomplete. incomplete means the publish stands but the archive stopped partway: a warning ARCHIVE_INCOMPLETE says where, and next gives ghostbranch feature archive add-search to finish.

Once a feature is archived, fixes to its pull request are a plain git push origin HEAD:<branch>, as next says: no gate, no plan, no approval, and no leak scan. git sends that push with your checkout’s own credentials for the public repo, not with GHOSTBRANCH_TOKEN. So an agent kept to a shadow-only token (see Strict isolation) can’t make it. If the agent should keep working through the gate, have it pass --keep.

Claude Code asks you before it runs a command it hasn’t been allowed to run. It may also refuse to run ghostbranch apply … --yes on its own, as a step that approves itself. If you’ve made a grant and want the agent to publish under it, allow the command. For example, to let it run the loop without asking each time:

.claude/settings.local.json
{
"permissions": {
"allow": [
"Bash(ghostbranch sync:*)",
"Bash(ghostbranch status:*)",
"Bash(ghostbranch feature start:*)",
"Bash(ghostbranch gate:*)",
"Bash(ghostbranch wait:*)",
"Bash(ghostbranch promote:*)",
"Bash(ghostbranch apply:*)"
]
}
}

Each rule lets one subcommand run without a prompt. Leave out the apply line until you’ve made a grant; without one, the agent stops at the publish anyway. Add others only when you see the agent stop for them. Set GHOSTBRANCH_ACTOR with env, as in How agents use Ghostbranch, so the agent runs bare ghostbranch … commands that these rules match.

  • Use .claude/settings.local.json, which is yours, or your user settings, never the shared .claude/settings.json. A shared settings file is meant to be committed, so it would go public with your next promote, and would show that you keep a shadow repo.
  • Keep .claude/settings.local.json out of your commits too. Check that git ignores it: git check-ignore .claude/settings.local.json prints the path if it does. If it prints nothing, add the path to your .gitignore, or to .git/info/exclude, which is never committed. Your user settings live outside the repo, so they need no check.
  • The allow-list isn’t the boundary. It only decides whether Claude Code asks you first. An allowed apply --yes still stops with APPROVAL_REQUIRED if there’s no grant, and still fails if the token can’t publish. Leaving a command off the list doesn’t stop an agent that finds another way to run it. The grant and the token decide what can go public.

The code, grants and the push guard stop an agent’s mistakes. They don’t stop an agent that’s set on getting round them. It runs as you, on your machine, so it could fake a terminal, edit the grants file, or push with git directly. On macOS and Linux, Ghostbranch’s files are readable only by you, but the agent is you, as far as your computer is concerned.

The real boundary is the token. Give the agent a token that can’t write to the public repo, and keep it away from your own tokens, and nothing it does on your machine can publish:

  1. Create a fine-grained token that reaches only the shadow repo, with Contents, Pull requests and Workflows set to read and write, and Actions and Commit statuses set to read. Actions write is optional: it lets sync cancel the workflow runs its pushes start.

  2. Set it as GHOSTBRANCH_TOKEN in the agent’s environment only. With it set, Ghostbranch never reads your stored login.

  3. Keep the agent away from your other tokens. If it can run gh auth token or read your keychain, it can get your own token, which can publish.

  4. Check it in the agent’s environment. ghostbranch agents status should say the token can’t publish:

    Terminal
    ghostbranch agents status --repo you/project
    No agent grants on this machine.
    The fine-grained token from GHOSTBRANCH_TOKEN can publish to you/project: false

    These two lines are built from Ghostbranch’s source, for a machine with no grant. ghostbranch doctor says the same in its token check.

Any token can read a public repo, so this token only needs the shadow repo. That’s also its limit: if your “public” repo is still private, the token must select it too. A fine-grained token gets the same permissions on every repo it selects, so with write access to the shadow repo it could write your public repo as well. You can’t give an agent one safely.

With strict isolation, the agent does everything up to promote --plan. Then you run ghostbranch apply <plan-id> in your own terminal on the same machine, where Ghostbranch uses your login, and type the code. A grant doesn’t help the agent here: it can approve, but its token still can’t push.

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.

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.

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.

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.