Skip to content
GhostbranchdocsGitHub

JSON output and next

The one JSON shape every command prints, the exit codes, and how the next list tells an agent what to run and when to stop.

Add --json to any command and it prints one JSON object on stdout, always with the same eight fields. An agent, or a script of yours, never has to read the human text. It acts on the exit code, the error code and the next list.

This is what feature start printed for a new feature:

feature start add-search --json
{
"schema_version": "0.15",
"command": "feature start",
"ok": true,
"exit_code": 0,
"data": {
"name": "add-search",
"branch": "add-search",
"base": "ec0c2b31243a8187ba7129d311bd390c4ba711ee",
"parent": "main",
"checkout": [
["git", "fetch", "shadow"],
["git", "switch", "add-search"]
],
"checkout_hint": "git fetch shadow && git switch add-search",
"preview": null
},
"warnings": [],
"next": [
{
"command": ["ghostbranch", "gate", "add-search", "--repo", "you/project"],
"why": "check out with `git fetch shadow && git switch add-search`, commit on add-search, push it with `git push shadow add-search` (the shadow repo, never origin), then gate it",
"needs_human": false,
"needs_input": []
}
],
"error": null
}
Field What it holds
schema_version The version of this JSON shape. It changes when the shape does.
command The command that ran, such as feature start.
ok true if the command did what it was asked.
exit_code The same number the process exits with.
data The command’s result. Its fields depend on the command. On an error it is usually {}.
warnings Things worth knowing that didn’t stop the command. Always a list, empty when there are none.
next The commands to run now. Always a list, empty when there’s nothing to do.
error null on success. On a failure, the code and its details.

The eight fields are always there, and the lists are never null, so a script can read them without checking first.

Ghostbranch is in beta, so the shape may still change between releases. Check schema_version if your script depends on it.

Here an agent ran wait with a job ID for a feature that had no gate job yet:

wait gate/add-search/000000000000 --json
{
"schema_version": "0.15",
"command": "wait",
"ok": false,
"exit_code": 2,
"data": {},
"warnings": [],
"next": [
{
"command": ["ghostbranch", "gate", "add-search", "--repo", "you/project"],
"why": "the feature has no gate job to wait on; gate it",
"needs_human": false,
"needs_input": []
}
],
"error": {
"code": "JOB_NOT_FOUND",
"message": "feature \"add-search\" has no gate job to wait on: it was never gated, or its last gate stopped before CI; gate it",
"field": "job",
"retryable": false,
"details": { "feature": "add-search" }
}
}

wait printed it on one line, as it prints everything with --json. It’s spread out here to read.

  • code is the stable name to act on. The message is for people, and its wording may change.
  • field names the flag or input at fault, or is null when no single input is.
  • retryable is true when running the same command again may work, such as after a network error.
  • details holds anything else the code documents, such as details.findings for a leak or details.plan_id for an interrupted publish. It’s {} when there’s nothing.

Even a failure has a next, when there’s something to do about it.

The exit code says what kind of outcome it was, so an agent knows how to react before it reads anything else:

Exit Meaning What an agent does
0 Success. Reads data and next.
1 It failed for another reason: GitHub, the network, a token, a lock. Reads error.code, and retries if retryable is true. Some of these need a person, and next says so.
2 It was called wrongly: a bad flag, missing input or an unknown name. Fixes the input error.field names, and runs it again.
3 The gate failed: CI failed or didn’t run, or the leak scan found something. Fixes the feature and gates again. Never works around the scan.
4 A conflict the command won’t resolve on its own, such as a rebase conflict or a name already taken. Resolves it, then runs the command again. A diverged shadow branch (SYNC_DIVERGED) goes to a person.
5 A person has to approve this. Stops, and gives you the command in next.
6 The repos changed since the plan was made, so nothing was published. Makes a new plan, from next.

Each entry in next is one command:

  • command is the full command as a list of arguments, with --repo filled in where the command needs one. The agent runs it exactly as given. It’s a list, not a shell string, so nothing needs quoting.
  • why says what it’s for, in a sentence. It often says what to do first, as the start result above does.
  • needs_input lists flags only the caller can fill in, such as a pull request title. The agent adds them itself.
  • needs_human says whether a person has to run it.

People see the same list as Next: at the end of the human output. A flag in needs_input shows with a placeholder after the command. This is the end of a gate on a feature with two commits, so the publish needs a title:

Terminal
the end of: ghostbranch gate search-filters
Next:
ghostbranch promote search-filters --mode preserve --plan --repo you/project --title <title>
the gate passed; plan the publish

With --json that’s one entry, with "needs_input": ["--title"]. The command list ends at you/project, and the agent adds --title "…" with a title it writes.

When an entry has needs_human: true, the agent stops. It tells you the exact command and why, and it doesn’t look for another way round. In the human output, such a line ends with (a person must run this).

The same step can need a person on one machine and not on another. After promote --plan saves a plan, next is the apply that publishes it. Without an agent grant, a person has to run it:

next, without a grant
{
"command": ["ghostbranch", "apply", "20261013T032248Z-ea7bcd"],
"why": "publish the plan: a person runs this in their own terminal and types the code it shows (or allows agents first with ghostbranch agents allow)",
"needs_human": true,
"needs_input": []
}

If a person has allowed agents to publish to this repo from this machine, the same step comes with --yes, and the agent may run it:

next, with a grant
{
"command": ["ghostbranch", "apply", "20261013T032248Z-ea7bcd", "--yes"],
"why": "a person allowed agents to publish here; publish the plan",
"needs_human": false,
"needs_input": []
}

Both entries are built from Ghostbranch’s source, with a sample plan ID. The why texts are the binary’s own.

A warning is something worth knowing that didn’t stop the command. It has a code, a message, and sometimes details. A warning never changes ok or exit_code. Here agents status ran in an agent’s session that was using a person’s login:

agents status --json (warnings)
[
{
"code": "AGENT_ON_PERSON_LOGIN",
"message": "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)"
}
]

An agent passes a warning on to you when it matters, such as this one, or a newer release (UPDATE_AVAILABLE). Without --json, warnings go to stderr as lines starting warning:.

With --json, stdout holds only the JSON. Progress lines, such as “Reading … from GitHub”, go to stderr. Keep the two apart: 2>&1 would mix progress into the JSON and break it. Save stdout to a file if you want to keep the JSON.

ghostbranch explain looks up any code, with no token and no network:

Terminal
ghostbranch explain JOB_NOT_FOUND
JOB_NOT_FOUND (error, exit 2): The feature has no gate job to wait on: it was never gated, or its last gate stopped before CI (a leak or a conflict).
what to do: Run ghostbranch gate <feature>, with --detach to get a job id.
ghostbranch explain 5
exit 5: A person has to approve this before it runs: a publish, or a delete or unregister that can't be undone.
what to do: Stop. Give the human the command in next; it needs a person at a console. For promote and apply, an agent grant they make with ghostbranch agents allow also works.
a person must act
then every code with exit 5 (here APPROVAL_REQUIRED), left out

It takes an error code, a warning code or an exit number. An exit number also lists the codes that exit with it. ghostbranch explain --all --json lists every code, with its summary, causes, what to do and whether a person must act. The error lists on these pages come from that list.

wait --json prints one JSON object per line while it waits for CI, so an agent can follow along. The last line is the result, the same envelope gate --json prints. The lines before it are events, each with a type:

wait gate/add-search/39a2e1652d1e --json
{"schema_version":"0.15","type":"waiting","job":"gate/add-search/39a2e1652d1e","at":"2026-10-14T14:03:05Z","candidate":"39a2e1652d1eca0158b5a78537a3d57d5fa48fbe","shadow_pr":22,"shadow_pr_url":"https://github.com/you/project-shadow/pull/22","ci_timeout":"1h0m0s"}
{"schema_version":"0.15","type":"check","job":"gate/add-search/39a2e1652d1e","at":"2026-10-14T14:03:07Z","name":"ci","status":"in_progress"}
{"schema_version":"0.15","type":"check","job":"gate/add-search/39a2e1652d1e","at":"2026-10-14T14:03:07Z","name":"ci","status":"completed","conclusion":"success"}
{"schema_version":"0.15","type":"settled","job":"gate/add-search/39a2e1652d1e","at":"2026-10-14T14:04:15Z","outcome":"passed"}
{"schema_version":"0.15","command":"wait","ok":true,"exit_code":0,"data":{…},"warnings":[…],"next":[…],"error":null}

The last line is shortened here: its data is the full gate result, and in this run it had one warning and one next entry.

  • waiting: CI has started on the gate candidate. It names the candidate commit, the shadow pull request and the time limit.
  • check: one CI check changed, with its name, status and, once finished, its conclusion.
  • settled: CI is done. outcome is passed, failed, no_ci or timeout, and a failure lists failed_checks.

A script that only needs the result reads the last line and ignores the rest. With --timeout 10m, wait gives up waiting after ten minutes and reports checks: pending, with exit 0. The job keeps running: run wait again later. Long CI has more on jobs.

status --json and feature list --json give each feature a state. It is one of draft, in_review, gate_passed, published, pr_behind, merged, merged_moved, pr_closed, branch_gone, archived, abandoned, parent_stale, public_pr_needs_update and unreadable. The repo pair says what each one means.

archived means the feature was handed off: its pull request is public, and fixes go straight to the pull request’s branch, not through the shadow repo. Its report also has an archived object, with the archive’s ref, the head it holds and when it was made. Promote and approval explains the hand-off.

apply --json has data.after_publish. It says, for each feature it published, what happened to its shadow branch:

  • archived: handed off. The shadow branch was archived, and the feature’s state is now archived.
  • kept: the shadow branch stays, and fixes go through it as before, with gate and promote.
  • incomplete: the publish worked, but the archive stopped partway. A warning ARCHIVE_INCOMPLETE says where, and next gives ghostbranch feature archive <name> to finish it.

It is an object keyed by feature name, such as {"add-search": "archived"}, and never null. Under --yes or --non-interactive, apply archives unless you pass --keep.

BAD_FLAGexit 2A flag, argument or option value is invalid.
What to do

Fix the value named in error.field.

INTERRUPTEDexit 1The command was interrupted (Ctrl-C) before it finished.
What to do

With error.details.plan_id the plan is saved and the publish may have started: resume it with ghostbranch apply <plan_id> (never promote again; a person runs it unless an agent grant exists). With error.details.job the gate's CI is still running in the shadow repo: run ghostbranch wait <job>. With neither, nothing is pending: run the command again.

JOB_NOT_FOUNDexit 2The feature has no gate job to wait on: it was never gated, or its last gate stopped before CI (a leak or a conflict).
What to do

Run ghostbranch gate <feature>, with --detach to get a job id.

JOB_STALEexit 4A newer gate replaced this job: the feature, public main or the overlay changed, and the feature was gated again.
What to do

Wait on the current job (error.details.current_job), or, if there is none, gate the feature again.

LOCKEDexit 1Another ghostbranch operation on this pair is running on this machine (error.details.held_by names it).
What to do

Wait a few seconds and run the command again (error.retryable is true; next repeats it, or waits on error.details.job). The lock frees itself when that process ends, even if it crashed; there is nothing to delete.

MISSING_INPUTexit 2Required input is missing.
What to do

Pass the input named in error.field, for example --title or -m for promote.