Skip to content

Working as a team

Access in Contexo is per repo. You are a member of a repo or you are not; there are no per-page or per-directory permissions. A repo has exactly two roles.

Role Can
owner everything a member can, plus mint / list / revoke invite keys and remove members
member pull and push pages, read history, list members, read the activity feed

You become an owner by creating the repo — from the dashboard, or implicitly the first time ctx push targets a repo id that doesn’t exist yet. You become a member by redeeming an invite key. There is no endpoint or command to change somebody’s role afterwards: the member routes are exactly GET /v1/repos/:id/members and DELETE /v1/repos/:id/members/:userId, and nothing promotes an existing member. owner is only ever written when a repo is created, or when a repo that has no owner row is claimed — which is also how the very first user to sign in adopts repos created earlier under a shared key.

An owner mints a key and sends the token to the teammate:

Terminal window
ctx invite mint --label "sam laptop"

The output is three lines: the ctxi_… token, then expires 2026-08-04 12:00 UTC · label "sam laptop", then share this with a teammate; they run 'ctx join <token>'. Keys live for seven days and can be revoked before that.

The token is shown once, at mint time. The server stores only a hash, so ctx invite list can never print it again — lose it and you mint another.

ctx invite list shows a repo’s keys:

ctx invite list
no invite keys

Once keys exist, each row is <key-id> active|expired expires <date> UTC <label>. Two things to know: the list includes expired keys — the active/expired word is computed by the CLI by comparing the expiry to the current time, not filtered server-side — and ctx invite revoke takes the short key id from the first column, never the ctxi_… token.

The teammate needs three things before ctx join will work: a .contexo/ directory, credentials, and a server URL.

Terminal window
ctx init # creates .contexo/, wires their agents
ctx login # browser sign-in, mints a personal access token
ctx join ctxi_… # redeems the invite
ctx pull --full # fetch everything the team already knows

ctx login fills the server URL in for them — it defaults to the hosted https://api.contexo.live when nothing is configured yet. Against a self-hosted server, pass ctx login --server https://… (or run ctx remote set first), otherwise ctx join stops with join: server URL not set.

ctx join writes the resolved repo id into .contexo/config.json, so push and pull work immediately afterwards. It prints Joined <repo-id> as member. 'ctx pull' to fetch existing pages.

ctx pull --full
Pulled 1 page(s); HEAD=a4b2c1d3

Re-joining is idempotent — redeeming a key you’ve already used just confirms the membership you already have, and it does not consume a member slot. Nor does it demote an owner who redeems a key for their own repo: an existing role is preserved.

Their agents are wired by ctx init, not by the invite — see connect your agent. At that point their agent can read the team’s pages, and the daily flow applies to them too.

ctx members
no members

Once people have joined, each row is the member’s email, their role, and joined <date>, ordered by when they joined (oldest first). Any member may list members.

Removing somebody is owner-only:

Terminal window
ctx members remove sam@example.com

The CLI resolves that email against the member list first (case-insensitively) to get the user id, so the command only works for people you can already see. Three refusals are worth recognising:

  • only an owner can remove members — a 403 from the server; you hold member.
  • can't remove the last owner — a repo must always have at least one owner, and no route promotes a member, so a sole owner cannot hand the repo over and leave.
  • not a member of this repo — the id resolved but the row was already gone.

Removal revokes access to future pulls and pushes. It does not rewrite history: commits that person authored keep their name and email, which is what makes ctx history useful a year later.

ctx members tells you who can read the repo. ctx activity tells you who actually did.

ctx activity
no activity yet
ctx activity --limit 5
no activity yet

Once events exist, each row is the actor’s email, pushed or pulled, and a UTC timestamp. What gets recorded is narrower than you might assume:

  • A push is recorded only when at least one file was actually committed. A push that 409s on every file records nothing.
  • A pull is recorded only when pages were actually received. Routine “already up to date” polls — which agents make constantly — never reach the feed.
  • Actions taken with a legacy shared API key are recorded not at all: there is no user id to attribute them to.
  • The feed keeps the newest 1000 events per repo; older ones are pruned as new ones arrive. --limit is capped at 500 per request by the server.

Each event carries a detail blob that the CLI’s three-column output doesn’t print:

  • push → {"paths":["wiki/concepts/stripe-subscription.md", …]}, capped at 50 paths so one enormous push can’t bloat the row.
  • pull → {"client":"claude-code"}, taken from the X-Contexo-Client header.

That client name is how you tell a human pull from an agent pull. The MCP server forwards whatever the agent sent as clientInfo.name in its initialize request — claude-code, cursor, and so on — while the CLI sends the literal ctx-cli. An agent that announces no client name produces a pull event with no detail at all.

The detail is returned by GET /v1/repos/:id/activity. To see it from a terminal today, query that endpoint directly rather than expecting ctx activity to show it.

A self-hosted server applies no caps — the open-source build injects an unlimited quota policy, so repo and member counts are unbounded. The hosted server at https://api.contexo.live injects a plan policy instead. When a cap is hit, the API answers 402 Payment Required with a structured body:

{
"error": "…upgrade prompt…",
"code": "quota_members",
"limit": 3,
"upgrade_url": "https://contexo.live/#pricing"
}

code is quota_repos (creating one repo too many) or quota_members (one join too many). The cap applies to the repo’s owners, not the joiner — a repo is uncapped if any of its owners is subscribed. Re-joins never count against it.

For the person joining:

  1. Install ctx and run ctx init in the project root.
  2. ctx login.
  3. ctx join <invite-key>.
  4. ctx pull --full.
  5. ctx status — confirm server, repo, authentication and page count.
  6. ctx mcp status — confirm their agents are wired to the knowledge base.
ctx mcp status
Agent integrations for this project:

  AGENT        MCP SERVER                CAPTURE (STOP HOOK)
  Claude Code  wired (.mcp.json)         installed
  Cursor       wired (.cursor/mcp.json)  installed
  Codex        not installed             not installed

Add another agent (Windsurf, OpenCode, Hermes, ...)? Run `ctx mcp guide`.