CLI & MCP
CLI and MCP server for Cotext preferences. Reads the .cotext folder
synced by the browser extension and exposes the active profile's
prompt to your terminal and to MCP-aware AI clients.
Install
npm install -g cotext
This installs two binaries: cotext (CLI) and cotext-mcp (stdio MCP
server).
CLI
Point the CLI at the folder the browser extension is syncing to:
cotext init # interactive — saves to ~/.config/cotext/config.json
cotext status # source, active profile, last sync
cotext list # all profiles (* marks the active one)
cotext pull # active prompt to stdout — pipe wherever
cotext pull --profile code-review
cotext cat <slug> # full markdown including header
cotext signal "be terser" --type dislike --tag too-verbose
cotext profile create "Code review" --starter code-review --activate
cotext daemon # process the folder's inbox in real time
cotext synth # one-shot library+prose synthesis on active profile
cotext mcp-install # register cotext-mcp with Claude Code / Codex
cotext mcp-prompt # print the CLAUDE.md snippet for agents
A common pattern is to refresh CLAUDE.md from the active profile:
cotext pull > CLAUDE.md
Recording signals
cotext signal drops a feedback signal into <folder>/inbox/; the
browser extension picks it up on next popup open, runs interpretation,
and folds the result into the active profile. Signals carry the same
shape as browser 👍/👎 reactions — see below for the full flag set.
# Minimal — note only
cotext signal "be more concise" --type dislike
# With a preset tag
cotext signal --type dislike --tag too-verbose
# With conversation context (gives the interpreter something to ground
# the rule against — without this, the note is the only signal)
cotext signal "this format is exactly right" --type like \
--response "$(cat /tmp/last-claude-output.txt)" \
--prompt "explain the redux flow"
# Pipe stdin as the AI response — most ergonomic with Claude Code
claude --print "explain X" | cotext signal "good shape" --type like
# Read from files
cotext signal "skip the boilerplate" --type dislike \
--response-file /tmp/output.txt \
--prompt-file /tmp/prompt.txt
Full signal flag set:
| Flag | Purpose |
|---|---|
--type <like|dislike|tag> | Signal kind (default: tag) |
--tag <slug> | Preset tag (e.g. too-verbose, preachy) |
--profile <slug> | Target profile (sets context.contextProfile) |
--response <text> | The AI response the signal is about |
--response-file <path> | Read the AI response from a file |
--prompt <text> | The user prompt that produced the response |
--prompt-file <path> | Read the user prompt from a file |
--provider <name> | Provider hint (default: cli) |
If --response/--response-file are omitted and stdin isn't a TTY, the
CLI reads stdin as the AI response. --type defaults to tag (neutral
note); --type like/dislike carry polarity.
Creating profiles
cotext profile create <name> creates a profile against whichever
source is configured.
- Folder source: queues a
create-profilecommand into<folder>/inbox/. The browser extension materializes it on the next popup open — until then it won't appear incotext list. - Cloud source (
COTEXT_API_TOKEN): builds the profile from the starter pack locally and POSTs it directly tocotext.io/api/push— visible immediately athttps://cotext.io/@<your-username>/<slug>.
# Minimal — new "Code review" profile, default context, no seed
cotext profile create "Code review"
# Seeded from a starter pack + activated immediately
cotext profile create "Code review" --starter code-review --activate
# Free-form context appears in the synthesized prompt header and
# biases LLM passes. Description lands as customInstructions.
cotext profile create "Writing" \
--context "product copy and internal docs" \
--description "I write product copy and internal docs"
# Free-form context + explicit /explore category. Useful when the
# context wouldn't map to a curated slug on its own.
cotext profile create "Startup code review" \
--context "code review at a 50-person startup" \
--category coding \
--starter code-review --activate
Full profile create flags:
| Flag | Purpose |
|---|---|
--context <text> | Free-form descriptor of what this profile is for. Appears in the synthesized prompt header and biases every LLM pass. e.g. coding, "code review at a 50-person startup". Default: general. |
--category <slug> | Curated category for the /explore listing on cotext.io. One of: general, coding, writing, research, creative, learning, business, productivity, analysis, support. Defaults to derive from --context (falls back to general if the context is free-form and doesn't match a slug). |
--starter <pack> | Seed entries + metrics from a starter pack id |
--description <text> | Free-form note kept verbatim as customInstructions |
--activate | Make this the active profile after creation |
Starter pack ids: code-review, email-writing, research,
daily-driver, tutor, patient-explainer, strategic,
creative-writing, brainstorm, sprint-planner, data-analyst,
support-helper. Run cotext list after opening the popup to see
how they materialize.
Daemon — Cotext without the browser extension
cotext daemon is a long-running process that drains the folder's
inbox/ in real time. Use it when:
- You don't have (or don't want) the browser extension installed
- You want CLI signals to be interpreted within seconds, not on the next popup open
- You're running Cotext on a server / headless box
The daemon supports three providers: Ollama (default), Anthropic Claude, and OpenAI. Pick whichever fits your environment.
# Default — Ollama running locally
ollama serve &
ollama pull qwen3.5:4b
cotext daemon
# Anthropic Claude — no Ollama install needed
export COTEXT_ANTHROPIC_KEY=sk-ant-...
cotext daemon --provider anthropic
# OpenAI
export COTEXT_OPENAI_KEY=sk-...
cotext daemon --provider openai
# Override endpoint / model / poll interval (Ollama)
cotext daemon --endpoint http://192.168.1.5:11434 --model llama3.2:3b --poll 1000
# Override cloud-provider model
cotext daemon --provider anthropic --anthropic-model claude-sonnet-4-6
When --provider isn't given, the daemon picks one based on which
keys are set: Anthropic key → Anthropic; else OpenAI key → OpenAI;
else Ollama. Easy to flip between providers via env vars without
touching the command line.
What the daemon does, on every poll (every 1.5s by default):
- Reads
<folder>/inbox/*.json - For each signal file: reads the active profile's
contextandinterpretationHintfrom disk, calls Ollama to interpret with both injected into the prompt, appends a newPreferenceEntryto the active profile, regenerates<slug>.json+<slug>.md, appends tosignals.jsonl, deletes the inbox file - For each command file (e.g.
cmd-*.json): creates the new profile on disk, optionally updatesactive.txt
The daemon picks up the active profile's interpretationHint (set
per-profile from the extension popup → Advanced → "Interpretation
hint") on every signal. Edit the hint in the popup; the next signal
the daemon processes will use it. No restart needed.
After 30s of cotext signal-ing in another terminal, you'll see the
new entries materialize in the profile markdown. Run cotext pull to
verify.
Cloud-only mode (no folder, no extension)
If no folder is configured but COTEXT_API_TOKEN is set, the daemon
runs against cotext.io directly:
# Headless server / remote machine — no browser, no folder
export COTEXT_API_TOKEN=ctx_...
# Pick a provider — same three options as folder mode:
# - Ollama running locally, OR
# - Anthropic via COTEXT_ANTHROPIC_KEY, OR
# - OpenAI via COTEXT_OPENAI_KEY
export COTEXT_ANTHROPIC_KEY=sk-ant-...
# Bootstrap an initial profile if you don't have one yet
cotext profile create "Daily" --starter daily-driver --activate
# Start the daemon
cotext daemon
What changes vs folder mode:
- The daemon polls
https://cotext.io/api/signals/pendinginstead of<folder>/inbox/(every 30s by default — cloud cadence is lower than folder). - Each interpreted signal appends a rule to the profile's prompt and
pushes the updated profile back via
/api/push. No file system involved. - No
signals.jsonlmirror — the server'sRemoteSignaltable is the canonical signal log.
This makes the extension genuinely optional — a CLI-first user on a
remote machine can run the whole loop (cotext signal → daemon
interprets → profile updates) without ever opening a browser.
Full synthesis (librarian + prose passes)
After every Nth appended entry (default 3, controlled by
--synth-every), the daemon runs the same two-pass synthesis the
extension does: a librarian pass to dedupe and group rules into
a clean library, then a prose pass to write the "Core Style"
summary paragraph. The output matches what you'd get from opening
the popup and pulling — same prompt header, same metric anchors,
same section layout — so the daemon's polished render is
indistinguishable from the extension's.
Spend control:
cotext daemon --synth-every 0 # disable cadence — run cotext synth manually
cotext daemon --synth-every 10 # synthesize once every 10 entries instead
cotext synth # one-off synthesis pass, any time
Cloud-provider users (Anthropic / OpenAI) usually want a higher
--synth-every since each pass costs two extra LLM round-trips per
signal. Ollama users can leave it at the default.
Daemon limitations (v1)
- No screenshots / images. Vision-aware interpretation is
extension-only today — the daemon ignores the
screenshotDataUrlfield on signals.
Don't run the daemon AND the extension at once
Both processes watch the same folder. The race on inbox-file deletes is harmless (whoever wins, wins), but two writers to the profile JSON can clobber each other. Pick one:
- Browser extension only — default workflow. Use the extension's popup, occasionally use the CLI to read or to queue signals.
- Daemon only — headless / server / CLI-first workflow.
The daemon refuses to start if another daemon is already running on
the same folder (PID lockfile at <folder>/.cotext-daemon.lock).
There's no equivalent guard against the extension; respect the rule
manually until coordination ships.
Running as a background service
The daemon is a foreground process by default. To run it under a service manager:
macOS (launchd) — drop a plist at ~/Library/LaunchAgents/io.cotext.daemon.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>io.cotext.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/cotext</string>
<string>daemon</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/cotext-daemon.log</string>
<key>StandardErrorPath</key><string>/tmp/cotext-daemon.err</string>
</dict>
</plist>
Then launchctl load ~/Library/LaunchAgents/io.cotext.daemon.plist.
Linux (systemd) — drop a unit at ~/.config/systemd/user/cotext-daemon.service:
[Unit]
Description=Cotext daemon
After=network.target
[Service]
ExecStart=/usr/local/bin/cotext daemon
Restart=on-failure
[Install]
WantedBy=default.target
Then systemctl --user enable --now cotext-daemon.
Source discovery
Priority chain (highest first):
--folder <path>flag → folder sourceCOTEXT_FOLDERenvironment variable → folder source~/.config/cotext/config.json(or$XDG_CONFIG_HOME/cotext/config.json) → folder sourceCOTEXT_API_TOKENenvironment variable → cloud source (https://cotext.io, override withCOTEXT_API_URL)
Folder always wins when both are set — connecting a folder switches you off the cloud automatically.
MCP server
cotext-mcp speaks MCP over stdio and exposes the active profile to any
compatible client.
Resources
| URI | Description |
|---|---|
cotext://preferences/active | Active profile's synthesized prompt (markdown) |
cotext://profiles | JSON summary of every profile |
cotext://profiles/{slug} | One profile's prompt by slug |
Tools
| Name | Args | Returns |
|---|---|---|
get_preferences | { profile?: string } | Prompt text for the given (or active) profile |
list_profiles | — | JSON summary list |
record_signal | { type?, tag?, note?, aiResponse?, userPrompt?, profile? } | Drops a feedback signal into the inbox (folder source) or POST /api/signals (cloud source). |
The expectation is that the assistant calls record_signal whenever the
user expresses a preference ("be more concise", "this format works") so
the loop closes back into the profile pipeline.
Wiring (one command)
cotext mcp-install
Registers cotext-mcp with Claude Code (~/.claude.json) and Codex
(~/.codex/config.toml) in one go. Idempotent — re-running does
nothing if the entry already matches. Skips Codex silently if
~/.codex doesn't exist.
cotext mcp-install --claude # only Claude Code
cotext mcp-install --codex # only Codex
cotext mcp-install --print-only # show the snippets, don't write
mcp-install writes the absolute path of cotext-mcp.js and the Node
binary running the install, so the MCP client doesn't need anything on
its PATH to find the server. Restart your AI client after running
this.
Teaching the agent to use it
The MCP server is wired, but the agent still needs to know when to
call get_preferences (load your style) and record_signal (capture
inline preferences). Pipe the canonical snippet into your CLAUDE.md
(or AGENTS.md for Codex):
cotext mcp-prompt >> ~/.claude/CLAUDE.md
# or for a single project:
cotext mcp-prompt >> ./CLAUDE.md
The snippet is short, self-contained, and tells the agent:
- Call
get_preferencesonce at session start, apply the returned style - Call
record_signalwhenever the user expresses a preference ("be more concise", "call me John", "this format is perfect") - Don't ask for confirmation — the user already opted in by installing Cotext
Run cotext mcp-prompt on its own to read the full text before pasting.
Manual wiring (if you prefer)
mcp-install is doing two things you can do by hand:
Claude Code — merge into ~/.claude.json top-level mcpServers:
{
"mcpServers": {
"cotext": {
"command": "cotext-mcp",
"args": []
}
}
}
Codex — append to ~/.codex/config.toml:
[mcp_servers.cotext]
command = "cotext-mcp"
args = []
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS) uses the same JSON shape as Claude Code.
For cloud-only setups (no local folder), pass the API token via env:
{
"mcpServers": {
"cotext": {
"command": "cotext-mcp",
"env": { "COTEXT_API_TOKEN": "ctx_..." }
}
}
}
The server re-resolves the source on every request, so prompt edits in the extension (or remote profile changes) show up live without a client restart.
How the folder is populated
The browser extension writes one <slug>.json + <slug>.md pair per
profile, plus active.txt pointing at the current one. This package
only writes to <folder>/inbox/ (signals); profile artifacts are owned
by the extension.
Cloud surface
CloudSource calls the following endpoints on cotext.io (or
COTEXT_API_URL), all authenticated with
Authorization: Bearer ${COTEXT_API_TOKEN}:
| Method | Path | Body / Result | Status |
|---|---|---|---|
| GET | /api/preferences/active | { profile: ProfileBundle } | shipped |
| GET | /api/profiles | { profiles: ProfileSummary[] } | shipped |
| GET | /api/profiles/{slug} | { profile: ProfileBundle } | shipped |
| POST | /api/signals | { ...RecordSignalInput } → { id } | shipped |
| GET | /api/signals/pending | { signals: [...], hasMore } | shipped |
| POST | /api/signals/{id}/drained | { ok: true } | shipped |
All endpoints are shipped. The signals surface is a cloud inbox, not
a server-side interpreter: POST /api/signals accepts + stores; a local
drainer (browser extension service worker, or cotext daemon with
cloud credentials) polls /pending, runs LLM interpretation locally,
then calls /{id}/drained to mark each one consumed. The LLM stays on
the user's machine — same privacy posture as folder mode.
Cloud-side signal recording works cotext signal against a cloud-only
configuration today; you'll need a local drainer running somewhere
(browser extension OR daemon) for those signals to actually fold into
the profile. Without a drainer the signals just queue at /pending.
The "active" profile is set when the extension pushes with
setActive: true (or, if never set, falls back to the most recently
updated profile — same semantics as a missing active.txt).
Development
npm install
npm run build:dev # esbuild bundle, sourcemaps on
npm run watch # rebuild on change
See also
- Getting started — first install + first profile
- Architecture — how the .cotext folder contract works
- Developer setup — CLI install, MCP wiring, daemon mode
- Self-hosting — run your own
cotext.iodeployment