Two Harnesses Walk into a Claude Subscription OpenCode and the pi-claude-code extension for Pi both let developers reuse an existing Claude Code login inside a different coding agent, but they diverge after the model requests a tool call, according to the author's source review of both integrations. OpenCode runs Anthropic's public Claude Agent SDK, which starts a `claude` process and keeps a Claude Code session open, pausing it while OpenCode executes the tool; Pi launches `claude -p` directly, speaks the CLI's `stream-json` protocol, and sets `ANTHROPIC_BASE_URL` on each worker to a random route on a loopback-only HTTP relay so it can read authenticated headers without touching the credentials file. Both integrations disable Claude Code's built-in tools and compaction, skip the user's Claude Code settings files and MCP servers, and expose host tools over MCP under names like `mcp__opencode__read` or `mcp__pi__read`, leaving the outer harness in charge of tool execution and approval. Two Harnesses Walk into a Claude Subscription OpenCode and Pi via pi-claude-code can both use a local Claude Code login. OpenCode keeps a Claude Code session open across tool calls, while Pi runs a fresh worker for each step and replays the transcript. 1. 1. Managing Context Windows with pi /tree: Branches, Summaries, and Subagent-like Workflows https://stacktoheap.com/blog/2026/02/26/pi-tree-context-window-management 2. 2. Pi Durable: A Look at the Future of Agent Harnesses https://stacktoheap.com/blog/2026/10/05/pi-durable 3. 3. Code Mode That Survives a Crash: Durable Scripts in Pi Durable https://stacktoheap.com/blog/2026/10/26/durable-code-mode-in-pi 4. 4. Two Harnesses Walk into a Claude Subscription OpenCode now has a Claude Agent provider that uses an existing Claude Code login. Pi has a similar option through the pi-claude-code https://github.com/earendil-works/pi-claude-code extension. Both let me use the Claude subscription I already pay for while keeping the tools and interface of another coding agent. I read the source of both integrations as of writing this post pinned revisions are linked at the end to see what each one does between its host, OpenCode or Pi, and Claude Code. The login part is straightforward. Where the two integrations split is what happens after the model asks to run a tool. OpenCode keeps a Claude Code session running and pauses it while OpenCode runs the tool. Pi asks Claude Code for one model response, takes the tool request out, and rebuilds the conversation for the next step. That choice shapes who owns the transcript, what the system prompt looks like, how prompt caching survives, and how much of Claude Code’s internals each integration has to understand. What both integrations do the same way The harness is the program around the model. It builds the context, runs tools, asks for approval, and decides what happens next. Both integrations keep that work in the outer harness: - The installed claude program handles model access using whatever login it already has. Neither integration copies Claude Code’s credentials. - Claude Code’s built-in tools are disabled. The outer harness runs every tool and owns the approval flow. - Claude Code’s own compaction is disabled. The outer harness compacts the conversation. - The user’s Claude Code settings files and MCP servers are skipped. Disabling Claude Code’s built-in tools keeps tool execution in one place. If OpenCode shows me an approval dialog while Claude Code can still run its own shell tool behind it, there are two execution paths to reason about. Turning off the built-in tools avoids that. Both integrations also expose the host’s tools to Claude Code over MCP, the Model Context Protocol, under names like mcp opencode read or mcp pi read . They diverge when Claude Code calls one of those tools. Two paths to the model The arrows below show the path a model request takes. Responses come back the same way. OpenCode uses Anthropic’s public Claude Agent SDK. The SDK is a programmatic wrapper around the Claude Code CLI: it starts a claude process and talks to it for you. OpenCode gives it the path to the installed claude from OPENCODE CLAUDE PATH or PATH , so it runs with the existing login. Claude Code sends the API requests itself, with nothing between it and the backend. Pi skips the wrapper. It starts claude -p itself and speaks the CLI’s stream-json protocol directly. It also sets ANTHROPIC BASE URL on each Claude Code worker to a random route on a loopback-only HTTP relay run by the extension. When Claude Code sends a request, it hits Pi’s local relay, which forwards the request upstream and reads the response before passing it back to Claude Code. Because of that relay, Pi sees the authenticated headers on every request even though it never reads the credentials file on disk. The extension’s architecture document https://github.com/earendil-works/pi-claude-code/blob/70abb65186592ca788b29b3db22c3160d629c41f/ARCHITECTURE.md calls this out directly. Both integrations still require a local Claude Code install and route every call through it, so neither turns a subscription into a general-purpose API key. Following one tool call For the rest of the post, I will trace one example where I ask the agent to review a diff and the model starts by reading one of the changed files. OpenCode waits inside the tool handler OpenCode passes an in-process MCP server to the Claude Agent SDK. When the model calls mcp opencode read , Claude Code invokes that server’s handler. Instead of reading the file itself, the handler waits on a promise until OpenCode runs the tool and hands back the result. OpenCode already sees the tool call in the SDK’s message stream, so why the MCP server? The stream only goes one way. OpenCode can watch it, but it cannot push a tool result back into Claude Code’s running turn. Claude Code will not continue until a tool it knows about returns, and with its built-in tools off, the MCP server is the only place its tools come from. So the MCP server does two jobs. It tells Claude Code which tools exist, and it gives Claude Code something to call and wait on while OpenCode does the real work. Watch the highlighted step move through the exchange. Hover over the diagram to pause it and see every step. From OpenCode’s side, this looks like any other model. The step ends at the tool call, OpenCode’s normal agent loop runs the tool, and its next model call carries the result. That call resolves the promise, and the MCP handler returns the file contents to Claude Code. Both OpenCode’s history and the native Claude Code session record the same real tool result, and the Claude Code process stays alive for the next model response and the next tool call. The implementation https://github.com/anomalyco/opencode/blob/bf5d12c588911a35ec56ad991c3b83cf937f424a/packages/core/src/plugin/provider/claude-agent/tools/pending.ts is a map of pending promises, resolved when OpenCode’s own tool execution finishes. A few hooks and settings around the MCP server handle the details: - Matching calls to IDs. The MCP handler does not receive Claude’s tool-call ID, so a PreToolUse hook captures it right before the handler runs. Because the bridge stores that ID in a single slot, it marks every tool as not read-only so Claude Code runs tool calls one at a time. - Rich results. A PostToolUse hook swaps in results that the plain MCP path cannot return in the shape Claude Code expects, such as PDFs and images. - Long waits. OpenCode removes Claude Code’s MCP tool timeout from the environment so permission prompts and slow subagents do not fail while waiting. OpenCode keeps one Claude Code process per chat session, because the SDK runs one process per conversation and cannot share it between sessions. After five minutes idle it stops the process, and the next message resumes the saved native session. Time spent waiting on a permission prompt or a running tool does not count as idle: stopping the process then would record the tool call as cancelled by the user. The system prompt, tools, connection settings, and output limit are fixed when the process starts, so changing any of them starts a new one. Pi intercepts the tool request Pi handles the same read differently. Like OpenCode, it gives Claude Code an MCP server so the model knows which tools exist. The difference is what happens when Claude Code calls one. OpenCode’s handler waits for the real result. Pi’s handler answers straight away with a fixed denial: Denied: Claude Code tools are inert; only Pi executes tools. Pi does not want Claude Code to get a result at all. It starts Claude Code with built-in tools off, --max-turns 1 , and --no-session-persistence , because all it wants is one model response, including any tool requests in it. Pi then runs the tool in its own agent loop. Its MCP server is only there so the model can see the tools. This is where it gets awkward, because Claude Code is an agent, not a plain API client. After the denial, it may try to recover by asking the model again, even with --max-turns 1 and retries set to zero. That second request would cost quota and produce a reply Pi throws away. Claude Code may also end the turn with a max-turns error or a recovery message of its own, which gets in the way of the original response Pi needs. So Pi puts the local relay from the earlier diagram between Claude Code and Anthropic. It lets the first model request through, keeps a copy of the raw response as it comes back, and refuses any request after that. The relay forwards one upstream Messages request per Pi step. Any further request from that worker gets a 400 with the message PI CLAUDE CODE TURN RELAY CONSUMED before it can reach Anthropic. Because the relay sits on the HTTP stream, it captures the first raw response before Claude Code processes it. By the time Claude Code turns the denied tool call into a max-turns error or a synthetic recovery message, Pi has already recorded the original tool arguments, stop reason, signed thinking blocks, and token usage. The extension converts those into Pi’s message format, and Pi runs the real tool. Once the inert MCP server returns the denial, Claude Code records that denial as the tool result in its own history. Because the worker’s transcript no longer matches Pi’s transcript, the extension discards the worker and replays Pi’s updated history into a new worker on the next step. One request across two tool calls Now consider a request that needs two tool calls before the final review reading a file and running the tests : OpenCode stays in the same Claude Code process across the whole request. Pi starts a fresh worker for each model step after a tool call, and each new worker begins by replaying the conversation so far. Pi only reuses a warm worker when the next request is an exact continuation with no tool call in between, such as a plain follow-up question. It keeps at most two warm workers, expiring each one five minutes after it was created. OpenCode instead expires its process after five minutes of idle time and can resume the saved native session later. Keeping the prompt prefix stable When Claude Code sends a turn, it injects extra context around the user’s messages: environment details, reminders, skill lists, and nested CLAUDE.md files. If those blocks move around or repeat on every fresh worker, a conversation that looks identical in Pi becomes a different prompt upstream. Because prompt caching matches on the exact prefix, any shift in those injected blocks causes a cache miss and burns quota on a long conversation. OpenCode avoids this by telling Claude Code to skip the extra context. It sets the SDK’s verbatimPrompts option, which sends each message as given without injecting per-turn context OpenCode has already expanded /commands and @file references itself . OpenCode’s design spec https://github.com/anomalyco/opencode/blob/bf5d12c588911a35ec56ad991c3b83cf937f424a/specs/v2/claude-agent-provider.md notes that this is why it requires Claude Code 2.1.248 or later, since older versions ignore the option and can treat user text as Claude Code commands. Pi lets Claude Code add the context and then rewrites its placement in the relay: - It records the native user prefix and system reminders from the first request, puts them back in the same place on every replay, and removes the reminders that move. - It moves cache breakpoints so each tool round does not invalidate the cached prefix. - It puts tools that were added or removed mid-conversation back at the point where they changed, using the matching beta header on models that support it. - It cancels the upstream socket as well as the child process when Pi cancels. Pi also normalizes tool schemas for Claude Code along the way, which may simplify nullable types and compositions such as oneOf , allOf , and anyOf . Rewriting requests in the relay gives Pi control over every model call, at the cost of depending on the exact request shape Claude Code produces. If a Claude Code update changes that shape, the relay can break. I would expect OpenCode’s warm session to skip some process start-up and replay work, but Pi’s relay is built specifically to keep the prompt cache warm across workers. Comparing speed or quota use between the two would need measured runs. System prompts For ordinary chats, OpenCode uses the SDK’s claude code preset Claude Code’s own system prompt and appends its own instructions. Pi writes a system-prompt file from its own transcript and passes it with --system-prompt-file , replacing Claude Code’s default. --system-prompt-file only replaces the base prompt; Claude Code still adds the per-turn context from the previous section, and Pi’s relay manages it. Here are the two choices, trimmed from the source: js // OpenCode: keep Claude Code's prompt, append host instructions. const primary = { type: "preset", preset: "claude code", snapshot: false, append: "