- name
- ship
- description
- Package and release a verified forge-built plugin: claude plugin validate --strict, frontmatter/scoped-name/freshness lints, README with the permission story, versioning decision (SHA vs pinned semver), distribution menu (skills-dir, private marketplace, community), and installing the trace-capture flywheel. Use when the user says ship, release, publish, or distribute the plugin.
- when_to_use
- Only when .forge/state.json phase is ship (verification green). User-gated release: the user invokes /plugin-forge:ship; the model never auto-invokes it.
- argument-hint
- [skills-dir|marketplace|community]
- disable-model-invocation
- true
- allowed-tools
- Bash(claude plugin *), Bash(grep *), Bash(python3 *), Bash(mkdir *), Bash(cp *), Bash(chmod +x *)
# Ship — validate, lint, document, version, distribute
Current forge state: !`cat .forge/state.json 2>/dev/null || echo NO_STATE`
If the line above shows a policy notice instead of JSON (org setting
`disableSkillShellExecution`), read `.forge/state.json` with the Read tool
before continuing.
## Phase gate (hard)
Proceed only when `phase` is `ship` — verification must already be green. For
any other state, run NOTHING and reply with exactly the remediation below,
then stop:
| Injected state | Reply and stop |
|---|---|
| NO_STATE | "No forge run in this project. Run `/plugin-forge:forge` to start one." |
| `interview` | "Interview incomplete. Run `/plugin-forge:forge`." |
| `contract` | "No approved contract yet. Run `/plugin-forge:write-contract`." |
| `evals` | "Suite not armed. Run `/plugin-forge:build-evals`, then `/plugin-forge:arm-evals`." |
| `armed` | "Nothing built yet. Run `/plugin-forge:build-loop`." |
| `building` | "Build loop has not gone green. Run `/plugin-forge:build-loop`." |
| `smoke` or `verify` | "Verification is not complete. Run `/plugin-forge:verify`." |
Inputs to read before Step 1: `plugin_name` and `plugin_dir` from the injected
state; `design/CONTRACT.md` (C6 distribution decisions, runtime answers —
cloud/routine or local, version floors, permission story, cost sheet); the
latest `runs/<ts>/verify-report.md` (baseline numbers for the README); and
`.forge/freeze.json` (suite version).
## Step 1 — Static gates (all four pass before anything is written)
**1a. Strict validation** — the same check the community pipeline runs:
```
claude plugin validate --strict <plugin_dir>
```
**1b. Frontmatter YAML lint** on every generated SKILL.md. Malformed YAML
fails silently: the body loads with no metadata, so the skill never
auto-triggers. Run:
```
python3 - <plugin_dir> <<'EOF'
import sys, re, pathlib
root, bad = pathlib.Path(sys.argv[1]), []
files = sorted(root.glob('skills/**/SKILL.md')) + sorted(root.glob('SKILL.md'))
for f in files:
t = f.read_text(encoding='utf-8')
m = re.match(r'\A---\r?\n(.*?)\r?\n---\r?\n', t, re.S)
if not m:
bad.append(f'{f}: frontmatter fence missing or unclosed'); continue
fm = m.group(1)
tops = [(k.group(1), k.start()) for k in re.finditer(r'^([A-Za-z][\w-]*):', fm, re.M)]
names = [k for k, _ in tops]
for req in ('name', 'description'):
if req not in names: bad.append(f'{f}: missing required key {req}:')
if '\t' in fm: bad.append(f'{f}: literal tab in frontmatter (YAML hazard)')
for b in re.finditer(r':\s*(yes|no|on|off)\s*$', fm, re.M | re.I):
bad.append(f'{f}: boolean "{b.group(1)}" needs >=2.1.218 - use true/false')
def block(key):
d = dict(tops)
if key not in d: return ''
starts = sorted(s for _, s in tops); i = starts.index(d[key])
return fm[d[key]:starts[i+1]] if i + 1 < len(starts) else fm[d[key]:]
if len(block('description')) + len(block('when_to_use')) > 1536:
bad.append(f'{f}: description+when_to_use exceed the 1536-char listing cut')
for b in bad: print('FAIL', b)
print('frontmatter lint:', 'FAIL' if bad else 'PASS')
sys.exit(1 if bad else 0)
EOF
```
**1c. Scoped-name lint** — bundled-MCP references using a bare server name
never fire; they must be `mcp__plugin_<plugin>_<server>__<tool>` (and
`plugin:<plugin>:<server>` in `mcp_tool` hook `server` fields):
```
grep -rnE 'mcp__' <plugin_dir> --include='*.md' --include='*.json' | grep -v 'mcp__plugin_'
```
Every hit in hook matchers, permission rules, agent tools lists, or skill
allowed-tools is a FAIL — unless it names an EXTERNAL server the plugin does
not bundle (check against the plugin's `.mcp.json` before failing).
**1d. Freshness lint** — deprecated shapes must not ship:
```
grep -rnE '"decision"[[:space:]]*:[[:space:]]*"(approve|block)"' <plugin_dir>
grep -rn '/mnt/skills' <plugin_dir>
```
Both greps must return nothing. The decision-JSON hook shape is deprecated
(current discipline: stderr + exit 2 to block, or `hookSpecificOutput`);
`/mnt/skills` paths are foreign-environment rot that never resolves in Claude
Code. Fix at the source, never by weakening the lint.
## Step 2 — Install the flywheel (trace capture)
```
mkdir -p <plugin_dir>/hooks/scripts
cp "${CLAUDE_PLUGIN_ROOT}/templates/hooks/trace-capture.sh" <plugin_dir>/hooks/scripts/trace-capture.sh
chmod +x <plugin_dir>/hooks/scripts/trace-capture.sh
```
Merge `${CLAUDE_PLUGIN_ROOT}/templates/hooks/hooks-snippet.json` into
`<plugin_dir>/hooks/hooks.json`: create the file from the snippet when the
plugin ships no hooks yet; otherwise append the snippet's PostToolUse entry to
the existing array — never clobber existing entries. Confirm the merged entry
is exec-form and references
`"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/trace-capture.sh"` — at the generated
plugin's runtime that variable resolves to ITS root, not to plugin-forge's.
Why this ships in every generated plugin: the hook appends tool
args/results/errors to `${CLAUDE_PLUGIN_DATA}/traces/*.jsonl`, so production
failures become eval tasks — `/plugin-forge:build-evals mine` converts traces
to tasks, `/plugin-forge:arm-evals` re-freezes vN+1, and
`/plugin-forge:build-loop` rebuilds until green. Document the loop in the
README (Step 4).
## Step 3 — Optional observability (fleet/long-job plugins)
Offer when the generated plugin dispatches subagent fleets or long jobs;
otherwise skip.
- **subagentStatusLine** — a plugin `settings.json` supports ONLY the `agent`
and `subagentStatusLine` keys:
```json
{"subagentStatusLine": {"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/status.sh"}}
```
with the script reading a `${CLAUDE_PLUGIN_DATA}/status.json` the plugin's
long-running jobs update.
- **Monitor** — pair each long-job skill with `monitors/monitors.json`:
```json
[{"name": "<job>-watch", "command": "tail -F <log path>",
"description": "streams <job> progress into the session",
"when": "on-skill-invoke:<skill-name>"}]
```
State the caveats wherever this is offered: monitors are experimental,
interactive-CLI only, do not load from project-scope skills-dir installs,
reject `${user_config.*}` in commands, and need a session restart after
plugin updates.
## Step 4 — README for the generated plugin
Instantiate
`${CLAUDE_PLUGIN_ROOT}/skills/generation-standards/templates/readme.tmpl.md`
into `<plugin_dir>/README.md`. Required sections:
1. **Overview + invocations** — what the plugin does; every user-facing skill
as `/<plugin_name>:<skill>` with its argument hint.
2. **Permission story** — a `permissions.allow` list covering EVERY command
the plugin's skills and agents run. allowed-tools grants are turn-scoped
(cleared on the next user message), so long flows need standing allows in
the consumer's `.claude/settings.json`:
`{"permissions": {"allow": ["Bash(<cmd> *)", "..."]}}`.
3. **Minimum Claude Code version** — the highest floor among features used
(from CONTRACT C6). Known floors: `${CLAUDE_SKILL_DIR}` in allowed-tools
2.1.129; `defaultEnabled` 2.1.154; marketplace `renames` 2.1.193;
`--json-schema` hard errors / LSP `restartOnCrash` 2.1.205; worktree resume
2.1.212; frontmatter `name` segment override 2.1.216; yes/no boolean forms
and background-fork default 2.1.218.
4. **Headless recipe** — the `claude -p` invocation for CI/routines, using the
suite's sandbox settings profile (never `--dangerously-skip-permissions`),
plus `CLAUDE_CODE_PLUGIN_SEED_DIR` for network-free container loading.
5. **How to run your evals** — the `evals/` layout; `evals/bin/run.py` is
vendored and stdlib-only, so the suite runs WITHOUT plugin-forge installed;
red baseline via the no-plugin target; the final `FORGE_EVAL:` line and
exit code as the CI signal; run the regression suite in CI; cost honesty
(`--max-cost-usd`); current baseline for future battle A/B: suite version
from `.forge/freeze.json`, pass^3 and cost from `verify-report.md`.
6. **Flywheel** — traces land in `${CLAUDE_PLUGIN_DATA}/traces/`; feed
failures back with `/plugin-forge:build-evals mine`.
## Step 5 — Versioning decision (ask the user)
Present exactly two options via AskUserQuestion:
- **Still iterating** — OMIT `version` from plugin.json. The git SHA becomes
the version: every pushed commit reaches users.
- **Releasing** — pin semver in plugin.json, add a CHANGELOG entry, and run
`claude plugin tag --push` (creates the `<name>--v<version>` tag that
dependency constraints resolve against).
Explain the trap before they choose: a pinned semver means users update ONLY
when the field is bumped — pushing commits under a stale pin does nothing
("already at latest version"). A plugin pinned at 1.0.0 and then iterated
strands every installed user on the first build. Never pin without a release
process that bumps it.
Also state: the plugin `name` is a permanent public identifier (the
marketplace `renames` map is the only migration path, 2.1.193+);
`displayName` is the safe-to-change label.
## Step 6 — Final re-validation
Steps 2–4 added files. Re-run on the FINAL tree:
```
claude plugin validate --strict <plugin_dir>
```
and repeat the Step 1c/1d greps over the added files. Ship nothing that is
not green here.
## Step 7 — Distribution menu
Ask via AskUserQuestion unless `$ARGUMENTS` pre-selects a path:
**A. skills-dir plugin (lightest)** — copy `<plugin_dir>` (with
`.claude-plugin/plugin.json`) to `~/.claude/skills/<plugin_name>/` (personal)
or `<repo>/.claude/skills/<plugin_name>/` (project). It loads as
`<plugin_name>@skills-dir` next session; SKILL.md edits are live, hooks/MCP
changes need `/reload-plugins`. Warn: project scope is trust-gated, does NOT
walk up to the repo root (launch Claude from the root), and monitors do not
load.
**B. Private marketplace** — add or extend `.claude-plugin/marketplace.json`
in the hosting repo:
```json
{"name": "<marketplace-name>", "owner": {"name": "<owner>"},
"plugins": [{"name": "<plugin_name>", "source": "./<relative-path>",
"description": "<one line>", "category": "<category>"}]}
```
Validate the marketplace repo with `claude plugin validate .`. Consumers run:
```
claude plugin marketplace add <org>/<repo>
claude plugin install <plugin_name>@<marketplace-name>
```
Maintenance: `claude plugin marketplace update <marketplace-name>`; inspection:
`claude plugin marketplace list --json`. Warn: marketplace names collide
globally per user (a second add replaces the first) and several names are
reserved — pick something distinctive.
**C. Community submission** — pre-submission requirements: kebab-case plugin
name (the claude.ai marketplace sync REJECTS non-kebab names);
`claude plugin validate --strict` clean (the community pipeline runs the same
check as Step 1a); README and LICENSE present; semver pinned and tagged via
`claude plugin tag --push`; public repo. Then submit the repo through the
community marketplace submission form, with the marketplace-entry fields
(name, source, description, category, tags) prepared to paste.
## Step 8 — Repo-declared settings (cloud/routine contracts)
If CONTRACT.md says the plugin runs in cloud sessions or scheduled routines,
personal installs are invisible there (cloud sessions never read
`~/.claude/skills`) — the consuming repo must declare the plugin. Instantiate
`${CLAUDE_PLUGIN_ROOT}/skills/generation-standards/templates/settings-snippet.tmpl.json`
into the consuming repo's `.claude/settings.json`:
```json
{"extraKnownMarketplaces": {"<marketplace-name>": {"source":
{"source": "github", "repo": "<org>/<repo>"}}},
"enabledPlugins": {"<plugin_name>@<marketplace-name>": true}}
```
Teammates get a one-time trust prompt, then auto-install. When the contract
includes long headless runs, fold the Step 4 `permissions.allow` block into
the same snippet.
## Step 9 — Wrap up
Report to the user:
- Gates re-passed on the final tree (Step 6).
- Files added: `hooks/scripts/trace-capture.sh`, the hooks.json entry,
`README.md`, plus `settings.json`/`monitors/monitors.json` if chosen.
- Version strategy chosen and why; distribution path with the exact consumer
commands; settings snippet emitted (or why not).
- Baseline carried into the README: suite vN, pass^3, cost, run timestamp.
- The honest claim: the plugin is "green + holdout + triaged" against suite
vN — not proven "correct".
Leave `.forge/` in place: it is the flywheel's memory (freeze hash, change
requests, run history). Phase stays `ship`. To start a fresh forge run in
this project later, `forge-eval doctor` clears stuck state safely.
GitHub에서 보기