Skip to content

ctx_push

ctx_push sends the pages you changed since the last sync to the team server. It is the tool with the most behaviour behind it: one call can come back as a plain result, as a PUSH_PAUSED handshake asking the agent to write up the decision record first, or with a MERGE_REQUIRED brief for pages a teammate changed too. Both of those are agent directives: instruction blocks the agent is expected to act on and then call again.

ctx_push runs the same sync engine as ctx push, so the agent and the CLI agree on what is changed, what is a conflict and what gets sent.

From the tool description: “Use when the user says something like ‘sync my stripe knowledge to contexthub’ or ‘share this with the team’. It sends only the pages you changed since the last sync.” Anything that reads as “publish what we just figured out” lands here. Filters let the agent push a subset: feature (an alias for tag), tag, or type.

ParameterTypeRequiredDescription
distill_donebooleannoSet true after authoring the source page that the previous PUSH_PAUSED response requested
featurestringnoTag to filter by (e.g. 'stripe')
messagestringnoCommit message
no_distillbooleannoSkip the decision-record handshake for this push
resolvedarray<string>norelpaths under .contexo/ that you merged after a MERGE_REQUIRED; your local content is kept, re-based on the server version you were shown, then pushed
source_slugstringnoSlug of the source page just written (required when distill_done=true)
tagstringnoAlias of feature
typeconcept | entity | source | analysisno

feature and tag are the same filter; feature wins if both are set. Both match tags case-insensitively. type filters on the page type. With no filter, every changed page is pushed.

resolved is how the agent finishes a merge. It lists page paths relative to .contexo/ (for example wiki/concepts/stripe-subscription.md) that the agent merged after a MERGE_REQUIRED. For each one, ctx_push keeps the local content, re-bases it onto the server version the agent was shown, and pushes it: the same thing ctx resolve <page> --ours does, followed by a push. See finishing a merge below. A resolved entry that is not a page path under .contexo/ (it must end in .md) fails with invalid_path before anything else runs.

  1. Plan, offline. The tool classifies the project without contacting the server and takes the pages in state modified or new that match the filters. Nothing else is ever sent: unchanged pages, conflicts, invalid pages, locally deleted pages and unverified pages stay where they are. With no changed page, no recorded conflict and nothing in resolved, the call ends with Nothing to push: no changed pages match the filters, which is not an error.
  2. Check the distill guard over those pages (and any resolved pages). See the distill handshake. Nothing is written or sent before it passes.
  3. Send through the sync engine. Under the project’s sync lock (.contexo/.sync/lock), ctx lists the server’s pages, classifies each page again against that live listing, and sends only the pages that are still modified or new. Each page carries its base sha as its parent. A page with no base is sent with expect_absent, so if the same path appeared on the server in the meantime, the server refuses it instead of overwriting it.
  4. Record what was accepted. For each page the server took, the sent bytes become the new base and the returned sha is recorded. The commit message is message, or agent push (N pages) by default.

The server commits each page as its own commit, all with the same message, so the reported HEAD is the last of them.

{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "ctx_push",
"arguments": {
"feature": "stripe",
"message": "stripe trials: subscription-level trial_end"
}
}
}

The result is a single text block. When the call pushed, its first line has this shape:

Pushed <N> page(s); HEAD=<first 7 characters of the new head>

Further lines can follow: the outcome of each resolved page, pages the engine did not send, warnings, and a merge brief when one is needed.

Resolved <path>: kept your version, re-based on server <sha>
Note: <path> needed no resolve (<why>)
Skipped <path> (<reason>): <what to do next>
Warning: <message>

Everything this tool returns (success, pause, merge brief, error) arrives as that one text block; the agent reads it and decides what to do next.

A push that would share knowledge pages while the session that produced them is still sitting in a capture buffer pauses instead. The tool returns <PUSH_PAUSED reason=distill_required> when all of these hold:

  • the pages about to be sent (the changed pages, plus any resolved pages) include at least one page whose local frontmatter type is concept or analysis, and
  • a capture buffer in raw/sessions/_pending/ has at least one recorded turn (a non-blank line) and was modified within the last 6 hours. Every pending buffer counts, not only the newest, and
  • no_distill and distill_done are not set, and
  • the environment variable CONTEXO_DISTILL_DISABLE is not 1.

This is the same check ctx push runs from the CLI; the difference is what happens next. The CLI refuses the push, while the MCP tool hands the agent the directive below. Entity-only or source-only pushes never trigger it, and neither does a project with no capture hooks installed.

The directive inlines the newest pending session’s turns and suggests a source slug made of today’s date, the first knowledge page’s slug and -decision. This is the template as the tool builds it; the placeholders in angle brackets are filled in:

<PUSH_PAUSED reason=distill_required>
Before pushing: write up the decision record, then make sure every named system
in this batch has its own entity page. Do the three steps below in order, then
re-invoke ctx_push with distill_done=true.
STEP 1 — call ctx_write_page with:
type: "source"
slug: "<YYYY-MM-DD>-<first page slug>-decision" (suggested; adjust topic to taste)
tags: [<the tags of the pages, space-separated>] (union of the batch's tags)
reasoning_summary: one-line summary of the decision (<= 100 chars)
body: follow the TEMPLATE below
TEMPLATE (drop sections that genuinely don't apply, keep them in this order):
## Decision
What we ended up doing (1-3 sentences).
## Why this approach
Bullets covering the load-bearing reasons.
## Rejected alternatives
- <alternative> -> rejected because <reason>
## Path of inquiry
1. ...
2. ...
## Dead-ends
- <thing we tried that didn't work> (so the next agent doesn't repeat it)
## Open questions
- Still TBD: ...
## Sources
- <doc URLs, related concept pages, etc.>
IMPORTANT: redact any API keys, tokens, passwords, or PII you encounter.
STEP 2 — entity coverage: if the concept/analysis pages reference named
systems, services, libraries, products, or databases that don't already have
a page under wiki/entities/, create one for each with ctx_write_page(type="entity").
Keep entity pages short: purpose, where it lives in this project, gotchas, and
links to related concepts. Skip this step if every named thing is already covered.
STEP 3 — call ctx_push again with the same filter args plus:
distill_done: true
source_slug: "<YYYY-MM-DD>-<first page slug>-decision"
That distill_done call will link the source into each changed concept/analysis page's
`sources:` frontmatter, archive the buffer, and push everything in one commit.
---
PAGES being pushed in this batch:
- <path> (<type>)
BUFFER (turn-by-turn summaries from this session, oldest first):
<one JSON turn record per line>
</PUSH_PAUSED>

A buffer over 50 turns is windowed to the first 10 and the last 40, with a truncation marker between them.

The second call carries distill_done: true and source_slug, plus the same filters as the first. source_slug is the slug of the page just written with type: "source"; the tool reads raw/sessions/<source_slug>.md and refuses to continue if it is not there:

{
"jsonrpc": "2.0",
"id": 13,
"method": "tools/call",
"params": {
"name": "ctx_push",
"arguments": {
"feature": "stripe",
"distill_done": true,
"source_slug": "2026-07-28-stripe-subscription-decision"
}
}
}

That call does not re-check the guard. It:

  • appends source_slug to the sources: frontmatter of every changed concept and analysis page in the batch, bumps their updated, and writes them back to disk;
  • adds the source page itself to the pages being pushed;
  • pushes, and once the push reached the server, archives the capture buffer (it moves to raw/sessions/_pending/_archive/), so the same session cannot trigger the handshake twice.

If linking or pushing fails, the error ends with Nothing was pushed; the capture buffer is still pending.

Set no_distill: true to skip the handshake for one push. That is what ctx_migrate and ctx_generate tell the agent to do, since imported or code-derived pages have no session behind them.

A page that you and a teammate both changed is never overwritten. ctx_push finds such pages in three places:

  • pages already recorded as conflicts (a conflict sidecar from an earlier push or pull) are not sent;
  • pages the live listing shows as changed on both sides are skipped with reason conflict;
  • pages the server refuses with a 409 during the send (a teammate’s push landed in between) get a conflict sidecar in .contexo/.sync/conflicts/. Their recorded sha and base stay exactly as they were.

Other pages in the same call are still pushed. Every page that needs a merge goes into one <MERGE_REQUIRED origin=push> brief. When the call tried to push pages, the brief follows the Pushed <N> page(s) line, which reads Pushed 0 page(s) when none of them went through. The page stays a conflict until it is resolved.

The brief carries, per page: the ancestor (base) sha both sides diverged from, the server’s sha, a per-section summary of what each side changed, the sections changed by both sides, and all three full versions:

<MERGE_REQUIRED origin=push>
<N> page(s) changed both locally and on the server since your last sync. They were not pushed; the server keeps the other version.
Local sync state was NOT changed; the page stays a conflict until you resolve it.
─── FILE 1/<N>: <path> ───
ancestor (both diverged from): <base sha, 7 characters, or (none)>
server now: <server sha>
YOUR changes vs ancestor:
<one line per changed section>
SERVER changes vs ancestor:
<one line per changed section>
CONFLICTING regions (both sides changed):
<section heading> — synthesize both edits into one coherent version
--- ANCESTOR VERSION ---
<full page>
--- YOUR VERSION (your local file) ---
<full page>
--- SERVER VERSION ---
<full page>
--- END OF FILE 1/<N> ---
STEPS:
1. For each file above, call ctx_write_page (same slug and type) with merged content that
keeps BOTH sides' changes. Where both sides changed the same section, synthesize one
coherent version; don't just pick one.
To accept the server version instead, write the SERVER VERSION verbatim.
2. Call ctx_push with the same filters plus resolved=["<path>"].
ctx_push keeps your merged content, re-bases it on the server version shown above and
pushes it. If the server moved again meanwhile, you get a fresh MERGE_REQUIRED instead
and nothing is overwritten.
</MERGE_REQUIRED>

When no section was changed by both sides, the conflicting-regions block is replaced by (No section was modified by both sides — auto-mergeable in principle.). The agent still has to write the merged page, but it can take both sides wholesale. Agent directives has the full rules of the brief.

After writing the merged page with ctx_write_page, the agent calls ctx_push again with the same filters plus resolved: ["<path>"]. For each resolved page the tool:

  1. looks up the server sha the agent was shown: the last brief this MCP process returned for the page, else the server sha in the page’s conflict sidecar;
  2. runs resolve --ours guarded by that sha: the local file is kept, the shown server version becomes the base, and the page turns modified;
  3. pushes it with the rest of the changed pages.

The result line is Resolved <path>: kept your version, re-based on server <sha>. If the server moved again since the brief, nothing is resolved or sent for that page and the agent gets a fresh MERGE_REQUIRED with the newer version. A page that needed no resolve (it is not on the server, or the server has not changed it) gets a Note: line and is pushed as it is. A page whose state makes --ours meaningless (incoming, incoming_new, deleted_local, invalid) is refused, and the error ends with Nothing was pushed.

Drift and conflicts walks through the whole cycle, including how to resolve by hand without an agent.

Sync failures come back as an error result in this shape:

ctx_push failed: <code>: <message>
Fix: <advice>; <advice>
Retryable: yes

<code> is one of the stable error codes that ctx push --json uses. Fix: appears when ctx has advice for the code. Retryable: yes appears only for network, server_error, rate_limited and busy. A busy error adds a line naming the process that holds the sync lock:

Busy: the sync lock is held by "<command>" (pid <pid>): another ctx (a teammate CLI or the VS Code extension) is syncing; retry in a few seconds.
Text Cause
ctx_push failed: not_authenticated: not authenticated with Fix: ctx auth login No credentials or an empty token
ctx_push failed: server_unresolved: … with Fix: ctx auth login --server <url> Global credentials that name no server
ctx_push failed: repo_not_configured: repo not configured with Fix: ctx remote set-repo <id> No repo_id in .contexo/config.json
ctx_push failed: invalid_path: … A resolved entry is not a page path under .contexo/
ctx_push: distill_done=true requires source_slug (the slug just authored with ctx_write_page type=source) distill_done set without naming the source page
ctx_push: distill_done set but source page "…" not found at raw/sessions/….md — … The source page was never written, or its slug differs
Nothing to push: no changed pages match the filters No changed page matches (not an error result)

ctx push runs the same engine with the same filters and sends only changed pages:

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

When a teammate’s push lands first, ctx push --json writes its document and then the error envelope, and exits 6:

ctx push --json (lost a race)
{
  "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)
FlagTypeDefaultDescription
--dry-runboolfalseshow what would be pushed without sending or writing anything
--fallback-serverboolfalse(planned) route decision-record distillation to the Contexo server; currently errors
--featurestringpush only pages tagged with this feature/tag
--jsonboolfalseemit the ctx.push.v1 JSON document; never prompts or previews (implies --yes and --no-preview)
--message, -mstringcommit message (default: 'ctx push (N pages)')
--no-distillboolfalsepush even when an un-distilled agent session is pending (bypasses the decision-record guard; use deliberately)
--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)

Two differences from the MCP tool:

  • The distill guard refuses instead of pausing. With a pending session and a knowledge page in the batch, ctx push stops (exit 1, or distill_required and exit 9 with --json) and prints a prompt to give your agent. --no-distill or CONTEXO_DISTILL_DISABLE=1 bypasses it, and the push document records that as distill.bypassed.
  • No merge brief. On a conflict the CLI lists the pages and points at ctx resolve --ours or --theirs; the MCP tool returns MERGE_REQUIRED and finishes the merge through resolved.
ctx push --yes (pending session)
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)

See ctx push for the full command reference.