From eddcf85a40701b94c6b95cfbe16b7cd644d57350 Mon Sep 17 00:00:00 2001 From: briankeefe Date: Mon, 18 May 2026 11:22:41 -0400 Subject: [PATCH 1/2] feat: add minimal scrub mode for prompt stability --- README.md | 29 +++++++++++------- src/index.ts | 27 +++++++++-------- src/scrub.ts | 84 +++++++++++++++++++++++----------------------------- 3 files changed, 70 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index ce64e66..1b4b0ec 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,25 @@ When Meridian routes OpenCode → Claude Max, those identity sections are: 1. **Redundant** — Claude Code's preset already owns identity, tone, safety, and tool-use guidance. 2. **A detection fingerprint** — they give Anthropic's detection a clear "this isn't Claude Code" signal that can trigger third-party extra-usage errors or rate-limit flags. -This plugin surgically removes those lines and replaces the identity paragraph with a neutral, generic coding-assistant framing. It removes the duplicate environment preamble and redundant fields while keeping a bare `` block with the client `Working directory` line. Tone rules, task management, tool usage policy, code references, and user CLAUDE.md content remain intact. +This plugin surgically removes those lines and replaces the identity paragraph with a neutral, generic coding-assistant framing. Everything else in OpenCode's prompt (tone rules, task management, tool usage policy, code references, the env block, any user CLAUDE.md appended by OpenCode) is preserved verbatim. The scrub also handles [OhMyOpenCode](https://github.com/anomalyco/ohmyopencode)-style custom personas (Sisyphus et al.): the `You are "Sisyphus" ... from OhMyOpenCode.` identity line and the `...` block are stripped, while the persona's orchestration rules (Phase 0 intent gate, explore/librarian delegation, Oracle consultation, tone guidelines) are preserved. The scrub is **idempotent** — running it twice on the same string is a no-op. +## Modes + +The plugin supports two modes via `MERIDIAN_OPENCODE_SCRUB_MODE`: + +- **`aggressive`** (default) — current behavior. Strips duplicate OpenCode env preamble, strips ``, replaces the identity line with a generic one, and normalizes leftover spacing. +- **`minimal`** — removes only the strongest fingerprint lines while preserving prompt structure. Keeps `` and `` blocks intact and does not inject a replacement identity line. + +Example: + +```bash +MERIDIAN_OPENCODE_SCRUB_MODE=minimal meridian +``` + ## Install ### Option 1: npm (recommended) @@ -65,16 +78,15 @@ Verify at `http://localhost:3456/plugins` — you should see `opencode-scrub` li |---|---| | No system prompt | unchanged | | System prompt without OpenCode identity markers | unchanged (idempotent) | -| Vanilla OpenCode (`anthropic.txt`) | identity line swapped for generic, feedback block removed, docs paragraph removed, "OpenCode honestly applies" neutralized | -| OhMyOpenCode/Sisyphus prompt | OMO identity line removed, `` block removed, "You are powered by..." line removed; persona rules preserved | +| Vanilla OpenCode (`anthropic.txt`) | aggressive: identity line swapped for generic; minimal: identity line removed; both remove feedback/docs blocks and neutralize `OpenCode honestly applies` | +| OhMyOpenCode/Sisyphus prompt | aggressive: OMO identity + `` removed; minimal: only OMO identity removed; both remove `You are powered by...`; persona rules preserved | | OpenCode prompt + user CLAUDE.md additions | identity stripped, all user content preserved | -| OpenCode environment preamble + `` | duplicate preamble and redundant fields removed; bare `Working directory` retained for Meridian's cwd extraction | -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 `` block, remain byte-for-byte unchanged. +The plugin is scoped to `adapters: ["opencode"]`, so it has no effect on requests from pi, Crush, Droid, ForgeCode, or the passthrough adapter. ## Rules -The scrub applies 8 independent regex replacements, each idempotent and each a no-op when its pattern is absent: +The aggressive scrub applies 8 independent regex replacements, each idempotent and each a no-op when its pattern is absent. Minimal mode applies only rules 1-5 and 7: 1. **Vanilla identity line** — `You are OpenCode, the best coding agent on the planet.` → generic 2. **Feedback block** — `If the user asks for help or wants to give feedback...github.com/anomalyco/opencode` @@ -85,10 +97,7 @@ The scrub applies 8 independent regex replacements, each idempotent and each a n 7. **Powered-by line** — `You are powered by the model named ...` 8. **Residual brand tokens** — bare `OpenCode` → `the assistant` -When `scrubOpencodeFingerprints` is called inside an OpenCode client hook, the -bare working-directory line keeps the client's project path available to -Meridian. Server-side use remains compatible: Meridian extracts the raw path -before applying its plugin transform. +`aggressive` also strips OpenCode's duplicate `` preamble block; `minimal` preserves it. ## Development diff --git a/src/index.ts b/src/index.ts index b4d5623..86dcfc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,34 @@ /** * Meridian plugin: strip opencode-identifying fingerprints from the system prompt. * - * 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`. + * Scoped to the `opencode` adapter. Runs on every opencode request — the + * scrub is idempotent, so fork/subagent replays produce identical output. */ import type { Transform, RequestContext } from "./types.js" -import packageMetadata from "../package.json" with { type: "json" } import { scrubOpencodeFingerprints } from "./scrub.js" export type { Transform, RequestContext } from "./types.js" -// The duplicate 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 |||You are (?:"|\*\*)Sisyphus(?:"|\*\*)[^\n]*OhMyOpenCode/ +function getScrubMode(): "aggressive" | "minimal" { + const env = (globalThis as typeof globalThis & { + process?: { env?: Record } + }).process?.env + + return env?.MERIDIAN_OPENCODE_SCRUB_MODE === "minimal" + ? "minimal" + : "aggressive" +} const plugin: Transform = { name: "opencode-scrub", - version: packageMetadata.version, + version: "0.1.0", description: "Strip opencode-identifying fingerprints from the system prompt before it reaches Claude", - adapters: ["opencode", "passthrough"], + adapters: ["opencode"], 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) + const scrubbed = scrubOpencodeFingerprints(ctx.systemContext, getScrubMode()) if (scrubbed === ctx.systemContext) return ctx return { ...ctx, systemContext: scrubbed } }, diff --git a/src/scrub.ts b/src/scrub.ts index d69fd7b..7799f54 100644 --- a/src/scrub.ts +++ b/src/scrub.ts @@ -1,34 +1,41 @@ /** * Scrub opencode-identifying fingerprints from a system prompt. * - * Targets both vanilla OpenCode (from anomalyco/opencode's built-in - * `anthropic.txt`) and OhMyOpenCode-style custom personas (Sisyphus, etc.). - * Each rule is an independent, idempotent regex — if a pattern isn't present - * the rule no-ops, so the same plugin handles both variants without - * configuration. + * Targets both vanilla OpenCode (from sst/opencode's built-in `anthropic.txt`) + * and OhMyOpenCode-style custom personas (Sisyphus, etc.). Each rule is an + * independent, idempotent regex — if a pattern isn't present the rule no-ops, + * so the same plugin handles both variants without configuration. * * Detection vectors scrubbed: * - "OpenCode" / "opencode" brand tokens in the opening identity line and * embedded prose * - The feedback block pointing to github.com/anomalyco/opencode * - The "When the user directly asks about OpenCode" docs paragraph - * - OhMyOpenCode's Sisyphus identity line (quoted or bold form, all - * occurrences) and the OMO 4.x wrapper block - * - Residual "OhMyOpenCode" brand tokens + * - OhMyOpenCode's "Sisyphus ... from OhMyOpenCode" identity line * - The runtime block * - The self-outing "You are powered by the model named ..." line that * opencode's environment() builder appends (strongest third-party tell — * Claude Code never emits this phrasing) * - The duplicate "Here is some useful information about the environment - * you are running in:" preamble and redundant fields. Keep a bare - * Working directory line so client-side users do not erase the only path - * Meridian can read before it chooses the SDK working directory. + * you are running in:" preamble + block. Claude Code's preset + * already injects this; opencode appending its own copy makes the + * preamble appear twice in the final system prompt, which Anthropic's + * billing layer treats as a third-party-impersonation signal and gates + * opus behind Extra Usage (sonnet/haiku unaffected). * * Preserved: all tool policy, tone rules, task management guidance, code * references section, Sisyphus orchestration rules (Phase 0, explore/ * librarian, Oracle), and any user CLAUDE.md content appended by opencode. + * + * Modes: + * - aggressive (default): maximum fingerprint removal, including env blocks + * and minor prompt cleanup + * - minimal: only remove the strongest identity/fingerprint lines while + * preserving prompt structure and orchestration/env blocks */ +export type ScrubMode = "aggressive" | "minimal" + /** Vanilla L1 identity line from anthropic.txt */ const OPENCODE_IDENTITY_LINE = /You are OpenCode, the best coding agent on the planet\.[^\n]*\n+/ @@ -48,27 +55,9 @@ const OPENCODE_OBJECTIVITY_BRAND = /** Any residual bare "OpenCode"/"opencode" tokens in preserved prose */ const OPENCODE_BRAND_TOKEN = /\bOpenCode\b/g -/** - * OhMyOpenCode Sisyphus identity line. OMO 4.x variants differ per model - * route: the default persona quotes the name (`You are "Sisyphus" ...`) while - * Claude-routed prompts bold it (`You are **Sisyphus** ...`), and some - * variants put "OhMyOpenCode" mid-sentence rather than sentence-final. - * Global, because the line now appears both inside and in - * . - */ +/** OhMyOpenCode Sisyphus identity line */ const OMO_IDENTITY_LINE = - /You are ("|\*\*)Sisyphus("|\*\*)[^\n]*OhMyOpenCode[^\n]*\n+/g - -/** - * OMO 4.x wrapper block ("Your designated identity for this - * session ... always identify as Sisyphus ..."), injected by - * buildAgentIdentitySection ahead of every Sisyphus variant. Strongest OMO - * fingerprint — removed wholesale like . - */ -const OMO_AGENT_IDENTITY_BLOCK = /[\s\S]*?<\/agent-identity>\n*/g - -/** Residual bare "OhMyOpenCode" tokens in preserved prose */ -const OMO_BRAND_TOKEN = /\bOhMyOpenCode\b/g + /You are "Sisyphus"[^\n]*from OhMyOpenCode\.[^\n]*\n+/ /** The ... block */ const OMO_ENV_BLOCK = /[\s\S]*?<\/omo-env>\n*/ @@ -86,17 +75,10 @@ const POWERED_BY_LINE = * own copy on top of the preset, the preamble appears twice in the final * system prompt and Anthropic gates opus behind Extra Usage. Bisected * 2026-04-21: removing this block (or just the preamble line) makes opus - * succeed; sonnet/haiku unaffected. Retain only a bare cwd field without the - * duplicate preamble: Meridian reads this field from the incoming prompt when - * the scrub function is used client-side, before Meridian receives the body. + * succeed; sonnet/haiku unaffected. */ const OPENCODE_ENV_BLOCK = - /\n?Here is some useful information about the environment you are running in:\n[\s\S]*?<\/env>\n?/ - -function keepClientCwd(block: string): string { - const cwd = block.match(/(?:^|\n)[ \t]*Working directory:[ \t]*([^\n<]+)/i)?.[1]?.trim() - return cwd ? `\n\n Working directory: ${cwd}\n\n` : "\n" -} + /\nHere is some useful information about the environment you are running in:\n[\s\S]*?<\/env>\n/ const GENERIC_IDENTITY = "You are an expert coding assistant. You help users with software engineering tasks by reading files, executing commands, editing code, and writing new files.\n" @@ -104,20 +86,28 @@ const GENERIC_IDENTITY = const GENERIC_OBJECTIVITY = "It is best for the user if the assistant honestly applies" -export function scrubOpencodeFingerprints(systemPrompt: string): string { +export function scrubOpencodeFingerprints(systemPrompt: string, mode: ScrubMode = "aggressive"): string { if (!systemPrompt) return systemPrompt - return systemPrompt - .replace(OPENCODE_IDENTITY_LINE, GENERIC_IDENTITY) + + const scrubbedIdentity = mode === "minimal" + ? systemPrompt.replace(OPENCODE_IDENTITY_LINE, "") + : systemPrompt.replace(OPENCODE_IDENTITY_LINE, GENERIC_IDENTITY) + + const scrubbedCore = scrubbedIdentity .replace(OPENCODE_FEEDBACK_BLOCK, "") .replace(OPENCODE_DOCS_PARAGRAPH, "") .replace(OPENCODE_OBJECTIVITY_BRAND, GENERIC_OBJECTIVITY) - .replace(OMO_AGENT_IDENTITY_BLOCK, "") .replace(OMO_IDENTITY_LINE, "") - .replace(OMO_ENV_BLOCK, "") .replace(POWERED_BY_LINE, "") - .replace(OPENCODE_ENV_BLOCK, keepClientCwd) + + if (mode === "minimal") { + return scrubbedCore.replace(/\s+$/, "") + } + + return scrubbedCore + .replace(OMO_ENV_BLOCK, "") + .replace(OPENCODE_ENV_BLOCK, "\n") .replace(OPENCODE_BRAND_TOKEN, "the assistant") - .replace(OMO_BRAND_TOKEN, "the assistant") .replace(/\n{3,}/g, "\n\n") .replace(/\s+$/, "") } From a2c479c08ec643528d229112400ca41d0f4a27b7 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 1 Oct 2026 01:51:08 -0600 Subject: [PATCH 2/2] fix: retain runtime deduplication in minimal scrub mode --- README.md | 42 ++++--- docs/evidence/5-minimal-mode.md | 93 ++++++++++++++++ scripts/e2e-minimal-opencode.mjs | 183 +++++++++++++++++++++++++++++++ src/__tests__/minimal.test.ts | 63 +++++++++++ src/index.ts | 17 ++- src/scrub.ts | 82 ++++++++------ 6 files changed, 430 insertions(+), 50 deletions(-) create mode 100644 docs/evidence/5-minimal-mode.md create mode 100644 scripts/e2e-minimal-opencode.mjs create mode 100644 src/__tests__/minimal.test.ts diff --git a/README.md b/README.md index 1b4b0ec..7dd9a2e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ When Meridian routes OpenCode → Claude Max, those identity sections are: 1. **Redundant** — Claude Code's preset already owns identity, tone, safety, and tool-use guidance. 2. **A detection fingerprint** — they give Anthropic's detection a clear "this isn't Claude Code" signal that can trigger third-party extra-usage errors or rate-limit flags. -This plugin surgically removes those lines and replaces the identity paragraph with a neutral, generic coding-assistant framing. Everything else in OpenCode's prompt (tone rules, task management, tool usage policy, code references, the env block, any user CLAUDE.md appended by OpenCode) is preserved verbatim. +This plugin surgically removes those lines and replaces the identity paragraph with a neutral, generic coding-assistant framing. It removes the duplicate environment preamble and redundant fields while keeping a bare `` block with the client `Working directory` line. Tone rules, task management, tool usage policy, code references, and user CLAUDE.md content remain intact. The scrub also handles [OhMyOpenCode](https://github.com/anomalyco/ohmyopencode)-style custom personas (Sisyphus et al.): the `You are "Sisyphus" ... from OhMyOpenCode.` identity line and the `...` block are stripped, while the persona's orchestration rules (Phase 0 intent gate, explore/librarian delegation, Oracle consultation, tone guidelines) are preserved. @@ -21,17 +21,22 @@ The scrub is **idempotent** — running it twice on the same string is a no-op. ## Modes -The plugin supports two modes via `MERIDIAN_OPENCODE_SCRUB_MODE`: +The default `aggressive` mode retains current scrubbing. Set +`MERIDIAN_OPENCODE_SCRUB_MODE=minimal` for surgical removal without inserting a +generic identity or replacing residual OpenCode/OhMyOpenCode words in preserved +prose. Both modes remove known identity wrappers, feedback/docs fingerprints, +powered-by lines and duplicate runtime environment blocks. Both retain the +client working-directory field, project instructions and tool/persona policies, +and normalize leftover blank lines. Minimal mode still removes the duplicate +environment preamble; preserving it would defeat the metering fix. -- **`aggressive`** (default) — current behavior. Strips duplicate OpenCode env preamble, strips ``, replaces the identity line with a generic one, and normalizes leftover spacing. -- **`minimal`** — removes only the strongest fingerprint lines while preserving prompt structure. Keeps `` and `` blocks intact and does not inject a replacement identity line. - -Example: - -```bash +```sh MERIDIAN_OPENCODE_SCRUB_MODE=minimal meridian ``` +Other or unset values select `aggressive`. Adapter and passthrough marker guards +apply in both modes; genuine Claude Code prompts stay unchanged. + ## Install ### Option 1: npm (recommended) @@ -78,15 +83,16 @@ Verify at `http://localhost:3456/plugins` — you should see `opencode-scrub` li |---|---| | No system prompt | unchanged | | System prompt without OpenCode identity markers | unchanged (idempotent) | -| Vanilla OpenCode (`anthropic.txt`) | aggressive: identity line swapped for generic; minimal: identity line removed; both remove feedback/docs blocks and neutralize `OpenCode honestly applies` | -| OhMyOpenCode/Sisyphus prompt | aggressive: OMO identity + `` removed; minimal: only OMO identity removed; both remove `You are powered by...`; persona rules preserved | +| Vanilla OpenCode (`anthropic.txt`) | identity line swapped for generic, feedback block removed, docs paragraph removed, "OpenCode honestly applies" neutralized | +| OhMyOpenCode/Sisyphus prompt | OMO identity line removed, `` 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 + `` | 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 `` block, remain byte-for-byte unchanged. ## Rules -The aggressive scrub applies 8 independent regex replacements, each idempotent and each a no-op when its pattern is absent. Minimal mode applies only rules 1-5 and 7: +The scrub applies 8 independent regex replacements, each idempotent and each a no-op when its pattern is absent: 1. **Vanilla identity line** — `You are OpenCode, the best coding agent on the planet.` → generic 2. **Feedback block** — `If the user asks for help or wants to give feedback...github.com/anomalyco/opencode` @@ -97,7 +103,10 @@ The aggressive scrub applies 8 independent regex replacements, each idempotent a 7. **Powered-by line** — `You are powered by the model named ...` 8. **Residual brand tokens** — bare `OpenCode` → `the assistant` -`aggressive` also strips OpenCode's duplicate `` preamble block; `minimal` preserves it. +When `scrubOpencodeFingerprints` is called inside an OpenCode client hook, the +bare working-directory line keeps the client's project path available to +Meridian. Server-side use remains compatible: Meridian extracts the raw path +before applying its plugin transform. ## Development @@ -111,3 +120,10 @@ The built plugin is a single ES module at `dist/index.js` with `dist/index.d.ts` ## License MIT + +## Manual live verification + +[Minimal/aggressive headless evidence and reproduction](https://github.com/rynfar/meridian-plugin-opencode-scrub/blob/main/docs/evidence/5-minimal-mode.md) +uses actual OpenCode, an independently installed tarball, Claude tool-result +receipts and same-session continuation. This manual credentialed gate is separate +from the ordinary unit/build CI and does not publish a package. diff --git a/docs/evidence/5-minimal-mode.md b/docs/evidence/5-minimal-mode.md new file mode 100644 index 0000000..e2f6e5d --- /dev/null +++ b/docs/evidence/5-minimal-mode.md @@ -0,0 +1,93 @@ +# OpenCode scrub #5: minimal mode with runtime deduplication + +## Accepted behavior and contributor credit + +Source PR [#5](https://github.com/rynfar/meridian-plugin-opencode-scrub/pull/5) +head `40094cd9adaef32456befa54c1b969d611785395`, by briankeefe +`brian.c.keefe@gmail.com`, was cherry-picked as +`eddcf85a40701b94c6b95cfbe16b7cd644d57350`, preserving Author/AuthorDate. +A separate maintainer correction restores current main's package metadata, +passthrough guard, OMO 4.x fixes and newline handling before adding the mode. +The owner's July 10 review explicitly welcomes this opt-in feature and requires +runtime environment removal to remain enabled. + +Unset/unknown modes keep aggressive behavior. `minimal` removes known identity +and runtime fingerprints, preserves client cwd and project/persona policy, and +avoids a generic replacement identity and residual brand-word rewriting. It +still removes duplicate environment preambles in both modes. No release, +package-version bump or core Meridian API change is part of this delivery. + +## Before/after and meaningful controls + +The actual authored minimal implementation retained the vanilla environment +preamble: `preambleRetained=true, cwdRetained=true, identityGeneric=false, +policyRetained=true`. This reproduces the owner's blocking finding with the +source function, rather than inferring it from a green unit suite. + +Corrected tests cover vanilla environment removal/cwd/project preservation, +OMO fixture identity removal with tool/persona policy intact, idempotence, +request-time mode switching, unknown-value default, genuine Claude passthrough +identity and headerless OpenCode requests. Full suite: 28 passed, zero failed. +Standalone TypeScript and build pass; default outputs are byte-identical to +current main for both real prompt fixtures. An initial test expected an OMO +marker absent from the fixture; corrected the test to assert actual Operating +Mode/Instruction priority markers. No product defect claimed for that setup error. + +## Actual headless client proof + +Independently `npm pack`ed and installed the built plugin tarball in a disposable +npm project (version remains 0.2.3; not published). Both modes ran through actual +OpenCode **1.18.33**, Meridian **1.79.0** compiled from tested PR #1209 head +`d08011f8624d62ef8f66d261cc9b1295eaf7883d`, SDK **0.2.141**, Claude Code +**2.1.284**, actual **claude-opus-5-5**, Node on macOS arm64. Credentials are +read-only; request bodies/client output stay in private temporary directories. + +| Mode | Actual read tools | Random client-only receipt in SDK request | Same client session continuation | Client errors | Scrub invocations/errors | +| --- | --- | --- | --- | --- | --- | +| minimal | 1 | yes | yes | 0 | 4 / 0 | +| aggressive | 1 | yes | yes | 0 | 4 / 0 | + +In both runs the actual pre-transform main requests have powered-by/environment +preambles and cwd. Post-transform requests remove both fingerprints and retain +cwd. Minimal post-transform contexts have no generic identity; aggressive main +requests have the generic identity. This establishes the mode distinction in +actual client traffic. The title request has no tools or fingerprint and is +unchanged. The private receipt proves actual client execution and SDK delivery; +client exit zero alone would not establish that. + +Escrowed harness: [scripts/e2e-minimal-opencode.mjs](../../scripts/e2e-minimal-opencode.mjs). +Reproduce after building Meridian and independently installing a packed plugin: + +```sh +npm test +npx tsc --noEmit +npm run build +npm pack --pack-destination /private/tmp +# Install the resulting tarball in a disposable npm project; use its dist/index.js. +E2E_MERIDIAN_ROOT=/path/to/built/meridian \ +E2E_PLUGIN_PATH=/path/to/disposable/node_modules/@rynfar/meridian-plugin-opencode-scrub/dist/index.js \ +E2E_SCRUB_MODE=minimal node scripts/e2e-minimal-opencode.mjs +# Repeat with E2E_SCRUB_MODE=aggressive. +``` + +The harness requires OpenCode and existing Claude subscription credentials. It +uses isolated client/config/session directories and asserts actual tool results, +completed text, zero JSON error events, same-session continuation, plugin stats +and pre/post runtime fields. It preserves sanitized outcome summaries without +publishing raw prompts, credentials or session IDs. + +## Adversarial findings and limits + +The original feature's environmental preservation is corrected; current main's +adapter and passthrough guards survive reconciliation. No unrestricted brand +rewrite occurs in minimal preserved prose, while known identity wrappers are +still removed. Pure negative controls preserve genuine Claude context unchanged. +Actual client tools and continuation succeed in both modes. No public plugin +configuration/lifecycle change, server dependency or global credentials write. + +The contributor's original failing client/model/flow was not specified. These +results do not claim to reproduce that original instability, guarantee billing +classification, or test a live OhMyOpenCode installation; OMO coverage uses the +repository's actual fixture. Billing classification can change upstream, so a +passing run alone is not a permanent metering guarantee. Durable results above +survive temporary local logs; full headless harness remains in the repository. diff --git a/scripts/e2e-minimal-opencode.mjs b/scripts/e2e-minimal-opencode.mjs new file mode 100644 index 0000000..2015bec --- /dev/null +++ b/scripts/e2e-minimal-opencode.mjs @@ -0,0 +1,183 @@ +/** + * Manual live gate for scrub modes with the actual headless OpenCode client. + * Requires OpenCode, Claude credentials for Meridian, and the OpenCode scrub plugin. + * Keeps all request bodies and client output in a private temporary directory. + */ +import assert from 'node:assert/strict' +import { mkdtempSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { spawn, spawnSync } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { once } from 'node:events' + +const meridianRoot = process.env.E2E_MERIDIAN_ROOT +assert(meridianRoot, 'Set E2E_MERIDIAN_ROOT to a built Meridian checkout') +const mode = process.env.E2E_SCRUB_MODE ?? 'minimal' +assert(['minimal', 'aggressive'].includes(mode)) +const scrubPath = process.env.E2E_PLUGIN_PATH +assert(scrubPath, 'Set E2E_PLUGIN_PATH to the installed OpenCode scrub plugin entrypoint') +const model = process.env.E2E_MODEL ?? 'claude-opus-5-5' +const clientBin = process.env.E2E_OPENCODE_BIN ?? 'opencode' +const concurrency = Number(process.env.E2E_CONCURRENCY ?? 1) +const receipt = `CLIENT-READ-${randomUUID()}` +assert(Number.isInteger(concurrency) && concurrency >= 1 && concurrency <= 8) +const root = realpathSync(mkdtempSync(join(tmpdir(), 'meridian-opencode-admission-'))) +console.log(JSON.stringify({ artifact: root })) +const meridianConfig = join(root, 'meridian-config') +for (const path of [meridianConfig, join(root, 'plugins')]) mkdirSync(path) +for (const key of Object.keys(process.env)) { + if (key.startsWith('MERIDIAN_') || key.startsWith('CLAUDE_PROXY_')) delete process.env[key] +} +Object.assign(process.env, { + MERIDIAN_CONFIG_DIR: meridianConfig, + MERIDIAN_SESSION_DIR: join(root, 'meridian-sessions'), + MERIDIAN_TELEMETRY_PERSIST: '0', + MERIDIAN_PASSTHROUGH: '1', + MERIDIAN_CREDENTIALS_READONLY: '1', + MERIDIAN_NO_UPDATE_CHECK: '1', + MERIDIAN_OPENCODE_SCRUB_MODE: mode, +}) +const before = [] +const after = [] +globalThis.__opencodeAdmissionBefore = before +globalThis.__opencodeAdmissionAfter = after +const probe = (name, target) => `export default { + name: ${JSON.stringify(name)}, + onRequest(ctx) { + const results = (ctx.messages || []).filter(m => m.role === 'user' && Array.isArray(m.content)) + .flatMap(m => m.content.filter(b => b?.type === 'tool_result')); + const receiptClients = Array.from({length: ${concurrency}}, (_, index) => index).filter(index => + results.some(b => JSON.stringify(b.content ?? '').includes(${JSON.stringify(receipt ?? '__unused_receipt__')} + '-' + index))); + globalThis.${target}.push({ adapter: ctx.adapter, toolCount: ctx.tools?.length ?? 0, + hasGenericIdentity: (ctx.systemContext || '').includes('You are an expert coding assistant.'), + hasPowered: (ctx.systemContext || '').includes('You are powered by the model named'), + hasEnvPreamble: (ctx.systemContext || '').includes('Here is some useful information about the environment you are running in:'), + hasWorkingDirectory: (ctx.systemContext || '').includes('Working directory:'), + hasClientReadResult: receiptClients.length > 0, receiptClients }); + return ctx; + } +}` +const beforePath = join(root, 'before.js') +const afterPath = join(root, 'after.js') +writeFileSync(beforePath, probe('before-opencode-admission', '__opencodeAdmissionBefore')) +writeFileSync(afterPath, probe('after-opencode-admission', '__opencodeAdmissionAfter')) +const pluginConfigPath = join(root, 'plugins.json') +writeFileSync(pluginConfigPath, JSON.stringify({ plugins: [ + { path: beforePath, enabled: true }, + { path: scrubPath, enabled: true }, + { path: afterPath, enabled: true }, +] })) +const clientVersion = spawnSync(clientBin, ['--version'], { encoding: 'utf8' }) +assert.equal(clientVersion.status, 0, `OpenCode version command failed: ${clientVersion.error?.message ?? clientVersion.stderr}`) + +async function runClient(index, url) { + const clientRoot = join(root, `client-${index}`) + const project = join(clientRoot, 'project') + const config = join(clientRoot, 'config') + for (const path of [clientRoot, project, config]) mkdirSync(path) + writeFileSync(join(project, 'receipt.txt'), receipt + '-' + index + '\n') + writeFileSync(join(config, 'opencode.json'), JSON.stringify({ + $schema: 'https://opencode.ai/config.json', + plugin: [join(meridianRoot, 'dist', 'meridian')], + model: `anthropic/${model}`, + small_model: `anthropic/${model}`, + share: 'disabled', + permission: 'allow', + provider: { anthropic: { options: { apiKey: 'local-fixture', baseURL: url }, + models: { [model]: { name: model, limit: { context: 200000, output: 1024 }, + modalities: { input: ['text'], output: ['text'] }, temperature: false, + reasoning: false, tool_call: true } } } }, + })) + const env = { ...process.env, OPENCODE_CONFIG_DIR: config, OPENCODE_DISABLE_AUTOUPDATE: '1' } + for (const kind of ['CONFIG', 'DATA', 'CACHE', 'STATE']) env[`XDG_${kind}_HOME`] = join(clientRoot, kind.toLowerCase()) + for (const key of Object.keys(env)) { + if (/^(ANTHROPIC_|CLAUDE_|OPENAI_|MERIDIAN_|CLAUDE_PROXY_)/.test(key)) delete env[key] + } + async function invoke(args, name) { + const child = spawn(clientBin, args, { cwd: project, env, stdio: ['ignore', 'pipe', 'pipe'] }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', chunk => { stdout += chunk }) + child.stderr.on('data', chunk => { stderr += chunk }) + const timeout = setTimeout(() => child.kill('SIGTERM'), 180000) + const exit = await new Promise((resolve, reject) => { + child.once('error', reject) + child.once('exit', resolve) + }).finally(() => clearTimeout(timeout)) + writeFileSync(join(clientRoot, `${name}.stdout`), stdout) + writeFileSync(join(clientRoot, `${name}.stderr`), stderr) + const events = stdout.split('\n').filter(line => line.startsWith('{')).flatMap(line => { + try { return [JSON.parse(line)] } catch { return [] } + }) + return { exit, textEvents: events.filter(event => event.type === 'text' && event.part?.text).length, + toolEvents: events.filter(event => event.type === 'tool_use').length, + errorEvents: events.filter(event => event.type === 'error').length, + billingErrors: events.filter(event => event.type === 'error' && JSON.stringify(event).includes('billing_error')).length, + eventTypes: [...new Set(events.map(event => event.type))], + sessionId: events.find(event => typeof event.sessionID === 'string')?.sessionID } + } + const first = await invoke(['run', '--format', 'json', '--model', `anthropic/${model}`, + `Use the read tool to read ${join(project, 'receipt.txt')}, then give a brief acknowledgement.`], 'first') + const continued = first.exit === 0 && first.sessionId + ? await invoke(['run', '--session', first.sessionId, '--format', 'json', '--model', `anthropic/${model}`, + 'Reply with another short acknowledgement. Do not use tools.'], 'continued') + : undefined + const publicTurn = ({ sessionId: _sessionId, ...turn }) => ({ ...turn, sessionCaptured: Boolean(_sessionId) }) + return { index, ...publicTurn(first), continuation: continued ? publicTurn(continued) : null, + continuationSameSession: continued ? continued.sessionId === first.sessionId : false, + artifact: clientRoot } +} + +const { startProxyServer } = await import(pathToFileURL(join(meridianRoot, 'dist/server.js')).href) +let proxy +try { + proxy = await startProxyServer({ port: 0, host: '127.0.0.1', silent: true, + pluginConfigPath, pluginDir: join(root, 'plugins') }) + if (!proxy.server.listening) await once(proxy.server, 'listening') + const url = `http://127.0.0.1:${proxy.server.address().port}` + const initial = await (await fetch(`${url}/plugins/list`)).json() + const scrub = initial.plugins.find(plugin => plugin.name === 'opencode-scrub') + assert.equal(scrub?.status, 'active', 'OpenCode scrub plugin did not load') + if (process.env.E2E_EXPECT_VERSION) assert.equal(scrub.version, process.env.E2E_EXPECT_VERSION) + const clients = await Promise.all(Array.from({ length: concurrency }, (_, index) => runClient(index, url))) + const final = await (await fetch(`${url}/plugins/list`)).json() + const scrubStats = final.plugins.find(plugin => plugin.name === 'opencode-scrub')?.stats?.hooks?.onRequest + const summary = { result: 'pending', mode, artifact: root, meridian: JSON.parse(readFileSync(join(meridianRoot, 'package.json'))).version, + opencode: clientVersion.stdout.trim(), model, plugin: scrub ? { version: scrub.version, onRequest: scrubStats } : null, + clients, before, after } + writeFileSync(join(root, 'summary.json'), JSON.stringify(summary, null, 2)) + try { + assert(before.length >= concurrency && before.every(entry => entry.adapter === 'opencode'), + `The OpenCode client plugin did not identify requests; see ${root}/summary.json`) + assert(before.some(entry => entry.hasPowered && entry.hasEnvPreamble), + `The reported OpenCode system fingerprint was absent; see ${root}/summary.json`) + assert(after.length >= concurrency && after.every(entry => entry.adapter === 'opencode'), + `The scrub plugin changed request identity; see ${root}/summary.json`) + assert(clients.every(client => client.toolEvents > 0), 'Actual OpenCode did not execute a tool') + assert(clients.every(client => before.some(entry => entry.receiptClients.includes(client.index))), + 'A random client-only read receipt never reached the SDK request') + assert(clients.every(client => client.exit === 0 && client.textEvents > 0 && client.errorEvents === 0 + && client.sessionCaptured && client.continuationSameSession && client.continuation?.exit === 0 + && client.continuation.textEvents > 0 && client.continuation.errorEvents === 0), + `OpenCode did not complete; see ${root}/summary.json and client logs`) + assert(scrubStats?.invocations >= concurrency * 2 && scrubStats.errors === 0, + `The OpenCode scrub plugin did not process every request; see ${root}/summary.json`) + if (mode === 'minimal') assert(after.every(entry => !entry.hasGenericIdentity), 'Minimal mode inserted a replacement identity') + assert(after.every(entry => !entry.hasPowered && !entry.hasEnvPreamble), + `The metering fingerprint remained after scrubbing; see ${root}/summary.json`) + assert(after.some(entry => entry.hasWorkingDirectory), + `The scrub plugin removed OpenCode's working-directory context; see ${root}/summary.json`) + } catch (error) { + console.log(JSON.stringify({ ...summary, result: 'FAIL' })) + throw error + } + summary.result = 'PASS' + writeFileSync(join(root, 'summary.json'), JSON.stringify(summary, null, 2)) + console.log(JSON.stringify(summary)) +} finally { + if (proxy) await proxy.close() +} diff --git a/src/__tests__/minimal.test.ts b/src/__tests__/minimal.test.ts new file mode 100644 index 0000000..ca6384a --- /dev/null +++ b/src/__tests__/minimal.test.ts @@ -0,0 +1,63 @@ +import { afterEach, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" +import plugin from "../index.js" +import { scrubOpencodeFingerprints } from "../scrub.js" +const saved = process.env.MERIDIAN_OPENCODE_SCRUB_MODE +afterEach(() => { if (saved === undefined) delete process.env.MERIDIAN_OPENCODE_SCRUB_MODE; else process.env.MERIDIAN_OPENCODE_SCRUB_MODE = saved }) +const runtime = `You are powered by the model named Claude Opus. +Here is some useful information about the environment you are running in: + + Working directory: /client/project + Platform: darwin + +` +const policy = "Keep my OpenCode tool policy and OhMyOpenCode project prose." +const vanilla = "You are OpenCode, the best coding agent on the planet.\n" + runtime + "\n\n\n" + policy + +test("minimal removes the duplicate preamble while retaining cwd and project prose", () => { + const out = scrubOpencodeFingerprints(vanilla, "minimal") + expect(out).not.toContain("Here is some useful information") + expect(out).not.toContain("You are powered by") + expect(out).not.toContain("Platform: darwin") + expect(out).toContain("Working directory: /client/project") + expect(out).toContain(policy) + expect(out).not.toContain("You are an expert coding assistant.") + expect(out).not.toContain("\n\n\n") + expect(scrubOpencodeFingerprints(out, "minimal")).toBe(out) +}) + +test("minimal retains OMO policies while removing all identity and runtime wrappers", () => { + const source = readFileSync(join(import.meta.dir, "fixtures/omo-sisyphus-claude.txt"), "utf8") + const out = scrubOpencodeFingerprints(source, "minimal") + for (const marker of ["", "", "You are **Sisyphus**", "You are powered by"]) expect(out).not.toContain(marker) + for (const marker of ["", "**Operating Mode**:", "**Instruction priority**:"]) expect(out).toContain(marker) + expect(scrubOpencodeFingerprints(out, "minimal")).toBe(out) +}) + +test("default and explicit aggressive mode retain the same result", () => { + expect(scrubOpencodeFingerprints(vanilla)).toBe(scrubOpencodeFingerprints(vanilla, "aggressive")) + expect(scrubOpencodeFingerprints(vanilla)).toContain("You are an expert coding assistant.") + expect(scrubOpencodeFingerprints(vanilla)).not.toContain(policy) +}) + +test("the environment mode applies at each request and unknown values remain aggressive", () => { + const ctx = { adapter: "opencode", systemContext: vanilla, metadata: {} } + process.env.MERIDIAN_OPENCODE_SCRUB_MODE = "minimal" + expect(plugin.onRequest?.(ctx)?.systemContext).toBe(scrubOpencodeFingerprints(vanilla, "minimal")) + process.env.MERIDIAN_OPENCODE_SCRUB_MODE = "unknown" + expect(plugin.onRequest?.(ctx)?.systemContext).toBe(scrubOpencodeFingerprints(vanilla)) +}) + +test("minimal preserves genuine Claude passthrough context byte-for-byte", () => { + process.env.MERIDIAN_OPENCODE_SCRUB_MODE = "minimal" + const ctx = { adapter: "passthrough", systemContext: "You are Claude Code.\n\n\n" + runtime.replace(/You are powered by[^\n]+\n/, ""), metadata: {} } + expect(plugin.onRequest?.(ctx)).toBe(ctx) +}) + +test("minimal still scrubs headerless OpenCode traffic", () => { + process.env.MERIDIAN_OPENCODE_SCRUB_MODE = "minimal" + const ctx = { adapter: "passthrough", systemContext: runtime + policy, metadata: {} } + expect(plugin.onRequest?.(ctx)?.systemContext).not.toContain("Here is some useful information") + expect(plugin.onRequest?.(ctx)?.systemContext).toContain("Working directory: /client/project") +}) diff --git a/src/index.ts b/src/index.ts index 86dcfc9..cd916e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,23 @@ /** * 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 packageMetadata from "../package.json" with { type: "json" } import { scrubOpencodeFingerprints } from "./scrub.js" export type { Transform, RequestContext } from "./types.js" +// The duplicate 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 |||You are (?:"|\*\*)Sisyphus(?:"|\*\*)[^\n]*OhMyOpenCode/ + function getScrubMode(): "aggressive" | "minimal" { const env = (globalThis as typeof globalThis & { process?: { env?: Record } @@ -22,12 +30,13 @@ function getScrubMode(): "aggressive" | "minimal" { const plugin: Transform = { name: "opencode-scrub", - version: "0.1.0", + version: packageMetadata.version, 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, getScrubMode()) if (scrubbed === ctx.systemContext) return ctx return { ...ctx, systemContext: scrubbed } diff --git a/src/scrub.ts b/src/scrub.ts index 7799f54..14942d3 100644 --- a/src/scrub.ts +++ b/src/scrub.ts @@ -1,37 +1,32 @@ /** * Scrub opencode-identifying fingerprints from a system prompt. * - * Targets both vanilla OpenCode (from sst/opencode's built-in `anthropic.txt`) - * and OhMyOpenCode-style custom personas (Sisyphus, etc.). Each rule is an - * independent, idempotent regex — if a pattern isn't present the rule no-ops, - * so the same plugin handles both variants without configuration. + * Targets both vanilla OpenCode (from anomalyco/opencode's built-in + * `anthropic.txt`) and OhMyOpenCode-style custom personas (Sisyphus, etc.). + * Each rule is an independent, idempotent regex — if a pattern isn't present + * the rule no-ops, so the same plugin handles both variants without + * configuration. * * Detection vectors scrubbed: * - "OpenCode" / "opencode" brand tokens in the opening identity line and * embedded prose * - The feedback block pointing to github.com/anomalyco/opencode * - The "When the user directly asks about OpenCode" docs paragraph - * - OhMyOpenCode's "Sisyphus ... from OhMyOpenCode" identity line + * - OhMyOpenCode's Sisyphus identity line (quoted or bold form, all + * occurrences) and the OMO 4.x wrapper block + * - Residual "OhMyOpenCode" brand tokens * - The runtime block * - The self-outing "You are powered by the model named ..." line that * opencode's environment() builder appends (strongest third-party tell — * Claude Code never emits this phrasing) * - The duplicate "Here is some useful information about the environment - * you are running in:" preamble + block. Claude Code's preset - * already injects this; opencode appending its own copy makes the - * preamble appear twice in the final system prompt, which Anthropic's - * billing layer treats as a third-party-impersonation signal and gates - * opus behind Extra Usage (sonnet/haiku unaffected). + * you are running in:" preamble and redundant fields. Keep a bare + * Working directory line so client-side users do not erase the only path + * Meridian can read before it chooses the SDK working directory. * * Preserved: all tool policy, tone rules, task management guidance, code * references section, Sisyphus orchestration rules (Phase 0, explore/ * librarian, Oracle), and any user CLAUDE.md content appended by opencode. - * - * Modes: - * - aggressive (default): maximum fingerprint removal, including env blocks - * and minor prompt cleanup - * - minimal: only remove the strongest identity/fingerprint lines while - * preserving prompt structure and orchestration/env blocks */ export type ScrubMode = "aggressive" | "minimal" @@ -55,9 +50,27 @@ const OPENCODE_OBJECTIVITY_BRAND = /** Any residual bare "OpenCode"/"opencode" tokens in preserved prose */ const OPENCODE_BRAND_TOKEN = /\bOpenCode\b/g -/** OhMyOpenCode Sisyphus identity line */ +/** + * OhMyOpenCode Sisyphus identity line. OMO 4.x variants differ per model + * route: the default persona quotes the name (`You are "Sisyphus" ...`) while + * Claude-routed prompts bold it (`You are **Sisyphus** ...`), and some + * variants put "OhMyOpenCode" mid-sentence rather than sentence-final. + * Global, because the line now appears both inside and in + * . + */ const OMO_IDENTITY_LINE = - /You are "Sisyphus"[^\n]*from OhMyOpenCode\.[^\n]*\n+/ + /You are ("|\*\*)Sisyphus("|\*\*)[^\n]*OhMyOpenCode[^\n]*\n+/g + +/** + * OMO 4.x wrapper block ("Your designated identity for this + * session ... always identify as Sisyphus ..."), injected by + * buildAgentIdentitySection ahead of every Sisyphus variant. Strongest OMO + * fingerprint — removed wholesale like . + */ +const OMO_AGENT_IDENTITY_BLOCK = /[\s\S]*?<\/agent-identity>\n*/g + +/** Residual bare "OhMyOpenCode" tokens in preserved prose */ +const OMO_BRAND_TOKEN = /\bOhMyOpenCode\b/g /** The ... block */ const OMO_ENV_BLOCK = /[\s\S]*?<\/omo-env>\n*/ @@ -75,10 +88,17 @@ const POWERED_BY_LINE = * own copy on top of the preset, the preamble appears twice in the final * system prompt and Anthropic gates opus behind Extra Usage. Bisected * 2026-04-21: removing this block (or just the preamble line) makes opus - * succeed; sonnet/haiku unaffected. + * succeed; sonnet/haiku unaffected. Retain only a bare cwd field without the + * duplicate preamble: Meridian reads this field from the incoming prompt when + * the scrub function is used client-side, before Meridian receives the body. */ const OPENCODE_ENV_BLOCK = - /\nHere is some useful information about the environment you are running in:\n[\s\S]*?<\/env>\n/ + /\n?Here is some useful information about the environment you are running in:\n[\s\S]*?<\/env>\n?/ + +function keepClientCwd(block: string): string { + const cwd = block.match(/(?:^|\n)[ \t]*Working directory:[ \t]*([^\n<]+)/i)?.[1]?.trim() + return cwd ? `\n\n Working directory: ${cwd}\n\n` : "\n" +} const GENERIC_IDENTITY = "You are an expert coding assistant. You help users with software engineering tasks by reading files, executing commands, editing code, and writing new files.\n" @@ -88,26 +108,22 @@ const GENERIC_OBJECTIVITY = export function scrubOpencodeFingerprints(systemPrompt: string, mode: ScrubMode = "aggressive"): string { if (!systemPrompt) return systemPrompt - - const scrubbedIdentity = mode === "minimal" - ? systemPrompt.replace(OPENCODE_IDENTITY_LINE, "") - : systemPrompt.replace(OPENCODE_IDENTITY_LINE, GENERIC_IDENTITY) - - const scrubbedCore = scrubbedIdentity + const scrubbed = systemPrompt + .replace(OPENCODE_IDENTITY_LINE, mode === "minimal" ? "" : GENERIC_IDENTITY) .replace(OPENCODE_FEEDBACK_BLOCK, "") .replace(OPENCODE_DOCS_PARAGRAPH, "") .replace(OPENCODE_OBJECTIVITY_BRAND, GENERIC_OBJECTIVITY) + .replace(OMO_AGENT_IDENTITY_BLOCK, "") .replace(OMO_IDENTITY_LINE, "") + .replace(OMO_ENV_BLOCK, "") .replace(POWERED_BY_LINE, "") + .replace(OPENCODE_ENV_BLOCK, keepClientCwd) - if (mode === "minimal") { - return scrubbedCore.replace(/\s+$/, "") - } - - return scrubbedCore - .replace(OMO_ENV_BLOCK, "") - .replace(OPENCODE_ENV_BLOCK, "\n") + // Both modes remove duplicate runtime fingerprints and retain client cwd. + // Minimal avoids inserting an identity and rewriting residual prose. + return (mode === "minimal" ? scrubbed : scrubbed .replace(OPENCODE_BRAND_TOKEN, "the assistant") + .replace(OMO_BRAND_TOKEN, "the assistant")) .replace(/\n{3,}/g, "\n\n") .replace(/\s+$/, "") }