Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Verify at `http://localhost:3456/plugins` — you should see `opencode-scrub` li
| OpenCode prompt + user CLAUDE.md additions | identity stripped, all user content preserved |
| OpenCode environment preamble + `<env>` | duplicate preamble and redundant fields removed; bare `Working directory` retained for Meridian's cwd extraction |

The plugin is scoped to `adapters: ["opencode"]`, so it has no effect on requests from pi, Crush, Droid, ForgeCode, or the passthrough adapter.
The plugin runs for the `opencode` adapter and for `passthrough` requests that still carry an OpenCode-specific identity or runtime marker. This covers OpenCode routed through LiteLLM when its client headers are removed. Other passthrough prompts, including genuine Claude Code prompts with an `<env>` block, remain byte-for-byte unchanged.

## Rules

Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "bun:test"
import plugin from "../index.js"
import type { RequestContext } from "../types.js"

const opencodePrompt = `You are powered by the model named claude-haiku-4-5. The exact model ID is anthropic/claude-haiku-4-5
Here is some useful information about the environment you are running in:
<env>
Working directory: /client/project
Platform: darwin
</env>`

describe("opencode-scrub adapter boundary", () => {
it("includes LiteLLM's passthrough classification and scrubs OpenCode content", () => {
expect(plugin.adapters).toContain("passthrough")
const ctx: RequestContext = { adapter: "passthrough", systemContext: opencodePrompt, metadata: {} }
const result = plugin.onRequest?.(ctx)
expect(result?.systemContext).not.toContain("You are powered by the model named")
expect(result?.systemContext).not.toContain("Here is some useful information")
expect(result?.systemContext).toContain("Working directory: /client/project")
})

it("preserves genuine Claude Code passthrough prompts byte-for-byte", () => {
const systemContext = "You are Claude Code.\n\n\nHere is some useful information about the environment you are running in:\n<env>\n Working directory: /client/project\n</env>\n\n"
const ctx: RequestContext = { adapter: "passthrough", systemContext, metadata: {} }
expect(plugin.onRequest?.(ctx)).toBe(ctx)
expect(ctx.systemContext).toBe(systemContext)
})

it("preserves incidental OpenCode mentions in other passthrough prompts", () => {
const systemContext = "Compare OpenCode and other editors.\n\n\nKeep this formatting.\n"
const ctx: RequestContext = { adapter: "passthrough", systemContext, metadata: {} }
expect(plugin.onRequest?.(ctx)).toBe(ctx)
})

it("keeps the existing direct OpenCode adapter behavior", () => {
const ctx: RequestContext = { adapter: "opencode", systemContext: opencodePrompt, metadata: {} }
const result = plugin.onRequest?.(ctx)
expect(result?.systemContext).not.toContain("Here is some useful information")
expect(result?.systemContext).toContain("Working directory: /client/project")
})
})
14 changes: 11 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
/**
* Meridian plugin: strip opencode-identifying fingerprints from the system prompt.
*
* Scoped to the `opencode` adapter. Runs on every opencode request — the
* scrub is idempotent, so fork/subagent replays produce identical output.
* Runs on the `opencode` adapter and on `passthrough` requests carrying an
* OpenCode-specific marker. LiteLLM can remove the client headers that let
* Meridian identify OpenCode, so its requests arrive as `passthrough`.
*/

import type { Transform, RequestContext } from "./types.js"
import { scrubOpencodeFingerprints } from "./scrub.js"

export type { Transform, RequestContext } from "./types.js"

// The duplicate <env> preamble is shared with genuine Claude Code traffic.
// Require an independent OpenCode/OMO marker before touching passthrough
// requests, so unrelated clients keep their original prompt bytes.
const PASSTHROUGH_OPENCODE_MARKER =
/You are OpenCode, the best coding agent on the planet\.|You are powered by the model named |<omo-env>|<agent-identity>|You are (?:"|\*\*)Sisyphus(?:"|\*\*)[^\n]*OhMyOpenCode/

const plugin: Transform = {
name: "opencode-scrub",
version: "0.1.0",
description: "Strip opencode-identifying fingerprints from the system prompt before it reaches Claude",
adapters: ["opencode"],
adapters: ["opencode", "passthrough"],

onRequest(ctx: RequestContext): RequestContext {
if (!ctx.systemContext) return ctx
if (ctx.adapter === "passthrough" && !PASSTHROUGH_OPENCODE_MARKER.test(ctx.systemContext)) return ctx
const scrubbed = scrubOpencodeFingerprints(ctx.systemContext)
if (scrubbed === ctx.systemContext) return ctx
return { ...ctx, systemContext: scrubbed }
Expand Down
Loading