- name
- generation-standards
- description
- House style for every file plugin-forge emits into a generated plugin. Load before writing or reviewing any generated SKILL.md, agent, hooks.json, hook script, .mcp.json, plugin manifest, marketplace entry, README, or settings snippet — and before ship runs the freshness-guard sweep. Covers description discipline, invocation-control decisions, paired-path allowed-tools, persistent-state placement, hook output discipline, degrees-of-freedom mapping, and the deprecated-shape greps.
- user-invocable
- false
# Generation standards — plugin-forge house style
These rules govern every file the build loop writes into a target plugin and every
file plugin-forge itself ships. Apply them at generation time: plugin skills cannot be
fixed post-install via `skillOverrides`, and a stale shape emitted today is a silently
dead component on the user's machine tomorrow. Fill from the templates in
`templates/` (index at the end of this file); the FRESHNESS GUARD greps below are the
exact block ship runs before packaging.
## 1. Description discipline
- Write `description` as **trigger conditions only** — when to invoke, never a summary
of the workflow. A description that summarizes the steps makes Claude follow the
description and skip the body (empirically observed by the superpowers CSO testing).
- Put the key use case in the **first 200 characters**. The listing may be cut early
under budget pressure; the front of the string does all the routing work.
- `description` + `when_to_use` combined must be **≤ 1,536 characters** — the listing
truncates there. Include the natural keywords users actually type (error messages,
symptoms, task nouns), not internal jargon.
- The skill listing budget is ~1% of the context window shared across all installed
skills. Every generated description competes with every other plugin on the user's
machine: shorter is safer; only `disable-model-invocation: true` skills cost
nothing in the listing (`user-invocable: false` skills still carry their full
description — the flag only hides them from the `/` menu).
- Third-person imperative everywhere: "Use when...", "Covers...", never "I will..."
or "This skill helps you...".
## 2. SKILL.md size and structure
- **< 500 lines.** The body is a recurring token cost; it persists for the whole
session once invoked.
- Write **standing instructions** (rules that hold every turn), not narration or
one-time setup steps.
- Critical guidance goes in the **first 5,000 tokens**: auto-compaction re-attaches
only the first 5,000 tokens of each invoked skill, inside a 25,000-token combined
budget, most-recently-invoked first. Tails of long skills vanish.
- Bulk material goes to `references/` (one level deep). SKILL.md must name each
supporting file with what it contains and when to load it, e.g.
`- For the full grader taxonomy, see references/<topic>.md — load when choosing graders.`
- Never use `@`-links to files (they force-load content into context). Never put
README/CHANGELOG inside a skill directory (plugin-level README is separate and
required).
- Scripts live in `scripts/` under the skill and are **executed, not loaded**.
## 3. Paths and portability
### Paired-path allowed-tools (the zero-prompt bundled-script pattern)
Every bundled script a generated skill runs gets an `allowed-tools` rule whose string
is **IDENTICAL** to the invocation string in the body. Make the script executable with
a shebang and invoke it by path (no `python3 ` / `bash ` prefix — a prefix breaks the
rule match):
```yaml
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.py *)
```
and in the body:
```
${CLAUDE_SKILL_DIR}/scripts/render.py --input data.csv
```
- Skill-local assets → `${CLAUDE_SKILL_DIR}/…`. Plugin-level assets (hooks/, bin/,
shared scripts/) → `${CLAUDE_PLUGIN_ROOT}/…`. Never relative paths, never `../`
escapes (the marketplace cache copy breaks them; symlinks outside the marketplace
are skipped).
- `${CLAUDE_SKILL_DIR}` inside `allowed-tools` requires Claude Code ≥ 2.1.129 (older
versions treat it as a literal string that never matches, so every run prompts).
State the floor in the generated README.
- `allowed-tools` grants last **one turn** — they clear on the user's next message
even though skill content persists. Multi-turn workflows need a `permissions.allow`
rule in the consumer settings snippet instead (see `templates/settings-snippet.tmpl.json`).
### Persistent state → ${CLAUDE_PLUGIN_DATA}, never ROOT
- `${CLAUDE_PLUGIN_ROOT}` is the install directory: it **changes on every update**
and old copies are garbage-collected (~14 days). Anything written there is lost.
- Durable data — traces, run artifacts, caches, learned state — goes to
`${CLAUDE_PLUGIN_DATA}` (survives updates, created on first reference, removed on
last-scope uninstall unless `--keep-data`).
- Both are substituted in skill/agent content, hook and monitor commands, and MCP/LSP
config fields, and exported as env vars to hook/MCP/LSP subprocesses.
## 4. Invocation control — decide per skill, record the rationale
| Situation | Frontmatter | Effect |
|---|---|---|
| Side-effectful, user-gated workflow (deploy, publish, ship, commit) | `disable-model-invocation: true` | Only the user invokes. Description never enters context (costs nothing). ALSO blocks Skill-tool programmatic invocation, subagent preloading, and scheduled-task invocation. |
| Pure background knowledge (schemas, conventions, house style) | `user-invocable: false` | Hidden from the `/` menu; the model loads it on demand. Does NOT block Skill-tool access. |
| Conventions relevant only to certain files | `paths:` globs | Auto-activation limited to matching files; cuts false triggers and listing cost. |
| Skill that must be BOTH user-callable and chainable by a conductor | neither flag (default) | Both can invoke. Guard misuse inside the body (state checks), not with invocation flags. |
- **The D2 worked example (record it in every PDR):** a conductor cannot Skill-tool
invoke a skill marked `disable-model-invocation: true`. Pipelines that chain phase
skills must leave those skills on default invocation and self-guard by reading their
state file, reserving `disable-model-invocation: true` for the entry and exit points
only. plugin-forge itself does exactly this: only `forge` and `ship` carry the flag.
- Write one line of rationale per skill into the PDR slot ledger. "Default because
nobody decided" is a finding, not a rationale.
- Booleans are `true`/`false` only — `yes`/`on`/`1` need ≥ 2.1.218 and silently
degrade on older versions.
## 5. Hook output discipline
- **Exec-form commands only**: `"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/x.sh"`
(plus optional `"args": []`). `${user_config.*}` is rejected in shell-form fields —
pass it via exec-form args or read `CLAUDE_PLUGIN_OPTION_<KEY>` from the env.
- Every hook script: `chmod +x`, `set -euo pipefail`, read stdin exactly once with
`INPUT="$(cat)"`, parse JSON with `python3 -c` (stdlib), and take a **fast no-op
path before reading stdin** when the guard cannot apply (< 10ms exit 0).
- **Exit-code contract**: exit 0 = no objection (it does NOT force-approve PreToolUse;
normal permission flow continues). Exit 2 = block, with the reason on **stderr**
(fed back to Claude). Any other exit code = action proceeds with a logged error.
- **Never mix exit 2 with JSON output.** Stdout JSON is IGNORED when the script exits 2.
Choose per hook: stderr-then-exit-2, OR exit 0 + structured JSON. Not both.
- Per-event output schemas differ — emit the right one:
- `PreToolUse`: `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow|deny|ask","permissionDecisionReason":"…"}}`.
Never the deprecated `{"decision":"approve"}` shape.
- `PostToolUse`, `Stop`: top-level `{"decision":"block","reason":"…"}` (blocking only;
exit 0 silent otherwise). Stop scripts must honor `stop_hook_active` to avoid loops.
- `UserPromptSubmit` / `SessionStart`: stdout enters context; structured additions go
in `hookSpecificOutput.additionalContext` (a top-level `additionalContext` key is
silently ignored).
- Matchers: event names are case-sensitive; tool matchers are regex (`Edit|Write`).
Matchers and permission rules that reference a **bundled** MCP server's tools must
use the plugin-scoped form `mcp__plugin_<plugin>_<server>__<tool>` — a bare
`mcp__<server>__<tool>` never fires for bundled servers.
- A malformed `hooks/hooks.json` prevents the ENTIRE plugin from loading. Validate the
JSON before shipping.
- Every generated hook ships a fixture test: `echo '<recorded event JSON>' | script.sh`
with asserted exit code and output channel. Hooks are code; untested hooks are prose.
## 6. Degrees-of-freedom mapping
Match the freedom level of each capability to its fragility, and let it drive the
whole composition (this is the C3/C5 backbone of the PDR):
| Freedom | Use for | Generate | Enforce with | Grade with |
|---|---|---|---|---|
| **Low** | Fragile/regulated operations: lab protocols, finance postings, destructive migrations, data-integrity rules | Locked, deterministic bundled scripts (stdlib-only), invoked via paired-path allowed-tools | PreToolUse/PostToolUse hooks (mechanical denial, not instructions) | Deterministic graders: workspace/file state, exit codes, `state_check`; numeric checks REQUIRE a `tolerance` |
| **Medium** | Repeatable procedures with judgment at the edges | Stepwise checklists / pseudocode in skill bodies | Prose + spot-check hooks | Transcript graders (tool_called, budgets) + judges for the edges |
| **High** | Judgment work: analysis, synthesis, review, writing | Prose skills with principles and worked examples | Prose (justify "prose" in the PDR C3 column) | Calibrated LLM judges — one rubric dimension per judge call, Unknown verdict allowed |
If a capability's enforcement column says "prose" but its failure cost is high, the
composition is wrong: push it down a row (script + hook + deterministic grader).
## 7. Scripts, manifests, and layout
- Python: stdlib only, `#!/usr/bin/env python3`, no third-party deps, no install step.
Bash: `#!/usr/bin/env bash`, `set -euo pipefail`, shellcheck-clean.
- Manifest (`.claude-plugin/plugin.json`): include
`"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json"`,
kebab-case `name`, full metadata. **No `version` field during development** —
omitted means the git SHA is the version and every commit reaches users; a pinned
semver that never gets bumped strands users on a stale cache. Pin semver at release
only (ship's job).
- Component directories (`skills/`, `agents/`, `hooks/`, `.mcp.json`, `bin/`, …) live
at the **plugin root**, never inside `.claude-plugin/` (only `plugin.json` goes
there — the #1 documented mistake).
- Never scaffold a `commands/` directory: it is the legacy form. Every entry point is
`skills/<name>/SKILL.md`.
- Always set frontmatter `name` on plugin skills (it becomes the command's last
segment; without it, marketplace installs can surface a version-string name).
- Live-state skills (dynamic `!`-backtick injection headers) need a documented
fallback for environments with `disableSkillShellExecution` — the injected line
degrades to a policy notice, so the body must say what to do when the state read
is absent.
## 8. FRESHNESS GUARD — the greps ship runs
Deprecated shapes rot silently: the host toolkit's own hook-builder emitted the old
`{"decision":"approve"}` JSON and skill-forge referenced dead `/mnt/skills` paths.
Before packaging, run this exact block against the generated plugin directory. Any
FG hit is a blocking finding — fix the emitter, not just the output.
```bash
PLUGIN_DIR="${1:?usage: freshness-guard <generated-plugin-dir>}"
FAIL=0
# FG-1: deprecated hook decision JSON — modern PreToolUse uses
# hookSpecificOutput.permissionDecision, never {"decision":"approve"}.
grep -rnE '"decision"[[:space:]]*:[[:space:]]*"approve"' "$PLUGIN_DIR" && FAIL=1
# FG-2: stale sandbox doc paths — /mnt/skills does not exist in Claude Code.
grep -rn '/mnt/skills' "$PLUGIN_DIR" && FAIL=1
# FG-3: bare MCP tool names — bundled-server tools are
# mcp__plugin_<plugin>_<server>__<tool>; bare mcp__<server>__<tool> never fires.
grep -rnE 'mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_-]+' "$PLUGIN_DIR" | grep -v 'mcp__plugin_' && FAIL=1
# FG-4: legacy commands/ scaffolding — entry points are skills/<name>/SKILL.md.
[ -d "$PLUGIN_DIR/commands" ] && { echo "$PLUGIN_DIR/commands: legacy commands/ directory"; FAIL=1; }
grep -rn '"commands"' "$PLUGIN_DIR/.claude-plugin/plugin.json" 2>/dev/null && FAIL=1
exit "$FAIL"
```
Reading the results:
- **FG-1** has no legitimate hits. `{"decision":"block"}` remains valid for
PostToolUse/Stop and is deliberately NOT flagged.
- **FG-3** allows one reviewed exception: references to servers the plugin does *not*
bundle (user-level or external MCP servers) legitimately use unscoped names. Confirm
each hit is external before waiving it; note the pipeline can miss a bare name that
shares a line with a scoped one, so scan multi-reference lines manually.
- **FG-4**: `"commands"` in the manifest is only valid when deliberately remapping the
legacy dir — for generated plugins, treat any hit as a failure.
- **Self-scans**: this file necessarily contains the patterns it hunts. When running
the guard over plugin-forge itself (dogfood lint), exclude
`skills/generation-standards/SKILL.md`; generated plugins never contain it, so ship
runs the block verbatim.
Advisory greps (warn, do not fail): `grep -rn 'streamable-http' "$PLUGIN_DIR"`
(`"streamable-http"` is an accepted alias for `"http"`; prefer the canonical
`"type": "http"` in generated `.mcp.json` for consistency), and
`grep -rn 'dangerously-skip-permissions' "$PLUGIN_DIR"` (generated harnesses use a
sandbox settings profile instead — see `templates/settings-snippet.tmpl.json`).
## 9. Templates index
All templates carry `{{PLACEHOLDER}}` markers and a header note naming the filler
(the build loop). JSON templates hold their notes in `"//"` keys — **delete every
`"//"` key and template comment after filling**. Load a template only when emitting
that file type.
| Template | Emits | Load when |
|---|---|---|
| [templates/skill.tmpl.md](templates/skill.tmpl.md) | `skills/<name>/SKILL.md` | Writing any generated skill |
| [templates/agent.tmpl.md](templates/agent.tmpl.md) | `agents/<name>.md` | Writing any generated subagent |
| [templates/hooks.tmpl.json](templates/hooks.tmpl.json) | `hooks/hooks.json` | Wiring generated hook events |
| [templates/hook-script.tmpl.sh](templates/hook-script.tmpl.sh) | `hooks/scripts/<name>.sh` | Writing any hook handler |
| [templates/mcp.tmpl.json](templates/mcp.tmpl.json) | `.mcp.json` | Bundling an MCP server |
| [templates/plugin-manifest.tmpl.json](templates/plugin-manifest.tmpl.json) | `.claude-plugin/plugin.json` | Creating the manifest |
| [templates/marketplace-entry.tmpl.json](templates/marketplace-entry.tmpl.json) | entry in `marketplace.json` `plugins[]` | Registering in a marketplace |
| [templates/readme.tmpl.md](templates/readme.tmpl.md) | plugin `README.md` | Writing the plugin's README (ship phase) |
| [templates/settings-snippet.tmpl.json](templates/settings-snippet.tmpl.json) | consumer `.claude/settings.json` snippet | Emitting the team-distribution / permission story |
GitHubで見る