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.
When the agent calls it
Section titled “When the agent calls it”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.
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
distill_done | boolean | no | Set true after authoring the source page that the previous PUSH_PAUSED response requested |
feature | string | no | Tag to filter by (e.g. 'stripe') |
message | string | no | Commit message |
no_distill | boolean | no | Skip the decision-record handshake for this push |
resolved | array<string> | no | relpaths 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_slug | string | no | Slug of the source page just written (required when distill_done=true) |
tag | string | no | Alias of feature |
type | concept | entity | source | analysis | no |
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.
What a plain push does
Section titled “What a plain push does”- Plan, offline. The tool classifies the project without contacting the server and
takes the pages in state
modifiedornewthat 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 inresolved, the call ends withNothing to push: no changed pages match the filters, which is not an error. - Check the distill guard over those pages (and any
resolvedpages). See the distill handshake. Nothing is written or sent before it passes. - 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 stillmodifiedornew. Each page carries its base sha as its parent. A page with no base is sent withexpect_absent, so if the same path appeared on the server in the meantime, the server refuses it instead of overwriting it. - 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, oragent 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.
Request and response
Section titled “Request and response”{ "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.
The distill handshake
Section titled “The distill handshake”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
resolvedpages) include at least one page whose local frontmatter type isconceptoranalysis, 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_distillanddistill_doneare not set, and- the environment variable
CONTEXO_DISTILL_DISABLEis not1.
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 systemin this batch has its own entity page. Do the three steps below in order, thenre-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 namedsystems, services, libraries, products, or databases that don't already havea 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, andlinks 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.
Re-invoking with distill_done
Section titled “Re-invoking with distill_done”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_slugto thesources:frontmatter of every changedconceptandanalysispage in the batch, bumps theirupdated, 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.
When someone else pushed first
Section titled “When someone else pushed first”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.
Finishing a merge: resolved
Section titled “Finishing a merge: resolved”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:
- 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;
- runs resolve
--oursguarded by that sha: the local file is kept, the shown server version becomes the base, and the page turnsmodified; - 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.
Errors
Section titled “Errors”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) |
CLI equivalent
Section titled “CLI equivalent”ctx push runs the same engine with the same filters and sends only changed pages:
About to push 1 page(s) to acme-api:
[NEW] wiki/concepts/stripe-subscription.md
Pushed 1 page(s); HEAD=a4b2c1d3When a teammate’s push lands first, ctx push --json writes its document and then the error
envelope, and exits 6:
{
"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)| 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) |
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 pushstops (exit 1, ordistill_requiredand exit 9 with--json) and prints a prompt to give your agent.--no-distillorCONTEXO_DISTILL_DISABLE=1bypasses it, and the push document records that asdistill.bypassed. - No merge brief. On a conflict the CLI lists the pages and points at
ctx resolve--oursor--theirs; the MCP tool returnsMERGE_REQUIREDand finishes the merge throughresolved.
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.
