ctx push
ctx push uploads the knowledge pages under .contexo/ to your project’s repo on the Contexo server.
When you’d reach for it
Section titled “When you’d reach for it”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, which is why it previews and then asks before it overwrites anything that already exists on the server.
Two things regularly surprise people. Push is not a directory sync — it sends only files it can parse as Contexo pages, and it never deletes anything on the server. And “one push” is not “one commit”: the server commits each file on its own, so a five-page push lands as five commits that happen to share a message.
Synopsis
Section titled “Synopsis”ctx pushctx push --dry-runctx push --yes -m "stripe webhook decisions"ctx push --type concept --show-diff| Flag | Type | Default | Description |
|---|---|---|---|
--dry-run | bool | false | show what would be pushed without sending |
--fallback-server | bool | false | (planned) route reasoning-trail distillation to the Contexo server; currently errors |
--feature | string | push only pages tagged with this feature/tag | |
--message, -m | string | commit message (default: 'ctx push (N pages)') | |
--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) |
--feature and --tag are the same filter under two names, matched case-insensitively against each page’s tags: frontmatter list. If you pass both, --feature wins and --tag is ignored. --type matches concept, entity, source or analysis, also case-insensitively. Neither filter is validated: a typo simply matches nothing and you get Nothing to push (no pages match filters) with exit status 0.
--yes matters only when the preview finds at least one [EDIT] row. A batch of nothing but new pages never prompts, so scripts that only ever create pages don’t need it. When there are edits and stdin isn’t a terminal, push refuses rather than guessing — see Common errors.
--no-preview skips the preview entirely, which also skips one HTTP round trip per file. It’s the flag to use in CI where the preview output is noise; the cost is that you lose the [EDIT] detection, and with it the confirmation prompt (--yes becomes irrelevant). Combined with --dry-run it prints a bare list instead — one line per file with the parent= sha it would be sent with, and no server reads at all.
--fallback-server is a placeholder for server-side distillation. It is not implemented and returns an error 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”Before sending anything, push fetches the current server copy of every file in the batch and diffs it against what you’re about to send. Each file gets one row:
| Row | Meaning |
|---|---|
[NEW] |
The path doesn’t exist on the server yet. |
[EDIT] |
The path exists and the content differs. The parenthesised summary lists what changed. |
[SAME] |
The path exists and nothing changed. |
[?] |
The server copy couldn’t be read (offline, 5xx, auth). Push continues anyway. |
The [EDIT] summary is section-aware, not line-based: frontmatter when any frontmatter key was added, removed or changed, then + Heading for an added section, - Heading for a removed one, ~ Heading for a modified one and ~> Heading for a renamed one, capped at four entries with a +N more tail. --show-diff keeps that one-liner and prints the full per-section rendering underneath it — the same format ctx diff prints, indented four spaces, headed diff: <server-sha>..local.
If any row is [EDIT] and you didn’t pass --yes, push stops at a Proceed? [y/N]: prompt. The default is no; anything other than y or yes aborts with Aborted. and exit status 0, having sent nothing.
Examples
Section titled “Examples”Look before you leap. --dry-run runs the whole preview — including the per-file server reads — 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’s no prompt; --yes here is habit, not necessity:
ctx push --yesAbout to push 1 page(s) to acme-api:
[NEW] wiki/concepts/stripe-subscription.md
Pushed 1 page(s); HEAD=a4b2c1d3The same page after an agent revised it. This is where the preview earns its keep — the summary tells you the frontmatter moved, one section was rewritten, one added and one dropped, before you commit to it:
ctx push --yesAbout to push 1 page(s) to acme-api:
[EDIT] wiki/concepts/stripe-subscription.md (frontmatter, ~ Concepts, + Webhooks, - Open Questions)
Pushed 1 page(s); HEAD=b7e9f204How it works
Section titled “How it works”- Reads
.contexo/config.jsonforrepo_idandserver_url, and.contexo/credentials.jsonfor the bearer token. If credentials carry their ownserver_urlit wins over the one inconfig.json. - Walks
.contexo/for.mdfiles. Dot-directories are skipped, as areindex.mdandtags.md(they’re generated, never pushed) and any file whose frontmatter doesn’t parse. - Applies
--typeand--feature/--tag, then serialises each surviving page back to bytes. - Reads
.contexo/.sync/state.jsonand attaches the recordedparent_shafor each path. Pages with no recorded sha are sent with an empty parent. - Unless
--no-preview, issues oneGET /v1/repos/<repo>/pages/<path>per file to build the preview, then prompts if needed. POST /v1/repos/<repo>/sync/pushwith your name and email from credentials, the message, and the file list.- Server side: if the repo doesn’t exist it is created and you become its owner — subject to the plan’s repo cap on the hosted service. Then each file is written,
git add-ed and committed individually with your name as the git author.new_headis whatever the last file’s write returned — its new commit, or the repo’s HEAD if that file turned out to be unchanged. Onepushevent is recorded in the repo activity feed if at least one file was accepted; a push where every file conflicted records nothing. - Back on the client, every path in the response’s
pushedlist gets its new sha written into.contexo/.sync/state.json, and push printsPushed N page(s); HEAD=<sha>.
Push writes exactly one local file: .contexo/.sync/state.json. It does not touch your pages, does not regenerate index.md/tags.md (that’s ctx pull), and does not create anything in your project’s own git repo.
The distill handshake
Section titled “The distill handshake”When your agent pushes through the MCP tool rather than the CLI, a batch containing any concept or analysis page pauses instead of uploading: ctx_push returns a PUSH_PAUSED reason=distill_required directive asking the agent to first write a source page capturing the reasoning trail, cover any named systems with entity pages, then re-invoke with distill_done: true and source_slug set to the slug it just wrote (omitting source_slug is an error). That second call links the source into each concept/analysis page’s sources: frontmatter and pushes everything together.
The handshake fires only when a capture buffer with at least one recorded turn exists from the last six hours, and can be bypassed with no_distill: true or CONTEXO_DISTILL_DISABLE=1.
Common errors
Section titled “Common errors”push: no credentials, run 'ctx auth login' first — .contexo/credentials.json is missing or unreadable. Run ctx login.
push: no server URL configured (run 'ctx remote add') — neither the config nor the credentials carry a server URL. The message names a subcommand that doesn’t exist; the real one is ctx remote set <url>. See ctx remote.
push: no repo_id configured in .contexo/config.json — run ctx remote set-repo <id>, or omit the id for an interactive picker.
push: open hub: … (did you run 'ctx init'?) — there’s no .contexo/ here. Check you’re in the project root, or pass --root.
push: refusing to alter existing pages in non-interactive mode; pass --yes to confirm — the preview found [EDIT] rows and stdin isn’t a TTY. Add --yes once you’re satisfied the edits are intended.
Conflicts. A 409 from the server means someone else committed to that path since the sha you recorded. Push prints each one as <path>: current=<sha> expected_parent=<sha> on stderr, still commits the files that didn’t conflict, and exits non-zero with push: N conflict(s) remain. The fix is ctx pull, merge, push again — walked through in drift and conflicts.
push: --fallback-server: server-side distillation not implemented yet … — drop the flag.
A plan-limit message with no sync: prefix — the hosted server answered 402 because creating this repo would exceed your plan’s repo cap. Only a push that would auto-create a new repo can hit this.
See also
Section titled “See also”ctx pull— the other half of the sync loop, and the fix for a 409ctx status— what’s unpushed and what’s drifted, before you pushctx_push— the MCP tool your agent calls, including the distill handshake- Drift and conflicts — how divergence happens and how to resolve it
