Essay 7.3 — The Plugin Kit, Part 3 of 9.


Essay 7.2 opened the universal skeleton — CLAUDE.md, hooks, scripts — and named the PLUGIN-LOCK ceremony that gates hard-substrate edits. This sub-essay opens the soft-memory organ that almost every plugin doubles: voice.xml. The hooks-side and scripts-side surfaces share one XML schema across different audiences. Getting them confused is the most common new-user error in plugin authoring.


voice.xml × Two — The Dual Surface

This is the organ that confuses new users most, and the one where the relational anatomy matters most.

The two voice surfaces are different files with different audiences. hooks/voice.xml and scripts/voice.xml look nearly identical structurally (both XML, both with <coaching>, <block>, <info>, <entry> elements identified by id), but they serve different consumers.

hooks/voice.xml — The Agent-Facing Surface

What it is. Strings the plugin emits at hook fire-time. Coaching voices that nudge the agent at a specific moment (entering a phase, crossing a context tier, having just dispatched a subagent). Blocks that refuse the agent when a guard fires.

Who reads it. The LLM agent. Voices delivered via hooks land in the agent’s context window — either as soft injections via additionalContext JSON (coaching) or as exit-2 stderr refusals (blocks). The LLM sees the string mid-turn and reacts.

Who writes it. Mostly CONDENSE step 4 (which consumes [VOICE-UPDATE] markers other phases emit). The historian subagent also updates voice files when the plugin’s evolution requires new coaching. In the current prototype, voice.xml is treated as a soft-memory surface, not a PLUGIN-LOCK-only code surface.

What it depends on. The shared lib/voice-helper/ helper (the get_voice function that loads voice.xml entries and substitutes {{var}} placeholders).

scripts/voice.xml — The Operator-Facing Surface

What it is. Strings the plugin’s CLI prints to the operator’s terminal. When safe-lock.sh reverts a test failure, the operator sees a short status line in their terminal. The text is what the operator’s eyes read in the shell.

Who reads it. The human operator. Terminal output. Not injected into the LLM’s context — printed to stderr or stdout of the shell, visible only to the person at the keyboard.

Who writes it. Same ownership pattern as hooks/voice.xml: CONDENSE and the historian repair wording as soft memory, while the scripts that consume those strings remain hard-substrate code.

What it depends on. Same voice-helper.

Why the split matters

Same intent — both surfaces carry the plugin’s voice. Different audiences — the LLM consumes structured paragraphs that frame the situation and propose action; the human consumes status lines that flag what happened. Wording often differs between the two for the same conceptual event. Auditor scripts that grep voice ids have to look in both files; auditing only one false-positives every id that lives in the other.

Coaching (soft, probabilistic) on the left, block (hard, deterministic) on the right, with a curving migration arrow between them
Image 7.3. Soft layer coaches. Hard layer refuses. Patterns migrate left to right when data warrants.

Soft vs hard at the element level. Inside each voice.xml, the <coaching> element produces a context injection — probabilistic, the LLM may absorb or ignore it. The <block> element produces a refusal — exit-2 stderr that fails the agent’s tool call. The two element types coexist in the same file. The Lock-13 over-engineering veto says: new behavioral controls start as coaching; only when measurement shows coaching consistently fails does the control harden into a block.

The yaml-injection pairing. Stage 3 of the maturation arc (covered in Essay 8) is a job whose plan_file is a .yaml — a Stage-3 job, chosen at cycle-1 PLAN like any other Stage, whose .yaml plan injects job-specific context at each phase entry. Stage 3 is identical to Stage 2 in completion semantics; only the plan-file format differs. A Stage-3 job’s .yaml per-phase fields pair with voice ids by convention. The yaml field name maps to a voice id directly; voice-helper augments the rendered voice text at phase entry — appending the yaml value by default, or replacing/prepending it when the yaml entry specifies a mode (the three modes are detailed in Essay 6.10b and Essay 8.2). The pairing has a contract surface: a yaml key must match a voice id that some hook or script actually calls, otherwise the plan loader rejects the yaml at validate-format time with a did you mean suggestion. New yaml fields require no parser change — add the voice id to the plugin’s voice.xml AND wire a get_voice callsite in a hook or script (making it callable — orphan ids are rejected by the yaml validator), and the seed agent picks up the new pairing on the next phase entry.

The new-plugin lens. When you guide your seed to add coaching for a new behavior, the seed writes the coaching string to hooks/voice.xml first (cheap, soft). If the operator later observes the coaching consistently fails to hold, the seed authors a <block> element in the same file for the hard variant — same id namespace, harder enforcement. The seed never invents a third voice surface. The split between LLM-facing and operator-facing voice surfaces is structural.

A consulting firm’s seed agent could carry the same dual-voice split: hooks/voice.xml coaches the agent on deliverable-checklist enforcement; scripts/voice.xml prints terminal status to the consultant when deliverable.sh validate runs. Same schema, the agent-facing and operator-facing surfaces, one ceremony.


The hooks-side and scripts-side surfaces share one schema. The LLM consumes the agent-facing surface; the operator consumes the CLI-facing surface. The next sub-essay opens the organ that almost every plugin needs but that no plugin lets anyone else touch — the private data.json state.


Essay 7.3 — The Plugin Kit, Part 3 of 9.

Previous: Essay 7.2 — Skeleton: CLAUDE.md, Hooks, and Scripts — the universal organs governed by PLUGIN-LOCK. Next: Essay 7.4 — data.json — The Hidden State — per-plugin private state, script-mediated.