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+$/, "")
}