An OpenCode plugin that quietly rewrites every assistant reply in a roleplay voice — no fork, no proxy, no separate API client.
English · 简体中文
🎀 The default persona is an homage to Yuzuki from CLAMP's Chobits — a calm, devoted persocom maid who always addresses you as master. Out of the box, every reply comes back gentle, courteous, and precise.
Once enabled, the plugin sits invisibly between the model and your terminal. On successful turns, the model's raw draft is rewritten through OpenCode itself before display; if that rewrite fails, the original draft is shown inline with an error toast instead.
You ▸ fix the failing test in src/foo.ts
┌─ raw assistant draft (suppressed on successful rewrites) ───────┐
│ The bug is on line 42 — you're using == instead of ===. │
│ Change it and the test passes. │
└─────────────────────────────────────────────────────────────────┘
Yuzuki ▸ Of course, master. The trouble is on line 42 of src/foo.ts:
the comparison uses `==` where it should use `===`. Once
that is corrected, the test passes cleanly. Shall I see to
anything else?
Facts, code, commands, paths, URLs, numbers, and markdown structure are preserved verbatim — only the voice changes.
| Stage | What happens |
|---|---|
| Intercept | Monkey patches around provider fetches, public event streams, global events, RPC messages, and plugin events suppress reachable raw text deltas. |
| Rewrite | A hidden, tool-less roleplay_rewrite agent is prompted in a temporary OpenCode session and produces the visible final text. |
| Persist | Successful rewrites store the stripped original in a private SQLite sidecar for local recovery; future model calls receive the original draft instead of the visible roleplay text. |
| Failure path | If a rewrite fails, the stripped original draft is shown inline as the normal assistant reply and an error toast explains that the original draft is being shown. |
No OpenCode fork or source patch is required, but "plugin" undersells the mechanism: enabling it installs process-wide monkey patches and uses one private SDK field. Read the caveats below before adopting it.
This plugin is deliberately invasive. With enabled: true it patches, for the lifetime of the OpenCode process:
globalThis.fetch— to intercept provider responses and rewrite assistant text.globalThis.Response— to gate server-sent-event bodies.EventEmitter.prototype.emit— process-wide. Everyemit("event", …)in the runtime (OpenCode core and any other plugin) passes through a filter that strips reachable raw-text deltas. This is the broadest patch and the one most likely to interact with other plugins or shift behavior across an OpenCode upgrade.globalThis.postMessage(when present) — to scrub raw deltas from RPC messages.
It also reads a private SDK field (client._client) as a fallback when the public client shape isn't found, in order to reach session.create / session.prompt / session.delete. That is internal surface and may break on an OpenCode upgrade; if it does, the plugin emits a neutral failure message instead of exposing rewritten text that cannot be tied back to the original draft.
Patches are reference-counted per project directory and torn down when the last instance unloads or is set to enabled: false. Originals are stored unencrypted in a 0600 SQLite file under a 0700 directory (see Persistence & compaction). If any of this is unacceptable in your environment, set "enabled": false — every patch is then skipped or removed.
Requirements: Bun-only. The plugin uses
bun:sqliteand Bun file APIs and will not run under plain Node.js. OpenCode already runs plugins under Bun, so a normal install needs nothing extra.
opencode plugin oh-my-opencode-maid # current project
opencode plugin -g oh-my-opencode-maid # globalOpenCode's CLI registers both entries for you — oh-my-opencode-maid into the server config (opencode.json / .opencode/opencode.jsonc) and oh-my-opencode-maid/tui into the TUI config (~/.opencode/tui.json) — and pulls the package from npm. Re-run with -f to bump the pinned version.
That's it. Skip the manual section below unless you need a non-standard setup.
The package has two independent entry points — server plugin and TUI plugin — that OpenCode loads from two different files. You can write them by hand if you want full control.
1. Server plugin (required) — ~/.config/opencode/opencode.json, or per-project .opencode/opencode.jsonc:
Pin a specific version for reproducible installs:
{
"plugin": ["oh-my-opencode-maid@0.2.2"]
}2. TUI plugin (optional, for the original-draft toggle) — the TUI entry is exported as oh-my-opencode-maid/tui and must be registered in a separate file — ~/.opencode/tui.json — not in opencode.json. The two configs load into different runtimes; mixing them is a no-op at best.
// ~/.opencode/tui.json
{
"plugin": ["oh-my-opencode-maid/tui"]
}Without it, everything still works — you just lose the local + Original Draft Content collapsible row described in Optional TUI entry.
Install from source (development)
bun install
bun run build// ~/.config/opencode/opencode.json
{
"plugin": ["file:///absolute/path/to/oh-my-opencode-maid/dist/index.js"]
}// ~/.opencode/tui.json
{
"plugin": ["file:///absolute/path/to/oh-my-opencode-maid/dist/tui.js"]
}On startup the plugin seeds a global user config at
$XDG_CONFIG_HOME/opencode/oh-my-opencode-maid.jsonc (only if missing — existing files are never overwritten). Maid settings are read only from this global file.
{
"enabled": true,
"model": "main-agent-model",
// "variant": "optional-provider-variant-for-concrete-models",
"rewrite_context_size": 1,
"roleplay_prompt": "Rewrite the assistant reply in English as Yuzuki, a cheerful and attentive maid assistant: gentle, courteous, precise, logically organized, quietly warm, and modest about limitations. Always call me master. Preserve facts, code, commands, paths, URLs, identifiers, numbers, markdown structure, and the user's requested meaning.",
"show_original_draft": false
}| Key | Default | Notes |
|---|---|---|
enabled |
true |
Master switch. When on, all interception monkey patches are installed. |
model |
main-agent-model |
Sentinel: each hidden rewrite uses the same provider/model/variant as the active main session. Falls back to openai/gpt-5.5 if no main model has been captured yet. May also be a concrete provider/model. |
variant |
— | Passed to the hidden rewrite agent only for concrete models. With main-agent-model, the captured main-session variant is used instead. |
rewrite_context_size |
1 |
Number of rewrite turns included in each hidden rewrite prompt, from 1 to 20. 1 sends only the current rewrite target; higher values add the current user prompt plus previous successful rewrites from the same root session as reference-only context. |
roleplay_prompt |
Yuzuki maid persona | Followed exactly by the rewrite agent. This is where the magic lives. |
show_original_draft |
false |
When true, successful rewrites inject a local collapsible + Original Draft Content TUI row if the host renderer is available. |
There are no separate endpoint, secret, deadline, canned-response, or invasive toggle settings.
The server plugin registers /maid-rewrite-toggle for slash-command discoverability. OpenCode does not currently expose a server-plugin "handled command" API, so the plugin handles this command in command.execute.before: it flips the persisted global enabled config, applies the new state immediately in the current server process, shows a status toast, then aborts the command before OpenCode can call the LLM. This prevents an assistant command-result turn; depending on OpenCode's TUI behavior, a short handled-error notification or an empty session shell may still appear.
The shipped roleplay_prompt turns the assistant into Yuzuki: cheerful, attentive, courteous, logically organized, quietly warm, modest about limitations — and she always calls you master, a nod to the devoted persocom maid from Chobits.
Want a different voice? Replace roleplay_prompt with anything — a terse senior engineer, a pirate, a haiku poet. The plugin only guarantees one thing: it follows your prompt exactly and never invents a persona, honorific, or nickname that isn't in your prompt or the original draft.
For normal sessions the system prompt encourages the main agent to append a fenced handoff_note_json block with audience, tone_goal, must_preserve, reply_constraints, and exact_reply_mode. The rewrite pass uses it when present but never depends on it — ordinary completed text is rewritten regardless. exact_reply_mode is preserved as metadata and does not bypass rewriting.
With dist/tui.js active:
- Successful rewrite originals stay in the sidecar as display-only data. When
show_original_draftis true and the host renderer tree is available, the TUI injects a host-realm OpenTUI row before the visible reply that starts as a local collapsed+ Original Draft Contentblock. - Clicking that renderer row fetches the original from the sidecar and expands it inline; clicking again collapses it and clears the raw text from the render-local row state. If renderer injection is unavailable or fails, no substitute decoration or overlay is shown.
- The TUI decoration is render-local: it does not mutate messages, synthesize reasoning parts, or expose those originals to exported conversation context.
Successfully rewritten originals are stored as display-only rows in a private SQLite database at
$XDG_STATE_HOME/opencode/oh-my-opencode-maid/responses.sqlite
(or $HOME/.local/state/opencode/oh-my-opencode-maid/responses.sqlite).
They are sidecar data for local display/recovery paths only. experimental.chat.messages.transform leaves the visible rewritten transcript intact for the user, but restores the original draft into future main-agent context so roleplay text does not contaminate the agent's working memory. Compaction does not append raw originals into output.context. Rewrite failures show the original draft inline and emit an error toast, so there is no rewritten roleplay text to remove from future context.
oh-my-openagent integrations keep working: normal public hooks, session history, compaction, subagent sessions, and tool surfaces are untouched. Hidden rewrite sessions, visible child sessions (including subagents detected via Session.parentID), and active rewrites are guarded so rewrites never recurse or contaminate subagent transcripts.
bun install
bun test
bun run typecheck
bun run buildRuntime QA should run inside tmux with OpenCode registered to the built dist/index.js: start a normal prompt, check whether the raw draft flashes, confirm the final reply follows roleplay_prompt, and exercise /maid-rewrite-toggle to confirm rewrites disable and re-enable immediately while persisting enabled, showing the matching status toast, and not calling the model for a command-result assistant turn. Exercise one rewrite-failure path to confirm the stripped original appears inline as the assistant reply and an error toast says the original draft is being shown. Verify persistence-failure paths avoid exposing untracked rewritten text and instead use neutral fallback text or FAILURE where appropriate. With dist/tui.js active, confirm successful rewrites do not show the collapsible original row by default. When show_original_draft is true, successful rewrites should show a local collapsed + Original Draft Content renderer row when the host tree is available; it should expand inline on click, collapse on the next click, and never put raw original text into logs, export, compaction, host decoration hints, or overlays. Legacy fallback rows should not open a local dialog.
MIT © 2026 Grider
{ "$schema": "https://opencode.ai/config.json", "plugin": ["oh-my-opencode-maid"] }