@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.
Downloads
59
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
