Deployment & config
How to run the CTXone Hub, and the configuration choices that matter — the shared daemon (the standard setup), running it as a service, giving an agent its own memory, and securing the hub with a token when you expose it beyond localhost. If you followed the Quickstart and everything works, you already have the standard setup; this page is the reference for the rest.
The one rule that governs every setup
Section titled “The one rule that governs every setup”One hub owns one db file at a time. On startup a hub writes a PID lockfile
next to the db (<db>.lock). If a second hub tries to open the same --path
while the first is alive, it refuses to start:
database is already locked by ctxone-hub pid 12345 (lockfile: …/memory.db.lock);refusing to start a second hub against the same dbThis is deliberate — two hubs writing one SQLite file corrupts it (the 2026-04-28 incident). See DATA_SAFETY.md. Everything below is really about who holds that single lock.
How a hub can run
Section titled “How a hub can run”ctxone-hub has two transports for talking to agents, plus the HTTP surfaces:
| Mode | Command | Serves | Owns the db |
|---|---|---|---|
| MCP stdio | ctxone-hub --path DB | MCP tools to the spawning agent | yes, while the agent runs |
| HTTP (+Lens) | ctxone-hub --http --lens --path DB | REST API + web UI + MCP at /mcp | yes, while the daemon runs |
The standard setup is one shared HTTP daemon. An --http hub serves MCP (at
/mcp, Streamable HTTP) + REST + the Lens web UI from one process, and
ctx init (default --transport http) points every tool at it by URL — no
lockfile race, web UI always up, all tools share one graph. ctx init --transport stdio is the escape hatch: each tool spawns its own hub that holds
the lock, so you can’t also run a web UI on that db. Design notes:
the unified-hub design.
Choosing a topology
Section titled “Choosing a topology”Most setups want Topology B (the shared daemon) — it’s what ctx init writes
by default. Topology A is the no-daemon fallback; C and D cover isolation and
one-off inspection.
A. Agent-only stdio (no daemon, fallback)
Section titled “A. Agent-only stdio (no daemon, fallback)”ctx init --transport stdio — each tool spawns and owns its own stdio hub.
Nothing else touches the db.
- ✅ Zero extra processes, no port, no daemon to run.
- ❌ No Lens; can’t attach the CLI-over-HTTP to this db while the agent runs (the CLI reads the same graph, but only through a running HTTP hub). One tool per db — a second tool needs its own db (topology C).
B. Shared HTTP daemon — the default & standard
Section titled “B. Shared HTTP daemon — the default & standard”Run one long-lived hub that serves everything, and point agents, the CLI, and
Lens at it. Nothing spawns its own child; there is exactly one db owner. This is
what a plain ctx init configures.
# 1. Run the one daemon (ideally as a login service — see below — so it's up at boot).ctxone-hub --http --lens --path ~/.ctxone/memory.db
# 2. Point the CLI at it (defaults to http://localhost:3001, so usually optional).export CTX_SERVER=http://localhost:3001
# 3. Configure your tools to connect by URL. http is the default, so plain:ctx init # all detected toolsctx init --tool claude # or one toolctx init (default --transport http) writes an MCP entry like:
{ "mcpServers": { "ctxone": { "type": "http", "url": "http://localhost:3001/mcp?namespace=<detected>" } }}The ?namespace= is filled in from the project detected in the current directory
(same scoping a per-project stdio hub would use). Override the endpoint with
--mcp-url, e.g. --mcp-url http://mybox:3001/mcp.
- ✅ One process, one lock, startup order irrelevant — the reboot race is gone. Lens always up, independent of any agent; multiple agents share one graph.
- MCP clients reach the daemon two ways, and
ctx init --transport httpwrites the right one per client:- Native URL — Claude Code, Cursor, VS Code get
{"type":"http","url":…}. mcp-remotebridge — Claude Desktop has no native HTTP MCP support, soctx initwrites an stdio bridge (npx -y mcp-remote <url> --transport http-only) that proxies to the daemon. Requires Node/npx on PATH; a note is printed when this is used.- Codex — native HTTP via a
urlkey inconfig.toml([mcp_servers.ctxone]\nurl = "…/mcp?namespace=…").
- Native URL — Claude Code, Cursor, VS Code get
- ⚠️ A single daemon has no per-cwd project detection, so one agent config maps to one namespace (baked into the URL). An agent that roams many repos uses one fixed namespace or per-workspace configs. Need per-cwd auto-scoping? Use stdio (topology A) — it’s still fully supported.
C. Give an agent its own dedicated db (isolation)
Section titled “C. Give an agent its own dedicated db (isolation)”Point one agent at a different --path so it never contends with your shared
hub or another agent. Two separate files → two separate locks → no conflict, and
two independent memory graphs.
Edit that agent’s MCP config (or run ctx init --tool <name> after setting the
path). The stdio entry looks like:
// e.g. ~/Library/Application Support/Claude/claude_desktop_config.json{ "mcpServers": { "ctxone": { "command": "/opt/homebrew/bin/ctxone-hub", "args": ["--path", "/Users/you/.ctxone/claude.db", "--agent-id", "claude-desktop"] } }}Use this when:
- You want Claude’s memory kept separate from a shared/team graph.
- Two agents on one machine should not see each other’s memory.
- You’re experimenting and don’t want to touch your main graph.
Isolated dbs do not sync. A fact remembered in
claude.dbis invisible to the hub servingmemory.db. To browse an isolated db in Lens, run a hub on it at a different port while the agent is stopped, or on a copy:ctxone-hub --http --lens --path /Users/you/.ctxone/claude.db --port 3002.
D. Browse a live agent’s memory without fighting the lock
Section titled “D. Browse a live agent’s memory without fighting the lock”You can’t open a second hub on a locked db. To look at the graph an agent is actively using, snapshot it and serve the copy:
cp ~/.ctxone/memory.db /tmp/lens.dbctxone-hub --http --lens --path /tmp/lens.db --port 3002Read-only snapshot; it won’t reflect writes after the copy. (The proper fix is the shared daemon in topology B — one owner, Lens live on the same graph the agents write.)
Configuration reference
Section titled “Configuration reference”Where the db lives
Section titled “Where the db lives”ctx init points agents at the canonical path:
- macOS / Linux:
~/.ctxone/memory.db - Windows:
%APPDATA%\ctxone\memory.db
Override per agent with --path (topology C). A bare ctxone-hub with no
--path defaults to ./target/ctxone.db (dev convenience) — always pass
--path in real setups.
Useful flags
Section titled “Useful flags”| Flag | Purpose |
|---|---|
--path <PATH> | SQLite db file (per-agent isolation lives here) |
--http | Enable the REST API |
--lens | Serve the web UI at / (requires --http) |
--port <N> | HTTP port (default 3001) — use distinct ports for parallel dbs |
--agent-id <NAME> | Attribution recorded on commits (ctx blame) |
--auth-token <TOK> | Bearer token guarding REST + /mcp (env CTXONE_AUTH_TOKEN); loopback exempt |
--allowed-origin <ORIGIN> | Extra browser origin allowed to call the API (repeatable; same-origin is always allowed) |
--init | Create the db file if missing (guards against path typos) |
--storage memory | Ephemeral, no file, no lock — good for tests/demos |
--asd-path name=PATH | Register an ASD code-graph repo for the process pool |
Environment
Section titled “Environment”| Var | Effect |
|---|---|
CTX_SERVER | Hub URL the ctx CLI targets (default http://localhost:3001) |
CTX_NAMESPACE | Project namespace for CLI/MCP scoping |
CTX_BRANCH | Default memory branch/ref |
CTX_TOKEN | Bearer token the ctx CLI sends (for a remote authed hub) |
CTXONE_AUTH_TOKEN | Same as --auth-token (bearer guarding REST + /mcp) |
CTXONE_ALLOWED_ORIGINS | Comma/space-separated extra allowed browser origins |
CTXONE_BACKUP_KEEP | Startup snapshots to retain (default 5) |
CTXONE_BACKUP_INTERVAL_SECS | Background VACUUM-INTO snapshot interval (default 1800; 0 disables) |
RUST_LOG | Log level (info default) |
Authentication
Section titled “Authentication”The hub binds 0.0.0.0 (all interfaces). With no token set, REST and /mcp are
reachable from the network unauthenticated — the hub logs a loud warning at
startup. Before exposing it beyond localhost, set a bearer token:
ctxone-hub --http --lens --path ~/.ctxone/memory.db --auth-token "$(openssl rand -hex 32)"# or: CTXONE_AUTH_TOKEN=… ctxone-hub --http …Behaviour:
- Loopback peers (127.0.0.1/::1) are always exempt — local CLI, Lens, and same-host agents keep working with no token.
- Non-loopback requests must send
Authorization: Bearer <token>(else 401). - Setting a token also relaxes
/mcp’s loopback-only Host check so authenticated remote clients can connect (the bearer becomes the gate).
How each client gets the token:
ctxCLI — pass--token <TOK>or setCTX_TOKEN(only for a remote hub).- AI tools —
ctx init --transport httpwrites it into each tool’s config: ctx init --transport http --auth-token <TOK>— embeds a literal bearer: aheadersentry for native http (Claude Code/Cursor/VS Code) and a--headerarg for the mcp-remote bridge (Claude Desktop). The token is written in plaintext into those config files (a warning is printed).ctx init --transport http --auth-token-env <VAR>— keeps the secret out of config files: Codex getsbearer_token_env_var = "<VAR>", and native http entries referenceBearer ${VAR}(for clients that expand env vars). The mcp-remote bridge can’t expand env vars, so Claude Desktop needs the literal--auth-tokenform.
Cross-origin (CSRF / DNS-rebinding)
Section titled “Cross-origin (CSRF / DNS-rebinding)”Independent of the token, the hub rejects browser requests whose Origin
isn’t same-origin or allow-listed — so a page you visit can’t drive the loopback
hub (loopback peers are auth-exempt, which is why this guard matters). CLI and
native MCP clients send no Origin and are unaffected; Lens is same-origin and
always allowed. CORS reflects only same-origin/allow-listed origins (never *).
To let a real cross-origin browser app (e.g. Lens hosted elsewhere) use the API:
ctxone-hub --http --allowed-origin https://lens.example.com # repeatable# or CTXONE_ALLOWED_ORIGINS="https://a.example, https://b.example"Lens over an authenticated hub — use a tunnel
Section titled “Lens over an authenticated hub — use a tunnel”Lens is a browser SPA; it can’t attach an Authorization: Bearer header to its
requests. So when a token is set, a remote browser hitting Lens directly is
rejected by the auth layer (loopback is exempt, remote is not). Treat Lens as a
local admin UI and reach it remotely over an SSH tunnel, which makes your
browser a loopback client of the hub:
ssh -L 3001:localhost:3001 you@hub-host # then open http://localhost:3001The hub prints this reminder at startup when --lens and --auth-token are both
set. (REST and /mcp remain directly reachable with the bearer token; only the
browser UI needs the tunnel.)
Running as a service
Section titled “Running as a service”To make the HTTP/Lens hub survive reboots and start before any agent (so it,
not an agent’s stdio child, owns the db), run it as a login service. ctx service
generates and registers the unit for you — launchd on macOS, a systemd user
unit on Linux, and a Task Scheduler logon task on Windows (via schtasks):
ctx service install # lens on, port 3001, canonical dbctx service install --port 3001 --path ~/.ctxone/memory.dbctx service install --auth-token "$(openssl rand -hex 32)" # embeds token (chmod 600)ctx service install --dry-run # print the unit + commands, write nothingctx service status # is it registered/running?ctx service uninstall # stop + removeThe unit runs ctxone-hub --http --lens --path <db> --port <port> with
RunAtLoad/KeepAlive (macOS), Restart=on-failure (systemd), or a
LogonTrigger + RestartOnFailure (Windows), logging to ~/.ctxone/hub.log.
Preview it first with --dry-run. If another hub already holds the db (an
agent’s stdio hub, or a manual one), the service will fail on the lockfile —
stop it first. On Windows the auth token can’t be embedded in the task; set it
with setx CTXONE_AUTH_TOKEN <token> instead.
Equivalent hand-written launchd 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>com.ctxone.hub</string> <key>ProgramArguments</key> <array> <string>/opt/homebrew/bin/ctxone-hub</string> <string>--http</string> <string>--lens</string> <string>--path</string> <string>/Users/you/.ctxone/memory.db</string> </array> <key>KeepAlive</key><true/> <key>RunAtLoad</key><true/> <key>StandardErrorPath</key><string>/Users/you/.ctxone/hub.log</string></dict></plist>launchctl load -w ~/Library/LaunchAgents/com.ctxone.hub.plistWith the daemon running, point agents at it with
ctx init --transport httpand they stop spawning their own hub — one process, one owner, no race.ctx inithandles every detected client (native URL, Codexurl, or the mcp-remote bridge for Claude Desktop), so they all connect to the daemon. Use a separate db (topology C) only for deliberate isolation, or for a client that supports neither native http nor the mcp-remote bridge. See the unified-hub design.
Troubleshooting
Section titled “Troubleshooting”database is already locked …— another hub owns that db. Find it (ps aux | grep ctxone-hub), or use a different--path/--port. Stale locks (owner gone) are reclaimed automatically.- Lens shows nothing / CLI can’t connect — check
CTX_SERVERand that a hub is running with--http.curl http://localhost:3001/api/health. - Agent and Lens show different memory — they’re on different db paths (topology C). That’s isolation working as configured.
See also: ARCHITECTURE.md · DATA_SAFETY.md · TROUBLESHOOTING.md