A shareable agentic setup for OpenCode and Claude Code from a single codebase: the core plugins, skills, commands, and global agent instructions. The plugins are written once against the OpenCode 2 plugin API; Claude Code gets the same tools through a thin stdio MCP server that loads them from this repo's build output.
/pair-program opens an adversarial thinking-partner session against any model (GPT or Claude) from either harness.
Claude Code dispatches a GPT-5.5 agent to redo what OpenCode just did natively, then switches that same session to a different model.
Both harnesses are installed via Homebrew. This repo targets OpenCode 2 (2.0.21), which is the opencode-v2 formula:
brew install anomalyco/tap/opencode-v2
brew install --cask claude-codeThe formula conflicts with opencode (1.x). To switch from 1.x, run brew uninstall opencode && brew install anomalyco/tap/opencode-v2 first.
pnpm install
pnpm run buildRequires Node 24 (mise.toml; mise install gets it) and pnpm 11 (pinned via the packageManager field; corepack enable gets the right version). Node is also needed at runtime: the session runner behind oc_run runs under node (set NODE_BIN to use another binary), and the Claude Code bridge and hooks run under Node too.
pnpm run symlink:opencodeThis rebuilds dist/ and runs scripts/link-opencode.sh, which:
- Copies
src/skills/*/to~/.config/opencode/skills/ - Copies
src/commands/*.mdto~/.config/opencode/commands/ - Copies
AGENTS.mdto~/.config/opencode/AGENTS.md, so the same global instructions load in every OpenCode session (the same role the CLAUDE.md copy plays for Claude Code). A repo's ownAGENTS.mdtakes precedence in that repo; an existing unmanaged global file is backed up with a timestamp first. - Copies
opencode.jsonto~/.config/opencode/opencode.jsonwithpluginsset to the absolute path of every built module that has aserver.js(dist/modules/<name>, one plugin per module; OpenCode loads a local plugin from a directory'sserver.js) and the compaction policy's per-model limits added (an existing global config is backed up first; see Context Compaction)
Rerun it whenever skills, commands, opencode.json, or plugin sources change. The running background service picks up a rebuilt plugin on its own, but not a changed configuration: after a new module, or a changed agent permission, run opencode reload (until then a new plugin's tools are simply missing). Everything is installed as a real copy, not a symlink (containers bind-mounting ~/.config/opencode would otherwise see dangling host paths), so edits to the installed copies are lost on the next rerun - always edit the sources here.
pnpm run symlink:claude-codeThis rebuilds dist/ and runs scripts/link-claude-code.sh, which:
- Registers the
opencodeMCP server in user scope (claude mcp add --scope user), so tools are available in every project asmcp__opencode__<tool>(e.g.mcp__opencode__codebase_find_definition) - Copies
src/skills/*/to~/.claude/skills/ - Copies
src/commands/*.mdto~/.claude/commands/, keeping the---fenced frontmatter (the only form OpenCode 2 parses) and dropping every key Claude Code does not read, such asagent,modelanduser-invocable; onlydescription,argument-hint,allowed-toolsanddisable-model-invocationare kept. A command without fenced frontmatter fails the install - Copies the subagents in
src/agents/*.mdto~/.claude/agents/as exact copies, since they are already in Claude Code's format. Today they are the agent profiles ofopencode.json, generated bypnpm run agents:generate(see Agent profiles in Claude Code). The OpenCode installer does not install them - Sets
autoCompactEnabledandautoCompactWindowin~/.claude/settings.jsonfrom the installed OpenCode config, leaving every other key alone (see Context Compaction); run the OpenCode installer first, aspnpm run symlinkdoes - Merges the
PreCompactandSessionStarthandoff-compaction hooks into~/.claude/settings.json'shooks, replacing only an earlier run's own entries and leaving every other hook the owner configured alone (see Handoff Compaction) - Copies
AGENTS.mdto~/.claude/CLAUDE.mdas an exact copy, so the same global instructions load in every Claude Code session. A~/.claude/CLAUDE.mdnot created by this installer is backed up toCLAUDE.md.backup.<timestamp>first - merge anything you want to keep into this repo'sAGENTS.mdand rerun.
Installs are tracked in ~/.claude/.opencode-cookbook-manifest.json; each rerun removes exactly what the previous run installed before copying, so renames and deletions propagate while skills, commands, subagents, and instruction files from other sources in ~/.claude are never touched, except that an install overwrites a skill or command of the same name. A subagent file of the same name that no earlier install placed, and an unmanaged CLAUDE.md, are moved aside to a timestamped .backup.<time> copy first.
pnpm run symlink runs both installers.
Claude Code asks for confirmation on every MCP tool call by default. To approve the whole server once, add to ~/.claude/settings.json:
{
"permissions": {
"allow": ["mcp__opencode"]
}
}Granular alternative: per-tool rules like "mcp__opencode__codebase_find_definition".
One policy covers both harnesses, COMPACT_AT_TOKENS_BY_PROVIDER in scripts/compaction-policy.sh, which both installers read. It lists a limit per provider: anthropic=750000 compacts Claude models at 750k tokens, and openai=max lets each GPT model compact as late as its own window allows. A model whose window is smaller than its provider's limit compacts at its own window, and a provider not in the list is not limited, so a new provider gets a limit by adding one entry. Each harness applies the entries of the providers it can run: Claude Code only anthropic, OpenCode all of them.
- OpenCode compacts once a session reaches a model's input limit minus
compaction.buffer(20,000 inopencode.json), so the installer reads each model's window fromopencode api model.list --standalone, run with an empty config home so no earlier install counts, and writes an input-limit override for each model whose window is larger than its provider's limit (an Anthropic model used through OpenCode, for example). Under theopenai=maxentry,gpt-5.4andgpt-6-*keep their 922k input window under a ChatGPT login and compact at 902k. Rerun the installer when the model list changes. - Measured windows. A
limitset inopencode.jsonreplaces the window OpenCode reports, and the policy applies on top of it. Under a ChatGPT login OpenCode hardcodesgpt-5.5andgpt-5.6-*at a 400k context and 272k input, but those models have served 440k-token requests since late August 2026 (after overflowing at 350k to 370k on 6 and 7 August), soopencode.jsongives them a 460k input limit and they compact at 440k. If the provider narrows the window again,compaction.autocompacts on the overflow error instead of failing the session. - Claude Code takes
autoCompactWindowfrom the policy'santhropicentry (750,000;maxleaves it unset) andautoCompactEnabledfrom the installed OpenCode config'scompaction.auto. It caps the window at each model's context, so a 200k model compacts at its limit. A headlessclaude -prun never compacts.
Neither harness lets a session compact on its own terms, and neither lets a model choose the moment. This makes every compaction moment safe instead: right before it happens, a profile decides what to preserve. default covers any session; orchestration adds task-directory specifics for a session that has called a wf_* tool. The choice is automatic - an explicit compaction profile: <name> line in the conversation overrides it - so nothing has to be configured per session.
The compaction module (src/modules/compaction/) implements this once and wires it into both harnesses differently, because the two hook surfaces are shaped differently:
- Claude Code has no hook that can replace the built-in compaction summary, so this uses two separate hooks around it.
PreCompactrunsnode dist/modules/compaction/cli.js pre-compact --writer-model <model> --writer-context-tokens <n>(installed with the writer model, its context window and the timeout fromscripts/compaction-policy.sh): it parses the transcript, chooses the profile, captures the git state of every repository the session touched (its working directory and those of the files it wrote), and asks a headlessclaude -pwriter (no tools, no session, hooks off, and auto-compaction at the writer's context window) to produce a handoff document, saved under~/.claude/handoffs/<session_id>/. The session record covers what the session's context holds now: after its last compaction, the messages that compaction kept, its summary and everything since. It lists every file the session wrote over the whole transcript and keeps every human message, all assistant text and every tool call whole. Tool results are the only thing dropped, and only when the record would not fit the writer's window: file reads and searches first, shell output next, everything else last, oldest first within each group.SessionStart(matchercompact) runscli.js session-start, which finds that handoff and prints a short directive telling the resumed session to read it, re-checkgit status, reload the skills it names, and (for the orchestration profile) read its task directory's durable files and journal first. A## Compact Instructionssection inAGENTS.mdsteers the harness's own summary as a backup for when the handoff itself is missing. - OpenCode has one hook,
compaction, whose result becomes the stored summary - so there is no separate handoff file or later injection step. The compaction plugin (src/modules/compaction/server.ts, hooks-only: it registersctx.session.hook('compaction', ...)and no tools, so the MCP bridge does not load it) parses the messages being compacted, chooses the profile, builds the session record with the same record service and writer prompt the Claude Code hook uses, and generates the handoff withctx.generate.texton the session's own model. The handoff followed by the profile's resume steps is set asevent.result, so OpenCode's own summary template is never used. If the generation fails, the compaction fails; there is no fallback to OpenCode's template. Two limits: the hook sees only the messages since the previous compaction, so a profile signal (awf_*tool call or acompaction profile:line) that lies before it is not seen, and the record's "Files written" list is empty on OpenCode because it matches Claude Code's tool names (the git state still lists changed files).
Both installers wire this up: pnpm run symlink:claude-code merges the two hooks into ~/.claude/settings.json (replacing only an earlier run's own entries, leaving every other hook the owner configured alone) and pnpm run symlink:opencode needs nothing extra - the compaction plugin is one of the module plugins it links.
Each module under src/modules/ is its own OpenCode 2 plugin: the entry is src/modules/<name>/server.ts and the tools come from the factory in src/modules/<name>/<name>.module.ts. Each module's README lists its tools, whether the Claude Code bridge loads it, and which OpenCode agents allow its tools.
| Module | What it does | Auth |
|---|---|---|
| AWS Session | Session-scoped AWS CLI configs backed by IAM Identity Center; wraps no AWS API (aws_session_*) |
Config file + human-approved aws sso login |
| Codebase | TypeScript-aware code navigation: definitions, usages, unused symbols, project structure | None |
| Compaction | Handoff compaction: a handoff document at every context compaction, in both harnesses | OpenCode model login |
| OpenCode | Local opencode run and claude -p dispatch (oc_run) and OpenCode session store access |
OpenCode login, claude |
| Workflow | A detached engine that runs Workflow-shaped scripts with GPT and Claude steps (wf_*) |
OpenCode login, claude |
| Structured Output | submit_result: a schema-checked way for a dispatched worker to return its result |
None |
| Local LLM | Models on this machine through one shared llama.cpp server (llm_*) |
None (brew install llama.cpp) |
| GitHub | github_ci_wait: one call that waits for CI on a pull request, a commit or a run |
gh auth login |
| Check | check_run: tests, typechecks, lint and builds in one call with real exit codes |
None |
| Markdown | markdown_outline and markdown_read_sections for large Markdown files |
None |
Agents use the gh and aws CLIs directly: they cover every operation, cost no tool definitions in the context window, and authenticate outside the agent. The GitHub module's one tool only replaces the polling loop of a CI wait, and the AWS Session module only prepares the CLI's session configuration.
The opencode plugin dispatches local agentic runs on the host - a thin wrapper around the CLIs you already use interactively, with no control plane, Docker, or environments. The model id on oc_run picks the runner: provider-prefixed ids (openai/gpt-5.5, openai/gpt-5.6-sol) spawn a session runner, bare claude ids or aliases (claude-fable-5-1, haiku) spawn claude -p. Any other un-prefixed model id is rejected - nothing routes to a default runner silently. Every opencode dispatch names the agent profile it runs as (agent: implementer, reviewer, pr-reviewer, researcher or orchestrator, defined in opencode.json, each starting from * deny), and a run without one is rejected; a claude run does not apply a profile yet.
The session runner (node dist/modules/opencode/session-runner.js <spec.json>, in src/modules/opencode/) connects to the shared OpenCode background service (the one the TUI uses; opencode service start starts it when none is running), checks that the configured agents include the requested one, creates or resumes the session, sends the prompt (session.prompt) or the slash command (session.command), fails a run whose command switched it to the agent the command pins, and writes OpenCode 2's own session events to the run log between a runner.started and a final runner.finished line. A dispatched session can therefore be followed live in the TUI with opencode -s <sessionId>; restarting or upgrading the service (opencode service restart) ends every in-flight run. A permission request inside a dispatched run is approved for that one call, as opencode run --auto does, so only deny rules in the static config stop a worker; a question is cancelled with feedback, since no one is there to answer it. oc_stop_run interrupts the session through the service's API and leaves the service running. Reasoning effort is a #variant suffix on the model (openai/gpt-5.6-sol#xhigh); the variant argument of oc_run is translated to it, and an unknown variant fails the run. An OpenCode 2 usage-limit error carries no x-codex-* headers, so a run knows only when the limit resets, not which window it was or how much is used. Dispatch modes (sync vs async), the oc_* tool surface, model selection, and the consultation patterns are documented in the workflow-agentic skill - this section covers only the operator-facing configuration. Long multi-agent work runs through the workflow engine, which dispatches its workers the same way.
Sessions are runtime-bound. Opencode sessions (ses_*) persist in OpenCode's local SQLite store (~/.local/share/opencode/opencode.db, tables session_v2 and session_message), the same place interactive sessions live, and are readable via oc_get_session / oc_list_sessions / oc_search_sessions. Claude sessions (UUID ids) are JSONL transcripts under ~/.claude/projects/ - the session tools reject them with an explicit error, and continuing one requires oc_run with a claude model (and the same cwd as the original run). Claude child processes inherit the user's global ~/.claude/settings.json permissions.
The binaries, the database path and the run log and registry locations each come from an environment variable; the OpenCode module README lists them with their defaults.
The commands:
/adversarial-consult- consult two adversarial LLMs for complex architectural decisions/pair-program- spawn a persistent thinking-partner session via a local opencode run and keep consulting it in the same thread/security-scan- run a security review of the current codebase with a structured 15-category vulnerability report
The skills:
develop-code-quality- language-neutral code-quality rules loaded with every code change and review: naming by intent, indirection, safe direct assignment, dead code and unread state, single source of truth, tie-breaks, commentsdevelop-code-spacing- vertical spacing conventions and a blank-line-only procedure for spacing passes across a repositorydevelop-debug- language-agnostic debugging playbookdevelop-opencode-plugins- OpenCode 2 plugin development: the module layout, the tool contract, credentials, testing, live loadingdevelop-refactoring- running refactor, cleanup and consistency work: evidence that behavior did not change, owner rulings, multi-module refactor runs with review and ordered integration, consistency surveys, cleanup playbooks, reportingdocs-opencode- OpenCode 2.0 platform reference: configuration, permissions, agents, providers, plugins, CLIlanguage-typescript- TypeScript conventions, with a monorepo addendumworkflow-agentic- dispatching local opencode and claude runs throughoc_*: sync vs async, model selection, session continuity, thinking partners, agent profiles, long-running worker resilienceworkflow-git-cli- Git andghworkflow: branches, commits, PRs, CI waits, review threadsworkflow-git-worktree- git worktrees for parallel agents editing the same repoworkflow-human-like-writing- human-voice and anti-AI-pattern conventions for proseworkflow-retrospective- find tooling friction after a session and propose self-healing improvementsworkflow-scripting- thewfworkflow engine: writing, starting, waiting for, resuming and reading Workflow-shaped scriptswrite-command,write-skill,write-onboarding-command,write-handoff-command- authoring conventions for commands, skills, onboarding and handoff commandswrite-documentation- documentation conventions: accuracy against source, content principles, edit restraintwrite-mermaid-diagram- Mermaid authoring and rendering mechanics
Each enabled agent in opencode.json starts from * deny and allows whole tools, never a command, a path or a skill. The profiles keep the plugin tools a job does not need out of the agent's context; they do not sandbox it, so the read-only profiles are read-only by their brief:
orchestrator- the default agent, with the full surface of this repo's toolsconductor- writes and runs workflow scripts; the only agent with thewf_*toolsimplementer,reviewer,pr-reviewer,researcher- the profiles an OpenCode (GPT) worker fromoc_runor the workflow engine runs as (oc_runalso takesorchestrator). A Claude worker applies no profile: it runs with the user's default Claude Code permissions, so its prompt must state its limits
Agent profiles in Claude Code. A default Claude Code session runs no profile: it has every tool the bridge serves, gated only by the mcp__opencode__* rules in settings. src/agents/ holds one Claude Code agent file per enabled profile, generated from opencode.json by pnpm run agents:generate and kept in step with it by a spec. The installer copies them to ~/.claude/agents/, and claude --agent <name> starts a session with that profile's tool list. A file narrows the tool list and nothing else: its body is empty so Claude Code's own system prompt stays. A command pins an agent in Claude Code with context: fork plus agent: in its frontmatter, which runs it as a subagent of that profile; a command that only carries agent: runs in the current session, so start claude --agent orchestrator to match the tool set a command expects.
OpenCode's built-in build, plan and general agents are disabled and have no permission list.
opencode.json grants external_directory access only through an ask rule. Add allow rules for the directories you work in (for example your projects folder and OpenCode's tool-output folder), or every access outside the working tree asks.
The modules in this repo store no credentials. AWS Session reads .opencode/config/aws-sso.json (account ids and a permission set per environment, no secrets) and leaves the login to the AWS CLI's SSO flow, which a human approves. oc_run and the workflow engine use the logins of the CLIs they wrap (opencode's provider login, claude), github_ci_wait uses gh auth login, and the rest take none. The plugin framework in src/modules/_core/ supports the /connect integration pattern (INTEGRATION_NAMES in credential-store.service.ts holds a placeholder entry) for modules you add that need an API key, and the develop-opencode-plugins skill describes it.
Permissions. Neither harness asks at run time. OpenCode 2 gives plugin tools no permission-prompt API, so access is gated only by static config: the OpenCode agents' permissions rules in opencode.json, and the mcp__opencode__* rules in Claude Code's settings.
src/mcp/server.ts starts a stdio MCP server. At startup it calls each module's factory (create<Name>Module) with the project directory, which is the server's working directory, converts each tool's zod 4 input schema to JSON Schema with z.toJSONSchema, and registers every tool over MCP: arguments parsed with the tool's schema in, markdown text out, image attachments as MCP image content, the abort signal wired through to the tool context. Tool progress is forwarded as MCP progress notifications, which keeps a long-running tool such as oc_wait_runs alive past Claude Code's idle timeout.
The bridge does not imitate an OpenCode host: there is no shim client and no permission prompt. Claude Code's own permission layer (mcp__opencode__* rules) is the single gate in front of every tool.
The bridge loads the modules flagged isInClaudeCode in the MODULES map in src/modules/registry.ts: AWS Session, Check, Codebase, GitHub, Markdown, OpenCode and Workflow. The others (Local LLM, Structured Output) exist only as OpenCode plugins, as does the hooks-only compaction module.
The server spawns per-session over stdio (no persistent background process). Verify the installation with:
claude mcp get opencode # Should show: Status ✔ Connected
ls ~/.claude/skills # Should show skill directories
ls ~/.claude/commands # Should show command .md filesThe toolchain: TypeScript 7 for compilation and typechecking, oxlint with type-aware rules (via oxlint-tsgolint), and oxfmt for formatting. Dependencies are pinned to exact versions and managed with pnpm.
pnpm run build # compile src/ to dist/
pnpm run typecheck # type-check without emitting
pnpm run lint # oxlint (.oxlintrc.json, type-aware)
pnpm run format # oxfmt write mode (.oxfmtrc.json)
pnpm run format:check # oxfmt check modelint/naming.plugin.mjs is a custom oxlint plugin enforcing I-prefixed interfaces, T-prefixed type parameters, camelCase private members, and intent-bearing boolean prefixes (is/has/can/...).
One note on the dual TypeScript dependency: TypeScript 7 (the native compiler) no longer ships the programmatic compiler API, so the codebase plugin's LSP service imports it from typescript-api, an alias for typescript@5.9. The typescript devDependency (7.x) provides tsc for building this repo; typescript-api is the runtime library the tools analyze other projects with.
Tools don't appear in Claude Code
- Restart Claude Code entirely (don't just switch sessions). The MCP server is loaded at session start.
- Verify
claude mcp get opencodeshows status ✔ Connected.
Skills/commands not updating after source change
- Edits to the installed copies at
~/.claude/or~/.config/opencode/are lost on the next symlink run. - Edit the sources in
src/skills/orsrc/commands/, then rerunpnpm run symlink.
My ~/.claude/CLAUDE.md was replaced
- The Claude Code installer backs up a CLAUDE.md it does not manage to
~/.claude/CLAUDE.md.backup.<timestamp>before installing its own. Merge anything you want to keep into this repo'sAGENTS.mdand rerun.