A local proxy that splits Claude Code traffic between your Claude Pro/Max OAuth subscription (for planning) and any OpenAI-compatible endpoint (for execution). Built because Claude Code's
/model opusplanonly works within Anthropic's models — forky brings the same UX with any execution backend, plus an opt-in pipeline for models that need help formatting tool calls.
Claude Code (VSCode + CLI)
│ ANTHROPIC_BASE_URL=http://127.0.0.1:3456
▼
┌─ forky ─────────────────────────────────────────────────────┐
│ │
│ plan mode ───► Anthropic OAuth (your Max plan) │
│ default ───► your OpenAI-compatible endpoint │
│ Write/Edit ≥50 lines ──► optional Opus reviewer hook │
│ │
│ • prompt caching on the OAuth path (~80% input savings) │
│ • per-provider concurrency limiter + 429 retry/backoff │
│ • stream watchdog + SSE keep-alive (no socket drops) │
│ • circuit breaker with persistent state across restarts │
│ • transparent OAuth Sonnet fallback on exec failure │
│ • optional rectifier: 2-model chain for XML-leaky models │
└─────────────────────────────────────────────────────────────┘
- Anthropic disables Claude Code's
/modelpicker the moment you setANTHROPIC_BASE_URL, so the built-inopusplanalias can't be selected through a custom endpoint. claude-code-routerand similar proxies are API-key only — they can't preserve your Claude Pro/Max OAuth subscription.- Plan mode is undetectable from the API (the system prompt is byte-identical to non-plan mode), so the proxy alone can't infer it.
forky solves all three:
- Reads your Claude Code OAuth token directly from the macOS Keychain (or
~/.claude/.credentials.jsonon Linux) and forwards it asAuthorization: Bearer …toapi.anthropic.com. Your Max subscription is preserved. - Routes execution traffic (anything that isn't
claude-opus-*) to any OpenAI-compatible/chat/completionsendpoint you own. - Detects plan mode via a Claude Code
UserPromptSubmithook that toggles a local sentinel file — fully automatic.
- macOS (uses the Keychain) or Linux (reads
~/.claude/.credentials.json) - Bun ≥ 1.3
- An active Claude Pro or Max subscription, already logged into Claude Code
- Any OpenAI-compatible API endpoint of your own (Groq, Together, Fireworks, vLLM, self-hosted Ollama, your own service, etc.)
git clone https://github.com/vladharl/forky ~/dev/forky
cd ~/dev/forky
bun installSet your execution-backend credentials in ~/.zshrc:
export EXEC_BASE_URL="https://your-endpoint.example.com/v1"
export EXEC_API_KEY="your-api-key"
export EXEC_MODEL="qwen-35b" # or whatever your backend exposes
export PORT=3456Optional (recommended for XML-leaky models like qwen):
export EXEC_REFORMAT_MODEL="gemma-micro" # see "two-model rectifier" below
export FORKY_FRESH_TURNS=on # cap context per user prompt
export FORKY_MAX_MESSAGES=20 # hard cap within a turnStart the proxy:
./bin/forky # foreground
# or
nohup ./bin/forky > ~/.forky/server.log 2>&1 & disownYou should see {"event":"server.start","port":3456} in the log.
VSCode extension — add to your User settings.json (Cmd+Shift+P → "Preferences: Open User Settings (JSON)"):
(ANTHROPIC_MODEL makes Claude Code default to Sonnet, which routes to your execution backend. Opus is still reachable via plan mode or forky-opus on.)
CLI — add to ~/.zshrc:
export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
export ANTHROPIC_AUTH_TOKEN="forky-dummy"
export ANTHROPIC_MODEL="claude-sonnet-4-6"Or scope to the VSCode integrated terminal only via terminal.integrated.env.osx.
Plan-mode auto-detection lives in ~/.claude/settings.json. Add:
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "/absolute/path/to/forky/bin/forky-hook" }] }
]
}Reload the hook watcher: in Claude Code type /hooks, or restart. Plan mode (Shift+Tab) now auto-routes to OAuth Opus; default mode goes to your execution backend.
| Claude Code mode | Routes to | Why |
|---|---|---|
| Default / auto-accept | EXEC_MODEL on EXEC_BASE_URL |
Cheap, fast execution; Max quota untouched |
| Plan (Shift+Tab) | claude-opus-4-7 via OAuth |
Quality reasoning for design/planning |
/model claude-opus-* |
Same as plan | If you ever explicitly request Opus |
| Manual override | forky-opus on/off |
Force Opus without entering plan mode (auto-expires after 4h) |
./bin/forky-status # circuit + per-pool semaphores + counters
tail -F ~/.forky/log/$(date +%F).jsonl # live request log (JSONL)
tail -F ~/.forky/reviews.jsonl # Opus reviewer audit log
./bin/forky-opus status # current routing mode
curl -s http://127.0.0.1:3456/health | jq . # JSON health summary| Env var | Purpose |
|---|---|
EXEC_API_KEY |
bearer for your OpenAI-compatible endpoint |
EXEC_BASE_URL |
e.g. https://api.together.xyz/v1 |
| Env var | Default | Purpose |
|---|---|---|
EXEC_MODEL |
qwen-35b |
model name sent to the execution backend |
EXEC_REFORMAT_MODEL |
unset | enables the 2-model rectifier (see below) |
PORT |
3458 |
listening port (set to 3456 for production) |
HOST |
127.0.0.1 |
bind address — keep loopback |
FORKY_FRESH_TURNS |
unset | when on, each request is trimmed to the latest user prompt + its tool chain |
FORKY_MAX_MESSAGES |
0 (unlimited) |
hard cap on messages per request (e.g. 20) |
FORKY_TRUNCATE_TOOL_RESULTS |
on |
replace already-consumed tool_result bodies with one-line placeholders (preserves decision trail, drops bulk Read/Bash outputs); set off to disable |
FORKY_TRUNCATE_MIN_BYTES |
500 |
min size of a tool_result body before truncation kicks in |
FORKY_SUMMARIZE_THRESHOLD |
0 (off) |
when messages-per-request exceeds this, fold the older half into a single summary system block via FORKY_SUMMARIZER_MODEL |
FORKY_SUMMARIZER_MODEL |
gemma-micro |
model used by the optional summarizer pass |
FORKY_REVIEW |
on |
toggle for the optional bin/forky-review-hook PreToolUse hook |
FORKY_CREDENTIALS_FILE |
~/.claude/.credentials.json |
Linux only — override the OAuth credentials path |
| Env var | Default | Purpose |
|---|---|---|
FORKY_FIRST_BYTE_MS |
30000 |
stream watchdog: first-byte timeout |
FORKY_INTER_CHUNK_MS |
15000 |
stream watchdog: inter-chunk timeout |
FORKY_REFORMAT_PRIMARY_TIMEOUT_MS |
60000 |
rectifier path: max wait for the primary model |
FORKY_REFORMAT_RECTIFIER_TIMEOUT_MS |
20000 |
rectifier path: max wait for the reformatter |
FORKY_HEARTBEAT_MS |
5000 |
SSE keep-alive heartbeat interval |
FORKY_EXEC_PRIMARY_MAX_CONCURRENT |
4 |
concurrency cap for primary model calls |
FORKY_EXEC_RECTIFIER_MAX_CONCURRENT |
8 |
concurrency cap for reformatter calls |
FORKY_EXEC_429_ATTEMPTS |
3 |
retry attempts on HTTP 429 (honours Retry-After) |
FORKY_EXEC_429_BASE_DELAY_MS |
250 |
base for exponential backoff between retries |
FORKY_CIRCUIT_THRESHOLD |
3 |
failures before circuit opens |
FORKY_CIRCUIT_WINDOW_MS |
60000 |
sliding window for failures |
FORKY_CIRCUIT_OPEN_MS |
60000 |
how long the circuit stays open |
AISTACK_* env vars (AISTACK_API_KEY, AISTACK_BASE_URL, AISTACK_MODEL) are accepted as legacy aliases.
Many open-weight models (including qwen-2.5/3 variants) mimic the XML tool-call examples in Claude Code's system prompt (<function_calls><invoke name="X">...</invoke></function_calls>) instead of using the OpenAI tool_calls field. The XML lands in message.content as plain text. Claude Code displays it but never executes the tool.
With EXEC_REFORMAT_MODEL set, forky runs a chain:
- Calls
EXEC_MODELnon-streaming (qwen-35b in the example). - If the response content contains XML tool patterns, sends qwen's output to
EXEC_REFORMAT_MODEL(gemma-micro) with the same tool definitions and a rectifier system prompt. - Streams the reformatter's structured
tool_callsthroughSseTranslatorso Claude Code seestool_useevents as they arrive. - The reformatter also fixes wrong parameter names (e.g. qwen writes
pathbut the schema requiresfile_path— gemma maps it).
Observed behaviour:
- ✅
gemma-micro/ Gemma-family — proper function calls even under aggressive XML priming - ✅
gpt-4o-mini,claude-haikuvia API — proper function calls - ❌
qwen-35b,qwen-27b— emit XML; need the rectifier
If you only use models from the green list, leave EXEC_REFORMAT_MODEL unset.
See docs/qwen-gemma-rectifier.md for the design write-up + test fixtures.
bin/forky-review-hook is a PreToolUse hook that runs Opus on every Write/Edit over 50 lines to catch bugs/typos before they land. Skips:
- tests/specs (
*/tests/*,*.test.*,*.spec.*) - docs (
*.md,*.mdx,*.rst,*.txt) - lock files (
*.lock,package-lock.json,bun.lock, …) - whitespace-only diffs (the change is a reformat — not worth Opus tokens)
If Opus suggests fixes, the corrected tool_input is handed back to Claude Code via hookSpecificOutput.updatedInput and the original change never runs.
Register in ~/.claude/settings.json:
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "/absolute/path/to/forky/bin/forky-review-hook" }]
}
]
}Disable temporarily with FORKY_REVIEW=off. Audit log at ~/.forky/reviews.jsonl.
- OAuth path: forky reads your stored OAuth token (macOS Keychain item
Claude Code-credentials, or~/.claude/.credentials.jsonon Linux), refreshes it viaconsole.anthropic.com/v1/oauth/tokenwhen expired, and forwards requests toapi.anthropic.comwith the requiredoauth-2025-04-20beta header and"You are Claude Code, Anthropic's official CLI for Claude."system block. The last system block and last tool definition getcache_control: {type: "ephemeral"}so Anthropic caches the (large, stable) prefix across turns — cuts plan-mode input tokens ~80% on warm hits. - Execution path: requests get translated from Anthropic Messages API → OpenAI Chat Completions (system blocks flattened, tools mapped, image blocks →
image_url).tools_enabled: falseis hard-pinned to prevent server-side tool loops on backends that expose them. Orchestration tools (Agent,TodoWrite,AskUserQuestion, etc.) are stripped — they're for the planner, not the executor. - Stream translation: OpenAI delta chunks → Anthropic event stream (
message_start→content_block_*→message_delta→message_stop), with partial-JSON accumulation for streaming tool calls.delta.reasoning_contentand end-of-sequence tokens (<eos>,<|eot_id|>, …) are dropped. - Concurrency limiter + retry: outbound execution-backend calls go through per-pool semaphores (primary cap 4, rectifier cap 8 by default — matching typical provider caps) so bursts from Claude Code can't trigger 429s. Any 429 that does slip through is retried with exponential backoff that honours
Retry-After. - Never-stuck guarantee: a stream watchdog fires
WatchdogTimeoutErroron first-byte / inter-chunk gap, SSE keep-alive heartbeats (: forky-keepalive) prevent socket drops during slow primary calls, the circuit breaker reroutes to OAuth Sonnet after N consecutive failures, and every code path emits a terminalmessage_stopevent in afinallyblock so Claude Code never waits forever. - Persistent state: counters and circuit state are written to
~/.forky/status.jsonafter every change. On startup, recent state is restored (counters always; circuit only if the OPEN window hasn't expired) so a forky restart mid-incident doesn't reset to "everything is fine." - Per-turn isolation (
FORKY_FRESH_TURNS=on): each user prompt becomes a self-contained agent invocation. Forky trims the conversation back to the latest user message + its tool chain. Sacrifices cross-prompt memory in exchange for never hitting context overflow on long sessions.FORKY_MAX_MESSAGESadds a hard cap on top, sliding the start forward to land on a fresh-user-prompt boundary so tool_use/tool_result pairing isn't broken.
bun test # 85 unit tests, sub-secondCovers request translation, response translation, SSE streaming with tool calls, schema contracts, circuit breaker (incl. restore-from-disk), stream watchdog, in-process semaphore, 429 retry/backoff, OAuth header injection + cache-control markers, fresh-turn / max-message trimming, XML-tool detector.
- Plan-mode auto-detection requires the hook. Plan mode leaves no trace in the API request — without
forky-hookwired into Claude Code settings, you'll needforky-opus on/offfor manual switching. - Claude Code's
/modelpicker stays disabled becauseANTHROPIC_BASE_URLis set. opusplan-style behaviour comes from the hook + sentinel mechanism, not Claude Code's built-in alias. - Tool-call fidelity depends on your execution model — see the rectifier section above for which models work without one.
context_managementbody field is stripped on the OAuth path — Anthropic's OAuth endpoint 400s on it. Claude Code's local context auto-edit still works; only the API-side context-management feature is disabled.
Anthropic's Terms of Service restrict OAuth tokens to "Claude Code and claude.ai." forky forwards the OAuth token only on behalf of Claude Code itself (a transparent local proxy), and execution traffic uses an unrelated API key — but Keychain extraction could plausibly be argued as circumvention. Use at your own risk. Keep the proxy bound to 127.0.0.1, never share tokens, and don't use this in production or multi-user environments.
MIT — see LICENSE.