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):
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.
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 |
skills/<name>/SKILL.md |
Writing any generated skill |
| templates/agent.tmpl.md |
agents/<name>.md |
Writing any generated subagent |
| templates/hooks.tmpl.json |
hooks/hooks.json |
Wiring generated hook events |
| templates/hook-script.tmpl.sh |
hooks/scripts/<name>.sh |
Writing any hook handler |
| templates/mcp.tmpl.json |
.mcp.json |
Bundling an MCP server |
| templates/plugin-manifest.tmpl.json |
.claude-plugin/plugin.json |
Creating the manifest |
| templates/marketplace-entry.tmpl.json |
entry in marketplace.json plugins[] |
Registering in a marketplace |
| templates/readme.tmpl.md |
plugin README.md |
Writing the plugin's README (ship phase) |
| templates/settings-snippet.tmpl.json |
consumer .claude/settings.json snippet |
Emitting the team-distribution / permission story |
1---2name: generation-standards3description: 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.4---56# Generation standards — plugin-forge house style78These rules govern every file the build loop writes into a target plugin and every9file plugin-forge itself ships. Apply them at generation time: plugin skills cannot be10fixed post-install via `skillOverrides`, and a stale shape emitted today is a silently11dead component on the user's machine tomorrow. Fill from the templates in12`templates/` (index at the end of this file); the FRESHNESS GUARD greps below are the13exact block ship runs before packaging.1415## 1. Description discipline1617- Write `description` as **trigger conditions only** — when to invoke, never a summary18 of the workflow. A description that summarizes the steps makes Claude follow the19 description and skip the body (empirically observed by the superpowers CSO testing).20- Put the key use case in the **first 200 characters**. The listing may be cut early21 under budget pressure; the front of the string does all the routing work.22- `description` + `when_to_use` combined must be **≤ 1,536 characters** — the listing23 truncates there. Include the natural keywords users actually type (error messages,24 symptoms, task nouns), not internal jargon.25- The skill listing budget is ~1% of the context window shared across all installed26 skills. Every generated description competes with every other plugin on the user's27 machine: shorter is safer; only `disable-model-invocation: true` skills cost28 nothing in the listing (`user-invocable: false` skills still carry their full29 description — the flag only hides them from the `/` menu).30- Third-person imperative everywhere: "Use when...", "Covers...", never "I will..."31 or "This skill helps you...".3233## 2. SKILL.md size and structure3435- **< 500 lines.** The body is a recurring token cost; it persists for the whole36 session once invoked.37- Write **standing instructions** (rules that hold every turn), not narration or38 one-time setup steps.39- Critical guidance goes in the **first 5,000 tokens**: auto-compaction re-attaches40 only the first 5,000 tokens of each invoked skill, inside a 25,000-token combined41 budget, most-recently-invoked first. Tails of long skills vanish.42- Bulk material goes to `references/` (one level deep). SKILL.md must name each43 supporting file with what it contains and when to load it, e.g.44 `- For the full grader taxonomy, see references/<topic>.md — load when choosing graders.`45- Never use `@`-links to files (they force-load content into context). Never put46 README/CHANGELOG inside a skill directory (plugin-level README is separate and47 required).48- Scripts live in `scripts/` under the skill and are **executed, not loaded**.4950## 3. Paths and portability5152### Paired-path allowed-tools (the zero-prompt bundled-script pattern)5354Every bundled script a generated skill runs gets an `allowed-tools` rule whose string55is **IDENTICAL** to the invocation string in the body. Make the script executable with56a shebang and invoke it by path (no `python3 ` / `bash ` prefix — a prefix breaks the57rule match):5859```yaml60allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.py *)61```6263and in the body:6465```66${CLAUDE_SKILL_DIR}/scripts/render.py --input data.csv67```6869- Skill-local assets → `${CLAUDE_SKILL_DIR}/…`. Plugin-level assets (hooks/, bin/,70 shared scripts/) → `${CLAUDE_PLUGIN_ROOT}/…`. Never relative paths, never `../`71 escapes (the marketplace cache copy breaks them; symlinks outside the marketplace72 are skipped).73- `${CLAUDE_SKILL_DIR}` inside `allowed-tools` requires Claude Code ≥ 2.1.129 (older74 versions treat it as a literal string that never matches, so every run prompts).75 State the floor in the generated README.76- `allowed-tools` grants last **one turn** — they clear on the user's next message77 even though skill content persists. Multi-turn workflows need a `permissions.allow`78 rule in the consumer settings snippet instead (see `templates/settings-snippet.tmpl.json`).7980### Persistent state → ${CLAUDE_PLUGIN_DATA}, never ROOT8182- `${CLAUDE_PLUGIN_ROOT}` is the install directory: it **changes on every update**83 and old copies are garbage-collected (~14 days). Anything written there is lost.84- Durable data — traces, run artifacts, caches, learned state — goes to85 `${CLAUDE_PLUGIN_DATA}` (survives updates, created on first reference, removed on86 last-scope uninstall unless `--keep-data`).87- Both are substituted in skill/agent content, hook and monitor commands, and MCP/LSP88 config fields, and exported as env vars to hook/MCP/LSP subprocesses.8990## 4. Invocation control — decide per skill, record the rationale9192| Situation | Frontmatter | Effect |93|---|---|---|94| 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. |95| 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. |96| Conventions relevant only to certain files | `paths:` globs | Auto-activation limited to matching files; cuts false triggers and listing cost. |97| 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. |9899- **The D2 worked example (record it in every PDR):** a conductor cannot Skill-tool100 invoke a skill marked `disable-model-invocation: true`. Pipelines that chain phase101 skills must leave those skills on default invocation and self-guard by reading their102 state file, reserving `disable-model-invocation: true` for the entry and exit points103 only. plugin-forge itself does exactly this: only `forge` and `ship` carry the flag.104- Write one line of rationale per skill into the PDR slot ledger. "Default because105 nobody decided" is a finding, not a rationale.106- Booleans are `true`/`false` only — `yes`/`on`/`1` need ≥ 2.1.218 and silently107 degrade on older versions.108109## 5. Hook output discipline110111- **Exec-form commands only**: `"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/x.sh"`112 (plus optional `"args": []`). `${user_config.*}` is rejected in shell-form fields —113 pass it via exec-form args or read `CLAUDE_PLUGIN_OPTION_<KEY>` from the env.114- Every hook script: `chmod +x`, `set -euo pipefail`, read stdin exactly once with115 `INPUT="$(cat)"`, parse JSON with `python3 -c` (stdlib), and take a **fast no-op116 path before reading stdin** when the guard cannot apply (< 10ms exit 0).117- **Exit-code contract**: exit 0 = no objection (it does NOT force-approve PreToolUse;118 normal permission flow continues). Exit 2 = block, with the reason on **stderr**119 (fed back to Claude). Any other exit code = action proceeds with a logged error.120- **Never mix exit 2 with JSON output.** Stdout JSON is IGNORED when the script exits 2.121 Choose per hook: stderr-then-exit-2, OR exit 0 + structured JSON. Not both.122- Per-event output schemas differ — emit the right one:123 - `PreToolUse`: `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow|deny|ask","permissionDecisionReason":"…"}}`.124 Never the deprecated `{"decision":"approve"}` shape.125 - `PostToolUse`, `Stop`: top-level `{"decision":"block","reason":"…"}` (blocking only;126 exit 0 silent otherwise). Stop scripts must honor `stop_hook_active` to avoid loops.127 - `UserPromptSubmit` / `SessionStart`: stdout enters context; structured additions go128 in `hookSpecificOutput.additionalContext` (a top-level `additionalContext` key is129 silently ignored).130- Matchers: event names are case-sensitive; tool matchers are regex (`Edit|Write`).131 Matchers and permission rules that reference a **bundled** MCP server's tools must132 use the plugin-scoped form `mcp__plugin_<plugin>_<server>__<tool>` — a bare133 `mcp__<server>__<tool>` never fires for bundled servers.134- A malformed `hooks/hooks.json` prevents the ENTIRE plugin from loading. Validate the135 JSON before shipping.136- Every generated hook ships a fixture test: `echo '<recorded event JSON>' | script.sh`137 with asserted exit code and output channel. Hooks are code; untested hooks are prose.138139## 6. Degrees-of-freedom mapping140141Match the freedom level of each capability to its fragility, and let it drive the142whole composition (this is the C3/C5 backbone of the PDR):143144| Freedom | Use for | Generate | Enforce with | Grade with |145|---|---|---|---|---|146| **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` |147| **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 |148| **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 |149150If a capability's enforcement column says "prose" but its failure cost is high, the151composition is wrong: push it down a row (script + hook + deterministic grader).152153## 7. Scripts, manifests, and layout154155- Python: stdlib only, `#!/usr/bin/env python3`, no third-party deps, no install step.156 Bash: `#!/usr/bin/env bash`, `set -euo pipefail`, shellcheck-clean.157- Manifest (`.claude-plugin/plugin.json`): include158 `"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json"`,159 kebab-case `name`, full metadata. **No `version` field during development** —160 omitted means the git SHA is the version and every commit reaches users; a pinned161 semver that never gets bumped strands users on a stale cache. Pin semver at release162 only (ship's job).163- Component directories (`skills/`, `agents/`, `hooks/`, `.mcp.json`, `bin/`, …) live164 at the **plugin root**, never inside `.claude-plugin/` (only `plugin.json` goes165 there — the #1 documented mistake).166- Never scaffold a `commands/` directory: it is the legacy form. Every entry point is167 `skills/<name>/SKILL.md`.168- Always set frontmatter `name` on plugin skills (it becomes the command's last169 segment; without it, marketplace installs can surface a version-string name).170- Live-state skills (dynamic `!`-backtick injection headers) need a documented171 fallback for environments with `disableSkillShellExecution` — the injected line172 degrades to a policy notice, so the body must say what to do when the state read173 is absent.174175## 8. FRESHNESS GUARD — the greps ship runs176177Deprecated shapes rot silently: the host toolkit's own hook-builder emitted the old178`{"decision":"approve"}` JSON and skill-forge referenced dead `/mnt/skills` paths.179Before packaging, run this exact block against the generated plugin directory. Any180FG hit is a blocking finding — fix the emitter, not just the output.181182```bash183PLUGIN_DIR="${1:?usage: freshness-guard <generated-plugin-dir>}"184FAIL=0185186# FG-1: deprecated hook decision JSON — modern PreToolUse uses187# hookSpecificOutput.permissionDecision, never {"decision":"approve"}.188grep -rnE '"decision"[[:space:]]*:[[:space:]]*"approve"' "$PLUGIN_DIR" && FAIL=1189190# FG-2: stale sandbox doc paths — /mnt/skills does not exist in Claude Code.191grep -rn '/mnt/skills' "$PLUGIN_DIR" && FAIL=1192193# FG-3: bare MCP tool names — bundled-server tools are194# mcp__plugin_<plugin>_<server>__<tool>; bare mcp__<server>__<tool> never fires.195grep -rnE 'mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_-]+' "$PLUGIN_DIR" | grep -v 'mcp__plugin_' && FAIL=1196197# FG-4: legacy commands/ scaffolding — entry points are skills/<name>/SKILL.md.198[ -d "$PLUGIN_DIR/commands" ] && { echo "$PLUGIN_DIR/commands: legacy commands/ directory"; FAIL=1; }199grep -rn '"commands"' "$PLUGIN_DIR/.claude-plugin/plugin.json" 2>/dev/null && FAIL=1200201exit "$FAIL"202```203204Reading the results:205206- **FG-1** has no legitimate hits. `{"decision":"block"}` remains valid for207 PostToolUse/Stop and is deliberately NOT flagged.208- **FG-3** allows one reviewed exception: references to servers the plugin does *not*209 bundle (user-level or external MCP servers) legitimately use unscoped names. Confirm210 each hit is external before waiving it; note the pipeline can miss a bare name that211 shares a line with a scoped one, so scan multi-reference lines manually.212- **FG-4**: `"commands"` in the manifest is only valid when deliberately remapping the213 legacy dir — for generated plugins, treat any hit as a failure.214- **Self-scans**: this file necessarily contains the patterns it hunts. When running215 the guard over plugin-forge itself (dogfood lint), exclude216 `skills/generation-standards/SKILL.md`; generated plugins never contain it, so ship217 runs the block verbatim.218219Advisory greps (warn, do not fail): `grep -rn 'streamable-http' "$PLUGIN_DIR"`220(`"streamable-http"` is an accepted alias for `"http"`; prefer the canonical221`"type": "http"` in generated `.mcp.json` for consistency), and222`grep -rn 'dangerously-skip-permissions' "$PLUGIN_DIR"` (generated harnesses use a223sandbox settings profile instead — see `templates/settings-snippet.tmpl.json`).224225## 9. Templates index226227All templates carry `{{PLACEHOLDER}}` markers and a header note naming the filler228(the build loop). JSON templates hold their notes in `"//"` keys — **delete every229`"//"` key and template comment after filling**. Load a template only when emitting230that file type.231232| Template | Emits | Load when |233|---|---|---|234| [templates/skill.tmpl.md](templates/skill.tmpl.md) | `skills/<name>/SKILL.md` | Writing any generated skill |235| [templates/agent.tmpl.md](templates/agent.tmpl.md) | `agents/<name>.md` | Writing any generated subagent |236| [templates/hooks.tmpl.json](templates/hooks.tmpl.json) | `hooks/hooks.json` | Wiring generated hook events |237| [templates/hook-script.tmpl.sh](templates/hook-script.tmpl.sh) | `hooks/scripts/<name>.sh` | Writing any hook handler |238| [templates/mcp.tmpl.json](templates/mcp.tmpl.json) | `.mcp.json` | Bundling an MCP server |239| [templates/plugin-manifest.tmpl.json](templates/plugin-manifest.tmpl.json) | `.claude-plugin/plugin.json` | Creating the manifest |240| [templates/marketplace-entry.tmpl.json](templates/marketplace-entry.tmpl.json) | entry in `marketplace.json` `plugins[]` | Registering in a marketplace |241| [templates/readme.tmpl.md](templates/readme.tmpl.md) | plugin `README.md` | Writing the plugin's README (ship phase) |242| [templates/settings-snippet.tmpl.json](templates/settings-snippet.tmpl.json) | consumer `.claude/settings.json` snippet | Emitting the team-distribution / permission story |