Apply when creating, modifying, or reviewing any Rembric agent plugin. Triggers on changes under `apps/plugin/`, on new clients added alongside Claude Code / Codex CLI / Hermes Agent / opencode / Pi, on edits to `apps/plugin/mcp-bridge/`, `apps/plugin/bin/rembric-plugin-core.mjs`, per-client manifest changes, or plugin install/uninstall scripts. End-to-end validation against a local dev server (`pnpm run dev`, host Next.js on :3000) is mandatory whenever local testing is feasible.
Pro scans all 3 files and shows the line behind each finding
Scanned 9/24/2026
npx -y skills add susomejias/rembric --skill rembric-plugin-development --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Rembric Plugin Development?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/susomejias-rembric-plugin-development)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: rembric-plugin-development
description: Apply when creating, modifying, or reviewing any Rembric agent plugin. Triggers on changes under `apps/plugin/`, on new clients added alongside Claude Code / Codex CLI / Hermes Agent / opencode / Pi, on edits to `apps/plugin/mcp-bridge/`, `apps/plugin/bin/rembric-plugin-core.mjs`, per-client manifest changes, or plugin install/uninstall scripts. End-to-end validation against a local dev server (`pnpm run dev`, host Next.js on :3000) is mandatory whenever local testing is feasible.
---
# Rembric plugin development
Authoritative specs: `openspec/specs/{claude-code-plugin,codex-distribution,hermes-agent-plugin,opencode-plugin,pi-plugin,plugin-session-protocol}/`. Archived decisions: `openspec/changes/archive/`.
## Mandatory workflow
1. **OpenSpec change first.** Run `/opsx:propose` (or amend an existing change). Plugin work always touches ≥2 specs and ≥3 files. Skipping the change is the failure mode that produces drift.
2. **Two release tracks: `server` + unified `plugin` (no cascade).** release-please runs exactly two components, no `node-workspace`/`linked-versions`/grouping. `server` (`apps/web`, package `@rembric/web`, tag `server-v*`) builds the Docker image. **`plugin`** (`apps/plugin` — the WHOLE tree, no `exclude-paths`, package `@rembric/plugin`, tag `plugin-v*`) carries **one unified version for all five clients**; its `extra-files` update every client carrier in lock-step (`.claude-plugin/{package,plugin}.json`, `.codex-plugin/{package,plugin}.json`, `.hermes-plugin/plugin.yaml`, `.opencode-plugin/plugin.ts` comment, `.pi-plugin/package.json`). A change to ANY plugin file bumps the single `plugin` version — claude/codex/opencode/hermes/pi never diverge; the CHANGELOG (scoped by conventional commit) records what actually changed. A `plugin` release NEVER rebuilds the server image (`publish-docker` gates on `server_release_created`), but it IS what publishes `@rembric/pi` to npm (trusted-publishing OIDC, provenance, no long-lived token). `.pi-plugin/` has to live inside `apps/plugin/` for that: release-please attributes a release by the paths of the commits under the component's `path`, so a client outside it would never _cause_ a release and its carrier would only move when something unrelated did. `release-please.yml` carries a `concurrency` guard (`cancel-in-progress: false`) so a rapid second merge can't cancel tag-minting. The former six-component + `node-workspace` cascade was retired (change `unify-plugin-release-track`) after its anchor-tag fragility produced phantom release PRs. Legacy per-client tags (`claude-code-plugin-v*`, …) stay in history, inert.
3. **End-to-end against a local dev server** (`pnpm run dev`, host Next.js on :3000) before reporting done — see [E2E discipline](#end-to-end-validation-discipline) below.
4. **Docs sweep**: `README.md`, `docs/agents.md`, `apps/plugin/README.md`, the in-plugin `README.md`, `apps/plugin/CHANGELOG.md`.
> **No tool watches the per-client manifest dirs for you.** `eslint.config.js` ignores `apps/plugin/*/**`, which matches the dot-directories, and none of them match `pnpm-workspace.yaml::packages` (`apps/*`, `packages/*`) — so `pnpm -r` does not reach them, ESLint does not lint their TypeScript, and any `dependencies` they declare are not installed by the repo's own install (`.claude-plugin/package.json`'s `workspace:*` dep is dead letter today). Their tests run **only** because `apps/web/vitest.config.ts::include` lists a literal glob per client; a new client without its glob leaves a test file written and never executed, and the suite is green on nothing.
## The three single-source-of-truth rules
- **`.rembric` is the only per-repo slug source.** Dotenv file with `PROJECT_SLUG=<lowercase-hyphen>`. Regex `^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$`.
- **`apps/plugin/mcp-bridge/rembric-dotenv.mjs` is the only JS/TS implementation** of `parseDotenv` + `readRembricSlug` + `SLUG_RE`. The bridge, opencode plugin, and Pi extension import it (the installer rewrites the opencode import to the installed absolute path). Inlining any of those in another `.mjs`/`.ts` file fails the build (invariant test).
- **`apps/plugin/bin/rembric-plugin-core.mjs` is the only JS/TS implementation** of the nudge strings, `stripPrivateTags`, `truncate`, `diag`, the session HTTP client, the transcript accumulator and the flush helpers. Both JS/TS clients (opencode, Pi) import it, which is what makes the nudge strings byte-identical and the `<private>` redaction identical **by construction** rather than by review. Its `agent` parameter is required with no default: `sessions.agent` is written once per session into append-only rows with no repair verb, so a default misfiles sessions permanently. `rembric-plugin-core.d.mts` is hand-written (`apps/plugin` has no build step and no typecheck) and is the only thing that makes the omission a compile error.
Bash (`apps/plugin/scripts/_api.sh`) and Python (`apps/plugin/.hermes-plugin/__init__.py`) keep their own implementations — cross-language wrappers cost more than the duplication. They MUST agree on the regex value.
## Per-client gotchas (read on touch)
Each client has 3–5 non-obvious behaviors that bit us. **Before modifying that client's files, skim its section in [references/per-client-gotchas.md](./references/per-client-gotchas.md).** Sample of what's there:
- **Claude Code**: `${user_config.*}` works in both `mcp.json::env` AND hook commands; keychain is the SoT for credentials.
- **Codex CLI**: `${user_config.*}` is NOT substituted; subprocess env is **cleared** before MCP spawn → MUST list every needed var in `env_vars: [...]`; `${CLAUDE_PLUGIN_ROOT}` doesn't work in MCP args.
- **Hermes Agent**: `plugin.yaml::hooks: [...]` array **gates lifecycle invocation** — overriding a method without listing the hook is a silent no-op.
- **opencode**: every named export of a plugin file is invoked as a Plugin function — export ONLY `RembricPlugin`. The bridge MUST live outside `~/.config/opencode/plugins/`. Sub-agent filtering (`parentID || title.endsWith(" subagent)")`) is mandatory.
- **Pi**: no built-in MCP (its own docs say so, deliberately) → this is the one client whose plugin holds the MCP client, discovering tools with `tools/list`; tools register under provider-safe names (`.`→`_`) because a real provider rejects the whole payload on a dot; shutdown is awaited and **two Ctrl-C presses within 500 ms fire it in the interactive TUI** (a single press does not, and print mode registers no SIGINT); nothing is injected from its settings file, so credentials come from the shell.
## Shared-vs-divergent discipline
Per memory `01KRNZM2VFCME5HNT8N78HZW18`: shared logic lives in shared paths. Divergence is allowed ONLY when the platform forces it.
- **MUST be shared**: `apps/plugin/mcp-bridge/rembric-dotenv.mjs`, `apps/plugin/bin/rembric-plugin-core.mjs`, `apps/plugin/scripts/*.sh` (Claude+Codex hooks), `apps/plugin/commands/*.md` (Pi consumes them verbatim as prompt templates — reference them, never copy). The published `@rembric/mcp-bridge` package is the shared stdio transport; there is no repository launcher to copy.
- **Legitimately divergent today**: `hooks/hooks.json` vs `hooks/hooks.codex.json` (env-substitution rules differ); `.claude-plugin/mcp.json` vs `.codex-plugin/mcp.json` (Claude keychain substitution differs from Codex's curated `env_vars`); Python in-process provider for Hermes; JS/TS in-process for opencode; the MCP transport in `.pi-plugin/index.ts`, because that host has no MCP client for the published bridge to plug into.
Sanity check: `git ls-files apps/plugin/` should show ONE copy of each shared resource. Two paths with near-identical content is a sync bug.
## Install / uninstall script invariants
- **Never edit the user's agent config file** (`settings.json`, `config.toml`, `opencode.json`, `~/.hermes/config.yaml`). Print the snippet with `<PLACEHOLDERS>`; the user pastes.
- **Idempotent**: a second run produces an empty diff.
- **`chmod 644` for files invoked as `node <path>`** (the +x bit isn't needed for `node x.mjs`).
- **Fail loudly on missing source files.**
## Don't overcommit the spec to behavior the server doesn't support
Before adding a handler that POSTs to an endpoint, verify the endpoint exists:
```bash
grep -rnE "export (async )?function (GET|POST)" apps/web/src/app/api/
```
If it doesn't exist: either add the endpoint first (separate OpenSpec change) OR drop the handler from this scope (`tasks.md` says DEFERRED, change CHANGELOG documents it). Don't ship a handler that 404s.
This bit us in `add-opencode-plugin` — first iteration speced `chat.message` POSTing to `/prompts/passive`, which didn't exist. Cost: spec rework + scope reduction post-merge.
## Adding a brand-new client
Always start with a Phase 0 spike in `tasks.md` that validates platform assumptions BEFORE any plugin code — the platform assumption you can't cheaply prove is the one that sinks the change. Spike the cwd/PWD propagation chain and the hook/lifecycle delivery shape for the new host before writing any plugin code.
The spike's outcome MUST be recorded as a comment in BOTH `tasks.md` (`<!-- spike result: plan-a -->`) and the plugin's source file (`// cwd-spike-result: plan-a`).
## End-to-end validation discipline
**You MUST exercise the change against a real Rembric dev stack before reporting work as done**, unless local e2e is genuinely impossible. Spec validation and unit tests are necessary but not sufficient for plugin work — the multi-process flow (agent → bridge → MCP → HTTP → SQLite → dashboard) breaks in non-obvious places.
Minimum required steps:
1. `REMBRIC_DATA_DIR=./data-dev REMBRIC_ADMIN_TOKEN=<16+-char-token> pnpm run dev` (host Next.js dev, `http://127.0.0.1:3000`). Wait for the server to answer `/healthz`, then capture the seeded `demo-writer` token from `seed-dev`'s output (see [references/e2e-walkthrough.md](./references/e2e-walkthrough.md) §1-2).
2. Install the plugin (`bash apps/plugin/.<X>-plugin/install.sh`), configure the client end-to-end with real URL + real token, drop `.rembric` with `PROJECT_SLUG=demo` in the working directory.
3. Exercise the lifecycle path your change affects. The exact commands per client (opencode `mcp list`, tsx-driven handler invocation, dashboard SQLite verification, etc.) are in [references/e2e-walkthrough.md](./references/e2e-walkthrough.md).
4. Tear down: stop the dev server, uninstall, restore user's config file to its prior state (placeholders if it didn't exist before).
**Prefer the isolated variant of steps 2-4 when the client's CLI supports it** — scratch `HOME`, per-run extension load instead of install, deliberately invalid API key. It touches none of the operator's config, so there is nothing to restore and no half-restored state to leave behind, and it costs no LLM calls. Six rails plus the paired-control method: [references/e2e-walkthrough.md § 5b](./references/e2e-walkthrough.md).
**If you still cannot drive the agent TUI** (a client with no per-run load, or keychain integration you can't script):
- **Direct handler invocation via `pnpm exec tsx`** covers ~95% of the handler logic. Pattern from `add-opencode-plugin`: import the installed plugin module, set env vars, call handlers with mock inputs that mirror the platform's event shape.
- **Tell the user explicitly** what you DID verify vs what you DID NOT. Don't say "verified e2e" when you only ran unit tests. List the manual smoke steps the user should run.
**If local e2e is genuinely impossible** (Codex `/hooks` interactive trust gate, Claude Code keychain integration, tool you don't have installed): say so out loud. Tell the user:
> "I can't drive `<specific path>` from this environment. Unit tests cover `<list>`. Manual verification needed for `<list>`. Want the steps?"
Honest > glossing-over.
## Self-check before commit
If any answer is "no" or "I don't know", stop and resolve it.
- [ ] OpenSpec change open or amended for this work
- [ ] Two-track release respected (`server` + unified `plugin`; no node-workspace/cascade; all five clients share the one `plugin` version via `extra-files`; plugin release never rebuilds Docker)
- [ ] No duplication of `parseDotenv` / `SLUG_RE` / the `rembric-plugin-core` helpers / endpoint strings without justification
- [ ] `pnpm vitest run` + `pnpm run typecheck` + `pnpm run lint` + `openspec validate <change> --strict` all clean
- [ ] Exercised against a local dev server (`pnpm run dev`, :3000) (or explicitly told the user what isn't verified)
- [ ] Docs sweep done (README, docs/agents.md, apps/plugin/README.md, in-plugin README, CHANGELOG)
- [ ] Install/uninstall idempotent (verified by running twice)
- [ ] User's local state restored (dev stack down, plugin uninstalled, config placeholders)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!