JSON output and exit codes
The sync commands of ctx have a machine mode for scripts, CI and the VS Code extension. This page is the reference for it: what --json prints, how a failure is reported, and what each exit code means. All of it is version 1 of the ctx contract, which ctx version --json announces.
The documents
Section titled “The documents”| Command | stdout document |
|---|---|
ctx version --json |
ctx.version.v1 |
ctx status --json (also with --offline) |
ctx.status.v1 |
ctx push --json |
ctx.push.v1 |
ctx pull --json |
ctx.pull.v1 |
ctx resolve <relpath> --ours or --theirs, with --json |
ctx.resolve.v1 |
ctx repair --json (ctx only; the extension never calls it) |
ctx.repair.v1 |
ctx show <relpath> --at ... |
the raw page bytes, no document |
ctx show is the exception: it always runs in machine mode and writes the page’s raw bytes, not JSON.
The --json rule
Section titled “The --json rule”With --json, a successful run writes exactly one JSON document to stdout and nothing else anywhere: stderr stays empty, there is no spinner, no prompt, no update nudge and no warning: line. Warnings go into the document’s own warnings array. ctx push --json never prompts or previews; --yes is accepted there and does nothing.
The document is written the same way every time:
- 2-space indent, a trailing newline, keys in a fixed order (the contract’s order, never alphabetical), all keys
snake_case. - Arrays are always present, never
null; an empty list is[]. - Optional values are left out when unknown or not applicable, never
null. One field is the exception:project.dashboard_urlinctx.status.v1is always present and is""for a self-hosted server with no dashboard configured (thenproject.dashboard_page_url_templateis left out). - Page and commit shas are full 40-hex. The build
commitanddateinctx.version.v1are build metadata (a short commit and a date), not page shas. - Times are UTC RFC 3339 in whole seconds, like
2026-07-28T09:14:07Z. - Content hashes are
sha256:followed by 64 hex digits. - Page paths are relative to
.contexo/and use/. Onlyrootandcontexo_dirin the status document are absolute paths in your operating system’s form.
Callers must ignore fields they do not know. Additive changes keep contract_version at 1: new optional fields, new commands or flags (announced by a new capability), new error, warning, note or skip-reason codes, new server capabilities. Breaking changes raise it to 2: removing, renaming or retyping a field, changing what a field means, a new page state or conflict kind, moving an error code to another exit code, or giving up the one-document rule.
The error envelope
Section titled “The error envelope”When a command fails in machine mode, stdout is empty and the last non-empty line of stderr is one line of JSON:
{"schema":"ctx.error.v1","exit_code":5,"error":{"code":"not_initialized","message":"status: no .contexo/ directory in /e2e/alice","advice":["ctx init"],"retryable":false}}exit_codealways equals the process’s exit code.codeis one of the 24 stable codes below. It is the authoritative field; the exit code is derived from it.messageis a human sentence with no colour codes.advicelists shell commands that fix the problem. It is[]when there are none, never left out.retryableis true only for codes worth retrying unchanged.http_statusappears only when an HTTP response caused the error.detailsappears only for the codes that have a details payload, listed below.
There is no other key.
The envelope is written only in machine mode, which means: the command was run with --json (or --json=true), or the raw arguments contain --json before any --, so even a flag-parsing failure is reported as an envelope; or the command is ctx show. ctx mcp, ctx capture turn and shell completion own their output streams and never write one.
Without --json, failures stay human: a rendered message, usually with a try line naming the fix, and exit code 1, whatever the cause. For example, ctx resolve <relpath> --ours --theirs exits 1 with a message, while the same command with --json exits 3 with a usage envelope.
There is one case with both a document and an envelope: ctx push --json where some pages hit a conflict. The pages that went through are in the stdout document, and the envelope says conflict, exit 6. See the example below.
To read a failure from a program: if the exit code is not 0, take the last non-empty stderr line and parse it. If it does not parse (an older ctx, or a crash), fall back on the exit code alone. Treat an unknown code by its exit code’s category.
Exit codes and error codes
Section titled “Exit codes and error codes”Every error code maps to exactly one exit code:
| Code | Exit | Retryable | Raised when |
|---|---|---|---|
internal |
1 | no | Anything unclassified: a local file that cannot be read or written, or an HTTP answer ctx does not expect (such as a 409 outside a push, or a redirect it cannot follow). |
usage |
3 | no | Bad flags or arguments: an unknown flag, both --ours and --theirs, a malformed --at or --if-server-sha, ctx show --json=false; also an HTTP 400 other than invalid_path. |
invalid_path |
3 | no | A page path that breaks the path rules (show, resolve, push), or a 400 from the server saying so. |
not_authenticated |
4 | no | No credentials, or an empty token, for a command that talks to the server. Advice: ctx auth login. |
auth_rejected |
4 | no | HTTP 401: the token was revoked or expired. |
forbidden |
4 | no | HTTP 403: you are not a member of that repo. |
not_initialized |
5 | no | No .contexo/ directory under the root. The sync commands (status, push, pull, resolve, show, repair) all need one; ctx version does not. Advice: ctx init. |
repo_not_configured |
5 | no | No repo_id in .contexo/config.json. Advice: ctx remote set-repo <id>. |
server_unresolved |
5 | no | Your global credentials have no server_url, so ctx will not guess where to send your token. Advice: ctx auth login --server <url>. |
conflict |
6 | no | ctx push: the server refused one or more pages because someone pushed them since your base (HTTP 409). The stdout document is written too. |
stale |
6 | no | ctx resolve --if-server-sha: the server is no longer at the sha you passed. |
network |
7 | yes | The request failed in transit or timed out. |
server_error |
7 | yes | HTTP 5xx, or an answer whose body does not decode. |
rate_limited |
7 | yes | HTTP 429. |
repo_not_found |
8 | no | The server has no repo with that id. (ctx status reports remote.repo_exists: false instead of failing.) |
page_not_found |
8 | no | ctx show --at server or ctx resolve on a page the server does not have. |
base_unavailable |
8 | no | ctx show --at base and the page has no valid base snapshot. Advice: ctx repair. |
sha_not_found |
8 | no | ctx show --at <sha>: the server does not know that commit, or the page did not exist at it. |
distill_required |
9 | no | ctx push was refused by the distill guard: a pending agent session must be shared through your agent first. See ctx push. |
not_resolvable |
9 | no | ctx resolve was asked for a strategy that does not apply to the page’s state; nothing was changed. |
server_unsupported |
10 | no | The server lacks something the command needs, for example ctx show --at <sha> on a server that cannot read history. |
not_implemented |
10 | no | Only a hidden stub command in a pre-release build returns this. Advice: ctx update. No release returns it for a capability it advertises. |
quota_exceeded |
11 | no | HTTP 402: a plan limit on the hosted service. |
busy |
12 | yes | Another ctx process (often an agent’s push or pull) held .contexo/.sync/lock for longer than CONTEXO_LOCK_TIMEOUT (default 10 seconds). |
The exit numbers on their own:
- 0: success. For
ctx pull, skipped pages are still success; forctx status, a server it could not reach is reported inside the document and is still success. - 1: internal error, and also every failure in human mode.
- 2: never used on purpose. It is what the Go runtime exits with on a crash, and callers treat it as
internal. - 3: usage. 4: authentication. 5: configuration or not initialized. 6: conflict or stale. 7: network or server, retryable. 8: not found. 9: refused by a precondition, nothing changed. 10: server unsupported. 11: quota. 12: busy, retryable.
- 130 or 143: the process was killed (SIGINT or SIGTERM), for example by Ctrl+C or by a caller’s timeout.
Details payloads
Section titled “Details payloads”These codes carry a details object in the envelope:
conflict:paths, the pages the server refused.stale:path,expected_sha,server_sha.not_resolvable:path,state(the page’s state),strategy(oursortheirs).server_unsupported:missing, the server capabilities that were needed, for example["pages.at"].busy:pid,host,command,started_at, describing the process that holds the lock.quota_exceeded:quota_code,limit, andupgrade_urlwhen the server sent one.distill_required:pages(the concept and analysis pages in the push),sessions(the pending agent sessions:session_id,turns,updated_at,fresh), andagent_prompt, a sentence you can paste into your agent so it pushes the pages for you.
The handshake: ctx version --json
Section titled “The handshake: ctx version --json”A program that drives ctx should start here. The ctx.version.v1 document tells it which contract this ctx speaks and what it can do:
ctx version --json{
"schema": "ctx.version.v1",
"contract_version": 1,
"version": "0.7.0",
"commit": "9f31c07",
"date": "2026-07-28",
"dev_build": false,
"os": "darwin",
"arch": "arm64",
"capabilities": [
"status.json",
"status.offline",
"show.base",
"show.server",
"show.sha",
"pull.json",
"push.json",
"push.paths",
"push.distill_guard",
"resolve",
"errors.envelope",
"repair"
]
}contract_version is an integer, 1 today. A caller built for version 1 should refuse a higher number and ask to be updated. dev_build is true for an unstamped go build.
capabilities lists what this build can do, in the contract’s order, never sorted. A ctx 0.7.0 release lists all of these:
| Capability | Means | Required by the VS Code extension |
|---|---|---|
status.json |
ctx status --json |
yes |
status.offline |
ctx status --json --offline |
yes |
show.base |
ctx show --at base |
yes |
show.server |
ctx show --at server |
yes |
show.sha |
ctx show --at <sha> |
no, optional |
pull.json |
ctx pull --json, the safe pull |
yes |
push.json |
ctx push --json |
yes |
push.paths |
ctx push with named paths |
yes |
push.distill_guard |
push refuses with distill_required |
yes |
resolve |
ctx resolve |
yes |
errors.envelope |
the ctx.error.v1 envelope |
yes |
repair |
ctx repair |
no, optional and ctx-only |
The first eleven are the version 1 list; repair comes after them. A caller should check for the capabilities it needs rather than compare version numbers, and treat a missing required capability as “this ctx is too old: run ctx update”.
The server has capabilities of its own: pages.at (reading a page at a past commit), push.expect_absent (refusing a create when the page already exists) and write.path_sha (an unchanged write reports the page’s own last commit). ctx learns them from the headers of responses it already gets, and never makes a request just to ask. ctx status --json reports them under server.capabilities and adds a server_outdated warning when one is missing.
CONTEXO_CLIENT
Section titled “CONTEXO_CLIENT”Every request ctx makes to the server carries an X-Contexo-Client header, which the server records in the repo’s activity so the team can see what synced. The value is, in order of precedence:
- the
CONTEXO_CLIENTenvironment variable, when set; - the name the running program gave itself: under
ctx mcp, the agent’s MCP client name, orctx-mcpwhen the agent sent none; ctx-cli.
The VS Code extension runs ctx with CONTEXO_CLIENT=contexo-vscode/<version>. A value that is empty or contains control characters is ignored. Set it in your own scripts to tell their syncs apart:
CONTEXO_CLIENT=nightly-ci/1 ctx pull --jsonExamples
Section titled “Examples”A command run outside a project. With --json, the failure is the envelope on stderr and exit 5:
ctx status --json --root ~/scratch{"schema":"ctx.error.v1","exit_code":5,"error":{"code":"not_initialized","message":"status: no .contexo/ directory in ~/scratch","advice":["ctx init"],"retryable":false}}
(exit 5)A push that lost a race: a teammate’s push landed between this push’s listing and its send. One page was created; two came back as conflicts. stdout holds the push document, stderr ends with the conflict envelope, and the advice lists the ctx resolve commands:
ctx push --json{
"schema": "ctx.push.v1",
"dry_run": false,
"message": "ctx push (3 pages)",
"server_url": "https://contexo.example.com",
"repo_id": "acme-api",
"new_head": "e5a3b9c807162534435261708f9e0d1c2b3a4958",
"pushed": [
{
"path": "wiki/entities/invoice-service.md",
"kind": "create",
"sha": "e5a3b9c807162534435261708f9e0d1c2b3a4958"
}
],
"conflicts": [
{
"path": "wiki/concepts/stripe-subscription.md",
"kind": "both_modified",
"base_sha": "d2f0e617a3b5c7d9e1f2031425364758697a8b9c",
"server_sha": "f0c47d2196385a7b4c3d2e1f0a9b8c7d6e5f4a3b"
},
{
"path": "wiki/entities/billing-worker.md",
"kind": "both_modified",
"base_sha": "c1d8a3f50e2b4c6d8f90a1b2c3d4e5f607182930",
"server_sha": "3b8e1a0947c2f6d85e90a1b2c3d4e5f60718293a"
}
],
"skipped": [],
"distill": {
"required": false,
"bypassed": false
},
"warnings": []
}
{"schema":"ctx.error.v1","exit_code":6,"error":{"code":"conflict","message":"push: 2 conflict(s) remain; other pages were pushed","advice":["ctx resolve wiki/concepts/stripe-subscription.md --ours","ctx resolve wiki/concepts/stripe-subscription.md --theirs","ctx resolve wiki/entities/billing-worker.md --ours","ctx resolve wiki/entities/billing-worker.md --theirs"],"retryable":false,"http_status":409,"details":{"paths":["wiki/concepts/stripe-subscription.md","wiki/entities/billing-worker.md"]}}}
(exit 6)The distill guard refusing a push. The envelope’s details.agent_prompt is the sentence to hand to your agent:
ctx push --json{"schema":"ctx.error.v1","exit_code":9,"error":{"code":"distill_required","message":"push: 1 un-distilled agent session is pending; share these pages through your agent so the decision record is captured","advice":[],"retryable":false,"details":{"pages":["wiki/concepts/stripe-subscription.md"],"sessions":[{"session_id":"docs-demo-session","turns":1,"updated_at":"2026-07-28T09:14:07Z","fresh":true}],"agent_prompt":"Push my pending Contexo changes with the ctx_push tool (wiki/concepts/stripe-subscription.md). Distill the pending session into a source page first when ctx_push asks you to."}}}
(exit 9)A successful status: one document, nothing else.
ctx status --json{
"schema": "ctx.status.v1",
"generated_at": "2026-07-28T09:14:07Z",
"offline": false,
"root": "~/projects/acme-api",
"contexo_dir": "~/projects/acme-api/.contexo",
"project": {
"repo_id": "acme-api",
"config_server_url": "https://contexo.example.com",
"dashboard_url": "",
"last_pull_sha": "a4b2c1d3e5f60718293a4b5c6d7e8f9012345678"
},
"server": {
"url": "https://contexo.example.com",
"url_source": "credentials",
"version": "dev",
"capabilities_known": true,
"capabilities": [
"pages.at",
"push.expect_absent",
"write.path_sha"
]
},
"auth": {
"state": "authenticated",
"credentials_source": "project",
"token_kind": "legacy",
"user_name": "dev",
"user_email": "dev@example.com"
},
"remote": {
"source": "live",
"fetched_at": "2026-07-28T09:14:07Z",
"repo_exists": true,
"head": "b7e9f204c8a1d35e6f70819a2b3c4d5e6f708192",
"page_count": 1
},
"counts": {
"unchanged": 0,
"modified": 1,
"new": 1,
"incoming": 0,
"incoming_new": 0,
"conflict": 0,
"deleted_local": 0,
"invalid": 0,
"unverified": 0,
"ahead": 2,
"behind": 0
},
"pages": [
{
"path": "wiki/concepts/stripe-subscription.md",
"state": "modified",
"type": "concept",
"slug": "stripe-subscription",
"tags": [
"stripe",
"billing",
"proration"
],
"author": "dev",
"agent": "claude",
"updated": "2026-07-24T16:31:00Z",
"reasoning_summary": "How trials, proration and webhooks fit together in acme-api.",
"local": {
"file": "wiki/concepts/stripe-subscription.md",
"hash": "sha256:d2d5406cf1ff46eee2577caf20c3509270c93ea29c4a765fe3227446cbe55cde"
},
"base": {
"sha": "b7e9f204c8a1d35e6f70819a2b3c4d5e6f708192",
"hash": "sha256:460dd05a3b5e41a66eff13f4af9ff5e88390e98b55645cd9ba093768d22423e5",
"synced_at": "2026-07-28T09:14:07Z",
"stale": false
},
"remote": {
"sha": "b7e9f204c8a1d35e6f70819a2b3c4d5e6f708192",
"author": "dev",
"email": "dev@example.com",
"time": "2026-07-28T09:14:07Z",
"message": "ctx push (1 pages)"
},
"notes": []
},
{
"path": "wiki/entities/billing-worker.md",
"state": "new",
"type": "entity",
"slug": "billing-worker",
"tags": [
"billing"
],
"author": "dev",
"agent": "claude",
"updated": "2026-07-25T11:20:00Z",
"local": {
"file": "wiki/entities/billing-worker.md",
"hash": "sha256:8bae15f3fa732a04b87f2c50d5e25834e1f0009bce68dde191409c51a947b828"
},
"notes": []
}
],
"captures": {
"pending": 0,
"fresh": 0,
"blocks_push": false,
"sessions": []
},
"warnings": []
}Global credentials with no server_url fail the same way in every command that talks to the server. This envelope comes from the real binary:
{"schema":"ctx.error.v1","exit_code":5,"error":{"code":"server_unresolved","message":"pull: server URL unresolved: the credentials have no server_url","advice":["ctx auth login --server <url>"],"retryable":false}}ctx status --json does not fail in that case: it reports server_unresolved under remote.error and exits 0.
See also
Section titled “See also”ctx status: thectx.status.v1document and the page statesctx push,ctx pull,ctx resolve,ctx show,ctx repairctx version: the handshake document- Global flags and environment:
CONTEXO_CLIENT,CONTEXO_LOCK_TIMEOUTand the other variables
