Skip to content

ctx push

ctx push uploads the knowledge pages under .contexo/ to your project’s repo on the Contexo server.

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.

Terminal window
ctx push
ctx push --dry-run
ctx push --yes -m "stripe webhook decisions"
ctx push --type concept --show-diff
FlagTypeDefaultDescription
--dry-runboolfalseshow what would be pushed without sending
--fallback-serverboolfalse(planned) route reasoning-trail distillation to the Contexo server; currently errors
--featurestringpush only pages tagged with this feature/tag
--message, -mstringcommit message (default: 'ctx push (N pages)')
--no-previewboolfalseskip the pre-push preview entirely (faster; loses the heads-up about what changes)
--show-diffboolfalseprint the full per-section diff in the preview, not just a one-line summary
--tagstringpush only pages with this tag (alias of --feature)
--typestringpush only pages of this type (concept|entity|source|analysis)
--yes, -yboolfalseskip 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:

FlagTypeDefaultDescription
--rootstringproject root directory (default: current directory)

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.

Look before you leap. --dry-run runs the whole preview — including the per-file server reads — and stops:

Terminal window
ctx push --dry-run
ctx push --dry-run
About 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:

Terminal window
ctx push --yes
ctx push --yes
About to push 1 page(s) to acme-api:
  [NEW]   wiki/concepts/stripe-subscription.md
Pushed 1 page(s); HEAD=a4b2c1d3

The 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:

Terminal window
ctx push --yes
ctx push --yes (second push)
About to push 1 page(s) to acme-api:
  [EDIT]  wiki/concepts/stripe-subscription.md  (frontmatter, ~ Concepts, + Webhooks, - Open Questions)
Pushed 1 page(s); HEAD=b7e9f204
  1. Reads .contexo/config.json for repo_id and server_url, and .contexo/credentials.json for the bearer token. If credentials carry their own server_url it wins over the one in config.json.
  2. Walks .contexo/ for .md files. Dot-directories are skipped, as are index.md and tags.md (they’re generated, never pushed) and any file whose frontmatter doesn’t parse.
  3. Applies --type and --feature/--tag, then serialises each surviving page back to bytes.
  4. Reads .contexo/.sync/state.json and attaches the recorded parent_sha for each path. Pages with no recorded sha are sent with an empty parent.
  5. Unless --no-preview, issues one GET /v1/repos/<repo>/pages/<path> per file to build the preview, then prompts if needed.
  6. POST /v1/repos/<repo>/sync/push with your name and email from credentials, the message, and the file list.
  7. 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_head is whatever the last file’s write returned — its new commit, or the repo’s HEAD if that file turned out to be unchanged. One push event is recorded in the repo activity feed if at least one file was accepted; a push where every file conflicted records nothing.
  8. Back on the client, every path in the response’s pushed list gets its new sha written into .contexo/.sync/state.json, and push prints Pushed 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.

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.

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.

  • ctx pull — the other half of the sync loop, and the fix for a 409
  • ctx status — what’s unpushed and what’s drifted, before you push
  • ctx_push — the MCP tool your agent calls, including the distill handshake
  • Drift and conflicts — how divergence happens and how to resolve it