* fix(test): stop the suite writing live API keys into the operator's real key store
`npm test` wrote real, UNREVOKED rows into ~/.ocp/ocp.db — the SAME sqlite database the
running server reads — two per run, unbounded. On the maintainer's host that had accumulated
**737 test-suite keys against 12 real operator keys** (749 rows total), all revoked=0.
Not a credential leak: createKey() mints `randomBytes(24)` and the suite discards the
plaintext, so nobody holds a usable token. But it is real damage:
- the operator's key store grows by 2 rows on every test run, forever
- `ocp keys list` is unusable (749 rows, 12 of them real)
- the suite is RACY: two concurrent runs (e.g. two review worktrees) share one file, so
listKeys() can miss "test-user-1" and `"quota_daily" in undefined` throws a TypeError
rather than failing cleanly. That is the ~1-in-6 flake in `listKeys includes quota
fields`, reported by a reviewer and initially not reproducible serially — it needs a
concurrent run to surface, which is exactly what four parallel reviewers produced.
Root cause: keys.mjs resolved `OCP_DIR`/`DB_PATH` at MODULE TOP-LEVEL and read no env var.
test-features.mjs carried a comment claiming it could "set env before the first getDb() call"
— it could not, on two counts: nothing in keys.mjs read an env var, and ESM hoists imports, so
the assignment would have run after keys.mjs was already evaluated anyway. The isolation was
never real; it just LOOKED real, which is why it survived.
Fix:
- keys.mjs resolves the dir lazily, inside getDb(), honoring OCP_DIR_OVERRIDE. Deliberately
NOT a generic `OCP_DIR`: pointing a RUNNING server at a different key store silently
changes which credentials authenticate, so this must be awkward to set by accident.
- new test-env.mjs, imported BEFORE keys.mjs, mints a per-run scratch dir. A separate module
is required — ESM hoisting means a statement in the test's own body is too late.
- export getDbPath() so the store's location can be asserted.
- as a side effect, importing keys.mjs no longer creates directories in the operator's home.
Two guards added, both MUTATION-TESTED (revert the override → both fail, 317/2):
- the key store under test is a scratch db, NOT the operator's real ~/.ocp/ocp.db
- listKeys does not depend on rows left behind by an earlier or concurrent run
Proven, not asserted: the real ~/.ocp/ocp.db held at 749 rows across two full test runs
(it previously grew by 2 each run). The 737 existing junk rows are NOT cleaned up here —
that is a destructive change to the maintainer's live database and is his call, not a
side effect of a test fix.
npm test: 319 passed, 0 failed (was 317).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR
* fix(keys): gate the test override behind NODE_ENV so a prod server can never honor it
Review fold-in. The reviewer landed the sharpest possible critique: the first cut closed a
test-hygiene hole by opening a quieter AUTH-CORRECTNESS one, and its only guard against that
was "the variable has an awkward name" — a naming convention plus a comment. That is precisely
the failure mode this whole PR exists to indict (a comment describing an intention that nothing
enforces). It was demonstrated live:
OCP_DIR_OVERRIDE=/tmp/evil-store -> server opens /tmp/evil-store/ocp.db, 0 keys visible
server.mjs imports keys.mjs, and `ocp start`'s nohup fallback inherits the invoking shell's env
— so a maintainer who exported the var while debugging THIS issue and then started the server
would get a server silently authenticating against an empty key store. In AUTH_MODE=multi that
is a total auth outage: every real key 401s, nothing logged, nothing on /health.
F1 — the gate is now the actual guard: OCP_DIR_OVERRIDE is honored ONLY when
NODE_ENV === "test". A production server runs without NODE_ENV and therefore CANNOT be
redirected, however the variable reached its environment. Proven both directions:
no NODE_ENV + OCP_DIR_OVERRIDE=/tmp/evil-store -> /Users/<op>/.ocp/ocp.db (ignored)
NODE_ENV=test + OCP_DIR_OVERRIDE=/tmp/scratch -> /tmp/scratch/ocp.db (honored)
Plus: getDb() now LOGS the store whenever it is not the default. Silence was the other half
of the bug — a server on the wrong key store looks exactly like one on the right store until
every request 401s.
F2 — restore the 0700 guarantee on ~/.ocp. Removing keys.mjs's top-level mkdirSync (a good
change on its own) silently dropped it: prepareSpawnHome (server.mjs:477) does
mkdirSync(recursive) with NO mode, so on a fresh install it can create ~/.ocp as a
world-listable 0755 parent. Verified: 755 via the spawn-home path vs 700 via resolveOcpDir.
The invariant used to be inherited by luck; it is now stated.
F3 — test-env.mjs removes its scratch dir on exit. Otherwise the fix traded unbounded growth in
~/.ocp/ocp.db for unbounded growth in $TMPDIR. Verified: 2 runs, delta 0 dirs.
F4 — closeDb() clears dbPath; getDbPath() no longer hands back a path to a closed db.
F5 — dropped the dead unlinkSync import and explained the leftover HOME normalization.
New test, and it is the one that matters: "a PRODUCTION process (no NODE_ENV) must IGNORE
OCP_DIR_OVERRIDE" — so nothing can re-widen the gate without a red test.
server.mjs IS touched (one mkdirSync mode). Not endpoint-touching: no request handler, endpoint,
header, or wire field — so no cli.js citation applies (ALIGNMENT.md Rule 2 / CLAUDE.md hard-req #1).
Note memory/constitution.md § II lists keys.mjs as a protected file requiring maintainer approval.
npm test: 320 passed, 0 failed. Real ~/.ocp/ocp.db unchanged at 751 rows throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR
* fix(test): make the F1 gate test REAL — it was theatre, and proved it
The reviewer deleted the entire NODE_ENV gate from keys.mjs and the suite still reported
320 passed, 0 failed. The one test written to stop this bug recurring was the one thing in
the PR that would have let it recur — and it would have merged green, with a false sense of
coverage.
Why it was worthless: it re-implemented the predicate INSIDE THE TEST BODY —
const resolve = (nodeEnv, override) =>
(nodeEnv === "test" ? override : null) || join(homedir(), ".ocp");
— and never called resolveOcpDir(), getDb(), or getDbPath(). It asserted that a closure
defined three lines above behaved as written. A copy of the predicate is not the predicate.
Its own comment said "exercising the same predicate keys.mjs uses" — that phrase was the tell.
This is the same failure class the PR exists to indict (an assertion of an intention that
nothing enforces), reproduced one layer up, in the fix for it. Fourth time in this repo that
a correctly-named test has vouched for nothing.
The real test must run OUT OF PROCESS: the parent is irreversibly NODE_ENV=test by the time
any test runs (test-env.mjs sets it before keys.mjs is imported), so the production path is
simply unreachable in-process. It now spawns a child with no NODE_ENV, the override set, and
HOME redirected to a temp dir (so the real key store is never opened), and asserts what the
REAL keys.mjs actually did.
MUTATION-PROVEN, against the exact revert that used to pass:
delete the whole NODE_ENV gate -> 319 passed, 1 failed
✗ a PRODUCTION process (no NODE_ENV) must IGNORE OCP_DIR_OVERRIDE
restore -> 320 passed, 0 failed
Also folded in: setup.mjs created ~/.ocp at the umask default (755, world-listable) on a fresh
install via the logs dir — pre-existing, self-healing on first server start, now stated
explicitly (mode 0700) rather than left to luck. Same class as the F2 fix.
npm test: 320 passed, 0 failed. Real ~/.ocp/ocp.db unchanged at 751 rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR
* fix(setup): rescue the semicolon from the comment; assert the child SAW the override
Two review nits on the way in.
setup.mjs:393 — the statement's semicolon had been swallowed INTO the trailing comment, so
the line parsed only because ASI rescued it (the next token is `if` on a new line). The repo
has no linter, so nothing would have caught it. Comment moved above the statement.
test-features.mjs — negative control on the prod-gate probe. The reviewer noticed the test's
robustness was INCIDENTAL: because the child env is spread from process.env, it inherits the
parent's own OCP_DIR_OVERRIDE, so a future refactor that renamed the var and missed this test's
explicit `env` object would still have gone red — but by luck, not by assertion. The child now
prints the override it SAW as well as the store it opened, and the test asserts both. The claim
is now 'a prod process saw the override and ignored it', not merely 'a prod process opened the
right store' (which could pass for the wrong reason).
Mutation re-proven after both edits: delete the NODE_ENV gate -> 319 passed, 1 failed; restore
-> 320 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
OCP — Open Claude Proxy
Already paying for Claude Pro/Max? Use your subscription as an OpenAI-compatible API — $0 extra cost.
Open source from day one, used daily by my family, maintained on nights and weekends. If OCP saves you money too, you can ☕ buy me a coffee — full story below.
If OCP saves you a setup, a ⭐ helps other folks discover it. Issue reports are even more useful — that's the highest-quality feedback this project gets.
OCP turns your Claude Pro/Max subscription into a standard OpenAI-compatible API on localhost. Any tool that speaks the OpenAI protocol can use it — no separate API key, no extra billing.
Cline ──┐
OpenCode ───┤
Aider ───┼──→ OCP :3456 ──→ Claude CLI ──→ Your subscription
Continue.dev ───┤
OpenClaw ───┘
One proxy. Multiple IDEs. All models. $0 API cost.
Why OCP?
There are several Claude proxy projects. OCP picks a specific lane: align tightly with what cli.js actually does, observe + multiplex what's already there, don't extend the protocol. What you get:
- LAN multi-user keys (v3.7.0) — reach one Claude Pro/Max subscription from your own devices across the LAN. Each device gets a per-key API token (no OAuth session leak), with independent usage tracking and one-line revocation. Pro/Max are per-user accounts — see Sharing with family / a team — honest limits before extending access to other people.
ocp-connectone-shot IDE setup — one command on the client machine detects and configures Claude Code, Cursor, Cline, Continue.dev, OpenCode, and OpenClaw. No pastingOPENAI_BASE_URLsix times.- Response cache with per-key isolation + singleflight (v3.13.0). Optional SHA-256 prompt cache, isolated per API key (cross-user pollution is impossible by hash construction, not by application logic), with stampede protection on concurrent identical prompts. Off by default. (PR #65, PR #66)
- Per-key request quotas (v3.8.0). Daily / weekly / monthly limits per key — set a kid's iPad to 20/day, a partner's laptop to 100/week. (PR #18)
- SSE heartbeat for long reasoning (v3.12.0, opt-in). If you've ever watched your IDE die at the 60s idle mark during a long Claude tool-use pause — that's nginx/Cloudflare default behavior. OCP emits an SSE comment frame to keep the connection alive without polluting the response. (PR #49)
cli.jsalignment + CI guardrail. LLM-assisted code drifts easily — it's tempting to invent plausible-looking endpoints thatcli.jsdoesn't actually use.ALIGNMENT.mdis binding: every endpoint OCP exposes must cite acli.jsline. Thealignment.ymlCI workflow blocks PRs that introduce known-hallucinated tokens. The payoff is boring: your setup keeps working whencli.jsships its next minor.models.jsonsingle source of truth (v3.11.0). Adding a model is one file edit; both/v1/modelsand the OpenClaw bootstrap derive from it. (PR #30)- Drives the official CLI as-is, no binary patching. OCP spawns the official
claudeCLI (or hosts it in an interactive tmux pane for TUI mode) — it does not extract OAuth tokens from memory, patch the binary, or invent protocol extensions. Traffic therefore looks like genuine Claude Code to Anthropic's classifiers (cc_entrypoint=cli). SeeALIGNMENT.mdfor why this constraint is load-bearing.
Comparison
OCP and the alternatives serve adjacent but distinct needs. Pick the one that fits your use case:
| Feature | OCP | claude-code-router | anthropic-proxy |
|---|---|---|---|
| Forwards Claude Code subscription as OpenAI API | yes | yes | yes |
| Routes to multiple model backends (OpenAI, Gemini, etc.) | no | yes | partial |
| SSE heartbeat for long reasoning | yes (opt-in) | no | no |
| Per-key quota + LAN multi-user keys | yes | no | no |
| Response cache | yes (opt-in) | no | no |
| OpenClaw / IDE auto-config | yes | no | no |
| Model-routing rules / model-switching | no | yes | no |
| GitHub stars / ecosystem size | small | large | mid |
| Governance discipline (CI-enforced alignment with cli.js) | yes | n/a | n/a |
Plain English: claude-code-router is the routing-and-switching power tool — pick it if you want to mix Anthropic, OpenAI, Gemini, and local models behind one endpoint. anthropic-proxy is the minimal forwarder. OCP focuses on disciplined cli.js-aligned forwarding plus subscription multiplexing — pick it if you want to reach one Claude Pro/Max subscription from your own IDEs and devices, with LAN auth, quotas, and a governance contract that prevents endpoint drift.
Related: OLP — Open LLM Proxy
OCP is Claude-only by design. If you want to spread across multiple LLM providers (not just Claude), see the sibling project OLP — Open LLM Proxy: the same spawn-the-provider-CLI approach, but across several provider CLIs behind one OpenAI-compatible endpoint, with intelligent fallback chains. It grew out of OCP in response to Anthropic's 2026-06-15 billing split — the idea being to spread subscription/quota risk across more than one provider. OCP remains the focused, Claude-only option; OLP is the multi-provider one.
OCP is single-maintainer + LLM-assisted, currently pre-1.0. It runs the maintainer's daily Claude Code workflow. If something breaks, open an issue.
Supported Tools
Any tool that accepts OPENAI_BASE_URL works with OCP:
| Tool | Configuration |
|---|---|
| Cline | Settings → OPENAI_BASE_URL=http://127.0.0.1:3456/v1 |
| OpenCode | OPENAI_BASE_URL=http://127.0.0.1:3456/v1 |
| Aider | aider --openai-api-base http://127.0.0.1:3456/v1 |
| Continue.dev | config.json → apiBase: "http://127.0.0.1:3456/v1" |
| OpenClaw 1 | setup.mjs auto-configures |
| Any OpenAI client | Set base URL to http://127.0.0.1:3456/v1 |
Installation
The simplest path: ask your AI.
Paste this prompt to Claude Code / Cursor / Copilot:
Install OCP for me. Read README §Manual Installation and follow it.
Tell me when I need to run `claude auth login`.
The AI will run git clone, npm install, node setup.mjs, and tell you
when to OAuth.
Manual Installation
OCP has two roles: Server (runs the proxy, needs Claude CLI) and Client (connects to a server, zero dependencies).
┌─ Server (always-on device) ─────────────────────────────┐
│ Mac mini / NAS / Raspberry Pi / Desktop │
│ Claude CLI + OCP server → bound to 0.0.0.0:3456 │
└───────────────────────┬─────────────────────────────────┘
│ LAN
┌───────────────────┼───────────────────┐
▼ ▼ ▼
Laptop Phone/Tablet Pi / Server
(client) (browser) (client)
Quick install with AI assistance
If you've got Claude Code, Cursor, or any other AI coding assistant on this machine, you can copy-paste one of these prompts and let the AI walk through the install for you. Each prompt pins the AI to the right README section, names the verification step, and forbids silent retries — so you stay in the loop.
Single-machine use — install OCP for IDEs on this same machine only:
I want to install OCP on this machine to use my Claude Pro/Max subscription
as an OpenAI-compatible API for local IDEs.
Please follow https://github.com/dtzp555-max/ocp/blob/main/README.md
"Server Setup" → "Single-machine use" path:
1. Verify prerequisites: macOS or Linux, Node.js 22.5+, git, Claude CLI
installed and logged in (`claude auth status`). Install missing pieces
using my system's package manager.
2. git clone the repo, cd in, and run `node setup.mjs`.
3. Verify with `curl http://127.0.0.1:3456/v1/models` (should list 5 models).
4. Add `export OPENAI_BASE_URL=http://127.0.0.1:3456/v1` to my shell rc.
5. Tell me to reload my shell and try a tool like Cline / Continue / Cursor.
Before each step, tell me what you'll run and wait for confirmation.
On any error, diagnose first — don't auto-retry.
LAN mode (server) — install OCP as a server so your own devices on the LAN can reach it (Claude Pro/Max are per-user accounts — review Anthropic's Usage Policy before extending access to other people):
I want to install OCP on this device as a LAN server so my own devices on the
network can reach my Claude Pro/Max subscription through a local
OpenAI-compatible endpoint.
Please follow https://github.com/dtzp555-max/ocp/blob/main/README.md
"Server Setup" → "LAN mode" path:
1. Verify prerequisites: macOS or Linux (Windows not supported), Node.js
22.5+, git, Claude CLI installed and authenticated.
2. Generate a strong admin key with `openssl rand -base64 32`. Save it —
I'll need it to manage per-user keys later.
3. git clone https://github.com/dtzp555-max/ocp.git && cd ocp
4. Run `node setup.mjs --bind 0.0.0.0 --auth-mode multi`.
5. Add OCP_ADMIN_KEY to my shell rc (~/.zshrc or ~/.bashrc).
6. Run `ocp lan` to show me the LAN IP and connect command.
7. Optionally create example keys: `ocp keys add laptop`, `ocp keys add tablet`.
8. Verify: `curl http://127.0.0.1:3456/v1/models` returns 5 models.
Tell me each step before running it. On error, diagnose before retrying.
Client connect — configure this device to use an existing OCP server on your LAN:
There's an OCP server at <SERVER_IP> on my LAN. Configure this machine to
use it for any local IDEs (Cursor, Cline, Continue.dev, OpenCode, Claude
Code, OpenClaw).
Server IP: <SERVER_IP>
API key (leave blank if the server has anonymous mode enabled): <OPTIONAL_KEY>
Please follow https://github.com/dtzp555-max/ocp/blob/main/README.md
"Client Setup" path:
1. Download ocp-connect:
curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect -o ocp-connect
chmod +x ocp-connect
2. Run `./ocp-connect <SERVER_IP>` (add `--key <KEY>` if you have one).
3. Follow any IDE-specific manual hints it prints.
4. Verify: `curl http://<SERVER_IP>:3456/v1/models` returns 5 models.
5. Tell me to reload my shell + restart any IDE that was already running.
Don't auto-retry on error. Tell me the failure mode first.
If you'd rather do everything manually, the Server Setup and Client Setup sections below have the same steps in handbook form.
Server Setup
Recommended: Install OCP on a device that stays powered on — Mac mini, NAS, Raspberry Pi, or a desktop that doesn't sleep. This ensures all clients always have access.
Prerequisites:
- macOS or Linux (Windows is not supported —
setup.mjsinstalls launchd / systemd auto-start) - Node.js 22.5+ (Node 23+ recommended —
node:sqliteis fully stable without flags from 23.0; on 22.5–22.x it works behind--experimental-sqlite) git- Claude CLI — install and authenticate:
Headless servers (Pi / NAS / VPS without a desktop browser): see Headless install notes below.
npm install -g @anthropic-ai/claude-code claude auth login # prints a URL + code — open URL on any browser, sign in, paste code back
# 1. Clone and run setup
git clone https://github.com/dtzp555-max/ocp.git
cd ocp
node setup.mjs
The setup script will:
- Verify Claude CLI is installed and authenticated
- Start the proxy on port 3456
- Install auto-start (launchd on macOS, systemd on Linux)
After install the ocp CLI lives at ~/ocp/ocp. To put it on your PATH, either symlink it manually (ln -sf ~/ocp/ocp ~/.local/bin/ocp if ~/.local/bin is on your PATH, or sudo ln -sf ~/ocp/ocp /usr/local/bin/ocp for a system-wide symlink) or add an alias (alias ocp=~/ocp/ocp). Otherwise invoke it as ~/ocp/ocp <subcommand>. The rest of this README assumes ocp is on your PATH.
Single-machine use — just set your IDE to use the proxy:
export OPENAI_BASE_URL=http://127.0.0.1:3456/v1
LAN mode — reach OCP from your own devices on the network (Claude Pro/Max are per-user accounts — see Sharing with family / a team — honest limits before extending access to other people):
# Enable LAN access with per-user auth (recommended)
node setup.mjs --bind 0.0.0.0 --auth-mode multi
Then create API keys for each person/device:
# Generate a strong admin key (one-time — save it for later key management):
export OCP_ADMIN_KEY=$(openssl rand -base64 32)
# Add the same export line to ~/.zshrc or ~/.bashrc so it persists.
ocp keys add wife-laptop
# ✓ Key created for "wife-laptop"
# API Key: ocp_example12345abcde...
# Copy this key now — you won't see it again.
ocp keys add son-ipad
ocp keys add pi-server
Run ocp lan to see your IP and ready-to-share instructions.
Verify:
curl http://127.0.0.1:3456/v1/models
# Returns: claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001
Headless install notes
OCP is designed for always-on devices that often don't have a desktop browser — Mac mini, NAS, Raspberry Pi, cloud VPS. The Claude CLI auth flow still works headless:
Option 1 — interactive OAuth over SSH (one-shot). claude auth login prints a URL + 8-digit code. Open the URL on any device with a browser (your laptop, phone), sign in to your Anthropic account, and paste the code back into the SSH session. No browser needed on the server itself.
Option 2 — long-lived token (auth once, no re-prompts).
claude setup-token # subscription-backed long-lived token
Same Claude subscription as Option 1; the token is stored in Claude CLI's normal config location. Useful when you'd rather not redo the OAuth flow when sessions expire.
If claude auth login errors out with something like cannot open browser, you've hit the same case — fall back to either option above.
Uninstall
# From the cloned repo
node uninstall.mjs
Removes the launchd (macOS) or systemd (Linux) auto-start entry. Handles both legacy (ai.openclaw.proxy / openclaw-proxy) and current (dev.ocp.proxy / ocp-proxy) service names. Does not delete ~/.openclaw/, ~/.ocp/, or the cloned repo — remove those manually if desired.
Client Setup
Clients do not need to install Node.js, Claude CLI, or the OCP repo. Only
curlandpython3are required (pre-installed on most Linux/Mac systems).Find the server's LAN IP by running
ocp lanon the server machine — it prints both the IP and a ready-to-share connect command.
One-command setup — download the lightweight ocp-connect script:
curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect -o ocp-connect
chmod +x ocp-connect
./ocp-connect <server-ip>
Zero-config — when the server admin has set PROXY_ANONYMOUS_KEY and opted in with PROXY_ADVERTISE_ANON_KEY=1 (see Anonymous Access below), just pass the server IP and nothing else. ocp-connect reads the anonymous key from /health and uses it automatically. Without the opt-in, /health does not expose the key (issue #109); pass --key or rely on anonymous access instead:
./ocp-connect <server-ip>
If the server requires a key, pass it with --key:
./ocp-connect <server-ip> --key <your-api-key>
Or as a one-liner (no file saved):
curl -fsSL https://raw.githubusercontent.com/dtzp555-max/ocp/main/ocp-connect | bash -s -- <server-ip>
Example:
$ ./ocp-connect 192.168.1.100
OCP Connect v1.3.0
─────────────────────────────────────
Remote: http://192.168.1.100:3456
Checking connectivity...
✓ Connected
Remote OCP v3.11.0 (auth: multi)
ⓘ Using server-advertised anonymous key: ocp_publ...n_v1
(set by admin via PROXY_ANONYMOUS_KEY; see issue #12 §14 Path A)
Testing API access...
✓ API accessible (5 models available)
Shell config:
✓ .bashrc
✓ .zshrc
OPENAI_BASE_URL=http://192.168.1.100:3456/v1
System-level (launchctl):
✓ OPENAI_BASE_URL set for GUI apps and daemons
IDE Configuration
─────────────────────────────────────
Detected: OpenClaw (~/.openclaw/openclaw.json)
Configure OpenClaw to use this OCP? [Y/n] y
Provider name (models show as <name>/model-id) [ocp]: ocp
How should OCP models be configured?
1) Primary — use OCP by default, keep existing models as backup
2) Backup — keep current primary, add OCP as additional option
Choice [1]: 1
Writing OpenClaw config...
✓ Per-agent auth profile seeded (2):
• ~/.openclaw/agents/main/agent/auth-profiles.json
• ~/.openclaw/agents/macbook_bot/agent/auth-profiles.json
✓ OpenClaw configured
Provider: ocp
Models:
• ocp/claude-opus-4-8
• ocp/claude-opus-4-7
• ocp/claude-opus-4-6
• ocp/claude-sonnet-4-6
• ocp/claude-haiku-4-5-20251001
Priority: PRIMARY (default model)
Restart OpenClaw to apply: openclaw gateway restart
Running smoke test...
✓ Smoke test passed: OK
Note: smoke test only verifies OCP is reachable and the key is valid.
It does not verify your IDE/agent end-to-end. To verify OpenClaw works,
restart it (`openclaw gateway restart`) and send a test message to your bot.
Done. Reload your shell to apply:
source ~/.zshrc
The script automatically:
- Writes env vars to all relevant shell rc files (
.bashrc,.zshrc) - Sets system-level env vars (
launchctl setenvon macOS,environment.don Linux) - Auto-discovers anonymous key from
/health.anonymousKeywhen no--keygiven (v1.3.0+, requires server v3.10.0+; server must also setPROXY_ADVERTISE_ANON_KEY=1— see Anonymous Access) - Configures OpenClaw automatically (including per-agent
auth-profiles.jsonfor multi-agent setups) - Detects Cline, Continue.dev, Cursor, and opencode, and prints setup hints (manual configuration required for these IDEs)
On macOS, launchctl setenv vars reset on reboot — re-run ocp-connect after restart.
Manual setup — if you prefer not to use the script:
export OPENAI_BASE_URL=http://<server-ip>:3456/v1
export OPENAI_API_KEY=ocp_<your-key>
Add these lines to ~/.bashrc or ~/.zshrc to persist across sessions.
Monitoring (Server-side)
# Per-key usage stats
ocp usage --by-key
# Key Reqs OK Err Avg Time
# wife-laptop 5 5 0 8.0s
# son-ipad 3 3 0 6.2s
# Manage keys
ocp keys # List all keys
ocp keys revoke son-ipad # Revoke a key
Web Dashboard: Open http://<server-ip>:3456/dashboard in any browser for real-time monitoring — per-key usage, request history, plan utilization, and system health.
Auth Modes
| Mode | Env | Use Case |
|---|---|---|
none |
CLAUDE_AUTH_MODE=none |
Trusted home network, no auth needed |
shared |
CLAUDE_AUTH_MODE=shared + PROXY_API_KEY=xxx |
Everyone shares one key |
multi |
CLAUDE_AUTH_MODE=multi + OCP_ADMIN_KEY=xxx |
Per-person keys for usage tracking + quotas (trusted users only — see Deployment model below) |
Usage scope (v3.14.0+):
/api/usagereturns the caller's own rows by default. Admin callers must pass?all=trueto retrieve data for all keys; doing so emits an audit log line.
Deployment model & security (read this)
What OCP is built for today: single-user, multi-IDE. Run OCP as a server on one machine and point all of your own IDEs/devices at it — one Claude Pro/Max subscription, used everywhere. This is the primary, solid use case.
Sharing with family / a team — honest limits. You can share OCP on a LAN, but be clear about what the auth modes do and don't give you:
- The per-key modes (
shared/multi) give per-key usage tracking, quotas, and cache separation — useful for seeing who used what and capping budgets. - They do not give a security isolation boundary. The spawned
clauderuns with the operator's filesystem access and is not sandboxed per key. Only share with people you fully trust, on a trusted network. - For simple trusted family sharing, the easiest setup is a single shared anonymous key (see Anonymous Access) — no per-person separation, same trust assumption.
- Account terms and ToS — read before sharing with others. Claude Pro/Max are per-user accounts. Pooling a single subscription across multiple distinct people may violate Anthropic's Consumer Terms of Service and risk account suspension by the abuse classifier. The defensible framing is "one person, your own devices" — sharing with friends or a team is not. OCP does not change your account terms, and whether any particular sharing setup complies with the ToS is the account holder's responsibility. Review Anthropic's Usage Policy before extending access to other people.
Real per-user isolation (sandboxed, multi-tenant-safe) is planned for after 2026-06-15 — per-key ephemeral home + tool lockdown + an OS sandbox. Until then, treat a multi-user OCP as a trusted-group convenience, not a security boundary. (This is also why CLAUDE_TUI_MODE is single-user-only — see Subscription-pool (TUI) mode.)
Anonymous Access (optional)
In multi mode, the admin can designate a single well-known "anonymous" key that bypasses validateKey() and grants public read/write access. This is useful for letting LAN users (or clients like OpenClaw multi-agent setups) connect without individual per-user keys.
Enable:
The anonymous key is wired into the service unit (launchd plist on macOS, systemd unit on Linux) at install time. Export PROXY_ANONYMOUS_KEY in your shell before running setup.mjs, and setup.mjs will write it into the service unit env so the auto-started proxy picks it up:
export PROXY_ANONYMOUS_KEY=ocp_public_anon # or any string of your choice
node setup.mjs --bind 0.0.0.0 --auth-mode multi
If OCP is already installed without it, re-export the env var and re-run node setup.mjs (the installer is idempotent — it refreshes the service unit). Then ocp restart so the running proxy picks up the new env. Setting PROXY_ANONYMOUS_KEY only in your interactive shell does not affect the auto-started proxy — the service unit is the source of truth for its environment.
Client side: the anonymous key value is exposed via GET /health as the field anonymousKey (null when not set) only to localhost callers or when the admin has also set PROXY_ADVERTISE_ANON_KEY=1 (default off — see issue #109). With that opt-in, clients like ocp-connect can auto-discover and use it, so the end user doesn't need to get a personal key from the admin.
Security note: setting this env var is an opt-in to public access — anyone who can reach your OCP endpoint can use it, up to any rate limits you configure. Don't enable this on internet-exposed OCP instances without additional protection.
Not a secret: because /health is an unauthenticated endpoint, the anonymous key is publicly readable by anyone who can reach the server. That is intentional — the key exists so clients can self-configure without out-of-band coordination. Treat it as a convenience handle, not as an access credential.
Per-Key Quota (Budget Control)
Prevent any single user from exhausting your subscription. Set daily, weekly, or monthly request limits per API key:
# Set a daily limit of 50 requests for a key
curl -X PATCH http://127.0.0.1:3456/api/keys/wife-laptop/quota \
-H "Authorization: Bearer $OCP_ADMIN_KEY" \
-d '{"daily": 50}'
# Set multiple limits at once
curl -X PATCH http://127.0.0.1:3456/api/keys/son-ipad/quota \
-H "Authorization: Bearer $OCP_ADMIN_KEY" \
-d '{"daily": 20, "weekly": 100}'
# Check current quota + usage
curl http://127.0.0.1:3456/api/keys/wife-laptop/quota
# → { "daily": { "limit": 50, "used": 12 }, "weekly": { "limit": null, "used": 34 }, ... }
# Remove a limit (set to null)
curl -X PATCH http://127.0.0.1:3456/api/keys/wife-laptop/quota \
-d '{"daily": null}'
When a key exceeds its quota, OCP returns HTTP 429 with a structured error:
{
"error": {
"message": "Quota exceeded: 50/50 requests (daily). Resets 6h 12m.",
"type": "quota_exceeded",
"quota": { "period": "daily", "limit": 50, "used": 50, "resetsIn": "6h 12m" }
}
}
null= unlimited (default for all keys)- Only successful requests count toward quota
- Admin and anonymous users are never subject to quotas
- PATCH is a partial update — omitted fields are left unchanged
Note: quotas are best-effort. Under concurrent bursts a key can exceed its cap by up to the server's max-concurrency (default 8), and cache hits are not counted toward quota. They cap budgets for cooperative family use, not adversarial abuse.
Important Notes
- All users share your Claude Pro/Max rate limits (5h session + 7d weekly)
ocp usageshows how much quota remains- Keys are stored in
~/.ocp/ocp.db(SQLite, zero external dependencies) - Admin key is required for key management API endpoints
- The dashboard (
/dashboard) and health check (/health) are always public - File modes for
~/.ocp(0700),admin-key+ocp.db(0600) are auto-tightened at server startup as of v3.14.0
Built-in Usage Monitoring
Check your subscription usage from the terminal:
$ ocp usage
Plan Usage Limits
─────────────────────────────────────
Current session 21% used
Resets in 3h 12m (Tue, Mar 28, 10:00 PM)
Weekly (all models) 45% used
Resets in 4d 2h (Tue, Mar 31, 12:00 AM)
Extra usage off
Model Stats
Model Req OK Er AvgT MaxT AvgP MaxP
──────────────────────────────────────────────────────
opus 5 5 0 32s 87s 42K 43K
sonnet 18 18 0 20s 45s 36K 56K
Total 23
Proxy: up 6h 32m | 23 reqs | 0 err | 0 timeout
All Commands
ocp usage Plan usage limits & model stats
ocp usage --by-key Per-key usage breakdown (LAN mode)
ocp status Quick overview
ocp health Proxy diagnostics
ocp keys List all API keys (multi mode)
ocp keys add <name> Create a new API key
ocp keys revoke <name> Revoke an API key
ocp connect <ip> One-command LAN client setup
ocp doctor Health & upgrade-readiness check; primary entry for AI-driven debugging. --json produces a next_action for AI agents.
ocp lan Show LAN connection info & IP
ocp settings View tunable settings
ocp settings <k> <v> Update a setting at runtime
ocp logs [N] [level] Recent logs (default: 20, error)
ocp models Available models
ocp sessions Active sessions
ocp clear Clear all sessions
ocp restart Restart proxy
ocp restart gateway Restart gateway
ocp update Update to latest version
ocp update --check Check for updates without applying
ocp --help Command reference
Install the CLI
# Symlink to PATH (recommended)
sudo ln -sf $(pwd)/ocp /usr/local/bin/ocp
# Verify
ocp --help
Cloud/Linux servers: If
ocp: command not found, the binary isn't in PATH. Full path:~/.openclaw/projects/ocp/ocp
Upgrading
The simplest path: ask your AI.
Paste this prompt:
Upgrade my OCP. Run `ocp update` and follow whatever it says.
If it tells me to run `claude auth login`, I'll do that.
What ocp update does:
- Patch bump (e.g.
v3.14.0 → v3.14.1): light path (git pull + npm install + restart). - Cross-minor (e.g.
v3.10 → v3.14): full path: pre-flight check, snapshot,setup.mjs(with plist env-merge), service restart, post-flight/healthand/v1/modelsverification. - Old version (< v3.4.0): fresh-install. Pre-v3.4 lacked admin-key/usage-db, so there is nothing to migrate. Your OAuth token (managed by the Claude Code CLI, not OCP) is preserved; you do not need to re-OAuth unless your token expired separately.
Snapshots are saved to ~/.ocp/upgrade-snapshot-<ISO-ts>/ and never
auto-deleted. Clean old ones with rm -rf ~/.ocp/upgrade-snapshot-* once
you're confident the upgrade is stable.
Manual upgrade — same command, no AI
ocp update # smart-pick path
ocp update --check # show available updates, don't apply
ocp update --dry-run # preview plan
ocp update --target v3.13.0 # pin a specific version
ocp update --rollback --yes # restore most recent snapshot (--yes confirms)
ocp update --rollback --list # list snapshots, no mutation
ocp update --rollback --dry-run # preview rollback plan
When upgrade fails
ocp update prints a recovery line on failure. To restore from the snapshot:
ocp update --rollback --yes # --yes confirms the destructive restore
ocp doctor
If ocp doctor still reports problems after rollback, open a GitHub issue
with the snapshot path and the doctor JSON output (ocp doctor --json).
OpenClaw Auto-Sync (v3.11.0+)
Whenever the model list in models.json changes, ocp update automatically reconciles your OpenClaw config so the model dropdown stays in sync — no more "I upgraded OCP but my Telegram bot still shows the old models" surprises.
What gets synced (and only this — all other config keys are preserved):
models.providers."claude-local".modelsin~/.openclaw/openclaw.jsonagents.defaults.models["claude-local/*"]aliases
Safety:
- Timestamped backup written before every change:
~/.openclaw/openclaw.json.bak.<ms> - Idempotent — already-in-sync runs are a no-op (no backup, no rewrite)
- Non-fatal — sync failure does NOT abort
ocp update;/v1/modelsstill works - Skips silently if OpenClaw is not installed (
~/.openclaw/openclaw.jsonmissing)
Manual trigger (e.g. after fixing a hand-edited config, or for the one-time v3.10.0→v3.11.0 bootstrap quirk):
node ~/ocp/scripts/sync-openclaw.mjs
node ~/ocp/scripts/sync-openclaw.mjs --quiet # silent unless changes
Opt-out: ocp update only invokes the sync if node and scripts/sync-openclaw.mjs are both present. Removing the script disables auto-sync; the rest of ocp update still works.
One-time bootstrap caveat (v3.10.0 → v3.11.0 only): the first ocp update to v3.11.0 runs the old cmd_update already loaded into your shell, so the new sync hook does NOT fire on this single jump. Run node ~/ocp/scripts/sync-openclaw.mjs once manually. Every future update from v3.11.0+ syncs automatically.
Other IDEs (Cline / Aider / Cursor / opencode) query /v1/models live, so they pick up new models on the next request — no sync needed. Continue.dev users edit their own config.json model id manually.
Runtime Settings (No Restart Needed)
$ ocp settings maxPromptChars 200000
✓ maxPromptChars = 200000
$ ocp settings maxConcurrent 4
✓ maxConcurrent = 4
Response Cache
OCP can cache responses to avoid redundant Claude CLI calls for identical prompts. This is useful during development when the same prompt is sent repeatedly.
Enable by setting CLAUDE_CACHE_TTL (in milliseconds):
# Cache responses for 5 minutes
export CLAUDE_CACHE_TTL=300000
# Or update at runtime (no restart)
ocp settings cacheTTL 300000
How it works:
- Cache key = SHA-256 of
v2|<keyId or "anon">|model + messages + temperature + max_tokens + top_p - Per-key isolation — different API keys never share cache entries; anonymous callers share one
anonpool - Cache hits return instantly — no Claude CLI process spawned
- Streaming hits are replayed as multiple SSE chunks (80 codepoints each), not one large delta — incremental render preserved
cache_controlbypass — if a request carries an Anthropiccache_controlannotation (top-level or nested incontent[]), OCP skips its own cache entirely so it doesn't interfere with Anthropic-side prompt caching- Singleflight stampede protection — concurrent identical cache-miss requests share one upstream
cli.jsspawn; followers receive byte-identical responses to the leader's call. Non-streaming path only (streaming-path singleflight is a known TODO) - Multi-turn conversations (with
session_id) are never cached - Expired entries are cleaned up automatically every 10 minutes
Management:
# View cache stats (now includes singleflight in-flight counts)
curl http://127.0.0.1:3456/cache/stats
# → { "entries": 42, "totalHits": 156, "sizeBytes": 284000, "inflight": 0, "requesters": 0 }
# Clear all cached responses
curl -X DELETE http://127.0.0.1:3456/cache
# Disable cache at runtime
ocp settings cacheTTL 0
Cache is disabled by default (CLAUDE_CACHE_TTL=0). All data is stored locally in ~/.ocp/ocp.db.
Hash format upgrade in v3.13.0: legacy v1 cache rows from earlier versions don't match new v2-format lookups; they orphan and are reaped by the TTL cleanup interval within one window. No migration script required.
How It Works
Your IDE → OCP (localhost:3456) → claude --output-format stream-json CLI → Anthropic (via subscription)
OCP translates OpenAI-compatible /v1/chat/completions requests into claude --output-format stream-json CLI calls. Anthropic sees normal Claude Code usage — no API billing, no separate key needed.
Client-tools boundary
OCP is a text-prompt bridge to the official claude CLI. It does not pass through OpenAI tools/functions payloads or Anthropic tool_use blocks to the client. Clients (Cline, Cursor, OpenClaw, etc.) pointed at OCP receive assistant TEXT only — they never get tool_calls to execute locally.
Any tool use happens server-side, under the --allowedTools set configured on the OCP host. In default mode (no CLAUDE_NO_CONTEXT), the claude CLI's own built-in tools are available to the model; in TUI mode, the operator controls the tool surface via OCP_TUI_FULL_TOOLS. Either way, the tools run under the operator's credentials on the server, and the client sees only the final text output.
Client-local tool execution is not supported by design. Supporting it would require bypassing the claude CLI to call the raw Anthropic API directly — that is a different product, and is out of scope per ALIGNMENT.md (every OCP endpoint must correspond to something cli.js actually does).
Available Models
| Model ID | Notes |
|---|---|
claude-opus-4-8 |
Most capable (default for opus alias) |
claude-opus-4-7 |
Previous Opus, retained for pinning |
claude-opus-4-6 |
Older Opus, retained for pinning |
claude-sonnet-4-6 |
Good balance of speed/quality (default for sonnet alias) |
claude-haiku-4-5-20251001 |
Fastest, lightweight (default for haiku alias) |
The canonical list lives in models.json — the single source of truth as of v3.11.0. Both server.mjs (the /v1/models endpoint) and setup.mjs (the OpenClaw registration) derive from it. Adding a new model is now a one-file edit:
# 1. Edit models.json — add an entry
# 2. Bump version, commit, tag, push
# 3. Users get it on next `ocp update`:
# - OpenClaw: auto-synced via scripts/sync-openclaw.mjs
# - Cline / Aider / Cursor / opencode: live /v1/models, picks up immediately
# - Continue.dev: user edits their own config.json
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/v1/models |
GET | List available models |
/v1/chat/completions |
POST | Chat completion (streaming + non-streaming) |
/health |
GET | Comprehensive health check (includes a tui block for TUI-mode drift/concurrency monitoring) |
/usage |
GET | Plan usage limits + per-model stats |
/status |
GET | Combined overview (usage + health) |
/settings |
GET/PATCH | View or update settings at runtime |
/logs |
GET | Recent log entries (?n=20&level=error) |
/sessions |
GET/DELETE | List or clear active sessions |
/dashboard |
GET | Web dashboard (always public) |
/api/keys |
GET/POST | List or create API keys (admin only) |
/api/keys/:id |
DELETE | Revoke an API key (admin only) |
/api/keys/:id/quota |
GET/PATCH | View or set per-key quota (admin only) |
/api/usage |
GET | Per-key usage stats (?since=&until=&hours=&limit=); returns self only by default — pass ?all=true (admin only) for all-keys data |
/cache/stats |
GET | Cache statistics (admin only) |
/cache |
DELETE | Clear response cache (admin only) |
OpenClaw Integration
OCP was originally built for OpenClaw and includes deep integration:
setup.mjsauto-configures theclaude-localprovider inopenclaw.jsonat install timeocp updateauto-syncs theclaude-localmodel registry frommodels.json(v3.11.0+) — no more stale model dropdowns after upgrades- Gateway plugin registers
/ocpas a native slash command in Telegram/Discord - Multi-agent — 8 concurrent requests sharing one subscription
- No conflicts — uses neutral service names (
dev.ocp.proxy/ocp-proxy) that don't trigger OpenClaw's gateway-like service detection
Install the Gateway Plugin
cp -r ocp-plugin/ ~/.openclaw/extensions/ocp/
Add to ~/.openclaw/openclaw.json:
{
"plugins": {
"allow": ["ocp"],
"entries": { "ocp": { "enabled": true } }
}
}
Restart: openclaw gateway restart
Telegram / Discord Usage
After installing the gateway plugin, use /ocp slash commands in your chat:
/ocp status — Quick overview
/ocp usage — Plan usage limits & model stats
/ocp models — Available models
/ocp health — Proxy diagnostics
/ocp keys — List all API keys (multi mode)
/ocp keys add <name> — Create a new key
/ocp keys revoke <name> — Revoke a key
Note: Terminal CLI uses
ocp <command>, Telegram/Discord uses/ocp <command>.
Troubleshooting
The simplest path: ask your AI.
Paste this prompt:
Run `ocp doctor` and follow its `next_action`. Tell me if you hit
anything that needs human input.
The doctor produces a JSON next_action with ai_executable[] (commands
the agent runs verbatim) and human_required[] (steps that need you,
typically just OAuth).
Manual debugging
Setup fails with "claude: command not found"
setup.mjs requires the Claude CLI to be on PATH. Install it via the official guide, confirm with which claude, then run claude auth login before re-running node setup.mjs.
Setup fails with "EADDRINUSE: port 3456 already in use"
Something else is already bound to port 3456 — usually an old OCP instance. Check what:
lsof -nP -iTCP:3456 -sTCP:LISTEN
If it's an old OCP process, stop it before re-running setup:
ocp stop # if the CLI is on PATH
launchctl bootout gui/$(id -u)/dev.ocp.proxy # macOS launchd fallback
sudo systemctl stop ocp-proxy # Linux systemd fallback
Setup fails with "node: command not found" or version error
OCP requires Node.js 22.5+. Install:
brew install node # macOS
# Linux: see https://nodejs.org/en/download for current install commands
Confirm with node --version (should be ≥ v22.5).
Requests fail or agents stuck
# Clear sessions and restart
ocp clear
ocp restart
# If using OpenClaw gateway
openclaw gateway restart
Env var change (e.g. CLAUDE_BIND, CLAUDE_CODE_OAUTH_TOKEN) doesn't take effect after restart
On macOS, ocp restart does a full launchctl bootout + bootstrap of the agent, which re-reads the plist EnvironmentVariables — so an env change you made (in ~/Library/LaunchAgents/dev.ocp.proxy.plist) actually takes effect:
ocp restart
This is deliberate: the older launchctl kickstart -k only re-execs the process and reuses launchd's cached environment, so plist env edits would be silently ignored. If you ever restart the agent by hand, use bootout+bootstrap, not kickstart -k:
launchctl bootout gui/$(id -u)/dev.ocp.proxy 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.ocp.proxy.plist
Verify the new value reached the running process:
ps -E -p "$(launchctl print gui/$(id -u)/dev.ocp.proxy 2>/dev/null | awk '/pid =/{print $3}')" | tr ' ' '\n' | grep CLAUDE_
On Linux, systemctl --user restart already re-reads the unit's EnvironmentFile, so no special handling is needed.
Usage shows "unknown"
Usually caused by an expired Claude CLI session. Fix:
claude auth login
ocp restart
Startup log warns "OpenClaw registry out of sync"
On boot, OCP compares OpenClaw's registered models against models.json and warns if they drift. Cause: someone (or an OpenClaw upgrade) modified ~/.openclaw/openclaw.json and removed entries OCP expects. Fix:
node ~/ocp/scripts/sync-openclaw.mjs
This is read-only at startup; the warning never blocks the gateway from running.
A TUI session vanished right after upgrading OCP
If you ran a pre-3.21.1 OCP instance and a post-3.21.1 instance on the same host at the same time during an upgrade, the new instance's one-time boot reap can, once, kill an old-format (ocp-tui-<8hex>) live TUI session belonging to the still-running old instance — restart the affected session (ocp restart or re-run your TUI turn) and it will come back under the new instance's port-scoped naming.
OpenClaw shows old models after ocp update (v3.10→v3.11 only)
One-time bootstrap quirk for the v3.10.0 → v3.11.0 jump only — the running shell had the old cmd_update cached. Run once manually:
node ~/ocp/scripts/sync-openclaw.mjs
openclaw gateway restart # so OpenClaw re-reads the config
Future ocp update invocations sync automatically.
TUI-mode returns Please run /login · API Error: 401 (re-login doesn't stick)
A long-running TUI-mode host can get stuck returning a permanent 401 that re-login cannot fix.
Root cause (two layers): interactive claude prefers ~/.claude/.credentials.json over the CLAUDE_CODE_OAUTH_TOKEN env var (this is unlike the -p path, where the env token wins). So (a) a stale/corrupt credentials.json shadows the env token — passing the token is not enough on its own; and (b) when claude does use credentials.json, its single-use OAuth refresh token can be corrupted (ending up an empty string) by the per-request spawn + kill-session teardown racing claude's token rotation. Re-login writes a fresh token, but the next spawn re-corrupts it. Proven live on PI231: env token passed + broken credentials.json present → 401; env token passed + credentials.json moved aside → works.
Fix: set CLAUDE_CODE_OAUTH_TOKEN on the OCP host and leave OCP_TUI_HOME unset. OCP then runs the TUI claude in a credential-isolated home ($HOME/.ocp-tui/home) that has no credentials.json at all, so the env token is the only credential (authoritative — nothing shadows it) and claude never runs the refresh path (so the single-use token can't be corrupted). Then restart — on systemd daemon-reload, on launchd bootout+bootstrap; kickstart -k does not reload env. Verify the env reached the process and the boot log shows the isolated home:
# Linux (systemd): confirm the token is in the service env
tr '\0' '\n' < /proc/$(pgrep -f server.mjs | head -1)/environ | grep CLAUDE_CODE_OAUTH_TOKEN
# Boot log should read: TUI-mode: ON home=$HOME/.ocp-tui/home ... auth=env-token (credential-isolated home — no credentials.json)
If you previously set
OCP_TUI_HOMEto the real home (or any home that contains acredentials.json), unset it so the credential-isolated default takes effect — otherwise the shadowingcredentials.jsonremains in play.
See Subscription-pool (TUI) mode and ADR 0007 PR-C / PR-D amendments.
Environment Variables
| Variable | Default | Description |
|---|---|---|
CLAUDE_PROXY_PORT |
3456 |
Listen port (server-side). Also consumed by the OpenClaw ocp-plugin to dial the local proxy. |
OCP_PROXY_URL |
(unset) | Plugin-side full URL override (e.g. http://10.0.0.5:3456). Wins over CLAUDE_PROXY_PORT when both are set. Read by ocp-plugin/index.js only — server ignores it. |
CLAUDE_BIND |
127.0.0.1 |
Bind address (0.0.0.0 for LAN access) |
CLAUDE_AUTH_MODE |
none |
Auth mode: none, shared, or multi |
OCP_ADMIN_KEY |
(unset) | Admin key for key management (multi mode) |
CLAUDE_BIN |
(auto-detect) | Path to claude binary |
CLAUDE_TIMEOUT |
600000 |
Request timeout (ms, default: 10 min) |
CLAUDE_HEARTBEAT_INTERVAL |
0 |
Streaming SSE keepalive interval (ms). 0 = disabled. See "Streaming heartbeat" section. |
CLAUDE_MAX_CONCURRENT |
8 |
Max concurrent claude processes (-p/stream-json path) |
CLAUDE_MAX_QUEUE |
16 |
Max requests waiting for a -p concurrency slot. Beyond CLAUDE_MAX_CONCURRENT, requests queue (up to this cap) instead of being rejected; when the queue is also full, the request gets HTTP 429 + Retry-After (not an opaque 500). Surfaced on /health.concurrency + /health.stats.queueRejections. |
CLAUDE_QUEUE_RETRY_AFTER |
5 |
Seconds advertised in the Retry-After header on a -p concurrency-overflow 429. |
CLAUDE_MAX_PROMPT_CHARS |
150000 |
Prompt truncation limit (chars) |
CLAUDE_SESSION_TTL |
3600000 |
Session expiry (ms, default: 1 hour) |
CLAUDE_CACHE_TTL |
0 |
Response cache TTL (ms, 0 = disabled). Set to e.g. 300000 for 5-min cache |
CLAUDE_ALLOWED_TOOLS |
Bash,Read,...,Agent |
Comma-separated tools to pre-approve |
CLAUDE_SKIP_PERMISSIONS |
false |
Bypass all permission checks |
CLAUDE_NO_CONTEXT |
false |
Suppress CLAUDE.md and auto-memory injection (pure API mode) |
PROXY_API_KEY |
(unset) | Bearer token for shared-mode authentication |
PROXY_ANONYMOUS_KEY |
(unset) | Well-known anonymous key allowlist (multi mode). When set, this exact string bypasses validateKey() and grants public access. Exposed via /health.anonymousKey only to localhost, or to all callers when PROXY_ADVERTISE_ANON_KEY=1. See Anonymous Access. |
PROXY_ADVERTISE_ANON_KEY |
(unset) | When =1, advertise PROXY_ANONYMOUS_KEY in the public /health body for remote zero-config discovery. Default off — /health is unauthenticated, so this exposes the shared key to any LAN-reachable device (issue #109). Localhost always sees it regardless. |
CLAUDE_TUI_MODE |
false |
Opt-in. Set to "true" to serve requests via interactive claude (no -p / --output-format → cc_entrypoint=cli, subscription pool). Single-user only — see Subscription-pool (TUI) mode for the security constraint. |
CLAUDE_CODE_OAUTH_TOKEN |
(unset) | OAuth bearer token (highest-precedence credential source for the -p path). Recommended for TUI-mode hosts: when set (and OCP_TUI_HOME unset), OCP runs the interactive claude in a credential-isolated home ($HOME/.ocp-tui/home, no credentials.json) so this long-lived token is the only credential and is authoritative — interactive claude otherwise prefers ~/.claude/.credentials.json over the env var, so a stale one shadows the token and its single-use refresh token gets corrupted by the spawn/teardown cycle (the permanent Please run /login 401 — see Subscription-pool (TUI) mode and ADR 0007 PR-D). The token appears in the pane command (ps-visible) — acceptable for the single-user A-path; the multi-user B-path is refused at boot. |
OCP_SPAWN_REAL_HOME |
(unset) | Kill-switch for the default -p/stream-json spawn-home isolation (latency fix). When unset and an OAuth token is resolvable, OCP runs the per-request claude spawn in a credential-free minimal scratch home ($HOME/.ocp/spawn-home, no .credentials.json/settings.json/plugins) with a neutral cwd and the env token — so it loads none of the operator's heavy global ~/.claude (plugins/skills/hooks) or the project CLAUDE.md, cutting per-request latency (measured ~10–28s → ~3–7s). Set to "1" to force the legacy real-HOME spawn (no cwd override) even when a token exists. With no resolvable token, OCP falls back to the real HOME automatically (zero regression). Active mode is shown at startup and on /health.spawn. |
CLAUDE_TUI_WALLCLOCK_MS |
120000 |
(TUI-mode) Maximum time in ms to wait for the native transcript to signal turn completion. Increase for long Opus thinking turns. |
OCP_TUI_CWD |
$HOME/.ocp-tui/work |
(TUI-mode) Scratch working directory where interactive claude sessions run. Transcripts land under <HOME>/.claude/projects/<encoded-cwd>/. Created automatically. |
OCP_TUI_HOME |
(auto) | (TUI-mode) HOME claude runs under. When unset, OCP picks it for you: if CLAUDE_CODE_OAUTH_TOKEN is set → a credential-isolated scratch home $HOME/.ocp-tui/home (no credentials.json, env-token auth — recommended); if no env token → the operator's real home (legacy shared credentials.json). Setting this to an explicit path overrides the auto-default. The credential handling at that path still follows the env token: with the env token it is credential-free (env-token auth, no credentials.json written); without the env token (and the path ≠ real home) it uses the legacy symlinked-credentials scratch mode, which carries the credential-fork caveat — see ADR 0007. |
OCP_TUI_ENTRYPOINT |
cli |
(TUI-mode) Billing-classifier labeling: cli (default) pins cc_entrypoint=cli deterministically; auto lets claude self-classify via TTY detection; off leaves the inherited env untouched. Honest only when the spawn is a genuine interactive PTY — see ADR 0007. |
OCP_TUI_EFFORT |
low |
(TUI-mode) Effort level passed to the interactive claude as an explicit --effort flag: low (default), medium, high, xhigh, max, or inherit to omit the flag (the pre-flag behaviour: the pane inherits a HOME-dependent effort — the operator's ~/.claude/settings.json effortLevel in real-home mode, claude's built-in default in env-token scratch mode). Explicit low cuts measured TTFT p50 by ~40% and collapses run-to-run variance ~15× versus an inherited xhigh (see docs/plans/2026-07-13-tui-latency/); proxied requests rarely benefit from extended thinking. Banner-verified to stay on the subscription pool (· Claude Max). An invalid value logs a warning and falls back to low. |
OCP_TUI_STREAM |
0 (off) |
(TUI-mode) When =1, stream:true requests emit real SSE delta.content chunks as claude generates them, instead of buffering the turn and replaying it. Deltas come from claude's own MessageDisplay hook (registered with --settings on the ordinary interactive spawn — banner-verified to stay on the subscription pool, · Claude Max). Granularity is block-level, not token-level. The transcript remains authoritative: the streamed text is asserted equal to it at end-of-turn, the auth-banner and truncation gates still run before anything is committed, and only the transcript text is cached. A turn whose stream cannot be reconciled with the transcript is refused (SSE error frame, not cached) and counted as tui.streamDivergences on /health. A total hook failure (e.g. --settings stops registering it after a claude version bump) is a different, silent failure mode — every streamed turn still succeeds, fully buffered, with no divergence and no error — so it is counted separately as tui.streamZeroDeltaTurns (streamed turns where the hook fired zero times) and logged as tui_stream_zero_deltas; watch it alongside streamDivergences. Default off — the buffered path is unchanged and remains the stable default. ⚠️ Tool-using turns: the transcript keeps only the model's last assistant message, so if the model narrates before calling a tool ("I'll check that file…") and that narration exceeds OCP_TUI_STREAM_HOLDBACK, it has already been streamed and cannot be retracted — the turn is then refused rather than served (measured live: Opus narrated 475 chars before a Bash call). If your deployment lets the model use tools (the TUI default, and anything with OCP_TUI_FULL_TOOLS=1), either raise OCP_TUI_STREAM_HOLDBACK above the typical narration length — the narration then stays held back and is correctly discarded, at the cost of a later first chunk — or leave streaming off. Streaming is best suited to tool-light chat proxying. See ADR 0007 (2026-07-13 amendment). |
OCP_TUI_STREAM_HOLDBACK |
100 |
(TUI-mode, streaming) Characters withheld before the first chunk reaches the client. Two jobs. (1) It keeps the auth-banner gate alive under streaming, via a guarantee with two required halves: (i) nothing is emitted for a message until its trimmed accumulation exceeds 100 chars — past the default banner detector's reach, since real banners are ≤100 chars — and (ii) once a message boundary follows an emit, nothing further is ever emitted for the rest of the turn, and the turn is refused outright. Half (i) alone only covers a turn's first message; half (ii) is what covers an error banner rendered as a later message (e.g. after tool-using prose). Raise the holdback if you replace the detector via CLAUDE_TUI_ERROR_PATTERNS with patterns that can match longer messages — that only affects half (i); OCP warns at boot if you do. (2) It is the knob for tool-using turns — see the OCP_TUI_STREAM caveat below. Answers shorter than the holdback are simply delivered whole at end-of-turn, exactly as the buffered path does. |
OCP_TUI_STREAM_DIR |
$HOME/.ocp-tui/stream |
(TUI-mode, streaming) Directory holding the static MessageDisplay hook script + settings file, and the per-session delta sink (<session-id>.jsonl, removed at turn teardown). One sink per session-id — this is what keeps concurrent TUI turns (OCP_TUI_MAX_CONCURRENT ≥ 2) from interleaving one client's deltas into another's stream. |
OCP_TUI_STREAM_POLL_MS |
100 |
(TUI-mode, streaming) Interval at which OCP drains the delta sink. The hook fires at block granularity (seconds apart), so a finer poll buys nothing. |
OCP_TUI_MAX_CONCURRENT |
2 |
(TUI-mode) Max concurrent interactive TUI turns. Independent of CLAUDE_MAX_CONCURRENT (which bounds the -p/stream-json path; TUI never uses it). A TUI turn is heavy (per-request cold-boot of tmux+claude + up to CLAUDE_TUI_WALLCLOCK_MS wallclock), so the default is low to keep small hosts (e.g. a Pi 4) alive under a burst. Excess turns queue (bounded); a full queue yields a 503. See ADR 0007 PR-B amendment. |
OCP_TUI_POOL_SIZE |
0 (off) |
(TUI-mode) Number of pre-booted warm claude panes kept ready, so a request does not pay the cold boot. 0 disables the pool entirely — the request path is then exactly the cold-boot path. Max 4; an unparseable value disables it rather than guessing. Measured on a Mac mini (Sonnet 4.6, --effort low): end-to-end p50 10.17s (n=6, pool off) → 6.00s (n=12 warm hits) — −4.2 s / −41% — the pool recovers both the ~1.2 s boot and ~2.9 s of post-input-bar init that a pane which has been idle a moment has already finished. Cost: each warm pane is a live idle claude process held whether or not a request ever arrives (peak processes ≈ pool size + OCP_TUI_MAX_CONCURRENT + 1 booting replacement) — which is why it is opt-in. Panes are single-use: one turn, then killed and replaced in the background. The first request after start (and after any model switch) is always a cold miss — the pool warms the most recently requested model, since OCP cannot know which model the next caller wants. See docs/plans/2026-07-13-tui-latency/. |
OCP_SKIP_AUTH_TEST |
(unset) | When =1, skip the claude -p auth probe during setup.mjs. After 2026-06-15 this probe draws from the Agent SDK credit pool; set this to avoid burning a metered credit on re-installs or ocp update runs. Auth is validated at the first real request. |
OCP_TUI_FULL_TOOLS |
(unset) | (TUI-mode, single-user only) When =1, grant the interactive session the same tool surface as the -p path — --allowedTools (+ optional --mcp-config, read from CLAUDE_ALLOWED_TOOLS / CLAUDE_MCP_CONFIG) — instead of the default MCP-walled, built-in-tools-only set. Lets a trusted single-operator TUI deployment run a tool-using / MCP agent (e.g. an OpenClaw assistant) on the subscription pool. Safe because TUI refuses to boot under AUTH_MODE=multi (hard exit) — no guest key can ever reach the TUI path, so this gate cannot expose tools to an untrusted caller. (Under AUTH_MODE=shared + OCP_TUI_ALLOW_LAN=1, anyone holding the single shared key reaches it — that is the existing TUI trust model, unchanged.) Note: --dangerously-skip-permissions / CLAUDE_SKIP_PERMISSIONS is not supported for TUI — claude v2.1.x shows an interactive bypass-acceptance screen in headless tmux that cannot be answered, bricking the pane. Use scratch-home settings.json additionalDirectories instead. See Subscription-pool (TUI) mode and ADR 0007. |
Streaming heartbeat
When CLAUDE_HEARTBEAT_INTERVAL is set to a positive integer (milliseconds), OCP emits an SSE comment frame (: keepalive\n\n) on streaming responses whenever the stream has been idle for that duration. The timer resets on every real chunk, so heartbeats only fire during genuine silent windows (for example, Claude CLI tool-use pauses of 30s–5min, or a long "processing large contexts" delay before the first token).
Use cases: downstream HTTP clients or load balancers with idle-connection timeouts that would otherwise abort a slow-but-alive request. CLAUDE_HEARTBEAT_INTERVAL=30000 (30s) is a reasonable starting value if your downstream has a 60s idle timeout.
Heartbeats are inert SSE comment lines — conforming SSE clients ignore them. If your downstream client's SSE parser crashes on comment frames, leave this disabled (the default) and file an issue so we can consider an alternate frame format.
OCP also sends X-Accel-Buffering: no on SSE responses so nginx-default proxy buffering does not hold heartbeats in an upstream buffer.
Subscription-pool (TUI) mode
SECURITY — read before enabling.
TUI-mode is single-user / single-operator only.clauderuns with the OCP process owner's filesystem access regardless ofHOMEsetting. If OCP serves multiple users or guest API keys, a guest prompt could exfiltrate files or exhaust the subscription. Never enableCLAUDE_TUI_MODE=trueon a multi-user OCP.
What it is and why
From 2026-06-15 Anthropic routes claude invocations by cc_entrypoint:
| Launch method | cc_entrypoint |
Billing pool |
|---|---|---|
claude -p / --output-format (OCP default) |
sdk-cli |
Agent SDK credit pool (~$20/mo on Pro) |
Interactive claude (no flags) |
cli |
Pro/Max subscription pool |
TUI-mode lets OCP serve requests via the interactive path so they bill against the subscription pool. The response is read from claude's native JSONL session transcript once the turn is complete, then replayed to the caller as a normal OpenAI completion or chunked SSE response.
Billing-classifier labeling (OCP_TUI_ENTRYPOINT)
OCP_TUI_ENTRYPOINT (default cli) controls how CLAUDE_CODE_ENTRYPOINT is set on the spawn
environment. The default (cli) pins the value deterministically — immune to a stray inherited
env var or a future stdout-redirect bug silently flipping it to sdk-cli. This label is honest
only when the spawn is a genuine interactive PTY (tmux pane, no -p, stdout not redirected,
and tmux new-session verified to succeed). If you need to observe the raw TTY-derived value, set
OCP_TUI_ENTRYPOINT=auto. See ADR 0007 for the full rationale and governing rule.
Enabling TUI-mode (opt-in)
# Prerequisites
mkdir -p ~/.ocp-tui/work # one-time scratch cwd setup
# tmux must be installed: brew install tmux / apt install tmux
# Enable
export CLAUDE_TUI_MODE=true
# STRONGLY RECOMMENDED on a TUI host — authenticate via the long-lived OAuth token.
# With this set (and OCP_TUI_HOME left UNSET), OCP runs the interactive claude in a
# credential-isolated home ($HOME/.ocp-tui/home, no credentials.json), so the env token
# is the only credential and is authoritative. This both stops a stale credentials.json
# from shadowing the token AND ends the refresh-token corruption that caused a permanent
# "Please run /login" 401 (no credentials file → claude never runs the refresh path).
# See the auth note below + ADR 0007 PR-D.
export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
# Optionally tune:
export CLAUDE_TUI_WALLCLOCK_MS=180000 # 3 min cap for long Opus turns
export OCP_TUI_CWD=$HOME/.ocp-tui/work # default; override if needed
export OCP_TUI_ENTRYPOINT=cli # default; use 'auto' to observe TTY-derived value
# Do NOT set OCP_TUI_HOME for the recommended setup — leaving it unset is what enables
# the credential-isolated home. Set it only to opt into the legacy symlinked-creds mode.
Then restart OCP. At boot you will see (with the env token set, isolated home auto-selected):
⚠️ TUI-mode ON — single-user only; do NOT enable on a multi-user OCP ...
TUI-mode: ON home=/home/user/.ocp-tui/home cwd=/home/user/.ocp-tui/work auth=env-token (credential-isolated home — no credentials.json) wallclock=120000ms maxConcurrent=2
What changes / what doesn't
- Callers see no API change. The response is a normal OpenAI completion object or chunked SSE — identical wire format.
- Real streaming is opt-in (
OCP_TUI_STREAM=1), and off by default. By default TUI-mode buffers the full response and replays it as chunked SSE — you see a delay, then the complete response. SetOCP_TUI_STREAM=1andstream:trueturns emit real SSEdelta.contentchunks asclauderenders them, sourced fromclaude's ownMessageDisplayhook (byte-faithful raw markdown, on the subscription pool, no-p). Two honest caveats: granularity is block-level — the hook fires once per rendered block, so a handful of chunks per answer, scaling with length, not token-by-token; and it moves the first byte, not the last, so a consumer that must parse a complete reply gains nothing. The transcript stays authoritative: every streamed turn is asserted against it at the end, and a turn whose stream disagrees is failed rather than served (watchtui.streamDivergenceson/health). Evidence:docs/plans/2026-07-13-tui-latency/streaming-spike.md. - Cache and singleflight work normally. TUI-mode writes the buffered response to the cache on success; cache-hits skip the interactive turn entirely.
- The host's
CLAUDE.md/ auto-memory is never injected. OCP is a proxy — the proxied client (OpenClaw / your IDE) owns its own context and memory. TUI-mode always runsclaudewithCLAUDE_CODE_DISABLE_CLAUDE_MDS+CLAUDE_CODE_DISABLE_AUTO_MEMORY, so aCLAUDE.mdon the OCP host can never leak into proxied turns (verified live; see #4). Built-in tool schemas + the interactive system prompt remain (the inherent ~20–35K context floor of interactive mode); MCP is hard-disabled. - Authenticate via
CLAUDE_CODE_OAUTH_TOKENin a credential-isolated home (recommended). tmux does not forward the parent process's env to the pane, so OCP sets the token explicitly on the spawnedclaudewhenCLAUDE_CODE_OAUTH_TOKENis present. But passing the token is not enough on its own: interactiveclaudeprefers~/.claude/.credentials.jsonover the env var (unlike the-ppath), so a stalecredentials.jsonwould shadow the token. With the env token set andOCP_TUI_HOMEunset, OCP therefore runs claude in a credential-isolated home ($HOME/.ocp-tui/home) that has nocredentials.json— so the env token is the only credential and is authoritative, and claude never runs the token-refresh path (so the single-use refresh token can't be corrupted by the spawn/teardown cycle). On a long-running host the credentials.json path produced a permanentPlease run /login · API Error: 401that re-login could not fix (the next spawn re-corrupted it); the isolated home ends that at the root. Transcripts land under the same isolated home, so the answer-reader is unaffected. Without the env token, claude falls back to the real home'scredentials.json(byte-for-byte the previous behaviour). (The token is visible inpson the pane command — acceptable for the single-user A-path; the multi-user B-path is refused at boot.) See ADR 0007 PR-C / PR-D amendments. - Stale tmux sessions are reaped. The pane's
claudeis a child of the tmux server (not OCP), so OCP cannot reap it directly;claudezombies can otherwise accumulate as<defunct>over a long-running host. OCP reaps them at boot and on a 15-min idle sweep by issuingtmux kill-server— but only when no foreign tmux session remains (it never disrupts a co-hostedolp-tui-*instance). See ADR 0007 PR-C amendment. - Default path unchanged. Unset
CLAUDE_TUI_MODEand restart →callClaude/callClaudeStreamingare used again, byte-for-byte identical to today. - Concurrency is bounded separately. TUI turns are heavy (per-request cold-boot + long wallclock), so the TUI path has its own limiter —
OCP_TUI_MAX_CONCURRENT(default2), independent ofCLAUDE_MAX_CONCURRENT. Excess turns queue; a full queue returns a 503. Tune it up only on a host that can run more interactiveclaudesessions at once. - Optional warm pane pool (
OCP_TUI_POOL_SIZE, default off). Pre-boots panes so a request skips the cold boot — measured p5010.17s→6.00s(−41%). Pooled panes are single-use (one turn, then killed and replaced in the background), each carrying its own fresh--session-id, so one session still means one exchange and no earlier-turn text can leak into a later answer. They are namedocp-tui-<port>-p<hex>and coexist with the reaper by design: the sweep drains the pool first, then reaps (sokill-serverstill flushes<defunct>zombies), then the pool refills in the background. Drain→reap→resume is synchronous, so no request can land mid-sweep; a request arriving while the pool is still re-booting simply misses it and cold-boots. A live pooled pane is never reaped — including one that is still booting, whose tmux session already exists — while an orphaned one (left by a previous process generation) still is.
⚠️ Latency: TUI mode has a ~6-second floor, and it is immovable
TUI mode cannot serve real-time or interactive-latency consumers. This is a hard property of the path, stated plainly so you can rule it out before building on it:
| measured | |
|---|---|
| TTFT floor (first token) | ≈ 6 s — immovable |
| cold boot → input bar ready | ~1 s (per request; not the bottleneck) |
| OCP's own overhead above the CLI | ~4 s (n=1 same-turn decomposition) |
| direct Anthropic API, same prompt (for scale) | 0.84–1.64 s |
The ~6 s floor is the claude CLI itself: it always injects the full Claude Code system prompt plus
its tool definitions before your prompt, on every turn, no matter what you ask. No flag removes it
(--exclude-dynamic-system-prompt-sections was measured: no effect on the floor). Extended
thinking is not the cause — OCP_TUI_EFFORT already defaults to low, which is what cuts a
formerly-inherited xhigh down to this floor and collapses its variance.
On top of the floor you pay the model's generation time (a function of output length). Progressive output is not wired up yet (see "No real token streaming" above — it is achievable and planned), so today a turn returns as one blob once generation completes. Note that streaming, when it lands, will move the first byte earlier — it does not shorten the turn, and a consumer that needs the complete answer gains nothing from it.
Use TUI mode for: batch, background, and latency-insensitive work where the subscription pool is
the point. Do not use it for: anything a person is waiting on interactively, or any consumer with
a sub-5-second budget. Full measurements and methodology:
docs/plans/2026-07-13-tui-latency/.
Monitoring drift via /health
GET /health includes a tui block so you can poll for a silent billing-pool drift (the top risk after the 6/15 flip — a lost TTY flipping cc_entrypoint from cli to the metered sdk-cli pool would still return answers but burn metered credits). The block is always present (with enabled:false when TUI-mode is off):
"tui": {
"enabled": true, // CLAUDE_TUI_MODE === "true"
"entrypointMode": "cli", // OCP_TUI_ENTRYPOINT (cli | auto | off)
"lastEntrypoint": "cli", // last cc_entrypoint observed in a transcript, or null
"entrypointMismatches": 0, // count of cli-expected-but-got-other turns — ALERT if this climbs
"inflight": 1, // TUI turns running right now
"queued": 0, // TUI turns waiting for a concurrency slot
"maxConcurrent": 2, // OCP_TUI_MAX_CONCURRENT
"pool": { // warm pane pool — null when OCP_TUI_POOL_SIZE=0 (the default)
"size": 2, // target warm panes (OCP_TUI_POOL_SIZE)
"warm": 2, // panes ready right now — each is a LIVE idle claude process
"booting": 0, // replacement panes currently pre-booting
"model": "claude-sonnet-4-6", // the model being warmed (the most recently requested one)
"hits": 12, // requests served by a warm pane
"misses": 1, // requests that fell back to the cold boot (the 1st is always one)
"boots": 14, // panes successfully pre-booted
"bootFailures": 0, // pre-boots that genuinely never reached the input bar — WATCH this
"cancelled": 4, // in-flight boots OCP killed on purpose (drain / model switch) — not faults
"dropped": 8 // panes discarded unused (drain sweep / expired / unhealthy)
}
}
Alert on entrypointMismatches > 0 (or lastEntrypoint !== "cli"): it means a turn drew from the metered Agent SDK pool instead of the subscription. inflight / queued show how close the TUI path is to its concurrency cap.
With the pool on, hits / misses is the hit rate (a steady single-model consumer should sit near 100% after the first request), and warm is your standing idle-process cost. A climbing bootFailures means panes are not reaching their input bar — the pool then degrades safely to the cold path, but latency reverts to the un-pooled numbers. cancelled counts boots OCP killed on purpose (a drain, a model switch) and is not a fault signal — do not alert on it. A steadily climbing dropped is likewise normal: the 15-min reap sweep drains and re-boots the pool on every tick so kill-server can still flush <defunct> zombies.
Kill-switch
unset CLAUDE_TUI_MODE
# restart OCP
The stream-json path is restored immediately. No other change is needed.
2026-06-15 operator checklist
Every host serving traffic must be flipped to TUI-mode and canary-verified before 2026-06-15, or it will bill the metered Agent SDK credit pool instead of the subscription.
- Flip/rollback runbook — how to set
CLAUDE_TUI_MODE=trueon systemd (Linux) and launchd (macOS) hosts. Covers thedaemon-reloadrequirement (systemd) and thebootout+bootstrapcycle requirement (launchd —launchctl kickstart -kdoes not reload plist env). - 615-canary runbook — after each flip, run one quiesced request and compare the Agent SDK credit balance before and after.
entrypoint:cliin the transcript (thecc_entrypointbilling classifier) is necessary but not sufficient — only a stable credit balance confirms the subscription pool is being used. Balance check is a manual step (no known programmatic API for the Agent SDK credit pool balance).
Architecture and design decisions
See docs/adr/0007-tui-interactive-mode.md for the full rationale, home-strategy options, MCP-disable mechanism, coexistence rules, and the B-path (multi-tenant isolation) roadmap.
Repository Layout
Top-level files a contributor or operator may need to know:
| Path | Role |
|---|---|
server.mjs |
The proxy itself; every request path lives here. Governed by ALIGNMENT.md. |
setup.mjs |
First-time installer — verifies Claude CLI, patches OpenClaw config, installs auto-start. |
uninstall.mjs |
Reverses the launchd / systemd auto-start install. |
keys.mjs |
API-key management module (multi-mode auth: create/list/revoke, quotas, usage tracking). |
models.json |
Single source of truth for model IDs, aliases, context windows. See ADR 0003. |
ocp / ocp-connect |
User-facing CLI wrappers (server-side / client-side respectively). |
dashboard.html |
Static dashboard served from /dashboard. |
scripts/sync-openclaw.mjs |
Idempotent OpenClaw registry sync invoked by ocp update. See ADR 0004. |
.claude/skills/ |
Project-specific Claude Code skills. |
ocp-plugin/ |
OpenClaw gateway plugin (optional installation). |
docs/adr/ |
Architecture Decision Records. Read these before proposing governance or SPOT changes — see docs/adr/README.md. |
ALIGNMENT.md |
The constitution. Binding for any server.mjs change. |
AGENTS.md / CLAUDE.md |
Agent and Claude-Code-specific session instructions. |
Security
- Localhost by default — binds to
127.0.0.1; setCLAUDE_BIND=0.0.0.0to enable LAN access - 3-tier auth —
none(trusted network),shared(single key),multi(per-user keys with usage tracking) - Timing-safe key comparison — prevents timing attacks on API keys and admin keys
- Admin-only key management — creating, listing, and revoking keys requires the admin key
- Public endpoints —
/healthand/dashboardare always accessible without auth - No API keys needed — authentication goes through Claude CLI's OAuth session
- Keys stored locally —
~/.ocp/ocp.db(SQLite), never sent to external services - Auto-start — launchd (macOS) / systemd (Linux)
Governance
OCP runs under a small set of binding documents so contributions stay aligned with what cli.js actually does, not what an LLM thinks it does:
ALIGNMENT.md— the constitution. Every endpoint OCP exposes must correspond to somethingcli.jsactually does, with a line-number citation. Background in ADR 0002..github/workflows/alignment.yml— CI guardrail. Grepsserver.mjsfor known-hallucinated tokens and fails the build on any hit. Not suppressible without anALIGNMENT.mdamendment PR.AGENTS.md— guidelines any AI coding agent (Claude Code / Cursor / Copilot / Codex / Gemini) should read before touching this repo.models.json— single source of truth for the model registry. See ADR 0003.docs/adr/— architecture decision records explaining why current structure exists.
If you want to contribute: read ALIGNMENT.md first, search cli.js for the operation you're proposing, and cite the line number in your PR.
Support OCP
OCP has been open source from day one — not a freemium tool, not a commercial product turned open, just open. It will stay that way forever. No paid tiers, no premium features, no "Pro" version locked behind a paywall.
I built it because my family and I needed it. We use OCP every day across our own machines and IDEs — keeping one Claude Pro/Max subscription powering everything, saving the per-token API cost we'd otherwise pay. It's been quietly heartwarming to hear from users online who say OCP has saved them money the same way it saves ours. That's the whole point.
Behind every version are hundreds of hours that don't show up in commits: building it from scratch, adding new features as the Claude Code ecosystem evolves, debugging across Mac / Windows / Linux machines, validating against half a dozen IDEs (Claude Code, Cursor, Cline, OpenCode, Aider, Continue.dev, OpenClaw), tracking down cli.js drift, OAuth refresh edge cases, SSE streaming quirks, concurrency leaks, and the occasional incident that turns into a multi-day investigation (the 2026-04-11 alignment drift, the v3.11.1 concurrency leak, the v3.12 SSE replay regression).
The commitment: this project will keep being updated, keep getting new features, and will stay open source as long as I'm able to maintain it.
Please try it. If something breaks or could be better, open an issue — feedback is genuinely what keeps the project moving.
And if OCP saves you (or your team, or your family) real money and you'd like to chip in toward the next debugging session:
Donations directly fund the time it takes to keep OCP saving the community money.
License
MIT — see LICENSE.
-
OpenClaw is an IDE-agnostic AI coding agent (sibling project to OCP). When OCP runs on the same machine, OpenClaw can use it as a local provider — see
scripts/sync-openclaw.mjsand ADR 0004. ↩︎
