# Provider: `claude-bridge` — Claude via the Code CLI (no Agent SDK)

An alternative Claude provider that routes through your **local Claude Code
CLI** instead of the `@anthropic-ai/claude-agent-sdk`. It uses your existing
Claude Code CLI authentication and needs no `ANTHROPIC_API_KEY`. All agent
capability (tools, sub-agents, MCP, hooks) is preserved because it lives in
Alvin's own orchestration, not in the SDK — so the bot can run fully
SDK-independent (useful as a fallback, or where the SDK isn't preferred).

> Advanced, opt-in option. As with any provider, make sure your usage complies
> with the relevant provider's terms of service.

## Why this works without losing features

Alvin's `OpenAICompatibleProvider` already runs its **own** tool loop
(`tool-executor.ts`, 8 tools) and its **own** sub-agent orchestration
(`subagents.ts`, which just calls `registry.queryWithFallback()`). So a provider
only has to: accept OpenAI `/chat/completions` **with a `tools` array** and
return real **`tool_calls`** for Alvin to execute. A local bridge that fronts
the `claude` binary does exactly that.

```
Alvin (OpenAICompatibleProvider tool loop + sub-agents)
   → POST /v1/chat/completions  (tools[], stream:false)
      → claude-bridge (localhost, bot-managed)
         → spawns the `claude` CLI (uses your Claude Code login)
      ← OpenAI tool_calls  (parsed from Claude's <tool_call> text)
   → executeTool() runs the tool (EXEC_SECURITY allowlist + path-safety guards)
   → next round → final answer
```

Claude itself never touches the filesystem: the bridge spawns it with
`--tools ''`, so it only *emits* `<tool_call>` blocks; **Alvin** executes them
under its own safety guards.

## What stays / what is dropped vs the Agent SDK

The parity work (capability flags + a few bridge-specific seams) brings the
bridge to near-feature-parity with the SDK. **Stays / now wired:** sub-agents
(parallel), **detached `claude -p` dispatch**, the 8-tool loop, **mid-task
stop**, **`/effort`** (→ `--effort`), **live `/btw` steering**, **per-workspace
overrides** (model / temperature / toolset / effort), external MCP client, hook
event bus, fallback chain, budget/usage, cron, memory, streaming.

**Dropped (genuinely SDK-native):** the SDK's *internal* `Task` tool (Alvin's
own sub-agent orchestration replaces it), `.jsonl` session continuity (falls
back to Alvin's own `history[]`), the 1M-context beta, the SDK `PostToolUse`
fence hook. External MCP tools are NOT in the LLM loop on *either* provider
(SDK or bridge) — they're only callable via `/mcp` — so that's parity, not a gap.

## Command compatibility

Coupling was moved from hardcoded `config.type === "claude-sdk"` checks to
Provider **capability flags** (`supportsDispatch`, `supportsLiveSteer`,
`supportsReasoning`), so the bridge opts into the same behaviours. All commands
work on the bridge:

| Command | Bridge | How |
|---|---|---|
| `/stop` `/cancel` `/stopall` | ✅ | abortSignal cuts the fetch + the tool loop bails at round/tool boundaries; the bridge kills its `claude` on disconnect. (Sync execSync tools finish their own 30s timeout.) |
| `/effort` | ✅ | provider forwards `reasoning_effort` → CLI `--effort` (gated on `supportsReasoning`). |
| `/btw` (live steering) | ✅ | `supportsLiveSteer`; the tool loop drains the SteerChannel between rounds and folds notes into the running turn. Plain mid-task messages steer too. |
| Detached sub-agents | ✅ | `supportsDispatch`; the `alvin_dispatch_agent` function tool spawns the same raw `claude -p` the SDK does. |
| Parallel sub-agents | ✅ | Alvin's own orchestration → `queryWithFallback`, provider-agnostic. |
| Workspace overrides | ✅ | model / temperature / effort forwarded; `toolset` (readonly/research) gates the exposed tools by name mapping — a readonly workspace can't write or run shell on the bridge. |
| `/model` | ✅ | bridge special-case lists/sets `claude-{opus,sonnet,haiku}-latest`, persisted via `CLAUDE_BRIDGE_MODEL`. |
| `/status` | ✅ | reasoning badge + context meter capability-keyed; rendered like the other CLI-based provider. |
| `/new` `/dir` `/verbosity` `/continuation` `/export` `/provider` `/mcp` `/cron` … | ✅ | bot-level, provider-agnostic. |

Steering nuance: the bridge folds notes at tool-round / message boundaries (the
bridge resumes the same `claude` session each round), not mid-token. This
matches the SDK's own behaviour, which integrates steer messages at message
boundaries too. Verified live: a mid-task /btw redirected a running turn's
output (topic + language) and dropped the original.

## Bot wiring

- `src/providers/types.ts` — `claude-bridge` preset (openai-compatible,
  `supportsTools:true`, loopback baseUrl, `options.managed:"claude-bridge"`).
- `src/providers/claude-bridge-provider.ts` — `ClaudeBridgeProvider` (subclass
  of `OpenAICompatibleProvider`) adds a `ProviderLifecycle` + capability flags.
- `src/services/claude-bridge-manager.ts` — on-demand **auto-spawn /
  health-check / stop**, exactly like `ollama-manager`. Spawns the bridge from
  `CLAUDE_BRIDGE_DIR` (default `~/.alvin-bot/claude-bridge`), pins `CLAUDE_BIN`,
  derives ports from `CLAUDE_BRIDGE_URL`. Degrades to an info-log (never a throw)
  when the bridge isn't installed or `claude` isn't logged in.
- `src/providers/registry.ts` — **always** registers `claude-bridge` (Ollama
  parity) so it's selectable at runtime; `createProvider` returns the
  lifecycle-aware `ClaudeBridgeProvider`. No API key.
- `src/providers/openai-compatible.ts` — the tool loop sends explicit
  `stream:false`; honors per-query model/temperature; gates tools by
  `allowedTools`; drains the SteerChannel for `/btw`.
- `bin/cli.js` — setup wizard lists it; `/provider use claude-bridge` switches at
  runtime. The bridge itself is bundled, so there's no separate install step.
- `claude-bridge/` — the vendored, patched bridge (CommonJS), shipped with the
  package. `express`/`uuid` are alvin-bot dependencies, so it resolves them from
  the parent install; state/log are written to `~/.alvin-bot/`.

Enable it — pick **Claude — Code CLI bridge** in `alvin-bot setup`, or:

```
PRIMARY_PROVIDER=claude-bridge          # or: Telegram → /provider use claude-bridge
# optional overrides:
CLAUDE_BRIDGE_URL=http://127.0.0.1:3456/v1
CLAUDE_BRIDGE_MODEL=claude-sonnet-latest   # or claude-opus-latest / claude-haiku-latest
CLAUDE_BRIDGE_DIR=~/.alvin-bot/claude-bridge
```

The bot **auto-spawns and health-checks** the bridge on demand.

## The bundled bridge

The bridge ships **with** alvin-bot (vendored at `claude-bridge/`), so there's
nothing to install — the bot auto-spawns it on demand and it updates with
`/update`. It's a slim fork of
[`shinglokto/openclaw-claude-bridge`](https://github.com/shinglokto/openclaw-claude-bridge)
(MIT — see `claude-bridge/LICENSE`) with the React dashboard removed and a few
patches applied (documented in `claude-bridge.upstream.patch`). Without those
patches the model refuses the protocol, hallucinates, or drops tool arguments:

1. **`src/claude.js`** — spawn `claude` with `--setting-sources ''` and
   `--strict-mcp-config`, so the CLI does NOT auto-load the host's global
   `CLAUDE.md` + any configured MCP servers. Otherwise that context (a) bloated
   input from ~600 to ~72,000 tokens and (b) made the model think its tools were
   those MCP servers and **refuse** `read_file`/`run_shell`. Also: `mapEffort`
   passes `low|medium|high|max` straight to `--effort`.
2. **`src/tools.js`** — include each tool's full **parameter schema** (names,
   types, required, descriptions) in the protocol instructions, not just
   name+description; plus stronger "nest args under `arguments`; STOP after the
   call; don't fabricate results" wording. Took tool-call compliance from ~50%
   to 5/5 in trials.
3. **`src/server.js` `parseToolCalls`** — robust argument extraction. The model
   often **flattens** args to the top level
   (`{"name":"python_execute","code":"…"}` instead of nesting under
   `arguments`); recover every non-reserved key. Plus a lenient JSON repair for
   raw control chars inside string values. Without this, multi-line args
   (python/code) were silently dropped → the tool ran with `{}` and errored.
4. **Bundling** — `src/index.js` binds the status port to **loopback** (was
   `0.0.0.0`); `src/server.js` writes its state file to a **writable** path
   (`CLAUDE_BRIDGE_STATE_FILE`, default `~/.alvin-bot/`) and only serves the
   dashboard if present (the slim fork ships none).

Prereq: be **signed in to Claude Code** (`claude auth status` → `loggedIn:true`).
No separate `claude` install is needed — `findClaudeBinary()` uses the Claude
Code binary that ships with the `@anthropic-ai/claude-agent-sdk` dependency (and
updates with alvin-bot), preferring your own newer system `claude` if present.
So the bridge inherits the same (newest) CLI the SDK path uses.

## Verified (live E2E, 2026-06-16, two machines)

- **Tool loop / all 8 function-callings:** run_shell, read_file, write_file,
  edit_file, list_directory, python_execute (un-fakeable SHA256), web_fetch,
  web_search — 8/8 routed bridge → `executeTool` (no SDK in chain).
- **Sub-agent fan-out:** 3 `spawnSubAgent()` concurrently, 3/3 correct.
- **Detached dispatch:** the bridge LLM called `alvin_dispatch_agent` → a real
  detached `claude -p` spawned, output files on disk.
- **Mid-task stop:** abort after the 1st of 5 tools → halted at 1/5.
- **/effort:** `effort:"high"` → bridge spawned `claude --effort high thinking=on`.
- **Live /btw steering:** a mid-task note redirected a running turn (ocean poem
  → German mountains poem; original dropped).
- **Workspace `readonly`:** a write was blocked (no write tool available, nothing
  on disk) while `read_file` still worked.
- **Lifecycle:** with the bridge stopped, `queryWithFallback` auto-spawned it.
- Full unit suite green incl. `bridge-parity`, `registry-claude-bridge`,
  `openai-compatible-stream-false`, `provider-models`, `steer-routing` tests.

> macOS note: the `claude` login token lives in the login keychain, which is
> **locked in a non-interactive SSH session** (`claude auth status` →
> `loggedIn:false` over SSH even when logged in). Run the bot/bridge in a GUI or
> launchd session (or set `CLAUDE_CODE_OAUTH_TOKEN`) so it can read the token.

## Caveats

- **One login = shared rate limits** across parallel sub-agents.
- Per-round latency: a fresh `claude` spawn per request (no `.jsonl` reuse).
- The bridge runs as a small bundled local service (loopback only, auto-managed,
  state/log in `~/.alvin-bot/`).