@koriit/opencode-claude-bridge
v0.3.0
Published
An OpenCode plugin that bridges enabled Claude Code plugins (commands, agents, skills) into OpenCode at runtime, namespaced so they never shadow your existing items.
Maintainers
Readme
opencode-claude-bridge
Use your Claude Code plugins inside OpenCode — no porting, no copying, no second install.
If you already manage plugins with claude plugin install, this bridge makes their commands,
agents, and skills available in OpenCode too. It reads Claude's enabled-plugin state at OpenCode
launch and injects the components live, namespaced so they never shadow anything you already have.
It is a single plugin — no wrapper binary, no generated files, no lockfile. You run plain
opencode; the bridge reads Claude's own settings to determine which plugins are enabled, resolves
each one's on-disk location, and injects the components on each launch.
Example. You install a plugin in Claude:
claude plugin install code-tools@acmeStart OpenCode in that project, and the plugin's audit command, reviewer agent, and SKILL.md
skills are already there — /audit, the reviewer subagent, and so on. Nothing else to do. If a
name is already taken by one of your own items, the bridge's copy is renamed (e.g.
/code-tools-audit) — your item is never touched.
Status: early development. Commands, agents, and skills from enabled Claude plugins are supported.
MCP and LSP servers are not supported. Two problems make a straight bridge unreliable:
- OAuth client mismatch. A plugin's
.mcp.jsonOAuth registration is tied to Claude's OAuth client — those credentials don't carry over to OpenCode, which authenticates as its own client. Copying the config across does not produce a working authenticated server.- Claude-specific porting. MCP/LSP servers ship with Claude-specific setup and porting instructions that don't translate cleanly to OpenCode, so injecting the raw
.mcp.json/.lsp.jsonis not enough to make them work.The feature has been removed for now. Track this in the project issues if you need it.
Contents
- Requirements
- Install
- Updating
- Configuration
- How it works (internals)
- Diagnostics
- Development
- Releasing (maintainer)
- License
Requirements
- OpenCode
>=1.15.0 <1.16.0(see Why the version is pinned). - The
claudeCLI on yourPATHat OpenCode runtime. The bridge callsclaude plugin marketplace list --jsonto resolve where each marketplace is installed on disk. Ifclaudeis missing the bridge logs a warning and falls back to whatever it can resolve from your settings files alone; OpenCode still starts normally.
Windows is best-effort only. The bridge is developed and tested on Linux/macOS. Core features (commands, agents, skills) should work, but path handling and
${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_DATA}resolution have not been validated on Windows.
Install
Add the plugin to your global ~/.config/opencode/opencode.json so it applies across all
projects. The bare-string form uses all defaults:
{
"plugin": ["@koriit/opencode-claude-bridge"]
}The tuple form lets you set options (all shown here at their defaults):
{
"plugin": [
[
"@koriit/opencode-claude-bridge",
{
"blockedPlugins": []
}
]
]
}That's the whole install. OpenCode fetches the package with ignoreScripts: true, so no build step
runs — the package entry points at src/index.ts on purpose, and OpenCode's Bun runtime imports the
TypeScript directly. The bridge has zero runtime dependencies (every @opencode-ai/plugin import is
import type, erased at runtime).
Updating
OpenCode resolves the bare @koriit/opencode-claude-bridge spec to @latest and then caches the
resolved package, so a new release on npm is not picked up automatically. To force an update, clear
the bridge's entry from OpenCode's package cache and restart:
rm -rf ~/.cache/opencode/packages/@koriit/opencode-claude-bridge@latestOn the next launch OpenCode re-fetches the latest published version. (If you pinned a specific
version in your config — e.g. @koriit/[email protected] — bump that version string
instead; the cache key includes the version.)
The path above is the per-package cache directory OpenCode creates for npm plugins (
~/.cache/opencode/packages/<sanitized-spec>/). Removing it is safe — it is regenerated on the next start.
Configuration
All keys are optional. Unknown keys and ill-typed values are ignored with a warning.
| Key | Type | Default | Meaning |
| ---------------- | ---------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| blockedPlugins | string[] | [] | Plugin ids (name@marketplace) to never inject — any component type. |
| strict | boolean | false | Promote fatal warnings (e.g. parse failures) to hard errors. A missing or failing claude CLI is non-fatal — the bridge resolves from settings files alone. |
| mode | string | mirror-claude | The only accepted mode: mirror exactly Claude's enabled set. |
What gets bridged
The bridge runs in mirror-claude mode (the only mode). It determines the enabled set exactly the
way Claude Code does — by merging the enabledPlugins maps from Claude's settings layers, in
precedence order (later layers override earlier ones):
~/.claude/settings.json(global /userscope)<project>/.claude/settings.json(project scope)<project>/.claude/settings.local.json(project scope)
A plugin is bridged when its final merged value is true. This honors both ways of enabling a
plugin in Claude: via the claude plugin CLI and by hand-editing enabledPlugins in a
settings.json. (Relying on claude plugin list --json alone would miss the hand-edited case — it
only reports plugins installed through the CLI.)
- A later
enabledPluginslayer settingfalsedisables a plugin an earlier layer enabled. - ids listed in
blockedPluginsare never bridged.
Once a plugin is enabled, its on-disk location is resolved from the marketplace manifest
(claude plugin marketplace list --json → <installLocation>/.claude-plugin/marketplace.json),
falling back to the claude plugin list --json entry's installPath for sources the manifest cannot
resolve directly (e.g. git-subdir plugins). Plugins whose location cannot be resolved by either method
are skipped with a log line.
What gets injected
| Component | Default | Source & mapping |
| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Commands | On | commands/**/*.md → cfg.command; $ARGUMENTS / $1..n pass through. |
| Agents | On | agents/*.md → cfg.agent; uses the prompt field; mode defaults to subagent (also primary/all); temperature, top_p, steps, hidden, color, variant pass through (sanitized). |
| Skills | On | skills/<name>/SKILL.md dirs → cfg.skills.paths and cfg.command by default (see dual routing below); user-invocable: false → skill only; disable-model-invocation: true → command only; both set → skipped. Zero files copied in the common case. |
Commands, agents, and skills are plain text prompts and are always bridged.
MCP and LSP servers are not bridged. See the status note above — Claude's MCP OAuth client doesn't carry over to OpenCode, and the servers need Claude-specific porting that doesn't translate, so the feature was removed.
How it works (internals)
This section is reference material for maintainers and the curious — you don't need it to use the bridge. It documents the naming rules, the skills cache, variable substitution, and the OpenCode-internal behavior the bridge depends on.
No-shadowing & naming
Injected items are namespaced so they never shadow your existing OpenCode commands, agents, or skills (including OpenCode's built-ins). On a name collision the bridge's item is renamed — the native/existing item is never touched. The rename ladder, tried in order until a free slot is found:
<name>— the bare name (e.g.audit)<plugin>-<name>(e.g.code-tools-audit)<marketplace>-<plugin>-<name>if still colliding<marketplace>-<plugin>-<name>-<8hex>— deterministic SHA-256 tiebreak
<plugin> and <marketplace> are the two halves of the plugin id name@marketplace. Processing
order is sorted by plugin id, so the first claimant of a bare name wins deterministically across
runs. Every injected item's description is suffixed with [plugin-id] for traceability.
Skills: no-copy in the common case, bridge cache on collision
Each skill in a plugin's skills/<name>/ directory is discovered by reading its SKILL.md
frontmatter name. In the common case — no collision — the plugin's own skill directory is pushed
directly onto cfg.skills.paths: zero files are copied.
On collision (the bare name is already taken by a native OpenCode skill, a built-in, or an earlier-processed plugin), the entire skill directory is copied to the bridge cache:
~/.cache/opencode-claude-bridge/skills/<marketplace>/<plugin>/<version>/<allocatedName>/The copy's SKILL.md frontmatter name is patched to the prefixed name; all other files (assets,
sub-directories) are preserved so relative references keep working. .git and other dot-directories
are excluded. Copies are keyed by <marketplace>/<plugin>/<version>/<allocatedName> and regenerated
when the source is newer; when the plugin version changes, old version directories are pruned (GC).
This cache is distinct from OpenCode's own ~/.cache/opencode/skills.
Override the cache location with the OPENCODE_CLAUDE_BRIDGE_CACHE_ROOT environment variable (set
before starting OpenCode) — useful in CI or test environments.
URL-sourced skills: if your
opencode.jsonlists entries incfg.skills.urls, the bridge cannot detect collisions against them at hook time (fetching URL skills would force the lazy Skill service to load before our injected paths). A warning is logged when URLs are present.
Variable substitution
The bridge resolves these variables in injected content before OpenCode sees it.
| Variable | Resolves to | Available in |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| ${CLAUDE_PLUGIN_ROOT} | Plugin resolved on-disk install directory — resolved from the marketplace manifest (or the claude plugin list --json installPath fallback) | Commands, agents, skills (body + SKILL.md) |
| ${CLAUDE_PLUGIN_DATA} | Plugin persistent data dir — ~/.claude/plugins/data/<sanitized-id> | Commands, agents, skills (body + SKILL.md) |
| ${CLAUDE_SKILL_DIR} | Skill source directory (dirname of SKILL.md) | Plugin skills only (body + SKILL.md) |
| ${CLAUDE_SESSION_ID} | The literal <use Session ID from context> (see note) | Commands, agents, skills (body + SKILL.md) |
<sanitized-id> is the plugin id with all characters outside [a-zA-Z0-9_-] replaced by -
(e.g. my-plugin@acme → my-plugin-acme).
${CLAUDE_SESSION_ID}limitation. The bridge runs in theconfighook, which produces a single config object shared by all sessions in the same directory. Baking a concrete session ID into content there would leak one session's ID into every other session that reuses the config. So${CLAUDE_SESSION_ID}is replaced with the literal<use Session ID from context>, which instructs the model to read the real ID from the system prompt. The bridge injects aSession ID: <id>line into every message's system prompt via the per-sessionexperimental.chat.system.transformhook, making the current ID available whenever the model needs it.
Security model
- Commands, agents, and skills are text prompts (lower risk) and are always bridged — but always namespaced so they can never shadow your own items.
- MCP and LSP servers (which would spawn processes / open connections) are not bridged — see the status note.
blockedPluginshard-excludes plugin ids from all bridge injection (commands, agents, skills). It governs what the bridge injects; it cannot suppress commands that OpenCode's own native Claude-plugin integration may load independently of the bridge.- Disabled plugins are always skipped.
Why the version is pinned
>=1.15.0 <1.16.0Verified against OpenCode 1.15.13. The bridge relies on OpenCode-internal behavior that is not a
documented public contract (config hook shape, cfg.skills object layout, skill discovery paths), so
it pins a conservative same-minor window.
This range is documentation only — the bridge does NOT read the running OpenCode version and does NOT warn at runtime. It relies on OpenCode-internal behavior verified against the version above and works across versions until something actually breaks. The range is widened only after the end-to-end suite passes against a new version.
Schema-safety invariant
Every field copied from a Claude component into OpenCode's config is schema-validated or sanitized
before write — never raw passthrough. This is not cosmetic: OpenCode validates the merged config
when the TUI issues config.get at startup, outside the bridge's config hook try/catch. A
Claude field that violates OpenCode's schema would therefore not be caught by the bridge's non-throw
guard — it would crash the whole instance later. Injecting nothing is always safer than injecting an
invalid value. (Known cases handled: agent color name → hex mapping, finite temperature/top_p.)
Diagnostics
Bridge log lines are written through OpenCode's own logging endpoint (client.app.log) under the
opencode-claude-bridge service, so they land in OpenCode's server logs alongside everything
else and honor OpenCode's log configuration. Logging is fire-and-forget — a logging failure can
never disrupt injection.
The bridge surfaces no in-TUI notifications. Many of its warnings reflect issues in the plugin
author's definitions and aren't actionable by you, so a per-session toast would be noise. To stream
the bridge's service=opencode-claude-bridge lines to stderr, run with --print-logs:
opencode --print-logsWithout --print-logs the entries still go to OpenCode's server log file; the flag just mirrors them
to stderr. Check there whenever a component you expect is missing or misbehaving.
Development
A Bun + TypeScript package.
bun install # install dev dependencies
bun run typecheck # type-check src + tests
bun run build # emit dist/
bun run test # unit tests (sets OCB_TMPDIR=.tmp)
bun run test:e2e # end-to-end tests (sets OCB_TMPDIR=.tmp; launches real opencode)
bun run test:all # unit + e2e
bun run test:coverage # unit tests with line/function coverage reportAlways use
bun run test(the npm script), notbun test test/directly. The scripts setOCB_TMPDIR=.tmpto keep test scratch off a potentially small system/tmp.
The end-to-end suite launches a real opencode serve with the plugin loaded and a fake claude CLI
on PATH, then asserts behavior against the live HTTP API. It also acts as the version-compatibility
canary: run it after every OpenCode upgrade before widening the supported range.
Releasing (maintainer)
- Bump
versioninpackage.json, commit. - Create a GitHub Release with tag
v<version>(e.g.v0.1.1). - The publish workflow runs the test gates and publishes to npm
automatically (requires the
NPM_TOKENrepo secret — see the workflow file header for setup).
License
MIT
