Scaffold a mod the governed way, instead of an ad-hoc one: the template already follows the rules ruflo's own mod (ADR-404) was reviewed against.
<name>. Commands it registers are namespaced <name>-<verb>; never register a built-in's name (/diff, /help) or another plugin's (/ruos*, ruflo-*).node plugins/ruflo-plugin-creator/scripts/scaffold-mod.mjs <name> <target-dir>. It copies templates/mod/, turns the manifest template plugin.json.tmpl into .claude-plugin/plugin.json (the template keeps it under a non-manifest name so plugin validators do not mistake templates/mod for a second plugin), and replaces my-mod everywhere (plugin.json name, the command, MY_MOD_ACTIVE → <NAME>_ACTIVE in both hooks/register.ts and hooks/classic.cjs, the status folder). Doing it by hand: copy the folder, move plugin.json.tmpl to .claude-plugin/plugin.json, and make the same replacements.hooks/status.ts writes .claude-flow/<name>-mod/status.json, the file the console's Mods section reads. The folder must be <name>-mod (lowercase, digits, hyphens, at most 41 characters): the scaffold script already sets STATUS_PATH to <name>-mod. Fields: version: 1 (required, anything else is ignored), summary (one line), modVersion, guard (boolean), calls (a number, not a per-tool object), blocked (number), lastDenied (a short reason, only once the mod has refused something), plus updatedMs/startedMs. Other fields are not shown. tests/status.test.ts asserts the shape.Host (see hooks/host.ts). In register.ts, build the host from literal $.noun.method(...) calls only — the engine reads a module's $ uses off its source, and claude plugin validate refuses $ passed around, computed ($[noun]), or deeper than $.noun.method(input).const r = await next(e); …; return r; rewrite with next({ ...e, … }); answer ({ deny }) only when you mean to stop the chain.tool.check, only ever tighten (the stricter of await next(e) and yours); never answer allow of your own.$ call so a refusal degrades instead of failing the turn (admins can withhold any $ affordance).$.process.run / $.http.fetch built from event input. Nothing slow on the hot path (about 10 s per dispatch).$.store / $.state, not module variables.userConfig in plugin.json, not environment variables.hooks.json is hybrid: the classic command hook runs everywhere, the module only where function hooks are on (Claude Code >= 2.1.287; 2.1.277+ with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1). The module sets <NAME>_ACTIVE at session.start; the classic script exits early when it sees it, so nothing fires twice.```bash
claude plugin validate
marketplace.json entry), installed with /plugin install <name>@<marketplace> and picked up with /reload-plugins. If ruflo-mods is installed with modTrust: refuse-risky, a mod using process.run/process.spawn, http.fetch, mcp.call/mcp.connect, session.send, config.set, env.set, fs.write, agent.spawn/agent.register, prompt.submit, session.append, tool.call or command.run, or hooking any event in its RISKY_EVENTS (tool.check, tool.call, prompt.*, agent.spawn, tool.describe, session.append, session.receive, turn.step, skill.prompt, session.compact, prompt.edit, fs.read/fs.stat/fs.exists/fs.list, settings.read, env.get, a hook on any of the calls above (it can rewrite another mod's call), plugin.register, classic.*, *; a glob or a ! negation is judged by what it selects), is refused unless its plugin id (<name>@<marketplace>) is in modTrustAllow — the template sets env.set (the fallback handshake), so allow-list it by id.