# Authoring a HarnessPlugin (/docs/plugins/authoring)



A `HarnessPlugin` is the typed extension surface for
`@pleach/core`. Every sibling SKU — `@pleach/compliance`,
`@pleach/gateway`, `@pleach/eval`, `@pleach/replay`,
`@pleach/mcp`, `@pleach/observe`, `@pleach/recipes`,
`@pleach/coding-agent`, `@pleach/sandbox`, `@pleach/langchain`,
`@pleach/base-tools` — is a plugin that implements this
contract. Your own consumer code extends the runtime through the
same surface.

This page covers the practical authoring story: import shape,
hook taxonomy, the canonical no-op for each return type, and the
worked starting point. For the structural-invariant view (what
plugins can and can't do, the lattice slots, the verdict ladder),
see [Plugin contract](/docs/plugin-contract).

<SourceMeta source="{ label: &#x22;examples/plugins/empty-plugin/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/examples/plugins/empty-plugin&#x22; }" />

## The canonical shape [#the-canonical-shape]

```typescript
import { definePleachPlugin, type HarnessPlugin } from "@pleach/core";

export const myPlugin = definePleachPlugin("my-plugin", {
  // structured cluster — 8 most-used hooks
  prompts: [personaBlock],
  streamObservers: [redactPiiObserver],
  safetyPolicies: [refusalPolicy],
  tools: [searchCorpus],
  // …
  _raw: {
    version: "0.1.0",
    // forward-compat escape hatch — any hook not in the cluster
    contributePostToolTier: () => myEntityRecognizer,
  },
});
```

The raw `HarnessPlugin` object literal still works — the type is
public and existing plugins keep loading. `definePleachPlugin()`
is the recommended path; it surfaces "what can my plugin
contribute?" through autocomplete instead of paging the 1100-line
interface.

## The 74-hook surface at a glance [#the-74-hook-surface-at-a-glance]

The full `HarnessPlugin` contract today exposes **74 hooks** —
67 `contribute*` methods plus 7 top-level lifecycle / extension
points. The table below groups them informally so you can scan for
the one you need. For the canonical, audit-gated layout — every
hook resolved to exactly one of nine namespaces — see
[Contribution namespaces](/docs/plugins/namespaces).

| Bucket                                       | Count  | Examples                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prompts                                      | 5      | `contributePrompts`, `contributeRuntimeAwarePrompts`, `contributePromptHints`, `contributePromptSections`, `contributePromptSectionGeneratorBundle`                                                                                                                                                                                                                                      |
| Safety + scrubbing + synthesis directives    | 4      | `contributeSafetyPolicies`, `contributeScrubbers`, `contributeSynthesisDirectiveBlocks`, `contributeFinalizationPasses`                                                                                                                                                                                                                                                                  |
| Tools + planning + intent                    | 13     | `contributeTools`, `contributeIntentToolMap`, `contributeToolCouplingHints`, `contributeBatchingHints`, `contributeIntentClassifiers`, `contributeIntentParameterResolvers`, `contributeIntentKeywordClasses`, `contributeDomainClassifierPatterns`, `contributePlanPrefixes`, `contributeGuestDeniedTools`, `contributeDefaultAgentGraphRegistries`, `contributeToolFollowUpIntents`, … |
| Stream observers + chunk handlers            | 8      | `contributeStreamObservers`, `contributeObserverConsumers`, `contributeStreamChunkHandlers`, `contributeStreamEventHandlersAdapters`, `contributeStreamEndDiagnosticsAnalyzer`, `contributeDetectionRules`, `contributeHallucinatedToolDetectors`, `contributeGarbledOutputRecorder`                                                                                                     |
| Synthesis + fabrication + post-tool          | 7      | `contributeFabricationDetectors`, `contributeFabricationDetectorRules`, `contributeFabricationGuard`, `contributeRefClassValidators`, `contributeContinuationPolicy`, `contributeContinuationShadowResolver`, `contributePostToolTier`                                                                                                                                                   |
| Citations + entities                         | 5      | `contributeCitationEntityExtractor`, `contributeCitationInjector`, `contributeCitationRuleSet`, `contributeEntityExtractors`, `contributeEntityNameCounter`                                                                                                                                                                                                                              |
| Sandbox + interrupts + approval              | 5      | `contributeSandboxBridge`, `contributeSandboxInitialization`, `contributeSandboxAvailabilityEvaluator`, `contributeInterruptUIHandlers`, `contributeApprovalFlow`                                                                                                                                                                                                                        |
| Family routing + retry                       | 3      | `contributeFamilyPivot`, `contributeFamilyExhaustedSurface`, `contributeRetryPolicy`                                                                                                                                                                                                                                                                                                     |
| Middleware + prompt bridge + domain          | 7      | `contributeMiddleware`, `contributeRuntimeAwareMiddleware`, `contributePromptContextBridge`, `contributeFallbackSystemPromptBuilder`, `contributeDomainSynonyms`, `contributeChainingFieldNames`, `contributeIntentMentionDetector`                                                                                                                                                      |
| Audit + meta-learning + manifest             | 6      | `contributeAuditEmitter`, `contributeMetaLearningContext`, `contributeChatManifestProvider`, `contributeEventTypes`, `contributePreserveDataRefFields`, `contributeProbes`                                                                                                                                                                                                               |
| Misc (artifact cache, prefetcher, get\_data) | 4      | `contributeArtifactCacheReader`, `contributeStructurePrefetcher`, `contributeGetDataHandlerFactory`, `contributeDataChannelRefetch`                                                                                                                                                                                                                                                      |
| Top-level non-`contribute*`                  | 7      | `extraGraphNodes`, `prePlanPrimer`, `postSynthesisGuard`, `onJobDispatch`, `onJobComplete`, `registerAsyncExecutors`, `eventResolver`                                                                                                                                                                                                                                                    |
| **Total**                                    | **74** |                                                                                                                                                                                                                                                                                                                                                                                          |

**Honest scope-limit.** Most authors fill 1–5 hooks. The 74-hook
surface is the substrate's total contribution ceiling, not the
typical author's checklist. The structured `capabilities` cluster
covers the 8 most-reached-for hooks; `_raw` catches the rest.

The full hook list with one-line descriptions is in
[`examples/plugins/empty-plugin/plugin.mjs`](https://github.com/pleachhq/core/tree/main/examples/plugins/empty-plugin/plugin.mjs)
— a copy-paste no-op scaffold where every hook is present with
the canonical empty return.

## Canonical no-op returns [#canonical-no-op-returns]

Each hook shape has one canonical "I don't contribute here" return.
The collector ignores the hook entirely; nothing is dispatched.

| Hook return type                                     | Canonical no-op |
| ---------------------------------------------------- | --------------- |
| `readonly Array<T>` (multi-contribution)             | `[]`            |
| `Record<string, T>` (named map)                      | `{}`            |
| `T \| null` (first-wins)                             | `null`          |
| `T \| undefined` (first-wins)                        | `undefined`     |
| `void` (lifecycle: `onJobDispatch`, `onJobComplete`) | bare `return`   |

The empty-plugin scaffold uses the canonical form for every hook —
copy it, then put real bodies on the ones you need and delete the
rest.

## The minimal walk-through [#the-minimal-walk-through]

```typescript
import { definePleachPlugin } from "@pleach/core";

// 1. Pick the hooks you need.
// 2. Provide bodies for them.
// 3. Register the plugin once at runtime construction.

export const auditPlugin = definePleachPlugin("audit-plugin", {
  _raw: { version: "0.1.0" },
  streamObservers: [
    {
      when: { callClass: "synthesize" },
      factory: () => ({
        observerId: "audit-stream",
        onChunk(chunk) {
          // mirror chunk into your audit log
          return { kind: "continue" };
        },
      }),
    },
  ],
});
```

Register at runtime construction:

```typescript
const runtime = new SessionRuntime({
  storage: new MemoryStorage(),
  plugins: [auditPlugin, anotherPlugin],
});
```

Registration order is the dispatch order. The substrate doesn't
re-order plugins; two non-commuting plugins (e.g. a PII redactor
and a metrics observer) should be sequenced explicitly at the
construction site. See
[Plugin contract — registration order](/docs/plugin-contract) for
the canonical non-commuting example.

## The structured `capabilities` cluster [#the-structured-capabilities-cluster]

`PluginCapabilities` surfaces the 8 most-used hooks as a flat
structured menu so authors don't page the full interface. Each
field takes a **value** (not a thunk), and the factory forwards
it into the corresponding `contribute*` method.

| Capability key         | Hook it maps to                                                   |
| ---------------------- | ----------------------------------------------------------------- |
| `prompts`              | `contributePrompts`                                               |
| `runtimeAwarePrompts`  | `contributeRuntimeAwarePrompts` (accepts function OR fixed array) |
| `safetyPolicies`       | `contributeSafetyPolicies`                                        |
| `fabricationDetectors` | `contributeFabricationDetectors`                                  |
| `tools`                | `contributeTools`                                                 |
| `streamObservers`      | `contributeStreamObservers`                                       |
| `intentToolMap`        | `contributeIntentToolMap`                                         |
| `toolCouplingHints`    | `contributeToolCouplingHints`                                     |

Any hook outside the structured cluster goes in `_raw` (typed as
`Partial<HarnessPlugin>`). Keys in `_raw` win on collision with
the structured cluster (last-write-wins), so authors migrating
bespoke plugins keep full control.

## The 5 top-level (non-`contribute*`) hooks [#the-5-top-level-non-contribute-hooks]

```typescript
import type { HarnessPlugin } from "@pleach/core";

export const myPlugin: HarnessPlugin = {
  name: "my-plugin",
  version: "0.1.0",

  // Register custom nodes into the compiled graph.
  extraGraphNodes: () => [],

  // Inject a one-shot system message before plan generation.
  prePlanPrimer: (_ctx) => null,

  // Inspect assistant content; emit a corrective system message.
  postSynthesisGuard: (_ctx) => null,

  // Lifecycle: paired job-dispatch + job-complete callbacks.
  onJobDispatch: (_ctx) => {},
  onJobComplete: (_ctx) => {},

  // Register async executors for long-running job-backed tools.
  registerAsyncExecutors: (_registrar) => {},

  // Resolve domain-specific event types when folding the event log.
  eventResolver: undefined,
};
```

These seven are top-level rather than `contribute*` because they
either return a typed array of graph nodes (`extraGraphNodes`),
operate on a per-call context with a single first-wins return
(`prePlanPrimer`, `postSynthesisGuard`), fire side-effect-only
lifecycle callbacks (`onJobDispatch`, `onJobComplete`), register
async executors (`registerAsyncExecutors`), or supply a
domain-event resolver property rather than a collector method
(`eventResolver`).

## Capability breadcrumbs [#capability-breadcrumbs]

Eleven `contribute*` collectors emit a one-shot
`[Pleach:capability-not-contributed]` console line when they
aggregate to an empty array across all registered plugins. The
breadcrumb is dedup-keyed on `${sessionId}:${capability}` and
surfaces a missing plugin without spamming logs.

| Capability hook                      |
| ------------------------------------ |
| `contributeStreamObservers`          |
| `contributeStreamFilters`            |
| `contributeSafetyPolicies`           |
| `contributeFabricationDetectors`     |
| `contributePostToolTier`             |
| `contributeRefClassValidators`       |
| `contributeMiddleware`               |
| `contributeRuntimeAwareMiddleware`   |
| `contributeToolCouplingHints`        |
| `contributeIntentToolMap`            |
| `contributeIntentParameterResolvers` |

To surface the same gaps at construction time, call
`inspectRuntime(runtime)` and walk `report.capabilities` for rows
whose `wiredCount === 0`. See
[Runtime inspector](/docs/runtime-inspector).

## Canonical reference [#canonical-reference]

| What                                                      | Where                                                                                                                                  |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Empty-plugin scaffold (every hook, every canonical no-op) | [`examples/plugins/empty-plugin/`](https://github.com/pleachhq/core/tree/main/examples/plugins/empty-plugin)                           |
| Stream observer with `amend` verdict                      | [`examples/observers/dialect-normalization/`](https://github.com/pleachhq/core/tree/main/examples/observers/dialect-normalization)     |
| Stream observer with `stop` verdict                       | [`examples/observers/halt-on-pattern/`](https://github.com/pleachhq/core/tree/main/examples/observers/halt-on-pattern)                 |
| Vertical entity recognizer via `contributePostToolTier`   | [`examples/plugins/domain-entity-recognition/`](https://github.com/pleachhq/core/tree/main/examples/plugins/domain-entity-recognition) |
| Custom event channel via `emit` verdict                   | [`examples/plugins/custom-event-channel/`](https://github.com/pleachhq/core/tree/main/examples/plugins/custom-event-channel)           |
| Full type surface                                         | `packages/core/src/plugins/types.ts`                                                                                                   |

Each example is a standalone npm package with a `node --test`
smoke. Clone, run, modify.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Plugin contract" href="/docs/plugin-contract" description="The structural-invariant view — what plugins can and can't do, lattice slots, verdict ladder." />

  <Card title="Stream observers" href="/docs/plugins/stream-observers" description="The contributeStreamObservers hook in detail — verdicts, factory pattern, replay determinism." />

  <Card title="Post-tool tier" href="/docs/plugins/post-tool-tier" description="The contributePostToolTier hook — uniform enrichment across tool batches." />

  <Card title="Plugin bundles" href="/docs/plugin-bundles" description="composePlugin() for assembling a larger plugin across multiple files." />
</Cards>
