ctx push
ctx push sends the pages you changed or created since the last sync to your project’s repo on the Contexo server: all of them, or only the ones you name. A page a teammate pushed in the meantime is never overwritten; it is reported as a conflict for you to settle.
When you’d reach for it
Section titled “When you’d reach for it”At the end of a working session, once pages have been written or edited and you want the rest of the team’s agents to start from them. Most pushes in practice come from the agent calling the ctx_push MCP tool. The CLI is what you use when you want to look at a batch before it leaves your machine, push just one page, or push from a script.
A few things are worth knowing up front:
- Only changed pages are sent. Push sends pages in the
modifiedandnewstates (seectx status) and nothing else. Pages you have not touched are not re-uploaded. - It never deletes anything on the server. A page you delete locally is
deleted_localand is skipped. - It never overwrites a newer server version. If someone pushed a page since your base, the server refuses that page and push records a conflict. The other pages still go through.
- It may refuse to push at all when an agent session that has not been distilled is still pending. See The distill guard.
Synopsis
Section titled “Synopsis”ctx pushctx push --dry-runctx push --yes -m "stripe webhook decisions"ctx push wiki/concepts/stripe-subscription.mdctx push --type concept --show-diffctx push --json -m "nightly" -- wiki/concepts/stripe-subscription.md| Flag | Type | Default | Description |
|---|---|---|---|
--dry-run | bool | false | show what would be pushed without sending or writing anything |
--fallback-server | bool | false | (planned) route decision-record distillation to the Contexo server; currently errors |
--feature | string | push only pages tagged with this feature/tag | |
--json | bool | false | emit the ctx.push.v1 JSON document; never prompts or previews (implies --yes and --no-preview) |
--message, -m | string | commit message (default: 'ctx push (N pages)') | |
--no-distill | bool | false | push even when an un-distilled agent session is pending (bypasses the decision-record guard; use deliberately) |
--no-preview | bool | false | skip the pre-push preview entirely (faster; loses the heads-up about what changes) |
--show-diff | bool | false | print the full per-section diff in the preview, not just a one-line summary |
--tag | string | push only pages with this tag (alias of --feature) | |
--type | string | push only pages of this type (concept|entity|source|analysis) | |
--yes, -y | bool | false | skip the pre-push confirmation prompt (required for non-interactive use when editing existing pages) |
Paths. Name pages to push only those: paths relative to .contexo/, such as wiki/concepts/stripe-subscription.md (a leading .contexo/, or an absolute path inside .contexo/, also works). You may put -- before them; the VS Code extension always does. A named page that is not modified or new is not sent: it is listed as skipped with its state (unchanged, incoming, conflict, …), and a path ctx has never heard of is skipped as not_found. Named paths cannot be combined with --feature, --tag or --type (a usage error).
Filters. --feature and --tag are the same filter under two names, matched case-insensitively against each page’s tags: list; if you pass both, --feature wins. --type matches concept, entity, source or analysis, also case-insensitively. A filter that matches nothing simply pushes nothing.
--dry-run goes through everything up to the send: it lists the server, classifies every page, runs the distill guard and shows what would be pushed. It sends nothing and writes nothing, not even the automatic base repair. In human mode it still shows the preview.
--yes matters only in human mode, when the preview finds at least one [EDIT] row. Without it, push asks Proceed? [y/N] at a terminal, and refuses when stdin is not a terminal. A batch of only new pages never asks.
--no-preview skips the preview, and with it the server reads and the prompt. Combined with --dry-run it prints a bare Would push N page(s): list with each page’s kind (create or update).
--show-diff prints the full per-section diff under each [EDIT] row, in the format of ctx diff.
--json prints the ctx.push.v1 document instead of the human report, and never previews or prompts. --yes is accepted alongside it and does nothing. See JSON output and exit codes.
--no-distill pushes even when the distill guard would refuse. See The distill guard.
-m, --message sets the commit message; the default is ctx push (N pages).
--fallback-server is a placeholder for server-side distillation. It is not implemented and fails immediately, before any network call.
ctx push also accepts the global flag:
| Flag | Type | Default | Description |
|---|---|---|---|
--root | string | project root directory (default: current directory) |
The preview
Section titled “The preview”In human mode, before anything is sent, push shows the batch, one row per page:
| Row | Meaning |
|---|---|
[NEW] |
The page does not exist on the server yet. |
[EDIT] |
The page exists on the server and your version differs. The summary in brackets lists what changed. |
[SAME] |
The page exists and the section diff finds no difference. |
[?] |
The server copy could not be read. The push itself still goes ahead. |
The [EDIT] summary is section-aware: frontmatter when any frontmatter key changed, then + Heading for an added section, - Heading for a removed one, ~ Heading for a modified one and ~> for a renamed one, capped at four entries with a +N more tail.
Building the preview reads the server copy of each page the batch would update, at most 4 at a time; new pages need no read. The preview runs after the distill guard and before the send, and if you answer anything but y or yes at the prompt, push prints Aborted., exits 0 and has sent nothing.
Conflicts
Section titled “Conflicts”Each page goes to the server with the sha of its base, the version you last synced. If someone else pushed that page since, the server refuses it (HTTP 409) and commits the rest of the batch.
For every refused page, push records a conflict under .contexo/.sync/ (with the server’s current content), and leaves your base and recorded sha alone, so the page shows as conflict in ctx status, online or offline, until you settle it. A later push skips it with the reason conflict; it does not try again. The way forward is ctx resolve: --ours to publish your version on top of theirs (then push again), or --theirs to take theirs.
A page you created that has never been synced has no base. Push sends it as a create that the server must refuse if the path already exists. If a teammate created the same page first, you get a conflict of kind both_added instead of silently overwriting their page.
In human mode the conflicts are listed as <path>: server=<sha> base=<sha> (base=none for both_added), followed by the ctx resolve commands to run, and push exits 1. With --json, stdout still holds the push document, the envelope says conflict and the exit code is 6. See the example below.
The distill guard
Section titled “The distill guard”Push refuses to send your changes when both of these are true:
- the pages it would send include a
conceptoranalysispage, and - an agent session captured in this project has not been distilled yet: a buffer under
.contexo/raw/sessions/_pending/holds at least one recorded turn and was written to within the last 6 hours.
Concept and analysis pages record decisions. When the agent session behind them is still pending, the reasons for those decisions, the options considered and why they were rejected, exist only in that session. Pushing through your agent lets it first write a source page, the decision record, from the session and link it to the pages. So push stops and tells you to share the pages through your agent instead.
In human mode the refusal prints the sentence to paste into your agent, the bypass hint, and exits 1:
Share via your agent: 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.
ctx push --no-distill bypasses this guard; use it only deliberately.
✖ push: 1 un-distilled agent session is pending; share these pages through your agent so the decision record is captured
(exit 1)With --json it is a distill_required envelope, exit 9. Its details list the pages, the pending sessions and the agent_prompt sentence:
{"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)The guard runs on every push, before the preview and before anything is sent. A pending buffer with no turns in it, or one older than 6 hours, does not trigger it, and neither does a batch of only entity and source pages.
To push anyway, pass --no-distill or set CONTEXO_DISTILL_DISABLE=1. Either way the push document records distill.bypassed: true. Use it deliberately. The VS Code extension never bypasses the guard: it offers Share via your agent, which copies details.agent_prompt for you to paste into your agent’s chat, and it removes CONTEXO_DISTILL_DISABLE from the environment it runs ctx with.
Examples
Section titled “Examples”Look before you leap. --dry-run shows the preview and stops:
ctx push --dry-runAbout to push 1 page(s) to acme-api:
[NEW] wiki/concepts/stripe-subscription.md
(dry-run; no changes sent)A first push of a brand-new page. Nothing exists on the server, so there is no prompt:
ctx push --yesAbout to push 1 page(s) to acme-api:
[NEW] wiki/concepts/stripe-subscription.md
Pushed 1 page(s); HEAD=a4b2c1d3The same kind of push as JSON, with one edited and one new page:
ctx push --json -m "Add the billing worker"{
"schema": "ctx.push.v1",
"dry_run": false,
"message": "Add the billing worker",
"server_url": "https://contexo.example.com",
"repo_id": "acme-api",
"new_head": "c1d8a3f50e2b4c6d8f90a1b2c3d4e5f607182930",
"pushed": [
{
"path": "wiki/concepts/stripe-subscription.md",
"kind": "update",
"sha": "d2f0e617a3b5c7d9e1f2031425364758697a8b9c"
},
{
"path": "wiki/entities/billing-worker.md",
"kind": "create",
"sha": "c1d8a3f50e2b4c6d8f90a1b2c3d4e5f607182930"
}
],
"conflicts": [],
"skipped": [],
"distill": {
"required": false,
"bypassed": false
},
"warnings": []
}A push that lost a race: a teammate’s push landed between this push’s listing and its send. The new page was created; the two edited pages came back as both_modified conflicts. stdout holds the document; stderr ends with the conflict envelope, exit 6:
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)How it works
Section titled “How it works”- Checks that
.contexo/exists, then resolves the server, token and repo id. The server is the one your credentials belong to: theirserver_urlwins; project credentials without one useserver_urlfrom.contexo/config.json; global credentials without one fail withserver_unresolved(exit 5 with--json) rather than send your token to a URL taken from the project. Ifconfig.jsonnames a different server, push still uses the credentials’ server;ctx statusshows the mismatch. - Takes
.contexo/.sync/lock, waiting up toCONTEXO_LOCK_TIMEOUT(default 10 seconds) if another ctx process, often an agent’s push or pull, holds it. After that it fails withbusy(exit 12). - Lists the repo’s pages once. A repo the server does not have yet counts as empty.
- Unless
--dry-run, repairs missing or stale base snapshots over that listing, likectx repair. - Classifies every page against its base and the listing, taking recorded conflicts into account, and builds the batch:
modifiedandnewpages, filtered by your paths or filters.conflict,invalid,deleted_localandunverifiedpages are listed as skipped with their reason. - Runs the distill guard over the batch.
- In human mode, shows the preview and asks if needed. A dry run stops here.
- Sends the batch in one push request. Each page is sent in normalized form with its base sha as parent; a page with no base is sent as create-only.
- Records the answer. For each page the server accepted, the exact bytes sent become its new base and its recorded sha moves to the new commit; for each refused page, a conflict is recorded instead. Then it prints
Pushed N page(s); HEAD=<sha>.
Push writes only under .contexo/.sync/ (base snapshots, bases.json, conflicts.json and state.json). It never changes your pages, does not regenerate index.md and tags.md (that is ctx pull), and touches nothing in your project’s own git repo.
Common errors
Section titled “Common errors”push: not authenticated, with try ctx auth login: no credentials. Run ctx login. Exit 4 with --json.
push: server URL unresolved: the credentials have no server_url, with try ctx auth login --server <url>: your global credentials do not say which server they belong to. Exit 5 with --json.
push: repo not configured, with try ctx remote set-repo <id>: no repo_id in .contexo/config.json. See ctx remote. Exit 5 with --json.
no .contexo/ directory in <root>: you are not in a project. cd to its root or pass --root. Exit 5 with --json.
push: refusing to alter existing pages in non-interactive mode; pass --yes to confirm: the preview found [EDIT] rows and stdin is not a terminal. Add --yes once you are sure, or use --json, which never prompts.
push: N conflict(s) remain; other pages were pushed: see Conflicts. Exit 6 with --json.
The distill guard refusal: see The distill guard. Exit 9 with --json.
busy: another ctx process held the lock past CONTEXO_LOCK_TIMEOUT. Exit 12 with --json; retry when it finishes.
quota_exceeded: the hosted server refused because of a plan limit (HTTP 402). Exit 11 with --json; the envelope’s details.upgrade_url says where to upgrade.
push: --fallback-server: server-side distillation not implemented yet ...: drop the flag.
Without --json, every failure exits 1.
See also
Section titled “See also”ctx resolve: settle a conflict, then push againctx pull: the other half of the sync loopctx status: what is waiting to be pushed, before you pushctx_push: the MCP tool your agent calls, including the distill handshake- JSON output and exit codes: the
ctx.push.v1document and the error envelope - Drift and conflicts: how pages diverge and how to bring them back together
