diff --git a/README.md b/README.md index ce64e66..7dd9a2e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,24 @@ The scrub also handles [OhMyOpenCode](https://github.com/anomalyco/ohmyopencode) The scrub is **idempotent** — running it twice on the same string is a no-op. +## Modes + +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. + +```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) @@ -102,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 b4d5623..cd916e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,16 @@ 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 |||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, @@ -27,7 +37,7 @@ const plugin: Transform = { 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..14942d3 100644 --- a/src/scrub.ts +++ b/src/scrub.ts @@ -29,6 +29,8 @@ * librarian, Oracle), and any user CLAUDE.md content appended by opencode. */ +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+/ @@ -104,10 +106,10 @@ 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 scrubbed = systemPrompt + .replace(OPENCODE_IDENTITY_LINE, mode === "minimal" ? "" : GENERIC_IDENTITY) .replace(OPENCODE_FEEDBACK_BLOCK, "") .replace(OPENCODE_DOCS_PARAGRAPH, "") .replace(OPENCODE_OBJECTIVITY_BRAND, GENERIC_OBJECTIVITY) @@ -116,8 +118,12 @@ export function scrubOpencodeFingerprints(systemPrompt: string): string { .replace(OMO_ENV_BLOCK, "") .replace(POWERED_BY_LINE, "") .replace(OPENCODE_ENV_BLOCK, keepClientCwd) + + // 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(OMO_BRAND_TOKEN, "the assistant")) .replace(/\n{3,}/g, "\n\n") .replace(/\s+$/, "") }