A host-side HTTP bridge that exposes Claude Code as a supervised build tool for automation systems. It runs on the host machine and provides a JSON API for spawning, managing, and interacting with Claude Code sessions — with structured permission review, live output streaming, and test result detection.
AI orchestrators (running inside Docker containers, remote servers, etc.) often need a builder that can write code, run tests, and interact with the host filesystem. Claude Code is an excellent builder, but it runs on the host as a CLI tool — not inside the container.
ClawBridge sits on the host as a lightweight HTTP service that bridges the gap, letting any orchestrator invoke Claude Code as a build tool while maintaining structured permission oversight.
Secondary capability — embedded tools extension. Some deployments also need to expose a small HTTP service alongside the Claude Code broker (for example, a domain-specific tools API consumed by the same orchestrator). ClawBridge supports this via a single, in-process extension slot mounted under /tools/* (set CLAWBRIDGE_TOOLS_MODULE). This is single-tenant by design — the bridge hosts at most one embedded handler, and does not proxy to other host-side services. It is not a general-purpose reverse proxy or multi-service docker_host bridge. See the tools extension contract.
Note on Anthropic's third-party policy: In January 2026, Anthropic banned the use of Claude subscription OAuth tokens (Pro/Max) in third-party tools — this was about token arbitrage, where third-party harnesses routed through cheaper subscription auth instead of API pricing. ClawBridge does not do this. It invokes Claude Code on the host as a build tool using proper API key authentication (
claude setup-token), which is the explicitly permitted path for developers building products that interact with Claude. ClawBridge does not spoof Claude Code's harness or use subscription credentials — it's a tool invocation bridge, not an engine substitution.
ClawBridge spawns Claude Code in a real PTY (pseudo-terminal), detects permission prompts from TUI output, and lets the orchestrator approve or deny each one through a structured API. The orchestrator gets live output streaming, test result detection, and full session control.
+----------------------------------------------+
| Orchestrator |
| Role: Architect / Reviewer |
| |
| Drives builds via HTTP calls to ClawBridge |
+------------------------+---------------------+
| HTTP (JSON API, Bearer token)
| http://host.docker.internal:<port>
v
+----------------------------------------------+
| ClawBridge (host machine) |
| Node.js HTTP service |
| launchd/systemd managed |
| |
| +----------------------+ +---------------+ |
| | PTY broker (core) | | Tools | |
| | always on | | extension | |
| | | | (optional, | |
| | /v2/session/* | | single- | |
| | /v2/sessions | | tenant) | |
| | /v2/api-docs | | | |
| | /health, /projects | | /tools/* | |
| +----------+-----------+ +-------+-------+ |
+-------------|----------------------|---------+
| PTY / child process | in-process HTTP
v v
+--------------------------+ +--------------------------+
| Claude Code | | Embedded HTTP handler |
| Interactive TUI session | | (e.g., a Fastify or |
| Permission prompts | | Express app supplied |
| surfaced via the | | by the consumer; one |
| bridge's event stream | | per bridge process) |
+--------------------------+ +--------------------------+
The right-hand column is mounted only when CLAWBRIDGE_TOOLS_MODULE points at a module implementing the tools extension interface. Absent that env var, the bridge runs as a pure PTY broker — no /tools/* routes, no tools block in /health.
- Orchestrator starts a session via
POST /v2/session/startwith anapprovalEnvelopeand apermissionMode(set todefaultto engage the bridge's parser; see note below) - ClawBridge spawns Claude Code in a PTY with
--permission-mode <value> - Claude Code works, triggering permission prompts for file writes, shell commands, etc.
- The bridge's permission parser detects prompts from raw PTY output
- The policy engine evaluates each permission against the approval envelope:
- auto_approve: Bridge sends Enter after 500ms delay
- deny: Bridge sends Escape after 500ms delay
- require_review: Bridge pauses and surfaces the permission via the event stream
- Orchestrator polls
GET /v2/session/peekfor a quick snapshot orGET /v2/session/outputfor full events - For permissions requiring review, orchestrator responds via
POST /v2/session/respondwith{ project, permissionId, decision }wheredecisionis one ofapprove_once,deny, orabort_session - Session ends via
POST /v2/session/endwith optional transcript export
Heads-up on
permissionMode. Claude Code 2.1.x defaults to auto mode, where it classifies and auto-handles permission prompts internally — the bridge's TUI parser never sees them, andapprovalEnvelopehas no effect. To engage the bridge's structured permission review, setpermissionMode: "default"(or any non-autovalue:acceptEdits,bypassPermissions,plan,dontAsk) on/v2/session/start. Omitting the field falls back to Claude's default (auto mode) for backward compatibility with existing deployments.
Use ClawBridge if you need:
- A Claude Code PTY broker on the host machine. The bridge spawns Claude Code in a real PTY, parses permission prompts from TUI output, and exposes a structured HTTP API so a container-side orchestrator can approve, deny, or pause each one. This is the primary use case — the broker is what 100% of
bridge/v2/is built for. - (Optional, advanced) A place to co-locate one additional HTTP service inside the bridge process. Set
CLAWBRIDGE_TOOLS_MODULEto mount an embedded handler under/tools/*. The extension shares the bridge's auth, port, and lifecycle — note that "shares the auth" means it inherits whatever the bridge is enforcing, which is nothing whenCLAWBRIDGE_ALLOW_UNAUTHENTICATED=true. v1 is single-tenant: one extension per bridge process, no multi-mount. Documented contract in docs/tools-extension.md.
ClawBridge is not the right tool if:
- You only need Claude Code on the host and have direct shell/SSH access — invoke
claudedirectly, no bridge required. - You need to fan out to multiple host-side services from a container — ClawBridge mounts at most one embedded extension and does not proxy to external services. Use a general-purpose reverse proxy (Caddy, Traefik, nginx) instead.
- You want to run Claude Code inside a container — that is a different deployment model (sandboxed Claude Code), not what ClawBridge addresses.
- You need a security boundary between orchestrator and host beyond bearer-token auth + the permission envelope. ClawBridge's safety model is structured permission review of a single CLI tool's actions, not network-level isolation.
For OpenClaw deployments specifically: recommend ClawBridge when the deployment uses Claude Code as a builder behind a containerized orchestrator. If a deployment also needs an embedded tools API (RentalClaw-style), point at CLAWBRIDGE_TOOLS_MODULE. If a deployment needs multiple distinct host-side services bridged, ClawBridge alone is insufficient — pair it with a reverse proxy.
ClawBridge's safety model is structured permission review of one CLI tool's actions, plus bearer-token auth. It is not a sandbox, not a jail, and not a network boundary between an untrusted orchestrator and the host. Deploy it as though the caller can do anything the API allows.
Authentication is mandatory. The bridge refuses to start without BRIDGE_TOKEN and exits non-zero naming the fix. It previously treated an unset token as "authentication optional" and served every route openly — a misconfigured daemon was an unauthenticated command-execution surface on the local network, while /health reported a healthy service. If you genuinely need the open mode, CLAWBRIDGE_ALLOW_UNAUTHENTICATED=true enables it; /health then reports insecure: true with auth.required: false, and every boot logs a warning. Monitor on insecure.
Two further properties are worth stating outright, because both are deliberate:
- One privilege tier. There is a single shared
BRIDGE_TOKENand no per-caller authorization. Any token holder can act on any project. - Transcripts are unfiltered. The event log and
/v2/session/transcriptstore raw PTY output verbatim, so anything Claude Code prints — the contents of a.env, an echoed key, a token in a stack trace — is held in memory and returned in full to any token holder. There is no redaction, by decision rather than by omission: this is a single-operator host daemon, and anyone who can read a transcript already has access to the host those secrets live on. Adding redaction would buy little and would make the transcript a less faithful record of what actually happened.
CORS depends on whether a token is set. With BRIDGE_TOKEN set, Access-Control-Allow-Origin is *. For every authenticated route that is a nuisance rather than a hole — a web page has no credential to send, and cannot attach Authorization without tripping a preflight.
But three routes are unauthenticated by design — GET /health, GET /exports, and GET /exports/* — so "no credential" does not protect them. With the wildcard, a page the operator visits can read the export listing and the contents of every file under EXPORTS_DIR cross-origin, in the default token-holding configuration. Point EXPORTS_DIR at a directory you are content to publish (the environment table says the same), and treat the wildcard as the reason that instruction is not advisory. Tracked as CRS-8N3P.
Without a token, cross-origin browser requests are refused before routing. Loopback origins are allowed, as is anything named exactly in CLAWBRIDGE_ALLOWED_ORIGINS; everything else gets a 403. This is what makes CLAWBRIDGE_ALLOW_UNAUTHENTICATED=true safe to state as "nothing else can reach the port" — previously it also required that no browser run on the host, which was not a condition an operator could actually satisfy.
The check reads two headers, because one is not enough: Origin when the browser sends it, and Sec-Fetch-Site when it does not. A no-cors GET — an <img src>, a <script src>, an <iframe>, a typed URL — carries no Origin at all, so keying on Origin alone would miss the easiest request there is to forge.
Destructive operations do not answer GET at all. That is a separate guarantee, and it holds even where the gate cannot see: GET is a safe method (RFC 9110 §9.2.1), and link unfurlers, prefetchers, proxies and crawlers all rely on that while sending neither header. Consuming a file therefore requires POST — see the 2.0.0 breaking changes.
Non-browser callers send neither header and are unaffected: curl, containers, and orchestrators behave exactly as before. Two consequences worth knowing: a browser page cannot read /health cross-origin unless its origin is allowed, and this defends against browsers — a direct attacker who can reach an unauthenticated port needs no CSRF, since every route is already open to them. Check the live posture at /health under cors.
If your deployment breaks either assumption — multiple mutually-untrusting callers, or transcripts leaving the trust boundary they were produced in — supply the missing control at the network layer. ClawBridge will not do it for you.
The full model, including known gaps, is in .prawduct/artifacts/security-model.md.
- Node.js 18+ (tested on v22)
- Claude Code CLI installed on the host
- A valid Claude Code auth token configured via
claude setup-token - Build tooling needed by
node-ptyon your host
git clone https://github.com/Jason-Vaughan/ClawBridge.git
cd ClawBridge
npm installnpm install runs the bundled scripts/postinstall.js, which restores the
exec bit on node-pty's prebuilt spawn-helper binaries on macOS (a common
silent failure on fresh installs). If you ever see posix_spawnp failed
errors at runtime, rerun npm rebuild node-pty from the project root and
make sure node_modules/node-pty/prebuilds/darwin-*/spawn-helper is
executable.
cp bridge/.env.example bridge/.envEdit bridge/.env and set at minimum:
BRIDGE_PORT=3201
# A secret you invent — see below. Note that comments must be on their own line:
# the loader takes everything after `=` verbatim, so a trailing comment would
# become part of the token.
BRIDGE_TOKEN=
# Issued by `claude setup-token`.
CLAUDE_CODE_OAUTH_TOKEN=
# Directory containing the projects the bridge may operate on.
PROJECTS_DIR=/path/to/your/projectsThese two tokens come from different places, which is the most common setup confusion:
BRIDGE_TOKENis a secret you invent. Nothing issues it. Callers present it asAuthorization: Bearer <token>. Generate one withopenssl rand -base64 32. The bridge refuses to start without it — deliberately, since running without a token serves every route unauthenticated on a0.0.0.0bind..env.exampleships it empty for the same reason: a placeholder would satisfy that check and leave you running on a guessable credential.CLAUDE_CODE_OAUTH_TOKENis issued to you byclaude setup-token(see Claude Code Headless Auth below).
cd bridge
node server.jscurl -H "Authorization: Bearer $BRIDGE_TOKEN" http://localhost:3201/v2/api-docsThis returns the full self-describing API reference with every endpoint, parameter types, and a quickstart workflow guide.
curl -X POST http://localhost:3201/v2/session/start \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $BRIDGE_TOKEN" \
-d '{
"project": "my-project",
"instruction": "Build the login page"
}'curl -H "Authorization: Bearer $BRIDGE_TOKEN" \
"http://localhost:3201/v2/session/peek?project=my-project&clean=true"| Variable | Required | Description |
|---|---|---|
BRIDGE_PORT |
Yes | Port to listen on (default: 3201) |
BRIDGE_TOKEN |
Yes | Bearer token for API authentication — a secret you invent, not one that is issued. openssl rand -base64 32. The bridge refuses to start without it. |
CLAUDE_CODE_OAUTH_TOKEN |
Yes | Token from claude setup-token for headless auth |
CLAUDE_BIN |
No | Path to Claude Code binary (default: /usr/local/bin/claude) |
PYTHON_BIN |
No | Path to Python 3 binary (auto-detected) |
PROJECTS_DIR |
No | Directory containing projects the bridge may operate on (default: $HOME/projects). Sessions run here, and it scopes /projects/* and /v2/session/file. |
EXPORTS_DIR |
No | Directory served by GET /exports and GET /exports/* (default: $HOME/exports). These two routes are unauthenticated by design — the listing exposes every filename and the download serves every file. Point it at a directory you are content to publish to anyone who can reach the port. |
CLAWBRIDGE_ALLOW_UNAUTHENTICATED |
No | Set to exactly true to start without BRIDGE_TOKEN, serving every route unauthenticated. Only where nothing else can reach the port (see Security Posture). Any other value — including 1, yes, or TRUE — is not accepted. |
CLAWBRIDGE_ALLOWED_ORIGINS |
No | Comma-separated browser origins permitted to call the bridge while it runs without a token; loopback is always permitted. Entries must be serialized origins (https://ui.example — no trailing slash, no path); anything else never matches, and is warned about at boot and listed under cors.invalidOrigins on /health rather than being silently ignored. Unset permits nothing beyond loopback. Ignored when BRIDGE_TOKEN is set. |
CLAWBRIDGE_TOOLS_MODULE |
No | Absolute path to a Node module implementing the tools extension interface. When set, the bridge mounts the module under /tools/* and merges its health into /health. Absent, the bridge runs as a pure PTY broker. |
Claude Code must be authenticated for non-interactive use (launchd/SSH):
claude setup-tokenThis generates the CLAUDE_CODE_OAUTH_TOKEN. Do not rely on keychain auth — it is GUI-session-scoped and will not work from launchd or SSH contexts.
All endpoints require Authorization: Bearer <token> except /health, GET /exports, and GET /exports/*, which are unauthenticated by design. (And note that in the CLAWBRIDGE_ALLOW_UNAUTHENTICATED=true mode there is no token to check, so nothing is authenticated — see Security Posture.)
GET /v2/api-docs returns the full self-describing reference — use it as the entry point for automation.
| Method | Path | Purpose |
|---|---|---|
POST |
/v2/session/start |
Spawn new PTY session for a project |
POST |
/v2/session/end |
Graceful shutdown with optional wrap message |
GET |
/v2/session/output |
Poll events (cursor-based, long-poll via waitMs) |
GET |
/v2/session/peek |
Quick snapshot — state, tail output, test results, pending permissions |
POST |
/v2/session/respond |
Submit permission decision (approve_once, deny, or abort_session) — requires permissionId from a pending permission |
POST |
/v2/session/send |
Send follow-up message to running session |
POST |
/v2/session/policy |
Update approval envelope mid-session |
GET |
/v2/session/transcript |
Full PTY transcript (live during session or after completion) |
GET |
/v2/session/status |
Check session state and inputReady flag |
GET |
/v2/sessions |
List sessions (active-only default, ?all=true for all) |
GET |
/v2/api-docs |
Self-describing API reference |
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Bridge status, Claude version |
GET |
/api/processes |
Process visibility for external orchestrators |
GET |
/projects |
List projects |
GET |
/projects/:project/files |
List project files (?recursive=true, ?path=subdir) |
GET |
/projects/:project/files/* |
Serve a specific file |
POST |
/prawduct/run |
Run prawduct governance commands (optional) |
GET /v2/session/peek?project=my-project&lines=30&clean=true
Returns a single operational snapshot without cursor management:
{
"ok": true,
"state": "running",
"active": true,
"inputReady": true,
"tail": "...last 30 lines of output (ANSI-stripped with ?clean=true)...",
"testResult": {
"runner": "vitest",
"passed": 42,
"failed": 0,
"total": 42,
"summary": "Tests 42 passed (42)",
"command": "npx vitest run"
},
"pendingPermission": null
}inputReady—truewhenPOST /v2/session/sendwill succeed (session running + PTY alive)testResult— auto-detected from PTY output (supports vitest, pytest, jest, mocha)?clean=true— strips ANSI escape codes fromtail(also available on/v2/session/transcript)pendingPermission— surfaced as first-class data with type, risk, target, and timeout
The envelope tells ClawBridge which permissions to auto-handle vs. pause for review. Important: the envelope only governs behavior when permissionMode is set to a non-auto value (e.g. default) on /v2/session/start. Auto mode bypasses the bridge's permission parser entirely and Claude makes its own decisions; the envelope is ignored. See the Session Flow note above.
{
"mode": "scoped",
"rules": {
"fileWrites": { "withinProject": "auto_approve", "outsideProject": "deny" },
"fileDeletes": { "withinProject": "require_review", "outsideProject": "deny" },
"shellCommands": {
"allowlist": ["npm test", "npm run build", "git status", "git diff"],
"allowlistPolicy": "auto_approve",
"otherPolicy": "require_review"
},
"gitOperations": { "safe": "auto_approve", "destructive": "deny" },
"dependencyChanges": "require_review",
"networkAccess": "deny",
"unknown": "require_review"
},
"defaults": {
"lowRisk": "auto_approve",
"mediumRisk": "require_review",
"highRisk": "deny"
}
}No envelope = everything requires review (fail-closed).
- Prompt timeout (default 5 min): Auto-denies pending permission and resumes session
- Session timeout (default 30 min): Sends SIGINT, then SIGKILL after 5s grace period
cp bridge/com.clawbridge.builder.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.clawbridge.builder.plist
launchctl stop com.clawbridge.builder # KeepAlive auto-relaunchesCreate /etc/systemd/system/clawbridge.service:
[Unit]
Description=ClawBridge host-side Claude Code bridge
After=network.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/home/YOUR_USER/ClawBridge/bridge
EnvironmentFile=/home/YOUR_USER/ClawBridge/bridge/.env
Environment=HOME=/home/YOUR_USER
ExecStart=/usr/bin/node /home/YOUR_USER/ClawBridge/bridge/server.js
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable clawbridge
sudo systemctl start clawbridgeFor the container to reach the host:
# docker-compose.yml
extra_hosts:
- "host.docker.internal:host-gateway"On macOS Docker Desktop, host.docker.internal resolves automatically.
External orchestrators can poll GET /api/processes to monitor active and recently completed sessions. This provides a lightweight sidecar integration point — no registration required, just poll the endpoint. ClawBridge works standalone or as part of a larger multi-project orchestration platform.
If a governance tool (e.g., prawduct) is installed on the host, ClawBridge can expose lifecycle commands (setup, sync, validate) via the /prawduct/run endpoint. This integration is optional.
Set CLAWBRIDGE_TOOLS_MODULE to the absolute path of a Node module that exports { init, handleToolsRoute, getToolsHealth, close } and the bridge will mount it under /tools/*, merge its health into /health, and close it on shutdown. Bridge auth runs before your handler, but it enforces nothing in the unauthenticated mode — see the auth caveat in the contract. See docs/tools-extension.md for the full interface contract, guarantees, and reference implementation.
# Run all tests (the 14 live-PTY e2e tests are skipped unless RUN_E2E=1)
npm test
# Run with live E2E (requires Claude Code installed)
RUN_E2E=1 npm test
# Watch mode
npm run test:watchClawBridge/
LICENSE
package.json
bridge/
server.js # HTTP server, auth, routing
.env.example # Environment template
com.clawbridge.builder.plist # launchd service definition
v2/
types.js # Enums: SessionState, EventKind, PermissionType, etc.
pty.js # PTY process wrapper (node-pty + child_process fallback)
permission-parser.js # Detects permission prompts from raw PTY output
policy.js # Evaluates permissions against approval envelopes
event-log.js # Append-only event log with cursor reads and long-poll
sessions.js # Session + SessionManager: lifecycle, timers, permissions
routes.js # HTTP route handlers (includes api-docs, peek, test detection)
__tests__/ # broker suites (the 14-test live-PTY e2e file is skipped unless RUN_E2E=1)
docs/
bridge-v2-maintainer-guide.md
bridge-v2-pty-broker-spec.md
bridge-v2-bug-index.md
bridge-v2-regression-checklist.md
| Document | Purpose |
|---|---|
| Maintainer Guide | Architecture, data flow, known fragility, operational reference |
| PTY Broker Spec | Design spec for the permission broker |
| Bug Index | Every numbered v2 broker bug, mapped to the regression test that guards it |
| Regression Checklist | What to verify after any change |
- OpenClaw — AI agent platform. ClawBridge was built to let OpenClaw drive Claude Code sessions on a remote host.
- TangleClaw — Multi-engine session orchestrator with persistent tmux sessions, mobile access, and sidecar polling via ClawBridge's
/api/processesendpoint. - PortHub — Port registry for development environments. Prevents port conflicts when running ClawBridge alongside other services.
- prawduct — Project governance framework. ClawBridge can optionally expose prawduct lifecycle commands via
/prawduct/run.
MIT. See LICENSE.
