Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ Verify at `http://localhost:3456/plugins` — you should see `opencode-scrub` li
| OhMyOpenCode/Sisyphus prompt | OMO identity line removed, `<omo-env>` block removed, "You are powered by..." line removed; persona rules preserved |
| 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 |
| Request for a non-Claude model (e.g. `gpt-5.3-codex`) | unchanged |

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.

Only requests that target a Claude model are scrubbed: Meridian's resolved aliases (`opus`, `sonnet`, `haiku`, `fable`, optionally with `[1m]`) and any model id containing `claude`. When Meridian serves another provider, such as OpenAI GPT/Codex models, the OpenCode prompt reaches that provider exactly as the client sent it.

## Rules

The scrub applies 8 independent regex replacements, each idempotent and each a no-op when its pattern is absent:
Expand Down
21 changes: 21 additions & 0 deletions src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,24 @@ describe("opencode-scrub adapter boundary", () => {
expect(result?.systemContext).toContain("Working directory: /client/project")
})
})

describe("opencode-scrub model boundary", () => {
const scrubbedWithoutModel = plugin.onRequest?.({ adapter: "opencode", systemContext: opencodePrompt, metadata: {} })

for (const model of ["opus", "opus[1m]", "sonnet", "sonnet[1m]", "haiku", "fable", "fable[1m]", "claude-opus-5-5", "anthropic/claude-sonnet-5"]) {
it(`scrubs requests for Claude model ${model}`, () => {
const ctx: RequestContext = { adapter: "opencode", model, systemContext: opencodePrompt, metadata: {} }
expect(plugin.onRequest?.(ctx)?.systemContext).toBe(scrubbedWithoutModel?.systemContext)
})
}

for (const adapter of ["opencode", "passthrough"]) {
for (const model of ["gpt-6-luna", "gpt-5.3-codex", "openai/gpt-5.5"]) {
it(`leaves ${adapter} requests for ${model} untouched`, () => {
const ctx: RequestContext = { adapter, model, systemContext: opencodePrompt, metadata: {} }
expect(plugin.onRequest?.(ctx)).toBe(ctx)
expect(ctx.systemContext).toBe(opencodePrompt)
})
}
}
})
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export type { Transform, RequestContext } from "./types.js"
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/

// Meridian resolves Claude requests to an SDK alias (`opus`, `sonnet[1m]`, ...)
// before plugins run; full `claude-*` ids cover other callers. Requests for
// other providers (e.g. OpenAI GPT/Codex models) keep their prompt as sent.
const CLAUDE_MODEL = /claude|^(?:opus|sonnet|haiku|fable)(?:\[1m\])?$/i

const plugin: Transform = {
name: "opencode-scrub",
version: packageMetadata.version,
Expand All @@ -26,6 +31,7 @@ const plugin: Transform = {

onRequest(ctx: RequestContext): RequestContext {
if (!ctx.systemContext) return ctx
if (ctx.model !== undefined && !CLAUDE_MODEL.test(ctx.model)) 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
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
export interface RequestContext {
/** Agent adapter name (readonly from meridian's perspective) */
readonly adapter: string
/** Model the request targets, as resolved by meridian */
model?: string
/** Client-provided system prompt / context string, if any */
systemContext?: string
/** Plugin-to-plugin state bag */
Expand Down