* docs(spec): design for #47 SSE heartbeat on streaming path Draft spec for an opt-in idle-watchdog SSE heartbeat covering both pre-first-byte and mid-stream silent windows. Default disabled, controlled by CLAUDE_HEARTBEAT_INTERVAL. Targets ~40 LOC. Decisions captured: D1 whole-stream reset-on-byte; D2 SSE comment frame; D3 default disabled; D4 relocate ensureHeaders() to post-spawn; D5 X-Accel-Buffering: no on both SSE header sites; D6 single log line per affected request. Scope-locked: does not touch CLAUDE_TIMEOUT semantics, the separate server.mjs:480-489 dangling-client bug, issues #41/#42, or the non-streaming path. Includes privacy preflight and cloud-testing plan per maintainer feedback. Refs: #47 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): implementation plan for #47 SSE heartbeat 6 phases: pre-work (file 480-bug), implementation (5 tasks on feat/47-sse-heartbeat), opus fresh-context review, cloud verification on Mac rig, privacy preflight, PR+release. Each implementation task carries concrete code, syntax check, and a scoped commit message. LOC budget enforced in Task 1.6 gate (~45 server.mjs lines max). Reviewer checklist scopes scope-lock, ALIGNMENT, privacy, and heartbeat-cannot-abort discipline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): add startHeartbeat helper + HEARTBEAT_INTERVAL env var Per design doc (refs #47). Helper is a per-request idle watchdog that emits `: keepalive\n\n` SSE comment frames; returns a {reset, stop} handle. No wiring yet — helper is unused, safe to commit in isolation. cli.js citation: N/A — SSE response shaping is an OCP-owned translation layer, not a cli.js operation. See AGENTS.md and ALIGNMENT.md Rule 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server): eagerly send SSE headers post-spawn (D4, refs #47) Moves the ensureHeaders() call from "on first stdout byte" to "immediately after successful spawn." This is a prerequisite for the heartbeat covering the pre-first-byte silent window (the 'processing large contexts' failure mode in #47). Behavioral consequence: the narrow "spawn succeeded but subprocess died before any byte" branch at server.mjs:610-611 becomes effectively dead in the common case. The post-headers SSE-stop path (612-619) handles it instead. The branch remains defensively for the client-closed-before- ensureHeaders race. Isolated commit per design doc §D4 so reviewer can focus on this one behavior change. cli.js citation: N/A — SSE header emission is OCP response-shaping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): wire heartbeat into streaming path (refs #47) - sendSSE() accepts optional hb handle and calls hb.reset() before write - callClaudeStreaming starts heartbeat after ensureHeaders() and passes hb to the three streaming sendSSE call sites - All three exit paths (proc close, proc error, res close) call hb.stop() to guarantee timer cleanup; no-op handle when disabled means zero runtime cost when CLAUDE_HEARTBEAT_INTERVAL=0 Heartbeat never aborts — only writes comment frames and re-arms. Aligns with v3.3 timeout discipline (single CLAUDE_TIMEOUT, no secondary client-killing timers). cli.js citation: N/A — SSE response shaping is OCP translation layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): add X-Accel-Buffering: no to SSE response headers (refs #47) nginx (and many LBs / Cloudflare) default to proxy_buffering=on, which would buffer heartbeat comment frames indefinitely and defeat the feature silently. This header hints no-buffering; other stacks ignore it. Applied at both SSE header sites (real streaming + cache-hit). cli.js citation: N/A — response header shaping is OCP translation layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): v3.12.0 — streaming heartbeat (refs #47) Bundles the release-kit companion files per Iron Rule 5.2 / 11 example: version bump across package.json + ocp-plugin + openclaw.plugin.json, CHANGELOG v3.12.0 section, README env var row + "Streaming heartbeat" explainer. Tag push to v3.12.0 triggers .github/workflows/release.yml to create the GitHub Release automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server): ensureHeaders returns true when already sent (refs #47) Phase 3 smoke test revealed every content chunk was being dropped after the D4 eager ensureHeaders() call: the stdout.on('data') guard `if (!ensureHeaders()) return;` early-returned on every chunk because ensureHeaders returned false for the already-sent case (conflated with the dead-connection case). Split the two conditions explicitly: return false only when res is ended/destroyed; return true when headers are (already or now) sent. This also fixes a latent multi-chunk bug on main that was masked because claude CLI typically outputs in one stdout chunk. Verified: node -c server.mjs; subsequent re-run of Phase 3 smoke test now sees streaming content chunks + heartbeat frames. cli.js citation: N/A — SSE response shaping is OCP translation layer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OCP — Open Claude Proxy
Already paying for Claude Pro/Max? Use your subscription as an OpenAI-compatible API — $0 extra cost.
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.
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 | setup.mjs auto-configures |
| Any OpenAI client | Set base URL to http://127.0.0.1:3456/v1 |
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)
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:
- Node.js 18+
- Claude CLI installed and authenticated (
claude auth login)
# 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)
- Symlink
ocpto/usr/local/binfor CLI access
Single-machine use — just set your IDE to use the proxy:
export OPENAI_BASE_URL=http://127.0.0.1:3456/v1
LAN mode — share with other devices on your network:
# 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:
export OCP_ADMIN_KEY=your-secret-admin-key
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-7, claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001
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).
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 (see Anonymous Access below), just pass the server IP and nothing else. ocp-connect reads the anonymous key from /health and uses it automatically:
./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 (4 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-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+) - 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 with usage tracking (recommended) |
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:
export PROXY_ANONYMOUS_KEY=ocp_public_anon # or any string of your choice
ocp start # or however you start the server
Client side: the anonymous key value is exposed via GET /health as the field anonymousKey (null when not set). 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
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
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 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
Self-Update
# Check if a new version is available
ocp update --check
# Pull latest, sync plugin, restart proxy — one command
ocp update
ocp update runs (in order): git pull → npm install → plugin sync → OpenClaw model registry sync (v3.11.0+) → proxy restart → health check.
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
model+messages+temperature+max_tokens+top_p - Cache hits return instantly — no Claude CLI process spawned
- Works for both streaming and non-streaming requests
- Multi-turn conversations (with
session_id) are never cached - Expired entries are cleaned up automatically every 10 minutes
Management:
# View cache stats
curl http://127.0.0.1:3456/cache/stats
# → { "entries": 42, "totalHits": 156, "sizeBytes": 284000 }
# 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.
How It Works
Your IDE → OCP (localhost:3456) → claude -p CLI → Anthropic (via subscription)
OCP translates OpenAI-compatible /v1/chat/completions requests into claude -p CLI calls. Anthropic sees normal Claude Code usage — no API billing, no separate key needed.
Available Models
| Model ID | Notes |
|---|---|
claude-opus-4-7 |
Most capable (default for opus alias) |
claude-opus-4-6 |
Previous 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 |
/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=) |
/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
Requests fail or agents stuck
# Clear sessions and restart
ocp clear
ocp restart
# If using OpenClaw gateway
openclaw gateway restart
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.
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.
Environment Variables
| Variable | Default | Description |
|---|---|---|
CLAUDE_PROXY_PORT |
3456 |
Listen port |
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 |
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 so clients auto-discover. See Anonymous Access. |
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.
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)
License
MIT
