# Generation Standards

> 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.

- Skill: `aznatkoiny/generation-standards` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add aznatkoiny/generation-standards`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aznatkoiny/generation-standards/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: aznatkoiny (https://skillmd.com/u/aznatkoiny)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aznatkoiny/generation-standards

---


# 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 |

