0
0
via GitHub · Posted Aug 5, 2026 · 1 min read

OCP - Open Claude Proxy

dtzp555-max/ocp
Tool

Turn your Claude Pro/Max subscription into an OpenAI-compatible API for your IDEs and devices — LAN auth, per-key quotas, response cache, disciplined cli.js alignment. $0 extra cost.

93Stars
20Forks
8Open issues
JavaScript MIT v3.28.0 Updated 1 week ago
View on GitHub

A lightweight proxy that exposes your Claude Pro/Max subscription as an OpenAI-compatible API for use across multiple IDEs and devices on your local network, with per-device quotas, response caching, and LAN authentication—at no additional cost.

0 comments

README

OCP — Open Claude Proxy

License: MIT GitHub release Buy Me a Coffee

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 coffeefull 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.

Contents

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-connect one-shot client setup — one command on the client machine auto-configures OpenClaw, and detects Cursor, Cline, Continue.dev, and opencode to print ready-to-paste setup hints for each. No hunting for where each tool keeps its OPENAI_BASE_URL.
  • 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.js alignment + CI guardrail. LLM-assisted code drifts easily — it's tempting to invent plausible-looking endpoints that cli.js doesn't actually use. ALIGNMENT.md is binding: every endpoint OCP forwards from cli.js (Class A) must cite a cli.js line. OCP's own compatibility surface — the OpenAI-compatible and administrative endpoints, which have no cli.js analogue — is Class B: it cites OpenAI's published specification (the OpenAI-compatible endpoints) or its authorizing ADR (the administrative ones), under the same anti-invention discipline (ADR 0006). The alignment.yml CI workflow blocks PRs that introduce known-hallucinated tokens. The payoff is boring: your setup keeps working when cli.js ships its next minor.
  • models.json single source of truth (v3.11.0). Adding a model is one file edit; both /v1/models and the OpenClaw bootstrap derive from it. (PR #30)
  • Drives the official CLI as-is, no binary patching. OCP spawns the official claude CLI (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). See ALIGNMENT.md for 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 [^openclaw] setup.mjs auto-configures
Any OpenAI client Set base URL to http://127.0.0.1:3456/v1

[^openclaw]: 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.mjs and ADR 0004.

Quickstart

The simplest path: ask your AI.

Paste this prompt to Claude Code / Cursor / Copilot:

Install OCP for me. Read README §Quickstart 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.

Prerequisites: macOS or Linux (Windows is not supported), Node.js 22.5+ (Node 23+ recommended), git, and the Claude CLI, authenticated:

npm install -g @anthropic-ai/claude-code
claude auth login   # prints a URL + code — open on any browser, sign in, paste code back

Install (Server role — runs the proxy):

git clone https://github.com/dtzp555-max/ocp.git
cd ocp
node setup.mjs

setup.mjs verifies the Claude CLI, starts the proxy on port 3456, and installs auto-start (launchd on macOS, systemd on Linux). The ocp CLI lands at ~/ocp/ocp — symlink it onto your PATH (sudo ln -sf ~/ocp/ocp /usr/local/bin/ocp, or ln -sf ~/ocp/ocp ~/.local/bin/ocp) or alias it (alias ocp=~/ocp/ocp); the rest of the docs assume ocp is on your PATH.

Verify — should list 7 models:

curl http://127.0.0.1:3456/v1/models
# claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001

Connect one IDE — point any OpenAI-compatible tool at the proxy, then reload your shell and start a tool (Cline / Continue / Cursor / OpenCode):

export OPENAI_BASE_URL=http://127.0.0.1:3456/v1

See Supported Tools for per-tool config.

LAN / multi-user — reach OCP from your own devices, with per-key auth, quotas, and anonymous access:

node setup.mjs --bind 0.0.0.0 --auth-mode multi

The full LAN server + client handbook, headless (Pi / NAS / VPS) OAuth, key/quota/anonymous-access management, AI-assisted install prompts, and the deployment/security model live in docs/lan-mode.md. Claude Pro/Max are per-user accounts — read the honest limits of sharing before extending access to other people.

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.

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.

Billing-policy status (as of 2026-07). Anthropic announced (2026-05-14) that from 2026-06-15 the claude -p / Agent SDK path would move to a separate metered credit pool — then paused the change on its effective date: "For now, nothing has changed: Claude Agent SDK, claude -p, and third-party app usage still draw from your subscription's usage limits" (official help article). So the default path above currently bills your subscription. Anthropic has said it will give notice before any future change; if the split re-lands, OCP's opt-in subscription-pool (TUI) mode is the ready-made hedge — see the billing table there.

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.

Offering tools is fine; forcing a tool call gets a 400. Because OCP never emits tool_calls, a request that requires one cannot be answered correctly, so it is refused rather than answered with prose that claims the turn ended normally:

Request OCP
tools with no tool_choice, or tool_choice "auto" / "none" / allowed_tools mode: "auto" served normally — text, finish_reason: "stop"
tool_choice "required", {"type":"function"}, {"type":"custom"}, or allowed_tools mode: "required" 400error.code: "unsupported_parameter", error.param: "tool_choice"
legacy function_call: {"name": …} 400, same shape, error.param: "function_call"

Simply sending a tool list is never an error — that is the common case (every OpenClaw turn carries one) and it is unchanged. Only the instruction OCP cannot obey is refused, and it is refused loudly so a client can fall back to another provider or retry with "auto" instead of silently receiving a wrong answer. See ADR 0013 for why this is a refusal rather than an implementation.

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. Note that on the -p path OCP prepends a system-prompt wrapper telling the model it has no local access (right for a shared gateway) — a single-user loopback instance whose model should use its tools can flip this with OCP_LOCAL_TOOLS=1 (see Environment Variables).

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).

What this means for choosing OCP (workload fit). LAN/multi-device OCP is built for chat-class workloads — Q&A, translation, scripting against the API, chat frontends, home-automation backends — where text in/text out is the whole job. It is not the right tool for a coding agent running on a client machine that needs the AI to read and edit that machine's files: tools execute on the OCP host, so the model can never touch the client's filesystem. For that workload, run claude (or a local OCP) directly on the machine where the code lives.

Available Models

Model ID Context window Notes
claude-opus-5 1M Most capable (default for opus alias)
claude-opus-4-8 1M Previous Opus, retained for pinning
claude-opus-4-7 1M Older Opus, retained for pinning
claude-opus-4-6 200k Older Opus, retained for pinning
claude-sonnet-5 1M Latest Sonnet (default for sonnet alias)
claude-sonnet-4-6 200k Previous Sonnet, retained for pinning
claude-haiku-4-5-20251001 200k Fastest, lightweight (default for haiku alias)

Context windows match the Claude Code CLI registry. Each model's prompt truncation ceiling is derived from its own window (contextWindow × 3 chars) — see ADR 0011 and CLAUDE_MAX_PROMPT_CHARS in Environment Variables.

The canonical list lives in models.json — the single source of truth as of v3.11.0, validated in CI against models.schema.json. 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, an auth block — see § "What auth.ok means" — and instanceName — see § "Running more than one instance on a host")
/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)
/dashboard GET Web dashboard (always public)
/api/keys GET/POST List or create API keys (admin only). POST body must be a JSON object — {} auto-names, {"name":"…"} names; anything else is 400 (ADR 0017)
/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)

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" below.
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 (derived per model) Prompt truncation limit in chars. By default there is no single limit: each request is bounded by the named model's own contextWindow × 3 from the models.json SPOT — 3,000,000 for the native-1M models (claude-opus-5, -4-8, -4-7, claude-sonnet-5) and 600,000 for the 200k models (claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5). Setting this env var (or ocp settings maxPromptChars) overrides the derivation absolutely, applying that one number to every model. See ADR 0011 (supersedes ADR 0009's single global ceiling). Note: very large prompts burn subscription-window quota quickly and slow TTFT; the TUI-mode paste path is untested beyond ~hundreds of KB. Applies to text only — image bytes bypass this budget (see Images / Multimodal).
OCP_STRUCTURED_MAX_ATTEMPTS 3 Max attempts (initial + retries) to coerce a schema-valid JSON reply when a request uses OpenAI response_format. Fail-closed: a non-numeric value keeps the default. See Structured Outputs.
CLAUDE_AUTH_CHECK_INTERVAL_MS 600000 How often the background claude auth status probe runs (ms, default: 10 min). Lower it for faster detection of a real credential outage — the verdict needs 2 consecutive conclusive rejections, so onset is reported within roughly one interval. Fail-closed parsing: an empty/garbage value keeps the default. Probe outcome is surfaced on /health.auth.lastOutcome (authenticated/token-present/rejected/timeout/unavailable) and the running tally on /health.auth.consecutiveFailures, so an operator can tell a host-load timeout from a real credential rejection. token-present is not authenticated — see § "What auth.ok means". See ADR 0010 and ADR 0014.
CLAUDE_AUTH_CHECK_TIMEOUT_MS 10000 Per-probe timeout for that same probe (ms). The probe runs asynchronously and never blocks request serving; this only bounds a stuck child. A probe killed by this timeout is inconclusive — it measures host load, not credentials, so it never changes /health.status or auth.ok. Fail-closed parsing: an empty/garbage value keeps the default.
CLAUDE_CACHE_TTL 0 Response cache TTL (ms, 0 = disabled). Set to e.g. 300000 for 5-min cache. See Response Cache.
CLAUDE_ALLOWED_TOOLS Bash,Read,...,Agent Comma-separated tools to pre-approve
CLAUDE_SKIP_PERMISSIONS false Bypass all permission checks
CLAUDE_MCP_CONFIG (unset) Path to an MCP server config JSON, passed to the spawned claude as --mcp-config (both the -p path and TUI OCP_TUI_FULL_TOOLS panes)
CLAUDE_MAX_BODY_SIZE 5242880 Max request body size, counted in characters (UTF-16 code units), not bytes — the body is accumulated as a JS string, so a multi-byte payload can be several times this size on the wire and still be admitted (5,242,880 CJK characters is ~15 MB). Base64 image payloads are ASCII, so for those the two counts coincide and the ~33% base64 inflation applies as written; raise this to admit larger multimodal requests. Fail-closed parsing: a garbage value keeps the default.
CLAUDE_IMAGE_ALLOW_URL false Allow remote http(s) image URLs in image_url parts. Off by default (v1 supports base64 data: URIs only). When on, the URL is passed through to Anthropic as a url image source — OCP does not fetch it (no OCP-side SSRF surface); unreachable/blocked URLs surface as an API error.
CLAUDE_MAX_IMAGE_BYTES 5242880 Per-image decoded-byte cap (default 5 MB). Over-cap images get HTTP 413.
CLAUDE_MAX_IMAGES 20 Max image parts per request. Over-cap gets HTTP 413.
CLAUDE_MAX_IMAGE_TOTAL_BYTES 20971520 Aggregate decoded-byte cap across all images in a request (default 20 MB). Over-cap gets HTTP 413.
CLAUDE_SYSTEM_PROMPT (unset) Operator-wide system-prompt text appended (last) to every request's composed system prompt on the default -p path. TUI-mode panes are unaffected (they keep the interactive CLI's own system prompt). Echoed truncated on /health.systemPrompt. Note: changing this value and restarting auto-invalidates the response cache (the key carries a boot-config epoch, #177).
OCP_LOCAL_TOOLS (unset) Single-user, loopback only. =1 swaps the default "you have no local filesystem/shell access" system-prompt wrapper for a positive one telling the model it may use its tools. These are the server-side claude tools OCP spawns via -p (--allowedTools) — which, on a loopback instance, run on the operator's own machine, i.e. local tools. For a personal instance (e.g. an OpenClaw agent on its own local OCP) the default wrapper otherwise makes the model refuse to use tools it legitimately has. Changes only the prompt, never the tool surface (governed by --allowedTools/--disallowedTools; multi-tenant still --disallowedTools the whole FS surface). Does not enable client-side tool_calls for OpenClaw/Cline/etc. — that remains unsupported by design (see § How tools work). Fail-closed: OCP refuses to boot if =1 is combined with CLAUDE_AUTH_MODE=multi, a non-loopback bind, or PROXY_ANONYMOUS_KEY (mirrors OCP_TUI_FULL_TOOLS, ADR 0007). Inert in TUI mode (the -p wrapper is unused there; the TUI tool surface is OCP_TUI_FULL_TOOLS) — a warning is logged. Off by default → the default path is byte-for-byte unchanged. Toggling it auto-invalidates the standard response cache (boot-config epoch, #177).
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 (multi mode) — 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. Full setup + security notes: docs/lan-mode.md § 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, single-user only. Set to "true" to serve requests via interactive claude (cc_entrypoint=cli, subscription pool). Refuses to boot under AUTH_MODE=multi. See Subscription-pool (TUI) mode.
CLAUDE_CODE_OAUTH_TOKEN (unset) OAuth bearer token — highest-precedence credential for the -p path, and the recommended credential for TUI-mode hosts (when set with OCP_TUI_HOME unset, OCP runs the TUI claude in a credential-isolated home). See docs/tui-mode.md and the permanent-401 fix.
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 auto-picks a credential-isolated scratch home (env token set) or the real home (no token). Full home/credential strategy: docs/tui-mode.md.
OCP_TUI_ENTRYPOINT cli (TUI-mode) Billing-classifier labeling: cli pins cc_entrypoint=cli; auto self-classifies via TTY; off leaves inherited env untouched. See docs/tui-mode.md.
OCP_TUI_EFFORT low (TUI-mode) --effort level for the interactive spawn (low/medium/high/xhigh/max/inherit). Explicit low cuts TTFT p50 ~40% vs an inherited xhigh; invalid values fall back to low. See docs/tui-mode.md.
OCP_TUI_STREAM 0 (off) (TUI-mode) =1 emits real SSE delta.content chunks (block-level) from claude's MessageDisplay hook instead of buffering; transcript stays authoritative and divergent turns are refused. Caveats (tool-using turns, zero-delta detection) in docs/tui-mode.md § OCP_TUI_STREAM.
OCP_INSTANCE_NAME (empty) Operator label for a NON-primary instance, reported on /health as instanceName. Empty means the primary. The server never branches on the value; ocp doctor reads the declaration off the unit file to tell a deliberate second instance from a leftover duplicate — see § "Running more than one instance on a host".
OCP_TUI_STREAM_HOLDBACK 100 (TUI-mode, streaming) Characters withheld before the first chunk — keeps the auth-banner gate alive and is the knob for tool-using turns. See docs/tui-mode.md § OCP_TUI_STREAM_HOLDBACK.
OCP_TUI_STREAM_DIR $HOME/.ocp-tui/stream (TUI-mode, streaming) Directory for the hook script/settings + per-session delta sink (one sink per session-id, so concurrent turns never interleave). See docs/tui-mode.md.
OCP_TUI_STREAM_POLL_MS 100 (TUI-mode, streaming) Interval at which OCP drains the delta sink; the hook fires at block granularity so a finer poll buys nothing. See docs/tui-mode.md.
OCP_TUI_MAX_CONCURRENT 2 (TUI-mode) Max concurrent interactive TUI turns, independent of CLAUDE_MAX_CONCURRENT. Excess turns queue (bounded); a full queue yields 503. See docs/tui-mode.md.
OCP_TUI_POOL_SIZE 0 (off) (TUI-mode) Number of pre-booted warm claude panes (max 32) so a request skips the cold boot — measured p50 10.17s6.00s. Each warm pane is a live idle process; panes are single-use. Keep it small on a small host. See docs/tui-mode.md § OCP_TUI_POOL_SIZE.
OCP_SKIP_AUTH_TEST (unset) When =1, skip the claude -p auth probe during setup.mjs. Under the announced (currently paused) 2026-06-15 billing split this probe would draw from the metered Agent SDK credit pool; set this to avoid burning a probe 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) =1 grants the interactive session the same tool surface as the -p path (--allowedTools + optional --mcp-config) so a trusted single operator can run a tool-using / MCP agent on the subscription pool. Safe because TUI refuses to boot under AUTH_MODE=multi. See docs/tui-mode.md § OCP_TUI_FULL_TOOLS.
OCP_TUI_TOOLS (unset) (TUI-mode) Restrict which built-in tools the interactive pane may use, via claude --tools (e.g. Read,Glob,Grep,WebSearch,WebFetch). --tools is the tool-availability registry, not a permission layer, so an omitted tool is simply never offered and cannot hang a headless pane on an unanswerable permission prompt. Unset, empty or whitespace-only = all built-in tools available (default). Applies to the default MCP-walled surface only (not OCP_TUI_FULL_TOOLS, whose surface is CLAUDE_ALLOWED_TOOLS or its hardcoded default set). Narrows what the model is offered; it is not a trust boundary. See docs/tui-mode.md § OCP_TUI_TOOLS.
OCP_DIR (auto) Tooling only — the server never reads it. Absolute path to the OCP install directory, used by ocp doctor and ocp update. Normally unnecessary: the maintenance scripts resolve the install from their own file location, so an install at /opt/ocp, or one driven under sudo (where $HOME is /root), is found without configuration. Set it only to override that resolution. A relative value is refused (and the install_dir check is raised to WARN so you see the refusal). ocp doctor prints the directory it actually used — and where that answer came from — on its install_dir line either way. Not a free-form path: the fresh-install path starts with rm -rf <install dir>, so a directory is only accepted as a deletion target when it is absent, empty, or verifiably an OCP install (package.json named open-claude-proxy, or ≥2 of server.mjs/setup.mjs/ocp/models.json); anything else is refused before any step runs. See Troubleshooting § "ocp update wants a fresh install".

What auth.ok means

/health's auth block answers "can this proxy authenticate", and it is deliberately conservative about saying yes. Two pairs of fields, deliberately separate: okSource / okAt say how and when auth.ok was established, while lastOutcome / lastCheck describe the last probe. A probe that cannot conclude updates only the second pair.

The table is keyed on okSource, because that is the field the verdict's meaning turns on. lastOutcome describes the probe, and the probe is not always what set auth.ok.

okSource auth.ok lastOutcome what was actually established
none null none no probe has completed yet — the state at boot
probe true authenticated the probe ran and the child resolved its own credential from a file or the keychain
probe null token-present a token was in the child's environment and the probe exited 0. That proves presence, not validity
probe false rejected the probe ran to completion and the credential was refused
request true preserved a real completion succeeded — the strongest evidence available, and free
expired null preserved a request established it, but longer ago than the freshness window: "it worked; we do not know now"

lastOutcome is not part of the request-verified verdict, and that is deliberate. A completed request sets okSource: "request" and leaves lastOutcome exactly as the probe last set it — overwriting it would make /health claim a probe ran when none did. So on a host that supplies the token through the environment, the steady state is okSource: "request" + lastOutcome: "token-present", and that pairing is normal rather than contradictory. It is also the state three of the four instances in the reference fleet spend most of their time in.

There is no lastOutcome: "verified-by-request". Earlier revisions of this table, of ADR 0014 and of the CHANGELOG all promised one; the server has never emitted it and by design never will (see the paragraph above). A monitor written against that value could not match any real response. Key on okSource: "request". Corrected in #342.

Inconclusive probes are not a row here, because they leave the verdict alone: a timeout or unavailable writes lastOutcome, lastCheck, message and consecutiveInconclusive, and leaves ok, okSource and okAt untouched. That is the point — a probe killed by host load measured load, not the credential.

Why token-present is not authenticated. claude auth status exits 0 whenever a token is present, without checking it: a fabricated token yields exit 0 and loggedIn: true. On a host that supplies CLAUDE_CODE_OAUTH_TOKEN through a systemd EnvironmentFile or an inlined unit — which is how OCP is normally deployed on Linux — the probe therefore cannot distinguish a working credential from an expired one. Reporting authenticated there is what issue #308 found: /health asserting the proxy was authenticated while every request failed on authentication. See ADR 0014.

A null is not a failure. It means no conclusive verdict, ocp doctor reports it as WARN rather than FAIL, and it does not block ocp update. On such a host the first successful request moves it to true.

auth.ok never moves status. proxyHealthStatus reads consecutiveFailures and never ok, so no verdict in the table above can flip a host to degraded.

The tally it reads is a different matter, and this is the part worth knowing. Conclusive probe rejections raise consecutiveFailures (ADR 0010), and any successful request clears it — a deliberate restoration of ADR 0010's self-heal, unqualified by which lane served the request (see ADR 0014 § Consequences). So a host reporting degraded can return to ok on the first request that succeeds, with no probe having run in between. If you are debugging statusocp update's post-flight check and the dashboard's status card both read it, and the dashboard has no other auth signal — do not assume it is probe-driven only.

(This paragraph said "status is unaffected by any of this … never by auth.ok" until #361. The second clause was true and the bolded headline was not: it read as though nothing in this section could touch status, while a successful request has always cleared the tally that decides it.)

Running more than one instance on a host

DEFAULT_PORT (3456) is the primary and never changes — it is the single source of truth in lib/constants.mjs, and CI hard-fails any other port literal in source. A host that n

Comments (0)

Sign in to join the discussion.

No comments yet

Be the first to share your take.