
Skill Repo
- 56 installs
- 14 repo stars
- Updated August 4, 2026
- netresearch/skill-repo-skill
Helps with ai & agent building tasks.
About
skill-repo is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- skill-repo
- AI & Agent Building
- AI-coding skill
Skill Repo by the numbers
- 56 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,668 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netresearch/skill-repo-skill --skill skill-repoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 14 |
| Last updated | August 4, 2026 |
| Repository | netresearch/skill-repo-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Skill Repository Structure Guide
Standards for Netresearch skill repository layout and distribution.
Repository Structure
{repo-name}/
├── .claude-plugin/plugin.json # Plugin metadata (required)
├── skills/{name}/SKILL.md # AI instructions (required)
├── README.md # Human docs (required)
├── LICENSE-MIT # Code license (required)
├── LICENSE-CC-BY-SA-4.0 # Content license (required)
├── composer.json # PHP distribution (required)
├── references/ # Extended docs for >500w content
├── scripts/ # Automation
└── .github/workflows/
├── release.yml # Tag-triggered release
├── validate.yml # Caller for reusable validation
└── auto-merge-deps.yml # Caller for dep auto-mergeLicensing (Split Model)
| Path pattern | License |
|---|---|
skills/**/*.md, references/**, README.md, docs/** | CC-BY-SA-4.0 |
scripts/**, .github/workflows/**, *.sh, *.py, *.php | MIT |
composer.json, plugin.json, config files | MIT |
SPDX: (MIT AND CC-BY-SA-4.0). Copyright: Netresearch DTT GmbH. No bare LICENSE — split files only.
SKILL.md Frontmatter
---
name: skill-name # lowercase, hyphens, max 64 chars
description: "Use when <trigger conditions>"
---Body ≤500 words; description ≤1,536 chars (target 100–300, trigger first). Every references/*.md must be reachable from SKILL.md (catalog conventions — no orphans). Audit: scripts/audit-skills.sh. See `references/skill-quality.md`. Put discovery/catalog fields in README or optional YAML, not frontmatter — `references/skill-discovery-metadata.md`.
plugin.json (.claude-plugin/plugin.json)
{
"name": "skill-name",
"version": "1.0.0",
"skills": ["./skills/skill-name"],
"license": "(MIT AND CC-BY-SA-4.0)",
"author": {"name": "Netresearch DTT GmbH", "url": "https://www.netresearch.de"}
}composer.json
Name must match GitHub repo. Type ai-agent-skill. No version field (from git tags). No composer.lock.
{
"name": "netresearch/{repo-name}",
"type": "ai-agent-skill",
"license": "(MIT AND CC-BY-SA-4.0)",
"require": {"netresearch/composer-agent-skill-plugin": "*"},
"extra": {"ai-agent-skill": "skills/{name}/SKILL.md"}
}Reusable Workflow Callers
Skill repos MUST delegate CI to skill-repo-skill reusable workflows:
# .github/workflows/validate.yml
uses: netresearch/skill-repo-skill/.github/workflows/validate.yml@mainCallers: validate.yml, release.yml (here); auto-merge-deps.yml (netresearch/.github). Auto-merge/pr-quality use pull_request_target. No inline Actions. Domain reusables: docs/ARCHITECTURE.md.
Releasing
Bump PR → merge → pull main → verify parity → signed tag → push → monitor Release. Tag only after bump PR merges. Never edit installed paths (~/.claude/skills/**, ~/.claude/plugins/**); always the worktree. Multi-repo (>3) needs dry-run + approval. See references/release-discipline.md.
Installation
1. Marketplace: /plugin marketplace add netresearch/claude-code-marketplace 2. Release: Download to ~/.claude/skills/{name}/ 3. Composer: composer require netresearch/{repo-name} 4. npm: npm i -D @netresearch/agent-skill-coordinator github:netresearch/{repo-name}. Use templates/package.json.template (minimal files default; see references/installation-methods.md).
Validation
scripts/validate-skill.shCross-platform Compatibility
grep -E (not -P); bash shebangs (zsh on macOS); [[ ]] for conditionals.
References (references/)
Distribution: installation-methods, composer-setup, release-discipline, review-replies · SKILL text: skill-quality · Repo/README: repository-quality-rules, readme-template · Discovery: skill-discovery-metadata · Done: validation-checklist · Sync: marketplace-integration · Retro: materialization-contract
See Also
`agent-rules-skill`, `agent-harness-skill`, `retro-skill`.
---
Contributing: <https://github.com/netresearch/skill-repo-skill>
# Checkpoints for skill-repo
# Validates Netresearch skill repository structure and conventions
version: 1
skill_id: skill-repo
preconditions:
- type: file_exists
target: .claude-plugin/plugin.json
- type: command
pattern: "find . -path '*/SKILL.md' -not -path './.skill-repo-tools/*' -not -path './node_modules/*' | head -1 | grep -q ."
mechanical:
# License files
- id: SR-01
type: file_exists
target: LICENSE-MIT
severity: error
desc: "LICENSE-MIT must exist"
- id: SR-02
type: file_exists
target: LICENSE-CC-BY-SA-4.0
severity: error
desc: "LICENSE-CC-BY-SA-4.0 must exist"
- id: SR-03
type: file_not_exists
target: LICENSE
severity: error
desc: "Bare LICENSE file must not exist (use split licensing)"
- id: SR-04
type: contains
target: LICENSE-MIT
pattern: "Netresearch DTT GmbH"
severity: error
desc: "LICENSE-MIT must use correct entity name"
- id: SR-05
type: not_contains
target: LICENSE-MIT
pattern: "GmbH & Co. KG"
severity: error
desc: "LICENSE-MIT must not use old entity name"
# Metadata
- id: SR-06
type: json_path
target: composer.json
pattern: ".license"
severity: error
desc: "composer.json must have license field"
- id: SR-07
type: contains
target: composer.json
pattern: "(MIT AND CC-BY-SA-4.0)"
severity: error
desc: "composer.json license must be SPDX compound expression"
- id: SR-08
type: contains
target: .claude-plugin/plugin.json
pattern: "(MIT AND CC-BY-SA-4.0)"
severity: error
desc: "plugin.json license must be SPDX compound expression"
- id: SR-09
type: json_path
target: .claude-plugin/plugin.json
pattern: ".version"
severity: error
desc: "plugin.json must have version field"
- id: SR-09b
type: not_contains
target: composer.json
pattern: '"version":'
severity: error
desc: "composer.json must NOT have a version field — Composer derives version from git tags. Adding it creates maintenance drift."
- id: SR-10
type: contains
target: composer.json
pattern: "ai-agent-skill"
severity: error
desc: "composer.json type must be ai-agent-skill"
- id: SR-11
type: contains
target: composer.json
pattern: "composer-agent-skill-plugin"
severity: warning
desc: "composer.json should require composer-agent-skill-plugin"
# Required files
- id: SR-12
type: file_exists
target: README.md
severity: error
desc: "README.md must exist"
- id: SR-13
type: file_exists
target: .gitignore
severity: error
desc: ".gitignore must exist"
- id: SR-14
type: file_exists
target: renovate.json
severity: warning
desc: "renovate.json should exist for dependency updates"
- id: SR-15
type: file_exists
target: .github/workflows/release.yml
severity: error
desc: "Release workflow must exist"
- id: SR-16
type: file_exists
target: .github/workflows/auto-merge-deps.yml
severity: warning
desc: "Auto-merge caller workflow should exist"
- id: SR-17
type: file_not_exists
target: composer.lock
severity: error
desc: "composer.lock must not be committed in skill repos"
# README quality
- id: SR-18
type: contains
target: README.md
pattern: "## License"
severity: warning
desc: "README should have License section"
- id: SR-19
type: contains
target: README.md
pattern: "## Installation"
severity: warning
desc: "README should have Installation section"
- id: SR-20
type: contains
target: README.md
pattern: "Netresearch"
severity: warning
desc: "README should reference Netresearch"
# SKILL.md checks
- id: SR-21
type: command
# Rewritten to satisfy the runner's command allowlist (no `;`, no `$()`):
# pipe `find` → `head` → `xargs wc -w` → `awk` for the threshold check.
# awk uses {if ($1<=500) ok=1} END {exit !ok} so empty input (no SKILL.md
# found, or one filtered out by `-not -path`) exits non-zero — preventing
# the previous vacuous-pass when no SKILL.md existed.
# `-not -path './node_modules/*'` matches the precondition filter so SKILL.md
# files inside dependencies don't trigger false positives.
pattern: "find . -path '*/SKILL.md' -not -path './.skill-repo-tools/*' -not -path './node_modules/*' | head -1 | xargs -r wc -w | awk '{if ($1<=500) ok=1} END {exit !ok}'"
severity: error
desc: "SKILL.md must be under 500 words"
# SKILL.md frontmatter checks
- id: SR-22
type: regex
target: skills/*/SKILL.md
pattern: "^name: [a-z][a-z0-9-]{0,63}$"
severity: error
desc: "SKILL.md frontmatter name must be lowercase, hyphens only, max 64 chars"
- id: SR-23
type: regex
target: skills/*/SKILL.md
pattern: 'description: "Use when '
severity: error
desc: "SKILL.md frontmatter description must start with 'Use when'"
# Composer package checks
- id: SR-24
type: contains
target: composer.json
pattern: "ai-agent-skill"
severity: error
desc: "composer.json extra must reference ai-agent-skill entry point"
- id: SR-25
type: json_path
target: .claude-plugin/plugin.json
pattern: ".name"
severity: error
desc: "plugin.json must have name field"
- id: SR-26
type: json_path
target: .claude-plugin/plugin.json
pattern: ".skills"
severity: error
desc: "plugin.json must have skills array"
- id: SR-27
type: json_path
target: .claude-plugin/plugin.json
pattern: ".author"
severity: warning
desc: "plugin.json should have author field"
# === WORKFLOW QUALITY CHECKS ===
- id: SR-28
type: regex
target: .github/workflows/auto-merge-deps.yml
pattern: 'pull_request_target'
severity: error
desc: "Auto-merge workflow must use pull_request_target trigger (not pull_request) for bot PR write permissions. LLM review SR-34 validates full correctness"
# === CROSS-PLATFORM COMPATIBILITY ===
- id: SR-29
type: command
pattern: "! grep -qr --include='*.sh' --exclude-dir=node_modules --exclude-dir=.git 'grep.*-P ' scripts/ .github/workflows/ 2>/dev/null"
severity: warning
desc: "Shell scripts must not use grep -P (Perl regex) — not available on macOS BSD grep. Use grep -E (extended regex) instead"
# Caller release workflow checks
- id: SR-30
type: regex
target: .github/workflows/release.yml
# Use POSIX bracket class instead of `\s`. The runner's YAML parser
# captures the raw text between double quotes, so `\\s` reaches grep
# as literal backslash-s and never matches. `[[:space:]]` works under
# both grep -E and grep -P regardless of YAML escape semantics.
pattern: "^[[:space:]]*tags:"
severity: error
desc: "Release workflow must trigger on tag push (releases require a signed annotated tag pushed locally)"
- id: SR-31
type: command
# Strip full-line comments before scanning so the docstring describing
# the historical anti-pattern doesn't trip the check. The previous
# `awk 'NF && $0 !~ /…/'` form contained `&&`, which the runner's
# allowlist rejects as a command-chaining metacharacter — replaced
# with a `grep -v` comment-strip pipeline that uses only `|`.
# Policy enforced (per skill-repo-skill PR #15, which removed the
# bump-job): no `workflow_dispatch:` trigger and no `--admin` merge
# in the caller release workflow. Manual dispatch on the wrapper
# would re-introduce the unsigned-tag bug the bump-job removal fixed.
pattern: "! grep -v '^[[:space:]]*#' .github/workflows/release.yml | grep -qE '^[[:space:]]*workflow_dispatch:|--admin'"
severity: warning
desc: "Release workflow should not declare workflow_dispatch or use --admin merges — the auto-bump path produced unsigned tags. Use signed tag-push only (see skill-repo-skill PR #15)."
llm_reviews:
- id: SR-32
domain: repo-health
prompt: |
Review the README.md for a Netresearch skill repository:
1. Does it clearly explain what the skill does?
2. Does it have Installation section with marketplace/release/composer options?
3. Does the License section correctly describe split licensing (MIT + CC-BY-SA-4.0)?
4. Is the structure diagram accurate (shows LICENSE-MIT, LICENSE-CC-BY-SA-4.0)?
severity: warning
desc: "README structure and content quality"
- id: SR-33
domain: repo-health
prompt: |
Review the SKILL.md for a Netresearch skill:
1. Does frontmatter have only name + description fields?
2. Does description start with "Use when"?
3. Is the content clear and actionable for an AI agent?
4. Does it reference extended docs in references/ where appropriate?
severity: warning
desc: "SKILL.md clarity and completeness"
# === REUSABLE WORKFLOW USAGE ===
- id: SR-34
domain: ci
prompt: |
Check if the skill repo uses reusable workflows from skill-repo-skill:
1. Verify .github/workflows/ contains callers that delegate to
netresearch/skill-repo-skill/.github/workflows/<name>.yml@main.
2. Baseline expectation: at minimum a caller for validate.yml and
release.yml. For the canonical list of all reusable workflows
hosted in skill-repo-skill, fetch
https://raw.githubusercontent.com/netresearch/skill-repo-skill/main/docs/ARCHITECTURE.md
and read the "Reusable CI Workflows" section — do NOT rely on
a list embedded in this prompt, since enumerating it here has
caused drift before.
3. Auto-merge for Dependabot/Renovate PRs is NOT hosted in
skill-repo-skill. The reusable workflow lives at
netresearch/.github/.github/workflows/auto-merge-deps.yml@main
and is called via a thin local caller (commonly named
.github/workflows/auto-merge-deps.yml in consuming repos).
Flag any caller that points at netresearch/skill-repo-skill for
auto-merge — that path is wrong.
4. Flag any workflow that reimplements logic available in
skill-repo-skill's reusable workflows (e.g., inline validation).
5. Exception: repo-specific test workflows (e.g., validate-agents.yml
in agent-rules-skill) that test repo-specific logic may use
reusable workflows from skill-repo-skill but also contain inline
steps.
Report missing reusable workflow usage, inline reimplementations,
and any caller that targets the wrong source repo.
severity: warning
desc: "Skill repos should use reusable workflows from skill-repo-skill for CI standardization"
- id: SR-35
domain: release
severity: error
desc: "Caller release workflow permissions must satisfy all shared workflow job requirements"
prompt: |
Review .github/workflows/release.yml as a caller of a reusable workflow.
After the bump-job removal in skill-repo-skill PR #15, the shared workflow
no longer creates release PRs, so `pull-requests: write` is NOT required.
The release job needs:
- contents: write (for publishing the GitHub Release)
- id-token: write (OIDC for sigstore: cosign sign-blob + attest)
- attestations: write (GitHub native attestation API for SLSA build provenance)
GitHub validates ALL job-level permissions at startup, even for skipped jobs.
Check: Does the caller grant all three? Flag missing permissions as error
(missing id-token: write or attestations: write causes the cosign / attest
steps to fail; missing contents: write blocks release creation).
Note: An older revision of this checkpoint required `pull-requests: write`
for the now-removed auto-bump path. Do not flag its absence.
- id: SR-36
domain: release
severity: warning
desc: "plugin.json version must stay in sync with latest git tag"
prompt: |
Check version synchronization:
1. Read version from .claude-plugin/plugin.json
2. Get latest git tag (strip 'v' prefix)
3. If plugin.json version is BEHIND the latest tag, flag as warning (version drift)
4. If AHEAD, flag as info (pending release)
[
{
"name": "create_new_skill_repo",
"prompt": "Create a new skill repo from scratch called 'my-awesome-skill' with proper Netresearch structure",
"assertions": [
{ "type": "content", "pattern": "\\.claude-plugin/plugin\\.json" },
{ "type": "content", "pattern": "SKILL\\.md" },
{ "type": "content", "pattern": "LICENSE-MIT" },
{ "type": "content", "pattern": "LICENSE-CC-BY-SA-4\\.0" },
{ "type": "content", "pattern": "composer\\.json" },
{ "type": "content", "pattern": "release\\.yml" }
]
},
{
"name": "validate_skill_repo_structure",
"prompt": "Validate this skill repo structure and tell me what's missing or wrong",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "validate-skill" },
{ "type": "content", "pattern": "(plugin\\.json|SKILL\\.md|LICENSE)" }
]
},
{
"name": "split_licensing_setup",
"prompt": "What license files does a Netresearch skill repo need and what SPDX expression goes in composer.json?",
"assertions": [
{ "type": "content", "pattern": "LICENSE-MIT" },
{ "type": "content", "pattern": "LICENSE-CC-BY-SA-4\\.0" },
{ "type": "content", "pattern": "\\(MIT AND CC-BY-SA-4\\.0\\)" },
{ "type": "content", "pattern": "Netresearch DTT GmbH" }
]
},
{
"name": "migrate_single_license",
"prompt": "This repo has a single LICENSE file. Migrate it to the split licensing model required by Netresearch skill repos.",
"assertions": [
{ "type": "content", "pattern": "LICENSE-MIT" },
{ "type": "content", "pattern": "LICENSE-CC-BY-SA-4\\.0" },
{ "type": "content", "pattern": "(remove|delete|old).*LICENSE" }
]
},
{
"name": "composer_json_setup",
"prompt": "Set up composer.json for a skill repo called 'netresearch/typo3-testing-skill'. What fields are required?",
"assertions": [
{ "type": "content", "pattern": "ai-agent-skill" },
{ "type": "content", "pattern": "composer-agent-skill-plugin" },
{ "type": "content", "pattern": "netresearch/typo3-testing-skill" },
{ "type": "content", "pattern": "SKILL\\.md" }
]
},
{
"name": "composer_no_version_field",
"prompt": "Should composer.json in a skill repo have a version field? Why or why not?",
"assertions": [
{ "type": "content", "pattern": "(no|not|never|omit|should not|must not)" },
{ "type": "content", "pattern": "(git tag|derive|tag)" }
]
},
{
"name": "composer_no_lock_file",
"prompt": "Should a skill repo commit composer.lock?",
"assertions": [
{ "type": "content", "pattern": "(no|never|must not|should not)" },
{ "type": "content", "pattern": "(librar|skill|package)" }
]
},
{
"name": "plugin_json_structure",
"prompt": "Create a valid plugin.json for a skill called 'security-audit' in .claude-plugin/",
"assertions": [
{ "type": "content", "pattern": "\"name\"" },
{ "type": "content", "pattern": "\"version\"" },
{ "type": "content", "pattern": "\"skills\"" },
{ "type": "content", "pattern": "\\./skills/" },
{ "type": "content", "pattern": "netresearch\\.de" }
]
},
{
"name": "skillmd_frontmatter_format",
"prompt": "Write valid SKILL.md frontmatter for a skill named 'docker-development'",
"assertions": [
{ "type": "content", "pattern": "^---" },
{ "type": "content", "pattern": "name: docker-development" },
{ "type": "content", "pattern": "description: \"Use when" }
]
},
{
"name": "skillmd_word_limit",
"prompt": "What is the word limit for SKILL.md and where should extended content go?",
"assertions": [
{ "type": "content", "pattern": "500" },
{ "type": "content", "pattern": "references/" }
]
},
{
"name": "reusable_workflow_caller",
"prompt": "Create a caller workflow for validate.yml that uses the reusable workflow from skill-repo-skill",
"assertions": [
{ "type": "content", "pattern": "netresearch/skill-repo-skill/\\.github/workflows/validate\\.yml@main" },
{ "type": "content", "pattern": "uses:" }
]
},
{
"name": "release_workflow_setup",
"prompt": "What release workflow does a skill repo need and how is it triggered?",
"assertions": [
{ "type": "content", "pattern": "release\\.yml" },
{ "type": "content", "pattern": "(tag|v\\*|push)" },
{ "type": "content", "pattern": "plugin\\.json" }
]
},
{
"name": "release_process_steps",
"prompt": "Walk me through the release process for a Netresearch skill repo at version 2.1.0",
"assertions": [
{ "type": "content", "pattern": "plugin\\.json" },
{ "type": "content", "pattern": "git tag" },
{ "type": "content", "pattern": "v2\\.1\\.0" },
{ "type": "content", "pattern": "git push" }
]
},
{
"name": "installation_methods",
"prompt": "What are the three ways to install a Netresearch skill?",
"assertions": [
{ "type": "content", "pattern": "(marketplace|Marketplace)" },
{ "type": "content", "pattern": "(composer|Composer)" },
{ "type": "content", "pattern": "(release|download|Release)" }
]
},
{
"name": "multi_skill_package",
"prompt": "How do I set up composer.json and plugin.json for a repo containing two skills: jira-communication and jira-syntax?",
"assertions": [
{ "type": "content", "pattern": "jira-communication" },
{ "type": "content", "pattern": "jira-syntax" },
{ "type": "content", "pattern": "(array|\\[)" }
]
},
{
"name": "cross_platform_scripts",
"prompt": "What cross-platform rules must scripts in a skill repo follow?",
"assertions": [
{ "type": "content", "pattern": "grep -E" },
{ "type": "content", "pattern": "(macOS|BSD|darwin)" },
{ "type": "content", "pattern": "(-P|Perl)" }
]
},
{
"name": "license_path_mapping",
"prompt": "Which license applies to scripts/*.sh files and which applies to references/*.md files in a skill repo?",
"assertions": [
{ "type": "content", "pattern": "MIT" },
{ "type": "content", "pattern": "CC-BY-SA" }
]
},
{
"name": "fix_validation_errors",
"prompt": "validate-skill.sh reports: ERROR: composer.json type must be 'ai-agent-skill' and ERROR: plugin.json author.url must be https://www.netresearch.de. Fix these issues.",
"assertions": [
{ "type": "content", "pattern": "ai-agent-skill" },
{ "type": "content", "pattern": "netresearch\\.de" }
]
},
{
"name": "readme_requirements",
"prompt": "What sections and content must a Netresearch skill repo README.md include?",
"assertions": [
{ "type": "content", "pattern": "(Installation|installation)" },
{ "type": "content", "pattern": "(License|license)" },
{ "type": "content", "pattern": "Netresearch" }
]
},
{
"name": "auto_merge_workflow",
"prompt": "Set up the auto-merge workflow for dependency PRs in a skill repo",
"assertions": [
{ "type": "content", "pattern": "auto-merge-deps" },
{ "type": "content", "pattern": "netresearch/skill-repo-skill" },
{ "type": "content", "pattern": "(dependabot|renovate|pull_request)" }
]
}
]
Composer Setup for Skills
Guide for adding Composer distribution to Netresearch skills.
When to Add composer.json
Add composer.json to ALL skills EXCEPT those explicitly targeting non-PHP ecosystems.
| Skill Type | composer.json |
|---|---|
| PHP/TYPO3 skills | Required |
| General skills | Required |
| Go-specific skills | Not needed |
| Rust-specific skills | Not needed |
composer.json Structure
Basic Structure
{
"name": "netresearch/{skill-name}-skill",
"description": "{Skill description from SKILL.md}",
"type": "ai-agent-skill",
"license": "(MIT AND CC-BY-SA-4.0)",
"authors": [
{
"name": "Netresearch DTT GmbH",
"email": "plugins@netresearch.de",
"homepage": "https://www.netresearch.de/",
"role": "Manufacturer"
}
],
"require": {
"netresearch/composer-agent-skill-plugin": "*"
},
"extra": {
"ai-agent-skill": "SKILL.md"
}
}Key Fields
| Field | Value | Purpose |
|---|---|---|
name | netresearch/{repo-name} | Must match GitHub repo name exactly |
type | ai-agent-skill | Enables plugin discovery |
require | composer-agent-skill-plugin | Plugin dependency |
extra.ai-agent-skill | Path to SKILL.md | Skill location |
Package Naming Convention
- Rule: composer name = GitHub repo name (
netresearch/{repo-name}) - All skill repos are named
*-skillon GitHub - Examples:
- Repo
netresearch/typo3-docs-skill→ composer namenetresearch/typo3-docs-skill - Repo
netresearch/jira-skill→ composer namenetresearch/jira-skill
Multi-Skill Packages
For packages containing multiple skills:
{
"name": "netresearch/{name}-skill",
"type": "ai-agent-skill",
"extra": {
"ai-agent-skill": [
"skills/skill-one/SKILL.md",
"skills/skill-two/SKILL.md"
]
}
}Example: jira-skill
{
"name": "netresearch/jira-skill",
"extra": {
"ai-agent-skill": [
"skills/jira-communication/SKILL.md",
"skills/jira-syntax/SKILL.md"
]
}
}Publishing to Packagist
Prerequisites
1. GitHub repository is public 2. composer.json is valid 3. Packagist account linked to GitHub
Steps
1. Create version tag:
git tag v1.0.0
git push --tags2. Register on Packagist:
- Go to https://packagist.org/packages/submit
- Enter repository URL
- Submit
3. Enable auto-update:
- Configure GitHub webhook for Packagist
- Or use GitHub Actions integration
Files to NOT Include
Never add these to skill repos:
composer.lock- Locks are for applications, not librariesvendor/- Dependencies installed by users
Validation
Check composer.json validity:
# Syntax check
composer validate
# Check type
grep '"type"' composer.json | grep "ai-agent-skill"
# Check plugin requirement
grep "composer-agent-skill-plugin" composer.jsonIntegration with Plugin
The composer-agent-skill-plugin automatically:
1. Discovers installed ai-agent-skill packages 2. Parses SKILL.md frontmatter 3. Generates AGENTS.md index 4. Provides composer list-skills command 5. Provides composer read-skill {name} command
Troubleshooting
"Package not found"
- Ensure package is published on Packagist
- Check package name matches exactly
- Run
composer clear-cache
"Plugin not activated"
When prompted, allow the plugin:
Do you trust "netresearch/composer-agent-skill-plugin"? [y,n,d,?]Or pre-authorize in composer.json:
{
"config": {
"allow-plugins": {
"netresearch/composer-agent-skill-plugin": true
}
}
}"SKILL.md not found"
- Verify path in
extra.ai-agent-skill - Paths must be relative from package root
- Absolute paths are rejected for security
Installation Methods
Four methods for installing Netresearch skills.
Method 1: Netresearch Marketplace (Recommended)
The marketplace aggregates all Netresearch skills in one place.
Setup
/plugin marketplace add netresearch/claude-code-marketplaceUsage
# Browse available plugins
/plugin
# Install specific skill
/plugin install {skill-name}Benefits
- Curated collection
- Automatic updates via sync
- Easy discovery
- No manual file management
Method 2: Download Release
Download packaged skill files from GitHub Releases.
Steps
1. Go to skill's GitHub repository 2. Navigate to Releases page 3. Download latest .zip or .tar.gz 4. Extract to ~/.claude/skills/{skill-name}/
Package Contents
Release packages contain only skill-relevant files:
SKILL.mdLICENSE-MITLICENSE-CC-BY-SA-4.0references/scripts/assets/templates/
Excluded from Packages
README.md(human documentation).github/(CI/CD)composer.json(separate distribution)- Dev configuration files
Method 3: Composer (PHP Projects)
For PHP projects, install skills as Composer packages.
Prerequisites
1. PHP 8.2+ 2. Composer 2.1+ 3. composer-agent-skill-plugin
Installation
# Install the plugin first (once per project)
composer require netresearch/composer-agent-skill-plugin
# Install skills
composer require netresearch/{repo-name}How It Works
1. Plugin discovers packages with type ai-agent-skill 2. Generates AGENTS.md index in project root 3. Skills available via composer read-skill {name}
Benefits
- Version management via Composer
- Dependency resolution
- Project-specific skill sets
- Easy updates with
composer update
Method 4: npm (Node Projects)
For Node.js / TypeScript projects, install skills as npm packages discovered by @netresearch/agent-skill-coordinator.
Prerequisites
1. Node.js 18+ and npm 9+ (or pnpm/yarn equivalent) 2. `@netresearch/agent-skill-coordinator` — peer dependency that scans node_modules and registers skills in AGENTS.md
Installation
npm install --save-dev \
@netresearch/agent-skill-coordinator \
github:netresearch/{repo-name}For pnpm, allowlist the coordinator's postinstall so it can write AGENTS.md:
{
"pnpm": {
"onlyBuiltDependencies": ["@netresearch/agent-skill-coordinator"]
}
}How It Works
1. Coordinator's postinstall walks node_modules for packages declaring aiAgentSkill: skills/<name>/SKILL.md 2. Validates frontmatter, then writes a <skills_system> block into the project's AGENTS.md 3. Skill content is then visible to any agent reading AGENTS.md
What ships in the npm tarball
The files allowlist in package.json controls what npm packs. The default in templates/package.json.template is intentionally minimal — the skill payload (skills/<name>/), plugin metadata (.claude-plugin/), the canonical agent rules entry-point (AGENTS.md), licenses, and README.md:
{
"files": [
"skills/{skill-name}/",
".claude-plugin/",
"AGENTS.md",
"LICENSE-MIT",
"LICENSE-CC-BY-SA-4.0",
"README.md"
]
}When to add a top-level data dir
Add a top-level dir to files only if your installed skill code reads from it at runtime (e.g. via $ROOT/<dir>/... or ../<dir>/... from a script under skills/<name>/scripts/). Common runtime data dirs:
catalog/—cli-tools-skillships this because its installer scripts read$ROOT/catalog/*.json.hooks/— Claude Code's plugin loader readshooks/hooks.json. Ship it if your skill ships PreToolUse/PostToolUse hooks.commands/— slash command definitions. Ship if present.outputStyles/— output style definitions. Ship if present.assets/— referenced assets (images, configs). Ship if your skill content references them at install paths.
When NOT to add a top-level dir
- Top-level `scripts/` is typically repo-maintenance only (e.g.
verify-harness.sh,generate-dashboard.sh). Keep it out unless your installed skill scripts read from$ROOT/scripts/at runtime. Runtime scripts belong underskills/<name>/scripts/(already covered byskills/<name>/). - Example:
context7-skilldoes NOT ship top-levelscripts/because its only file isverify-harness.sh(repo-maintenance). - Example:
cli-tools-skillDOES shipcatalog/because its installer scripts read$ROOT/catalog/*.json. Build/— dev-only build artifacts. Never ship.evals/,docs/— repo-internal. Never ship..github/, lint configs (.markdownlint*,.yamllint*),.envrc— repo-internal. Never ship.
Heuristic when looking at a top-level dir:
package-root/
catalog/ # consumed at runtime -> MUST be in files
Build/ # dev-only build artifacts -> DO NOT include
scripts/ # inspect: runtime or repo-maintenance? include only if runtimeCI safeguard
templates/.github/workflows/npm-pack-smoke.yml.template is a ready-to-copy GitHub Actions workflow that runs npm pack --dry-run on every PR and asserts:
1. No internal leakage — fails if the tarball contains .github/, evals/, docs/, Build/, verify-harness.sh, lint configs, etc. 2. Runtime-referenced dirs are present — greps skills/*/scripts/ for $ROOT/<dir> and ../<dir>/ references; if a script reads $ROOT/catalog but catalog/ is missing from the tarball, the job fails.
Copy it into .github/workflows/npm-pack-smoke.yml in your skill repo. It catches both kinds of files-allowlist mistakes (over-inclusion and under-inclusion) before they ship.
"private": true on 0.0.0-source
Skill repos use the placeholder version 0.0.0-source and must set "private": true to guard against accidental npm publish of the placeholder. Real publishes (when/if added later) flip private to false (or remove it) at release time and set a real semver.
Limitation: SKILL.md content only
The npm path registers only SKILL.md content into AGENTS.md. Slash commands (defined under commands/) and PreToolUse / PostToolUse hooks (defined under .claude-plugin/) are loaded by Claude Code's plugin mechanism — not by the coordinator scanning node_modules. Repos that ship those features should document this explicitly in their README (a > **Limitation:** callout immediately after the npm install snippet) so consumers know they need the marketplace install for the full skill.
Benefits
- Lock-file pinning via
package-lock.json/pnpm-lock.yaml - Renovate / Dependabot can bump skill versions like any other dep
- No PHP / Composer required for Node-only projects
Choosing a Method
| Scenario | Recommended Method |
|---|---|
| General Claude Code use | Marketplace |
| Offline/air-gapped | Release download |
| PHP project | Composer |
| Node / TypeScript project | npm |
| CI/CD automation | Composer or npm |
| Quick trial | Marketplace |
Directory Locations
| Method | Location |
|---|---|
| Marketplace | Managed by Claude Code |
| Release | ~/.claude/skills/{skill-name}/ |
| Composer | vendor/netresearch/{repo-name}/ |
| npm | node_modules/@netresearch/{repo-name}/ |
Marketplace Integration
How skills are synced to the Netresearch Claude Code Marketplace.
Separation from skill-repo rules
- This document explains sync mechanics and source-vs-synced files.
- Discovery, SEO, catalog completeness, and orphan rules for the marketplace Hub live only in the marketplace repository: [`AGENTS.md` in `netresearch/claude-code-marketplace`](https://github.com/netresearch/claude-code-marketplace/blob/main/AGENTS.md).
- Single skill repository quality (README sections,
agents/openai.yaml, GitHub Description/Topics, discovery YAML) lives here in skill-repo-skill — see `repository-quality-rules.md`, `readme-template.md`, `skill-discovery-metadata.md`.
Do not duplicate marketplace governance prose in skill-repo references; link instead.
Overview
The claude-code-marketplace aggregates skills from individual repositories via automated sync workflows.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ skill-repo-1 │ │ skill-repo-2 │ │ skill-repo-N │
│ (source) │ │ (source) │ │ (source) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
▼
┌────────────────────────┐
│ claude-code-marketplace│
│ (aggregator) │
│ │
│ skills/ │
│ ├── skill-1/ │
│ ├── skill-2/ │
│ └── skill-N/ │
└────────────────────────┘Sync Workflow
Automatic Sync (Scheduled)
- Frequency: Weekly (Monday 2 AM UTC)
- Trigger: GitHub Actions schedule
- Process:
1. Clone each source repository 2. Extract semantic version from .claude-plugin/plugin.json 3. Append commit date for versioning 4. Copy skill files to skills/ directory 5. Update marketplace metadata
On-Demand Sync
Source repositories can trigger immediate sync via:
# notify-marketplace.yml in source repo
name: Notify Marketplace
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Trigger marketplace sync
run: |
gh workflow run sync-skills.yml \
--repo netresearch/claude-code-marketplace
env:
GH_TOKEN: ${{ secrets.MARKETPLACE_SYNC_TOKEN }}Adding a Skill to Marketplace
Prerequisites
1. Skill repository follows standard structure 2. .claude-plugin/plugin.json has valid version 3. Repository is public
Configuration
Add skill to .sync-config.json in marketplace repo:
{
"skills": [
{
"name": "my-skill",
"repo": "netresearch/my-skill",
"path": "skills/my-skill"
}
]
}Versioning
Marketplace versions combine:
- Semantic version from
.claude-plugin/plugin.json - Last commit date
Example: 1.2.3-20251021
Sync Workflow Details
Files Synced
From source repository:
SKILL.mdreferences/scripts/assets/templates/LICENSE-MITLICENSE-CC-BY-SA-4.0
Files NOT Synced
README.md(marketplace has its own).github/composer.json- Dev configuration files
claudedocs/
Marketplace Metadata
Updated in .claude-plugin/marketplace.json:
{
"skills": {
"my-skill": {
"version": "1.2.3-20251021",
"description": "Skill description",
"source": "netresearch/my-skill"
}
}
}User Installation
Add Marketplace
/plugin marketplace add netresearch/claude-code-marketplaceBrowse Skills
/pluginInstall Skill
/plugin install {skill-name}Benefits of Marketplace Distribution
| Benefit | Description |
|---|---|
| Discoverability | All skills in one place |
| Curation | Quality-controlled collection |
| Versioning | Automatic version tracking |
| Updates | Sync keeps skills current |
| Simplicity | One-command installation |
Troubleshooting
Skill Not Appearing
1. Check .sync-config.json includes the skill 2. Verify source repo is accessible 3. Check sync workflow logs 4. Ensure SKILL.md has valid frontmatter
Outdated Version
1. Check last sync time in marketplace README 2. Trigger manual sync if needed 3. Verify source repo has new commits/releases
Sync Failures
Common causes:
- Invalid SKILL.md frontmatter
- Missing required files
- Network/API issues
- Permission problems
Check GitHub Actions logs in marketplace repo.
Materialization Contract
How external tools (notably retro-skill) materialize skill improvements or new skills by submitting PRs to skill repositories that follow skill-repo-skill conventions.
Scope
This contract covers two destinations from retro-skill's destination taxonomy:
- `skill-update` — PR against an existing skill repo
- `new-skill` — Scaffolding of a new skill repo
For the full destination taxonomy and routing heuristics, see retro-skill/references/destination-taxonomy.md.
Rule 1: Patches target source repos, never local cache
Local skill cache (~/.claude/plugins/cache/) is overwritten on plugin update. Edits there are lost. Always locate the source repository before patching.
Resolution order:
1. <skill-root>/.claude-plugin/plugin.json → repository field 2. <skill-root>/composer.json → support.source or homepage 3. <skill-root>/.git/config → remote.origin.url 4. Walk up parent directories from the resolved-symlink path looking for any of the above 5. Last resort: ask the user
If unresolvable: do NOT patch local cache. Either ask user or refuse.
Rule 2: Workspace preference order
For each skill-update target, select working directory in this order:
1. Existing worktree: ~/p/<repo-name>/main/ exists AND is a clean git worktree → use it. <repo-name> is the full GitHub repo name (e.g. skill-repo-skill, not skill-repo). 2. Existing flat checkout: ~/p/<repo-name>/ exists AND is a clean flat git checkout on main → use it. 3. Fresh clone: Otherwise clone into /tmp/retro-workspace/<repo-name>/.
Dirty checkouts: do NOT use. Fall back to /tmp. Always tell the user why.
Rule 3: Branch convention
feat/retro-<short-slug><short-slug> is kebab-case, derived from the friction title. Maximum 60 characters for the slug part (full branch ≤ 71 chars including feat/retro- prefix; comfortable on most terminals).
Examples:
feat/retro-add-bun-trigger-keywordsfeat/retro-fix-yaml-tool-choice-for-data-tools
For new-skill: the new repo's first branch is main (initial scaffolding commit).
Rule 4: Commit conventions
- Format: Conventional Commits (
<type>(<scope>): <summary>) - Types:
feat,fix,docs,refactor,chore - No bot attribution. Never append "Generated with Claude Code" or "Co-Authored-By: Claude"
- Preserve signing. Never pass
--no-gpg-signor-c commit.gpgsign=false - Preserve hooks. Never pass
--no-verify - DCO sign-off. All commits include
Signed-off-by: <name> <email>trailer (usegit commit -sorgit rebase --signoff) - Atomic. One logical change per commit
Commit message body should reference the friction:
feat(triggers): include bun in skill description keywords
Found via /retro session 2026-05-11: the assistant suggested npm
in 4 turns despite the project clearly using bun. SKILL.md description
didn't include 'bun' as a trigger.
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>Rule 5: PR template
PRs created by retro-skill should use the named template retro.md:
gh pr create --template retro.md ...
# or via URL query when opening manually:
# https://github.com/<org>/<repo>/compare/main...<branch>?template=retro.mdThis invokes .github/PULL_REQUEST_TEMPLATE/retro.md (not the repo's default template, if any). The retro template has:
## Summary## Came from(session date, finding signal ID)## Change(concrete diff scope)## Target area(skills/<name>/SKILL.md/references//scripts//templates//checkpoints.yaml/evals/evals.json)## Learning source(checkboxes: from /retro, reusable, scoped, eval included)## Test plan(verification steps)
Rule 6: Target area mapping
Paths are relative to the skill's own subdirectory in the repo (this repo's convention: skills/<skill-name>/..., not repo-root).
Each skill-update PR should touch one primary area; multi-area is allowed when cohesive (e.g. SKILL.md update + corresponding eval).
| Target | When |
|---|---|
skills/<name>/SKILL.md | Trigger description, workflow guidance, key principles |
skills/<name>/references/*.md | Detailed knowledge, examples, schemas |
skills/<name>/scripts/* | Mechanical operations |
skills/<name>/templates/* | Output formats |
skills/<name>/checkpoints.yaml | Quality gates (mechanical or LLM checks) |
skills/<name>/evals/evals.json | Behavioral regression tests |
Multi-area PRs split unrelated concerns into separate PRs.
Rule 7: Eval format (skill-repo convention)
This repo's eval format is a single `evals/evals.json` file containing an array of objects. Each object has at minimum:
{
"name": "<scenario-name>",
"prompt": "<what to ask the agent>",
"assertions": [
{ "type": "content", "pattern": "<regex>" },
{ "type": "tool_use", "tool": "<ToolName>", "pattern": "<regex>" }
]
}Validation: bash skills/skill-repo/scripts/validate-evals.sh.
When a skill-update PR changes behavior expectations, append a new eval object to the existing `evals/evals.json` array in the target skill's directory. Do NOT emit evals/*.md files — they will be rejected by the validator.
Other skill repos may use different eval conventions; consult each target's evals/ directory before submitting.
Rule 8: Per-private-repo confirmation
Before pushing to a private host (git.netresearch.de, gitlab.com/<private-org>, etc.), prompt the user. Decision is remembered per (session, repo-url) for the duration of the active retro-skill session; not persisted across sessions.
Rule 9: New-skill scaffolding
For new-skill destination, use the templates in skills/skill-repo/templates/:
composer.json.template(withtype: ai-agent-skill, split licensing in SPDX)package.json.template(for npm-distributable variants)LICENSE-MIT.template,LICENSE-CC-BY-SA-4.0.templateREADME.md.templaterelease.yml.template(GitHub Actions release workflow)validate.yml.template(CI caller for the reusable skill-validation workflow — without it the SKILL.md word cap, plugin.json schema, and markdown/yaml/action lints run only in local pre-commit, never in CI)pr-quality.yml.template(PR validation)auto-merge-deps.yml.template(Dependabot/Renovate auto-merge)pre-commit.template
Required files in the new repo:
.claude-plugin/plugin.json(Claude marketplace manifest)composer.json(from template)skills/<name>/SKILL.md(skill definition)LICENSE-MIT,LICENSE-CC-BY-SA-4.0(from templates)README.md(from template)AGENTS.md(agent-harness convention).gitignore- One initial
skills/<name>/references/*.mdcovering the friction pattern - One initial entry in
skills/<name>/evals/evals.jsoncovering the friction (TDD) - Optionally
skills/<name>/checkpoints.yaml(start with structural checkpoints) .github/workflows/release.yml(from template).github/workflows/validate.yml(from template — required so skill validation runs in CI, not just pre-commit)
Run bash skills/skill-repo/scripts/validate-skill.sh <new-repo-path> to confirm the scaffold passes structural validation.
Marketplace listing is a separate manual step (out of scope for this contract).
See also
- retro-skill destination-taxonomy — Six destinations
- retro-skill patch-workflow — Workflow on the retro-skill side
- retro-skill eval-integration — How retro reads evals when proposing skill-update
skills/skill-repo/templates/— Scaffolding templatesskills/skill-repo/scripts/validate-evals.sh— Eval validatorskills/skill-repo/scripts/validate-skill.sh— Structure validator- User memory:
feedback_preserve-commit-signing,feedback_merge-strategy,feedback_no-version-bumps-in-feature-prs
README structure for Netresearch skill repositories
Human-facing documentation for a skill repo. Marketplace pages may summarize this content but must not become the only place where these facts exist.
Boilerplate generator: `../templates/README.md.template` — align templates with the headings below when updating scaffolding.
---
First screen (above-the-fold)
Without long scrolling, the reader must see answers to:
1. What problem does this skill solve? 2. When should it be used? 3. What does it produce? 4. What inputs or project context does it need? 5. How do I install it or find it in the marketplace?
---
Required Markdown sections (English)
Use exact level-2 headings so agents can grep them:
## What this skill solves## Use when## Expected outputs## Context requirements## Example prompts— minimum three fenced or bulleted realistic prompts (distinct scenarios).## Related skills— slugs/URLs or explicitnone (justified: …).## Installation— must include Netresearch marketplace path (/plugin marketplace add netresearch/claude-code-marketplace) or pointer to org-standard install doc.## Contributing## License
Optional (skills that ship a `commands/` dir): ## Commands — a table or list enumerating every slash-command and each of its modes / sub-commands / flags. When you add or rename a command or a mode, update this list in the same change — the implementation, its reference docs, and the README/AGENTS menus drift apart otherwise (a mode added everywhere except the README menu is a common, user-visible miss). validate-skill.sh warns when a command's /<name> (from commands/<name>.md) is not mentioned in the README, but it cannot see mode-level drift — that part is on the author.
Optional: ## German summary — short paragraph if DACH/TYPO3/Oro/agency audience; technical detail may remain English. The scaffold `../templates/README.md.template` ships this as a commented block after ## License — uncomment when needed.
---
Voice: present tense, never narrate change
Reference docs (README, SKILL.md, reference files, module headers) describe what the skill is and how to use it, in plain present tense — as if it had always been this way. They must not describe the skill by what it is not or how it changed.
- Banned framing: "no longer", "instead of", "reframed", "previously", "used to", "X derives from Y / downstream … not its source", "it is not a router/wrapper/…". This is the curse of knowledge as negative / apophatic documentation — it only parses for a reader who knew a prior design.
- Before the first release there is no audience for change. Nobody ran a prior public version, so any before/after framing is noise and actively misleads (readers hunt for a "router" that never existed). The same holds for unreleased, in-between edits: the diff is the commit; the README must not know a change happened.
- History lives only in `CHANGELOG` / `UPGRADING` / release notes / ADRs / the commit log — those have a reader (someone upgrading from a version they used) who needs the contrast. The README and
SKILL.mdnever do. - A deprecation notice ("X is deprecated, use Y") is the one legitimate non-history contrast and belongs in the changelog/upgrade doc, not the "what this is" sections.
---
Cross-checks (machine-friendly)
| Check | PASS criterion |
|---|---|
Use when | Section exists and contains trigger phrases (ticket prefixes, stacks, commands). |
Example prompts | ≥3 prompts. |
Related skills | ≥1 link/slug or justified none. |
Installation | Mentions marketplace or documents exclusive alternate with owner approval in README. |
| Present-tense voice | grep -rin -e 'no longer' -e 'reframed' -e 'previously' -e 'used to' -e 'derive' -e 'downstream' -e 'not its source' -e 'is not a router' over README/SKILL.md returns nothing — change-narration belongs in CHANGELOG/UPGRADING. |
Commands (if commands/ exists) | Every commands/<name>.md, and each mode/flag it documents, is enumerated in the README. |
---
Alignment with other files
- Discovery YAML (optional): `skill-discovery-metadata.md` — keep summaries consistent with README sections.
- `agents/openai.yaml`: short end-user description; must not contradict README summaries.
- GitHub: see `repository-quality-rules.md` for description/topic rules.
Release Discipline
Every step that caused the "30 failed plugin releases" incident, codified as rules.
Canonical Order: Bump PR Merged → Tag Pushed
Tag a version only after the version-bump PR is merged to the default branch. Tagging first causes the Release workflow to run against the old code, fail CI, and produce an immutable GitHub release locked to a bad tag.
WRONG: git tag -s v1.2.4 → git push → open bump PR
RIGHT: open bump PR → merge → pull main → git tag -s v1.2.4 → git pushPre-Release Version-Parity Check
Before pushing any tag, all version identifiers must match. This is the single check that would have prevented the 30-repo release failure.
Use the shipped script scripts/check-version-parity.sh (in this repo under skills/skill-repo/scripts/check-version-parity.sh):
# No arguments — compare plugin.json against SKILL.md metadata.version
skills/skill-repo/scripts/check-version-parity.sh
# With tag argument — also require plugin.json.version == tag (v prefix optional)
skills/skill-repo/scripts/check-version-parity.sh v1.2.4What it checks:
.claude-plugin/plugin.jsonhas a.versionfield — exits with an error if missing.composer.jsondoes not have a.versionfield — composer versions come from the git tag via the Release workflow, so a hard-coded version drifts silently.- If a tag argument is provided,
plugin.json.versionequals that tag with thevprefix stripped. - Every
skills/*/SKILL.mdthat declaresmetadata.versionin frontmatter matchesplugin.json.version.
If called without an argument and all parity passes, the script prints an advisory suggesting the next tag call. Run before every git push origin vX.Y.Z.
Cache Safety: Never Edit the Installed Copy
Installed skills and plugins live under ~/.claude/ (or wherever the marketplace resolves them). Editing these paths directly is always wrong — the next /plugin update or marketplace sync will silently overwrite your changes, taking any local fixes with it.
Paths that are off-limits for edits
~/.claude/skills/**~/.claude/plugins/cache/**~/.claude/plugins/marketplaces/**- Anything inside a
.bare/directory (git bare clone; worktree source)
Pre-edit check
Before every Write or Edit in skill-repo workflows:
pwd_real=$(realpath .)
case "$pwd_real" in
*/.claude/skills/*|*/.claude/plugins/*|*/.bare/*)
echo "REFUSING to edit installed/cache path: $pwd_real"
echo "Navigate to the source worktree first."
exit 1
;;
esacRecovery when edits landed in the wrong place
1. Stop. Do not run /plugin update or composer update — they may wipe your edits. 2. diff -r ~/.claude/skills/<name>/ ~/projects/<name>-skill/main/skills/<name>/ to see what drifted. 3. Copy the legitimate changes into the source worktree. 4. Commit from the worktree; never from the cache.
Multi-Skill-Repo Release Dry-Run
When releasing >3 skill repos in one sweep, produce this manifest and wait for user approval before executing:
Skill-Repo Release Plan (2026-04-18)
| Repo | Current | Target | Change type | Notes |
|-----------------------------|---------|---------|-------------|------------------------|
| netresearch/git-workflow | 1.9.0 | 1.10.0 | minor | adds critical-rules |
| netresearch/github-project | 2.10.0 | 2.11.0 | minor | multi-repo-operations |
| netresearch/skill-repo | 1.18.0 | 1.19.0 | minor | release-discipline ref |
Preconditions (verified per repo):
[✓] default branch CI green
[✓] no pending version-bump PR
[✓] version-parity check passes
[✓] working tree clean
Execution order per repo:
1. Create version-bump PR on feat/release-vX.Y.Z branch
2. Wait for CI green and approval
3. Merge via merge-commit (respects atomic-commit policy)
4. Pull main; run check-version-parity.sh vX.Y.Z
5. Create signed tag vX.Y.Z
6. Push tag
7. Monitor Release workflow to green
8. Halt all further releases if this one fails — produce rollback
Reply "go" to proceed, or name repos to skip.Immutable-Release Caveat
Deleted GitHub releases do NOT free the tag for reuse. Once a release is published and deleted, that tag string is permanently locked as a deleted release — a new release with the same tag will fail. See git-workflow-skill → references/github-releases.md. Therefore: get it right the first time. The version-parity check above is what "right the first time" means in practice.
Tag Signing (Mandatory)
git tag -s vX.Y.Z -m "vX.Y.Z" # -s: sign with GPG/SSH
git push origin vX.Y.Z # signed tag reaches the remoteNever git tag vX.Y.Z (unsigned). Repos with protected tag rulesets will reject unsigned tags.
No --latest Drift for Non-Default Branches
When releasing from a non-default branch (e.g. a v1.x maintenance line while v2.x is default), pass --latest=false to avoid stealing the "Latest" badge by timestamp:
gh release create v1.5.12 --latest=false --title "v1.5.12" --notes-file CHANGELOG-v1.5.12.mdGitHub marks releases "Latest" by creation timestamp, not semver. A v1.5.12 created after v2.0.0 will become "Latest" without this flag — wrong, misleading, and often noticed only by downstream consumers.
Supply-Chain Attestation
Every release ships with provenance-attested archives and a Cosign-signed SHA256SUMS.txt that binds those archives by digest. Only the checksum file is Cosign-signed; the .zip/.tar.gz archives are integrity-protected through it — verifying the signature on SHA256SUMS.txt and then running sha256sum --check against the downloaded archive proves the archive was produced by this workflow.
All of this happens in the SAME job that publishes the GitHub Release, BEFORE the assets are made public — there is no window where unsigned or unattested artifacts are downloadable. The flow, in order:
1. Build *.zip and *.tar.gz archives. 2. Generate SHA256SUMS.txt over them. 3. Cosign keyless sign-blob the SHA256SUMS.txt → produces SHA256SUMS.txt.sigstore.json (Sigstore bundle format: cert + signature + Rekor inclusion proof in a single self-contained JSON; cosign v3+ default). The .sigstore.json extension is chosen so OSSF Scorecard's signed-releases probe recognises the signature — the content is identical to cosign's --bundle default output. 4. `actions/attest-build-provenance` generates a SLSA build-provenance attestation for the archives + checksums file → published to GitHub's attestation API. 5. `softprops/action-gh-release` publishes the GitHub Release with all assets attached at once.
Callers must grant three permissions on the calling job:
# .github/workflows/release.yml in the consuming repo
jobs:
release:
uses: netresearch/skill-repo-skill/.github/workflows/release.yml@main
permissions:
contents: write # release upload
id-token: write # OIDC for sigstore (Cosign + attest-build-provenance)
attestations: write # GitHub native attestation APIIf any of those scopes is missing the job fails fast with Resource not accessible by integration; contents: write alone is not enough.
Verify a downloaded release archive
Both commands below pin verification to the specific repository that's expected to have produced the release. --owner netresearch and https://github.com/netresearch/.* are tempting shortcuts but match every workflow run in the org — meaning a compromised or unrelated netresearch repo could mint a valid-looking attestation against an artefact that was never released from this repo. Always pin to the named repo.
# SLSA build provenance (GitHub-native attestation API)
# Substitute <repo-name> with the actual skill repo, e.g. matrix-skill.
# Archive name patterns: <skill>-skill-vX.Y.Z.zip and <plugin>-plugin-vX.Y.Z.zip.
gh attestation verify <skill-name>-skill-vX.Y.Z.zip --repo netresearch/<repo-name>
# Cosign sign-blob signature on the checksums (no GitHub API needed).
# The cert SAN reflects the SIGNER, which is the shared reusable release
# workflow (`netresearch/skill-repo-skill`), NOT the consuming repo. Pin the
# regex to skill-repo-skill, not the consumer. (`gh attestation verify` above
# walks the chain automatically; cosign's verifier doesn't.) The org-wide form
# `https://github.com/netresearch/.*` would accept signatures from any repo,
# branch, or workflow in the org — too loose for supply-chain verification.
cosign verify-blob \
--bundle SHA256SUMS.txt.sigstore.json \
--certificate-identity-regexp "^https://github\.com/netresearch/skill-repo-skill/\.github/workflows/release\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
SHA256SUMS.txt
# Then verify the archive matches the (now-signed) checksum
sha256sum --check SHA256SUMS.txtIf verification fails:
gh attestation verifyreturnserror: no attestations foundwhen--repois wrong (or when the release predates this workflow).cosign verify-blobreturnserror: certificate identity does not matchwhen the regex is wrong, orbundle verification failedwhen.sigstore.jsondoesn't correspond to the file.
Why one atomic job
Splitting attestation into a separate needs: release job (the original design here) creates a race: the GitHub Release publishes BEFORE the attestation exists, so anyone downloading in that window gets unsigned, un-attested artifacts. Folding everything into the same job before the upload eliminates the window — either the whole bundle (archives + signature + provenance) ships, or nothing does.
Same pattern as netresearch/.github/.github/workflows/golib-create-release.yml and netresearch/typo3-ci-workflows/.github/workflows/release.yml. No reason for skill repos to diverge.
The previously-documented with: attest: true opt-in is gone; the input is still declared as DEPRECATED — ignored so any caller that still passes it doesn't error syntactically, but every release now gets provenance unconditionally. Drop the with: block if attest was its only entry (also true for bump).
Repository Quality Rules (skill repositories)
Prüfbare Regeln für einzelne Skill-Repositories (netresearch/*-skill). Nicht für das Marketplace-Repository — Discovery-Katalog- und SEO-Governance für den Hub liegen in `netresearch/claude-code-marketplace` (AGENTS.md dort).
---
Lizenz (Netresearch Split-Modell)
- PASS, wenn
LICENSE-MITundLICENSE-CC-BY-SA-4.0vorhanden sind und keine bareLICENSE-Datei existiert (siehevalidate-skill.sh/ Checkpoints). - PASS für „LICENSE oder LICENSE.md“-Anforderungen außerhalb Netresearch: Split-Lizenz gilt als erfüllte Lizenzpflicht; einzelne
LICENSE-Datei ist hier FAIL, wenn sie das Split-Modell ersetzt.
---
Mindestbestandteile eines Skill-Repos
Jedes Repo muss die folgenden Elemente enthalten oder eine explizite Begründung in README.md unter z. B. ## Repository extras (warum ein Pflichtobjekt fehlt).
| Element | Prüfregel |
|---|---|
README.md | Datei existiert (validate-skill.sh). |
| Lizenz | LICENSE-MIT + LICENSE-CC-BY-SA-4.0 (Policy). |
CONTRIBUTING.md | Datei existiert oder README verlinkt auf ein externes Contributing-Dokument und nennt den Ort (ein kanonischer Ort). |
SECURITY.md | Datei existiert oder README enthält Abschnitt „Security“ mit Kontakt/Ort der Policy. Ausnahme: klar als private/internal-only gekennzeichnete Repos — dann muss das im README stehen (Private-only: no SECURITY.md). |
.github/pull_request_template.md | Datei existiert oder Issue/PR-Richtlinie ist in CONTRIBUTING.md als PR-Checkliste beschrieben (mind. 5 konkrete Checkboxen). |
Skill-Verzeichnis mit SKILL.md | Pfad entspricht .claude-plugin/plugin.json → skills. |
agents/openai.yaml | Datei existiert oder Begründung + Alternative (z. B. „Agent Stack nicht OpenAI“) im README. |
references/, scripts/, assets/ | PASS, wenn SKILL.md alle Referenzen erreichbar macht oder README erklärt bewusst schlankes Repo („no references: …“). |
---
Pflicht für README-Oberfläche
Siehe `readme-template.md` für die exakten Überschriften und `skill-discovery-metadata.md` für YAML-Zusatzfelder außerhalb von SKILL.md.
---
SKILL.md vs. Discovery
- `SKILL.md`: Laufzeitverhalten, Trigger, Arbeitsablauf — siehe `skill-quality.md`.
- Discovery / SEO / Marketplace-Felder: README,
agents/openai.yaml, optionale Metadatei(en), GitHub Description/Topics — nicht als zusätzliche YAML-Schlüssel imSKILL.md-Frontmatter für Katalogzwecke.
Frontmatter (technische Grenze)
- Erforderlich:
name,description(mit PräfixUse when…). - Verboten für Discovery: eigene Schlüssel wie
slug,tags,category,keywords,seo_*im Frontmatter. - Optional (Agent Skills / Validator):
license,compatibility,metadata,allowed-tools— nur wenn technisch nötig; keine Marketing-/SEO-Felder dort.
---
Related Skills (Repo-Ebene)
- Im README oder in Discovery-YAML als Slugs oder volle URLs angeben.
- PASS, wenn mindestens ein Eintrag oder die Zeile
Related skills: none (justified — …)mit Grund vorhanden ist. - FAIL, wenn beliebige Links nur für SEO gesetzt sind (nicht fachlich nachvollziehbar).
---
Marketplace-Sync (Quelle bleibt Repo)
Bei Änderungen an Discovery-Inhalten: siehe `marketplace-integration.md`. Agents müssen am Ende einer Änderung prüfen, ob Marketplace-Felder aktualisiert werden müssen (oder Override dokumentiert ist).
---
GitHub Repository SEO
Repository Description
- PASS: String length ≤ 160 characters (count before save).
- PASS: Mentions concrete technology (e.g. TYPO3, OroCommerce, Docker) or a named task domain (e.g. “extension PHPUnit matrix”, “Vite sitepackage build”).
- FAIL: Generic phrases such as “Useful AI skill for developers”, “ultimate automation assistant”.
- PASS: Understandable without opening
README.md.
Good: Agent skill for TYPO3 Vite setup, SCSS architecture and frontend asset integration. Bad: Useful AI skill for developers.
GitHub Topics
- PASS: Includes `agent-skill`.
- PASS: At least one stack tag (
typo3,php,docker, …) or domain tag (testing,security,frontend, …) matching the skill. - FAIL: Irrelevant trending tags just for visibility (keyword stuffing).
- Document proposed topics in README under
## Repository extrasif maintainers cannot edit Topics immediately.
---
GitHub Pages policy
The marketplace is the canonical public discovery and storytelling layer for all Netresearch Agent Skills. Repository Pages are secondary, skill-specific documentation surfaces.
Default: Pages disabled
Skill repositories must not enable GitHub Pages by default.
- PASS:
gh api repos/netresearch/<repo>/pagesreturns HTTP 404 (Pages disabled). - FAIL: Pages is enabled without satisfying the criteria below.
When Pages is appropriate
Enable GitHub Pages only when the repository contains standalone public material that is too large, too visual, too navigational, or too strategically important to live well in README.md. At least one of the following must be true:
- the documentation requires multiple pages,
- the skill has a gallery of examples, reports, dashboards, screenshots or demos,
- the skill publishes generated reference documentation,
- the skill provides versioned documentation,
- the skill is a public reference implementation,
- the skill explains a reusable methodology or assessment model,
- the skill has a specific SEO target that the marketplace landing cannot cover without becoming too broad.
When Pages is NOT appropriate
Do not enable Pages if the site would only duplicate:
- the README,
- the marketplace detail page (
https://github.com/netresearch/claude-code-marketplace#<slug>or the future landing), - installation instructions,
SKILL.md,- the basic example prompts.
Mandatory artefacts when Pages is enabled
If Pages is enabled, the repository must include:
- a short justification block in
README.md(which criterion above is satisfied), - a documented canonical URL pointing at the Pages site,
- a clear source path (default:
docs/), - documented build and deployment commands (
make docs,npm run docs, or equivalent — referenced from the README), - link-checking or equivalent validation in CI,
- a note explaining which content belongs on Pages vs. README vs. marketplace.
Mirroring rule
Skill-specific metadata originates in the skill repository (metadata/discovery.yaml, README sections, agents/openai.yaml, GitHub settings). The marketplace consumes it. Do not duplicate the same metadata across README, Pages site and marketplace landing — pick one canonical surface per fact and link from the others.
Reviewer-Reply Boilerplate
Canonical responses to recurring reviewer comments on Netresearch skill PRs (Copilot, Gemini Code Assist, peer review). Lift the fenced block verbatim or paraphrase to context. Each entry includes the criteria for whether to accept or decline.
These were extracted from review patterns across 14+ skill PRs. Use them to keep responses consistent and to avoid re-litigating settled architectural decisions on every new PR.
1. "Set private: true"
Verdict: Accept — already the template default.
Criteria: Skill packages distributed via github:org/repo (not the npm registry) must set "private": true to guard against accidental npm publish of the placeholder 0.0.0-source version. The current package.json.template bakes this in. If a reviewer flags it on a fresh PR, the package was scaffolded from an outdated template — accept the suggestion and add it.
Accepted. Adding `"private": true` — current scaffolding template (`skills/skill-repo/templates/package.json.template` in `netresearch/skill-repo-skill`) bakes this in to guard against accidental publish of the `0.0.0-source` placeholder. This PR was scaffolded before the template was updated.2. "Pin github:org/repo#vX.Y.Z in install instructions"
Verdict: Decline as primary advice. Document #vX.Y.Z as an opt-in for users who want reproducibility.
Criteria: These skills are markdown content (procedural knowledge), not executable code where pinning protects against breakage. Consumers want skill-content updates by default. Pinning is an advanced opt-in.
Declined as primary advice. The default `github:netresearch/{repo-name}` form intentionally tracks the default branch so consumers receive skill-content updates — these skills are markdown content (procedural knowledge), not executable code where breakage matters. Pinning is an advanced use-case, not a default. Consumers can append `#vX.Y.Z` themselves for reproducibility (`github:netresearch/{repo-name}#v1.2.3`); we don't surface that in the README to keep the install path simple.(Reference: this is the response we used on skill-repo-skill PR #82.)
3. "Drop .claude-plugin/ (or commands/, outputStyles/, AGENTS.md) from files"
Verdict: Decline (won't-fix). This is the dual-distribution invariant.
Criteria: Netresearch skill packages are dual-distributed — the same package serves both the Claude Code marketplace install path AND the npm install path. Plugin metadata, slash commands, output styles, and AGENTS.md are part of the skill's installable surface, not internal repo configuration. Excluding them from the npm tarball delivers a partial skill to npm consumers.
Declining. Netresearch skill packages are *dual-distributed* — the same tarball feeds both the Claude Code marketplace install path AND the npm install path. Plugin metadata (`.claude-plugin/plugin.json`), slash commands (`commands/`), output styles (`outputStyles/`), and the canonical `AGENTS.md` rules file are part of the skill's installable surface, not internal repo configuration.
Excluding them from the npm tarball would deliver a partial skill to npm consumers — the same partial-install problem documented in [github-release-skill PR #19](https://github.com/netresearch/github-release-skill/pull/19)'s `> **Limitation:**` callout. The current `@netresearch/agent-skill-coordinator` (v0.1.x) `node_modules` scanner can't load plugin-mechanism features; those need Claude Code's plugin loader. Shipping these directories preserves the option to switch install methods without re-installing.(Reference: lifted from skill-repo-skill PR #83.)
4. "Top-level scripts/ shouldn't ship"
Verdict: Accept if scripts/ is repo-maintenance only. Decline if installed code reads from $ROOT/scripts/ at runtime.
Criteria: This is the opposite call from #3. Top-level scripts/ is not part of the dual-distribution surface unless the skill's runtime explicitly reads from it. To tell them apart:
- Repo-maintenance (DO accept the suggestion, remove from
files): scripts that only run in CI or by repo maintainers —verify-harness.sh,generate-dashboard.sh,run-ab-evals.sh, lint runners, release helpers. Look for invocation only in.github/workflows/orMakefile/package.json scripts. - Runtime (DO decline the suggestion, keep in
files): scripts the installed skill executes, typically referenced fromskills/<name>/SKILL.mdorskills/<name>/scripts/*.shvia$ROOT/scripts/...or../scripts/....
Quick check: grep -r '\$ROOT/scripts\|\.\./scripts' skills/. If empty, it's repo-maintenance.
Accepted. `scripts/` at the repo root only contains `verify-harness.sh` (repo-maintenance, run via `.github/workflows/`). The installed skill code does not read from `$ROOT/scripts/` at runtime — runtime scripts live under `skills/<name>/scripts/` (already covered by the `skills/<name>/` entry). Removed from `files`. The npm-pack-smoke CI job will keep this honest going forward.If declining (runtime usage):
Declining. Top-level `scripts/` is consumed at runtime — `skills/<name>/SKILL.md` references `$ROOT/scripts/<file>.sh` for [specific feature]. Removing it from `files` would break npm consumers. The npm-pack-smoke CI job asserts this dir is present.5. "AGENTS.md shouldn't ship"
Verdict: Decline (won't-fix). Same dual-distribution invariant as #3.
Criteria: AGENTS.md is the canonical agent rules entry point for the skill repo. npm consumers expect the same rules file marketplace consumers see — without it, agents reading the package don't get the harness contract.
Declining. `AGENTS.md` is the canonical agent rules entry point for the skill — npm consumers must receive the same rules file that marketplace consumers do, otherwise agents reading the installed package miss the harness contract documented there. This is part of the dual-distribution surface (see [skill-repo-skill PR #83](https://github.com/netresearch/skill-repo-skill/pull/83)).See Also
installation-methods.md— Method 4 (npm) for the fullfiles-allowlist rationale.release-discipline.md— version-parity check, multi-repo dry-run.- skill-repo-skill PR #83 — original dual-distribution decision record.
Skill Discovery Metadata (repository scope)
Defines skill-repo-local discovery and classification data. Does not replace the marketplace catalog — the marketplace aggregates and may normalize display. Governance for marketplace listings is in `netresearch/claude-code-marketplace`/`AGENTS.md`.
---
Where metadata lives
| Kind | Allowed location | Forbidden |
|---|---|---|
| Runtime triggers, workflow | SKILL.md body + references/ | — |
| Minimal listing | SKILL.md frontmatter: `name`, `description` only (plus optional technical keys allowed by `validate-skill.sh`: license, compatibility, metadata, allowed-tools) | Discovery-only keys (slug, category, tags, …) in frontmatter |
| Discovery / SEO / partner fields | README.md (sections), optional metadata/discovery.yaml or equivalent outside SKILL.md, agents/openai.yaml, GitHub repo settings | Duplicating full SKILL.md body into README |
Rule: SKILL.md describes how the agent behaves. Marketplace-oriented fields belong in README or a separate metadata file consumed by humans/tools — not stuffed into frontmatter beyond name/description (and optional technical keys above).
---
Recommended discovery YAML (optional file)
Place at repo root or under metadata/ — not inside SKILL.md. Example schema:
slug: typo3-vite
display_name: TYPO3 Vite Frontend Pipeline
summary:
en: >
Configures Vite for TYPO3 v13+ with vite-asset-collector, SCSS entrypoints, and CSP-safe asset URLs.
de: >
Richtet Vite für TYPO3 v13+ mit vite-asset-collector, SCSS-Entrypoints und CSP-tauglichen Asset-URLs ein.
category: typo3-frontend
tags:
- vite
- typo3
- scss
- frontend
use_cases:
- Bootstrap a Vite pipeline for a TYPO3 sitepackage.
- Split CSS/JS entrypoints per content element.
expected_outputs:
- Vite config and npm scripts aligned with vite-asset-collector.
- Documented build and deployment steps for frontend assets.
context_requirements:
- TYPO3 v13+ sitepackage or extension with asset collector available.
- Node.js LTS matching project policy.
action_level: modifies_files
risk_level: medium
related_skills:
- typo3-frontend-patterns
- dxp-frontend
example_prompts:
- "Add Vite 7 to our TYPO3 13 sitepackage with SCSS partials per content element."
- "Wire vite-asset-collector entrypoints for tt_content templates."
primary_keywords:
- vite
- typo3
- sitepackageFields:
- `slug`: stable id; usually matches plugin/skill name.
- `display_name`: public title (may differ from
nameinSKILL.md). - `summary.en` / `summary.de`: one short paragraph each;
deoptional unless DACH/TYPO3/Oro focus. `summary.en` should be ≤ 300 chars (snippet-friendly target; the marketplace enforces a hard cap of 500). - `category`: one of the canonical marketplace categories —
development,devops,security,design,workflow,productivity. Keep this in sync with the marketplaceAGENTS.mdcanonical list; do not invent ad-hoc values. - `tags` / `use_cases`: for README tables and marketplace sync.
- `expected_outputs` / `context_requirements`: must mirror README sections (single source: generate README from this file or maintain parity explicitly).
- `related_skills`: slugs or full GitHub URLs; see `repository-quality-rules.md`. Entries that don't yet exist in the catalog can be tagged
(planned)or(external)— never invent fake links for SEO. - `example_prompts`: align with README Example prompts (≥3 in README per checklist).
- `primary_keywords`: align with GitHub Topics + first sentence of summaries.
---
Action level
| Value | Definition |
|---|---|
read_only | Reads/analyses repo or docs only; no writes. |
suggests_changes | Proposes patches/text but does not apply them. |
modifies_files | Writes or edits files in the working tree. |
runs_commands | Executes local shell/commands (build, tests, linters). |
external_write | Creates/updates data in external systems (GitHub API, Jira, Matrix, email, deployment APIs, …). |
Risk level
| Value | Definition |
|---|---|
low | No durable impact or easily reversible edits. |
medium | Local file changes and/or command execution with repo impact. |
high | External writes, destructive operations, releases/deployments, security-sensitive changes. |
PASS rule: every skill repo should state action_level and risk_level in discovery YAML or in README table „Classification“ with the same labels.
---
Sync with marketplace
When this metadata changes, open or update the corresponding marketplace entry per `marketplace-integration.md`. Do not silently diverge.
SKILL.md Quality Rules — Detail and Examples
Detailed guidance backing the summary in `SKILL.md` (§ SKILL.md Quality Rules).
Why these rules exist
Claude Code skills have two distinct context costs:
- Listing cost (per turn): Only
name,description, and optionalwhen_to_usefrom each skill's frontmatter enter context on every assistant turn — whether the skill is invoked or not. Per-skill cap: 1,536 chars (skillListingMaxDescChars). Total listing cap:skillListingBudgetFraction × context_window(default0.01). - Body cost (per invocation): The full SKILL.md body loads when the skill is invoked, and persists for the rest of the session. Reference files (
references/*.md) are not auto-loaded — the model only reads files it sees referenced.
Description bytes are the always-on tax; body bytes are the on-demand tax. References are free unless the model knows they exist and decides to read them.
Description rules
Caps
- Hard cap: 1,536 chars — Claude Code truncates above this regardless of budget.
- Target: 100–300 chars — fits comfortably in the listing, leaves headroom for other skills.
- Justify above ~500 chars — long descriptions are reasonable when they enumerate triggers (e.g., ticket-key prefixes, file-extension lists). Keep the structure tight.
Position matters
Truncation is position-based. Put your primary trigger first. The convention is Use when <trigger> as the opener.
Anti-patterns
| Anti-pattern | Example |
|---|---|
| Marketing language | "blazingly fast", "powerful", "comprehensive" |
| Vagueness | "a tool to help with X", "general-purpose helper" |
| Restating the skill name | description: "The frobnicator skill frobnicates." |
| Redundancy with body | duplicating the skill's intro paragraph in both fields |
Examples
Good:
description: "Use when reviewing your diff, writing a commit message, or asking what changed. Summarizes uncommitted changes and flags risky patterns."Bad:
description: "A comprehensive solution for managing your repository state with advanced semantic understanding"Body rules
Size
Body loads on invocation and persists for the rest of the session. The threshold goal is per-invocation token cost — every additional word in the body is paid every time the skill is invoked. Move what isn't strictly needed at decision time into references/ (lazy-loaded when SKILL.md instructs).
Word count translates to tokens at roughly 1.4× (English prose; code fences run higher). audit-skills.sh reports three tiers:
- INFO above 500 words (~700 tokens) — modest cost; skim for split candidates.
- WARN above 1,000 words (~1,400 tokens) — almost any body at this size has lookup content that could move to references.
- FAIL above 2,000 words (~2,800 tokens) — body is acting as a manual; split required.
These tiers are advisory across the wider skill ecosystem. This repo enforces a stricter 500-word hard cap on its own SKILL.md (via scripts/validate-skill.sh) — that is the per-skill house rule, not a universal claim. Treat the audit tiers as triage levels for any skill repo; treat the 500-word cap as policy for the canonical skill-repo template.
Empirical anchor: the Netresearch skill corpus (n=52) has p95 ≈ 994 words. WARN at 1,000 catches the actual outliers in our own work. Anthropic's longer skills (e.g., writing-skills at 3,193 words) FAIL under this rule — that's the honest signal: even shipped skills can have content moved out. We can't fix theirs; we hold ourselves to the rule.
- Bloat signals: body >3 KB, body >150 lines, single fenced code block >25 lines (long code examples are the primary lazy-load target).
When to split to references/
Move to references/<topic>.md when content is:
- Long examples (>20 lines)
- Configuration templates
- API/syntax tables
- Migration matrices
- Detailed checklists
- Repository directory trees (often the worst offenders)
Keep in SKILL.md body:
- High-level workflow
- The "what does this skill do" summary
- A reference catalog (see Pattern 2 below) when there are many topic-files
- One canonical quick example
Reference patterns
Every .md file in references/ must be discoverable to the model from SKILL.md. Three acceptable patterns:
Pattern 1: Direct cite
For form validation, see [`references/forms.md`](references/forms.md).
For multi-profile auth, see [`references/multi-profile.md`](references/multi-profile.md).Best for ≤10 reference files. Each gets a one-line cite with the path. The model sees the path on invocation and reads on demand.
Pattern 2: Catalog-with-convention
## Reference Files (in `references/`, `.md` implied)
- **Frameworks** (`*-security`): react, vue, angular, nextjs, nuxt
- **Languages** (`*-security-features`): php, python, go, rust, javascript-typescript
- **Cloud** (`*-security`): aws, azure, gcpBest for 10+ topic-grouped files. The model sees:
- The directory (
references/) - The filename suffix convention (
-security,-security-features) - The list of stems
It can resolve any combination on demand. The security-audit skill uses this pattern for ~60 reference files in ~12 lines of body content.
Pattern 3: List-and-pick
For language-specific guidance, list `references/` and read the file matching your stack.Use sparingly — burns a tool call (the model has to enumerate the directory). Useful when there's no naming convention but the directory is small.
Anti-pattern: Hub-and-spoke
# DON'T DO THIS
In SKILL.md:
> See [references/index.md](references/index.md) for the catalog of references.
In references/index.md:
> - [forms.md](forms.md)
> - [auth.md](auth.md)Multi-hop traversal isn't reliable. Anthropic's docs phrase the rule as "Reference supporting files from SKILL.md so Claude knows what each file contains" — direct visibility from SKILL.md is the model. The hub gets read but its leaves aren't deeply traversed. Anthropic's published skills use flat / catalog patterns, never hub-and-spoke.
Anti-pattern: Orphan refs
Files in references/ with no path to discovery from SKILL.md (no direct cite, no catalog mention, no list-and-pick instruction). The model never reads them. Either cite them or delete them — they consume disk and signal "stale" to readers without contributing to the skill's behavior.
Auditing
Run scripts/audit-skills.sh from the repo root. It scans SKILL.md files for:
- Description length violations (warn >500 chars, fail >1,536)
- Body length tiers (info >500 words, warn >1,000, fail >2,000)
- Long code fences (info >25 lines — primary lazy-load candidate)
- Orphan refs (files in
references/not reachable by any pattern)
Pattern 2 detection is heuristic: a reference file counts as P2 when its stem matches a convention stated in SKILL.md (filename suffix or topic-list pattern). Files outside any P1/P2/P3 path are reported as ORPHAN.
Sources
- Anthropic Claude Code docs on skills: <https://code.claude.com/docs/en/skills>
- Anthropic published skills: <https://github.com/anthropics/skills>
- Settings schema:
skillListingBudgetFraction,skillListingMaxDescChars(Claude Code settings.json)
Validation checklist — skill repository changes
Agents must walk through this list before declaring a skill-repo task complete. Marketplace-only checks live in `netresearch/claude-code-marketplace`/`AGENTS.md` — do not merge those steps here.
README
- [ ]
README.mdcontains all required sections listed in `readme-template.md`. - [ ] Example prompts: ≥ 3 realistic prompts in
## Example prompts. - [ ] Related skills: declared or
none (justified: …). - [ ] First screen answers: problem, when, outputs, context, installation — verifiable without scrolling past ~1 screen (approx. first 40 lines).
SKILL.md
- [ ] Frontmatter includes `name` and `description`; no discovery-only keys (
slug,tags,category,keywords, …). - [ ] Optional keys only if needed:
license,compatibility,metadata,allowed-tools(per validator). - [ ]
descriptionstarts withUse when. - [ ] Body describes triggers and use cases without duplicating full README marketing copy.
Agents / OpenAI
- [ ]
agents/openai.yamlexists or README documents exception. - [ ] File contains a short, user-understandable description of the skill (what/when).
Discovery metadata
- [ ] Optional
metadata/discovery.yaml(or documented equivalent) matches README if used — see `skill-discovery-metadata.md`. - [ ] `action_level` and `risk_level` present in discovery YAML or README classification table.
GitHub repository SEO
- [ ] Repository Description ≤ 160 characters, names concrete tech or use case — not generic “AI assistant”.
- [ ] Topics include
agent-skillplus relevant tech/domain tags; no irrelevant stuffing.
GitHub Pages
- [ ]
gh api repos/netresearch/<repo>/pagesreturns HTTP 404 (Pages disabled — the default). - [ ] If Pages is enabled: the PR description names which
repository-quality-rules.mdPages criterion is satisfied, and the README contains the mandatory artefacts (justification, canonical URL, source path, build/deploy commands, link-checking, content-split note).
Related skills
- [ ] Related skills list is honest (exists, planned, or external) — see `repository-quality-rules.md`.
Marketplace sync expectations
- [ ] README contains note or checkbox: when discovery fields change, marketplace entry must be updated or override documented (see `marketplace-integration.md`).
Links and automation
- [ ] All links in touched docs resolve (internal paths and GitHub URLs).
- [ ]
bash skills/skill-repo/scripts/validate-skill.shexits 0 from repo root. - [ ] If repo has CI calling
netresearch/skill-repo-skill/.github/workflows/validate.yml, PR checks are green.
Optional extended validation
- [ ]
scripts/audit-skills.sh(if present in repo) reports no new orphanreferences/files.
#!/usr/bin/env bash
#
# check-version-parity.sh — verify plugin.json, composer.json, and SKILL.md
# versions are consistent before a release.
#
# Usage:
# check-version-parity.sh # compare plugin.json vs SKILL.md
# check-version-parity.sh v1.2.3 # also require plugin.json == 1.2.3
# check-version-parity.sh 1.2.3 # same, leading v optional
#
# Exit codes: 0 = parity OK, 1 = mismatch or missing version.
#
# Behavior:
# * Reads .claude-plugin/plugin.json version (required)
# * If SKILL.md frontmatter has metadata.version, requires it matches
# * composer.json MUST NOT have a version field (version is derived from
# git tags by the release workflow). Composer-shipped version would
# drift from the tag.
# * If a tag argument is provided, requires plugin.json version == tag
# with 'v' prefix stripped.
set -euo pipefail
TAG_ARG="${1:-}"
TAG_VERSION="${TAG_ARG#v}" # strip leading v if present (empty stays empty)
PLUGIN_JSON=".claude-plugin/plugin.json"
COMPOSER_JSON="composer.json"
if [[ ! -f "$PLUGIN_JSON" ]]; then
echo "ERROR: $PLUGIN_JSON not found — run from skill repo root" >&2
exit 1
fi
PLUGIN_VERSION=$(jq -r '.version // empty' "$PLUGIN_JSON")
if [[ -z "$PLUGIN_VERSION" ]]; then
echo "ERROR: $PLUGIN_JSON has no .version field" >&2
exit 1
fi
# composer.json MUST NOT have a version field
if [[ -f "$COMPOSER_JSON" ]] && jq -e 'has("version")' "$COMPOSER_JSON" > /dev/null 2>&1; then
CJ_VERSION=$(jq -r '.version // empty' "$COMPOSER_JSON")
echo "ERROR: $COMPOSER_JSON has a version field ($CJ_VERSION) — remove it" >&2
echo " Git tag is the source of truth for composer packages." >&2
exit 1
fi
# Tag argument must match plugin.json
if [[ -n "$TAG_VERSION" && "$PLUGIN_VERSION" != "$TAG_VERSION" ]]; then
echo "ERROR: plugin.json=$PLUGIN_VERSION does not match tag $TAG_ARG" >&2
exit 1
fi
# SKILL.md metadata.version (if present) must match plugin.json
MISMATCH=0
shopt -s nullglob
SKILL_FILES=(skills/*/SKILL.md)
shopt -u nullglob
if [[ ${#SKILL_FILES[@]} -eq 0 ]]; then
echo "WARN: no skills/*/SKILL.md files found" >&2
fi
for skill_md in "${SKILL_FILES[@]}"; do
# Extract metadata.version from YAML frontmatter (indented key).
# Tolerate quoted or unquoted values; return empty if absent.
SKILL_VERSION=$(awk '
/^---$/ { fm = !fm; next }
fm && /^[[:space:]]+version:[[:space:]]*/ {
gsub(/^[[:space:]]+version:[[:space:]]*/, "")
gsub(/["\047]/, "")
gsub(/[[:space:]]+$/, "")
print
exit
}
' "$skill_md")
if [[ -n "$SKILL_VERSION" && "$SKILL_VERSION" != "$PLUGIN_VERSION" ]]; then
echo "ERROR: $skill_md metadata.version=$SKILL_VERSION does not match plugin.json=$PLUGIN_VERSION" >&2
MISMATCH=1
fi
done
if (( MISMATCH )); then
exit 1
fi
if [[ -n "$TAG_VERSION" ]]; then
echo "OK: plugin.json and tag match at $PLUGIN_VERSION"
else
echo "OK: plugin.json and SKILL.md versions match at $PLUGIN_VERSION"
echo " (pass a tag argument like v$PLUGIN_VERSION to verify tag parity)"
fi
#!/bin/bash
# migrate-licensing.sh - Migrate a skill repo from single LICENSE to split licensing
# Usage: ./migrate-licensing.sh <repo-root-path>
set -euo pipefail
REPO_DIR="${1:-.}"
YEAR="2025-2026"
echo "Migrating licensing in: $REPO_DIR"
# 1. Create LICENSE-MIT
if [[ -f "$REPO_DIR/LICENSE" ]]; then
if grep -q "GNU GENERAL PUBLIC LICENSE" "$REPO_DIR/LICENSE"; then
echo "INFO: Repo has GPL license — creating MIT from scratch"
cat > "$REPO_DIR/LICENSE-MIT" << 'MITEOF'
MIT License
Copyright (c) 2025-2026 Netresearch DTT GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MITEOF
else
# Existing MIT — copy and update year
cp "$REPO_DIR/LICENSE" "$REPO_DIR/LICENSE-MIT"
if ! grep -q "2026" "$REPO_DIR/LICENSE-MIT"; then
sed -i -E "s/Copyright \(c\) ([0-9]{4})/Copyright (c) \1-2026/" "$REPO_DIR/LICENSE-MIT"
fi
fi
# Stage removal of old LICENSE
git -C "$REPO_DIR" rm -f LICENSE 2>/dev/null || rm -f "$REPO_DIR/LICENSE"
else
echo "INFO: No LICENSE found — creating LICENSE-MIT from scratch"
cat > "$REPO_DIR/LICENSE-MIT" << MITEOF
MIT License
Copyright (c) $YEAR Netresearch DTT GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MITEOF
fi
# 2. Create LICENSE-CC-BY-SA-4.0
cat > "$REPO_DIR/LICENSE-CC-BY-SA-4.0" << CCEOF
Creative Commons Attribution-ShareAlike 4.0 International
Copyright (c) $YEAR Netresearch DTT GmbH
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
International License. To view a copy of this license, visit
https://creativecommons.org/licenses/by-sa/4.0/ or send a letter to
Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
You are free to:
- Share: copy and redistribute the material in any medium or format
- Adapt: remix, transform, and build upon the material for any purpose,
even commercially
Under the following terms:
- Attribution: You must give appropriate credit, provide a link to the
license, and indicate if changes were made.
- ShareAlike: If you remix, transform, or build upon the material, you
must distribute your contributions under the same license as the original.
CCEOF
# 3. Update composer.json and plugin.json
python3 - "$REPO_DIR" << 'PYEOF'
import json, sys, os
repo_dir = sys.argv[1]
for rel_path, label in [("composer.json", "composer.json"), (".claude-plugin/plugin.json", "plugin.json")]:
full_path = os.path.join(repo_dir, rel_path)
if not os.path.isfile(full_path):
continue
with open(full_path, 'r') as f:
data = json.load(f)
data['license'] = '(MIT AND CC-BY-SA-4.0)'
with open(full_path, 'w') as f:
json.dump(data, f, indent=4)
f.write('\n')
print(f"Updated {label} license")
PYEOF
# 5. Update README.md license section
if [[ -f "$REPO_DIR/README.md" ]]; then
python3 - "$REPO_DIR" << 'PYEOF'
import re, sys
repo_dir = sys.argv[1] if len(sys.argv) > 1 else "."
readme_path = f"{repo_dir}/README.md"
with open(readme_path, 'r') as f:
content = f.read()
# Replace license section
new_license = """## License
This project uses split licensing:
- **Code** (scripts, workflows, configs): [MIT](LICENSE-MIT)
- **Content** (skill definitions, documentation, references): [CC-BY-SA-4.0](LICENSE-CC-BY-SA-4.0)
See the individual license files for full terms."""
# Match ## License section until next ## heading or --- or end of file
pattern = r'## License\n.*?(?=\n## |\n---|\Z)'
content = re.sub(pattern, new_license, content, flags=re.DOTALL)
# Fix structure diagrams
content = re.sub(
r'├── LICENSE\s+# (?:MIT|GPL[^\n]*)',
'├── LICENSE-MIT # Code license (MIT)\n├── LICENSE-CC-BY-SA-4.0 # Content license (CC-BY-SA-4.0)',
content
)
with open(readme_path, 'w') as f:
f.write(content)
PYEOF
echo "Updated README.md license section"
fi
echo "Done! Review changes with: git -C $REPO_DIR diff"
#!/usr/bin/env bash
# validate-evals.sh - Structural validation of evals.json files
# Supports three formats:
# Unified (recommended): {"skill_name": "...", "evals": [{id, eval_name, prompt,
# expected_output?, expectations?: ["..."], assertions?: [{type, pattern}]}]}
# - Evals may use expectations (string[], LLM-as-judge), assertions (object[],
# regex matching), or both. At least one grading mechanism is required.
# Legacy A: {"skill_name": "...", "evals": [{id, eval_name, prompt, assertions: [...]}]}
# Legacy B: [{name, prompt, assertions: [{type, value/pattern, description?}]}]
#
# Usage: bash validate-evals.sh [path-to-evals.json]
# If no path given, searches skills/*/evals/evals.json then evals/evals.json
set -euo pipefail
PASS=0
FAIL=0
WARN=0
pass() { PASS=$((PASS + 1)); echo " PASS: $1"; }
fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
warn() { WARN=$((WARN + 1)); echo " WARN: $1"; }
# --- Locate evals.json ---
EVALS_FILE="${1:-}"
if [[ -z "$EVALS_FILE" ]]; then
for candidate in skills/*/evals/evals.json evals/evals.json; do
if [[ -f "$candidate" ]]; then
EVALS_FILE="$candidate"
break
fi
done
fi
if [[ -z "$EVALS_FILE" ]] || [[ ! -f "$EVALS_FILE" ]]; then
echo "ERROR: No evals.json found"
echo "Searched: skills/*/evals/evals.json, evals/evals.json"
exit 1
fi
echo "Validating: $EVALS_FILE"
echo "---"
# --- Valid JSON ---
if ! python3 -c "import json, sys; json.load(open(sys.argv[1]))" "$EVALS_FILE" 2>/dev/null; then
fail "Invalid JSON"
echo ""
echo "Results: $PASS passed, $FAIL failed, $WARN warnings"
exit 1
fi
pass "Valid JSON"
# --- Run all structural checks via Python ---
RESULT=$(python3 - "$EVALS_FILE" <<'PYEOF'
import json
import sys
with open(sys.argv[1]) as f:
raw = json.load(f)
# Detect format and normalize to list of evals
if isinstance(raw, dict) and "evals" in raw:
# Format A: {skill_name, evals: [...]}
evals = raw["evals"]
fmt = "A"
elif isinstance(raw, list):
# Format B: [...]
evals = raw
fmt = "B"
else:
print("FAIL|Top-level structure must be an array or object with 'evals' key")
sys.exit(0)
print(f"INFO|Detected format {'A (object with evals key)' if fmt == 'A' else 'B (top-level array)'}")
print(f"INFO|Total evals: {len(evals)}")
if not isinstance(evals, list):
print("FAIL|'evals' must be an array")
sys.exit(0)
if len(evals) == 0:
print("FAIL|No evals found")
sys.exit(0)
# Check eval count thresholds
if len(evals) < 10:
print(f"FAIL|Eval count {len(evals)} < 10 minimum")
elif len(evals) < 15:
print(f"WARN|Eval count {len(evals)} < 15 recommended")
else:
print(f"PASS|Eval count {len(evals)} >= 15")
# Track names for duplicate check
names = []
ids_found = []
has_ids = False
for i, ev in enumerate(evals):
label = f"eval[{i}]"
if not isinstance(ev, dict):
print(f"FAIL|{label}: not an object")
continue
# Name/ID check: accept 'name', 'eval_name', or 'id' (integer) as identifier
name = str(ev.get("name") or ev.get("eval_name") or "").strip()
if not name:
# Fall back to id as identifier for Anthropic format
eid = ev.get("id")
if isinstance(eid, int):
name = f"id={eid}"
else:
print(f"FAIL|{label}: missing or empty name/eval_name/id")
if name:
names.append(name)
# Prompt check (accept 'prompt' or legacy 'input')
prompt = ev.get("prompt") or ev.get("input") or ""
if not prompt or not str(prompt).strip():
print(f"FAIL|{label} ({name}): missing or empty prompt/input")
else:
print(f"PASS|{label} ({name}): has prompt")
# ID check (optional but validated if present; must be integer)
if "id" in ev:
if not isinstance(ev["id"], int):
print(f"FAIL|{label} ({name}): id must be an integer")
else:
has_ids = True
ids_found.append(ev["id"])
# expected_output check (recommended for unified format, skip for legacy)
is_unified = any(key in ev for key in ("eval_name", "id", "expectations", "expected_output"))
if is_unified and "expected_output" not in ev:
print(f"WARN|{label} ({name}): missing expected_output (recommended)")
# Grading check: must have expectations OR assertions (or both)
expectations = ev.get("expectations")
assertions = ev.get("assertions")
has_expectations = False
has_assertions = False
# Validate expectations (string[], 2+ items)
if expectations is not None:
if not isinstance(expectations, list):
print(f"FAIL|{label} ({name}): expectations must be an array")
elif len(expectations) < 2:
print(f"FAIL|{label} ({name}): has {len(expectations)} expectations, need >= 2")
else:
invalid_exp = 0
for j, e in enumerate(expectations):
if not isinstance(e, str):
invalid_exp += 1
print(f"FAIL|{label} ({name}): expectations[{j}] must be a string")
elif not e.strip():
invalid_exp += 1
print(f"FAIL|{label} ({name}): expectations[{j}] is empty")
if invalid_exp == 0:
has_expectations = True
print(f"PASS|{label} ({name}): {len(expectations)} valid expectations")
# Validate assertions (object[] or string[], 2+ items)
if assertions is not None:
if not isinstance(assertions, list):
print(f"FAIL|{label} ({name}): assertions must be an array")
elif len(assertions) < 2:
print(f"FAIL|{label} ({name}): has {len(assertions)} assertions, need >= 2")
else:
invalid_assertions = 0
for j, a in enumerate(assertions):
if isinstance(a, str):
if not a.strip():
invalid_assertions += 1
print(f"FAIL|{label} ({name}): assertion[{j}] is empty")
elif isinstance(a, dict):
if "type" not in a:
invalid_assertions += 1
print(f"FAIL|{label} ({name}): assertion[{j}] missing 'type'")
val = a.get("value") or a.get("pattern") or ""
if not str(val).strip():
invalid_assertions += 1
print(f"FAIL|{label} ({name}): assertion[{j}] missing 'value' or 'pattern'")
else:
invalid_assertions += 1
print(f"FAIL|{label} ({name}): assertion[{j}] invalid type (not string or object)")
if invalid_assertions == 0:
has_assertions = True
print(f"PASS|{label} ({name}): {len(assertions)} valid assertions")
# Must have at least one grading mechanism
if not has_expectations and not has_assertions:
if expectations is None and assertions is None:
print(f"FAIL|{label} ({name}): missing grading (need expectations or assertions)")
# else: already reported specific validation errors above
# Duplicate names
seen = set()
dupes = set()
for n in names:
if n in seen:
dupes.add(n)
seen.add(n)
if dupes:
print(f"FAIL|Duplicate eval names: {', '.join(sorted(dupes))}")
else:
print(f"PASS|No duplicate eval names")
# ID validation (if IDs are present)
if has_ids:
# Check for duplicates
id_counts = {}
for eid in ids_found:
id_counts[eid] = id_counts.get(eid, 0) + 1
dupe_ids = [k for k, v in id_counts.items() if v > 1]
if dupe_ids:
print(f"FAIL|Duplicate IDs: {dupe_ids}")
else:
print(f"PASS|No duplicate IDs")
# Check sequential (1-based)
numeric_ids = sorted([x for x in ids_found if isinstance(x, int)])
if numeric_ids:
expected = list(range(1, len(numeric_ids) + 1))
if numeric_ids != expected:
gaps = set(expected) - set(numeric_ids)
extra = set(numeric_ids) - set(expected)
msg = ""
if gaps:
msg += f"missing: {sorted(gaps)}"
if extra:
if msg:
msg += ", "
msg += f"unexpected: {sorted(extra)}"
print(f"FAIL|IDs not sequential: {msg}")
else:
print(f"PASS|IDs sequential (1-{len(numeric_ids)})")
PYEOF
)
# --- Parse Python output ---
while IFS='|' read -r level msg; do
case "$level" in
PASS) pass "$msg" ;;
FAIL) fail "$msg" ;;
WARN) warn "$msg" ;;
INFO) echo " INFO: $msg" ;;
esac
done <<< "$RESULT"
# --- Summary ---
echo ""
echo "---"
echo "Results: $PASS passed, $FAIL failed, $WARN warnings"
if [[ $FAIL -gt 0 ]]; then
exit 1
fi
exit 0
#!/usr/bin/env bash
# validate-skill.sh - Validate Netresearch skill repository structure
# Usage: ./validate-skill.sh [repo-root-path]
#
# Checks: SKILL.md frontmatter, word count, composer.json, plugin.json,
# cross-file consistency, required files
# Exit: 0 = valid, 1 = errors found
set -euo pipefail
REPO_DIR="${1:-.}"
ERRORS=0
WARNINGS=0
NAME=""
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
error() { echo -e "${RED}ERROR:${NC} $1"; ((ERRORS++)) || true; }
warning() { echo -e "${YELLOW}WARNING:${NC} $1"; ((WARNINGS++)) || true; }
success() { echo -e "${GREEN}OK:${NC} $1"; }
# Check python3 availability (required for JSON parsing)
if ! command -v python3 &>/dev/null; then
echo -e "${RED}ERROR:${NC} python3 is required for JSON parsing but not found in PATH"
exit 1
fi
echo "Validating skill repository: $REPO_DIR"
echo "========================================"
# --- Discover SKILL.md ---
SKILL_FILE=""
if [[ -f "$REPO_DIR/SKILL.md" ]]; then
SKILL_FILE="$REPO_DIR/SKILL.md"
else
for f in "$REPO_DIR"/skills/*/SKILL.md; do
if [[ -f "$f" ]]; then
SKILL_FILE="$f"
break
fi
done
fi
# --- SKILL.md checks ---
if [[ -n "$SKILL_FILE" ]]; then
success "SKILL.md found: ${SKILL_FILE#"$REPO_DIR"/}"
# Frontmatter delimiter
if head -1 "$SKILL_FILE" | grep -q "^---$"; then
# Verify closing --- delimiter exists (within first 30 lines)
CLOSING_LINE=$(sed -n '2,30{/^---$/=}' "$SKILL_FILE" | head -1)
if [[ -z "$CLOSING_LINE" ]]; then
error "SKILL.md frontmatter has opening --- but no closing --- delimiter"
else
success "SKILL.md has frontmatter"
fi
# Extract frontmatter fields (between first two --- lines)
FRONTMATTER=$(sed -n '2,/^---$/{ /^---$/d; p; }' "$SKILL_FILE")
# Check frontmatter fields match Agent Skills spec
# Allowed: name, description, license, compatibility, metadata, allowed-tools
EXTRA_FIELDS=$(echo "$FRONTMATTER" | grep -E "^[a-z_-]+:" | grep -vE "^(name|description|license|compatibility|metadata|allowed-tools):" || true)
if [[ -z "$EXTRA_FIELDS" ]]; then
success "Frontmatter fields are valid per Agent Skills spec"
else
FIELD_NAMES=$(echo "$EXTRA_FIELDS" | sed 's/:.*//' | tr '\n' ', ' | sed 's/,$//')
error "Frontmatter has non-spec fields: $FIELD_NAMES (allowed: name, description, license, compatibility, metadata, allowed-tools)"
fi
# Check name field
if echo "$FRONTMATTER" | grep -q "^name:"; then
NAME=$(echo "$FRONTMATTER" | grep "^name:" | head -1 | sed 's/name: *//' | tr -d '"')
if [[ "$NAME" =~ ^[a-z0-9-]{1,64}$ ]]; then
success "SKILL.md name valid: $NAME"
else
error "SKILL.md name invalid (lowercase, hyphens, max 64): $NAME"
fi
else
error "SKILL.md missing 'name' field"
fi
# Check description field and prefix
if echo "$FRONTMATTER" | grep -q "^description:"; then
# Parse the *YAML value* of description so every valid scalar style
# (plain, single/double-quoted, block) is accepted as long as the
# parsed value starts with "Use when". Uses PyYAML when available,
# otherwise a stdlib-only fallback covering the common scalar styles,
# so the script keeps running with just python3 (no yq/PyYAML needed).
# When PyYAML is present it is authoritative: invalid YAML is
# reported (sentinel __PARSE_ERROR__), not silently re-parsed by the
# fallback. The stdlib-only fallback runs solely when PyYAML is
# absent, so the script still works with just python3.
DESC=$(FRONTMATTER="$FRONTMATTER" python3 <<'PYEOF' 2>/dev/null || echo "__PARSE_ERROR__"
import os, re, sys
fm = os.environ["FRONTMATTER"]
try:
import yaml
except Exception:
yaml = None
if yaml is not None:
# PyYAML available: trust it fully so semantics match CI exactly.
try:
data = yaml.safe_load(fm)
except Exception:
print("__PARSE_ERROR__")
sys.exit(0)
desc = data.get("description") if isinstance(data, dict) else None
print(desc if desc is not None else "")
sys.exit(0)
# Fallback without PyYAML: best-effort for the common scalar styles
# (plain, single/double-quoted, block). description: is a column-0 key.
desc = None
lines = fm.splitlines()
for i, line in enumerate(lines):
m = re.match(r"description:[ \t]*(.*)$", line)
if not m:
continue
val = m.group(1).strip()
if val[:1] in ("|", ">"):
# Block scalar: first non-blank line that is indented into the block.
# A column-0 (non-indented) line is the next sibling key -> empty body.
for nxt in lines[i + 1:]:
if not nxt.strip():
continue
if not nxt[:1].isspace():
break
desc = nxt.strip()
break
else:
dq = re.match(r'"((?:[^"\\]|\\.)*)"[ \t]*(?:#.*)?$', val)
sq = re.match(r"'((?:[^']|'')*)'[ \t]*(?:#.*)?$", val)
if dq:
desc = dq.group(1)
elif sq:
desc = sq.group(1).replace("''", "'")
else:
# Plain scalar: strip a trailing ' #' comment (YAML needs the space).
desc = re.sub(r"[ \t]+#.*$", "", val)
break
print(desc if desc is not None else "")
PYEOF
)
if [[ "$DESC" == "__PARSE_ERROR__" ]]; then
error "SKILL.md frontmatter is not valid YAML (could not parse 'description')"
elif [[ "$DESC" == Use\ when* ]]; then
success "Description starts with 'Use when'"
else
error "Description must start with 'Use when': ${DESC:0:60}..."
fi
else
error "SKILL.md missing 'description' field"
fi
else
error "SKILL.md missing frontmatter (must start with ---)"
fi
# Word count check (max 500)
WORDS=$(wc -w < "$SKILL_FILE")
if [[ $WORDS -le 500 ]]; then
success "SKILL.md is $WORDS words (under 500 limit)"
else
error "SKILL.md is $WORDS words (max 500)"
fi
# Check for relative script paths that should use ${CLAUDE_SKILL_DIR}
# Matches: uv run scripts/, python3 scripts/, python scripts/, bash scripts/, ./scripts/, sh scripts/
# But ignores lines already using ${CLAUDE_SKILL_DIR}
RELATIVE_PATHS=$(grep -nE '(uv run|python3?|bash|sh|\./)([ ]+)scripts/' "$SKILL_FILE" | grep -v 'CLAUDE_SKILL_DIR' || true)
if [[ -n "$RELATIVE_PATHS" ]]; then
COUNT=$(echo "$RELATIVE_PATHS" | wc -l)
warning "SKILL.md has $COUNT script reference(s) using relative paths instead of \${CLAUDE_SKILL_DIR}/scripts/"
fi
else
error "SKILL.md not found (checked root and skills/*/)"
fi
# --- Required files ---
for file in README.md LICENSE-MIT LICENSE-CC-BY-SA-4.0 .gitignore; do
if [[ -f "$REPO_DIR/$file" ]]; then
success "$file exists"
else
error "$file not found"
fi
done
# Warn about old single LICENSE file
if [[ -f "$REPO_DIR/LICENSE" ]] && [[ -f "$REPO_DIR/LICENSE-MIT" ]]; then
warning "Old LICENSE file still exists alongside LICENSE-MIT — remove it"
elif [[ -f "$REPO_DIR/LICENSE" ]] && [[ ! -f "$REPO_DIR/LICENSE-MIT" ]]; then
warning "Single LICENSE file found — migrate to LICENSE-MIT + LICENSE-CC-BY-SA-4.0"
fi
# Release workflow
if [[ -f "$REPO_DIR/.github/workflows/release.yml" ]]; then
success "release.yml exists"
else
error ".github/workflows/release.yml not found"
fi
# No composer.lock
if [[ -f "$REPO_DIR/composer.lock" ]]; then
error "composer.lock must not exist in skill repos"
else
success "No composer.lock"
fi
# --- composer.json checks ---
if [[ -f "$REPO_DIR/composer.json" ]]; then
success "composer.json exists"
# Type
if grep -q '"type".*"ai-agent-skill"' "$REPO_DIR/composer.json"; then
success "composer.json type is ai-agent-skill"
else
error "composer.json type must be 'ai-agent-skill'"
fi
# License SPDX expression
COMP_LICENSE=$(python3 - "$REPO_DIR" <<'PYEOF' 2>/dev/null || echo ""
import json, sys
with open(f'{sys.argv[1]}/composer.json', 'r') as f:
print(json.load(f).get('license', ''))
PYEOF
)
if [[ "$COMP_LICENSE" == "(MIT AND CC-BY-SA-4.0)" ]]; then
success "composer.json license is correct SPDX expression"
else
warning "composer.json license should be '(MIT AND CC-BY-SA-4.0)', got: $COMP_LICENSE"
fi
# Name must match GitHub repo name (netresearch/{repo-name})
COMP_NAME=$(python3 - "$REPO_DIR" <<'PYEOF' 2>/dev/null || echo ""
import json, sys
with open(f'{sys.argv[1]}/composer.json', 'r') as f:
print(json.load(f).get('name', ''))
PYEOF
)
REPO_NAME=""
if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then
REPO_NAME="${GITHUB_REPOSITORY#*/}"
elif git -C "$REPO_DIR" remote get-url origin &>/dev/null; then
REMOTE_URL=$(git -C "$REPO_DIR" remote get-url origin 2>/dev/null)
REPO_NAME=$(basename "$REMOTE_URL" .git)
fi
if [[ -n "$REPO_NAME" ]]; then
EXPECTED_NAME="netresearch/$REPO_NAME"
if [[ "$COMP_NAME" == "$EXPECTED_NAME" ]]; then
success "composer.json name matches repo: $COMP_NAME"
else
error "composer.json name must match repo name: expected '$EXPECTED_NAME', got '$COMP_NAME'"
fi
elif [[ "$COMP_NAME" =~ ^netresearch/.*-skill$ ]]; then
success "composer.json name: $COMP_NAME (repo name check skipped - no git remote)"
else
error "composer.json name must match netresearch/{repo-name}: $COMP_NAME"
fi
# Plugin dependency
if grep -q "composer-agent-skill-plugin" "$REPO_DIR/composer.json"; then
success "composer.json requires skill plugin"
else
warning "composer.json should require netresearch/composer-agent-skill-plugin"
fi
# ai-agent-skill extra path(s) exist (supports both string and array values)
SKILL_PATH_ERRORS=$(python3 - "$REPO_DIR" <<'PYEOF' 2>/dev/null || echo "ERROR"
import json, os, sys
repo_dir = sys.argv[1]
data = json.load(open(os.path.join(repo_dir, 'composer.json')))
val = data.get('extra', {}).get('ai-agent-skill', '')
paths = val if isinstance(val, list) else [val] if val else []
if not paths:
print('MISSING')
else:
for p in paths:
if not os.path.isfile(os.path.join(repo_dir, p)):
print('NOTFOUND:' + p)
else:
print('OK:' + p)
PYEOF
)
if [[ "$SKILL_PATH_ERRORS" == "MISSING" ]]; then
error "composer.json missing extra.ai-agent-skill"
elif [[ "$SKILL_PATH_ERRORS" == "ERROR" ]]; then
error "composer.json extra.ai-agent-skill could not be parsed"
else
while IFS= read -r line; do
case "$line" in
OK:*) success "composer.json skill path exists: ${line#OK:}" ;;
NOTFOUND:*) error "composer.json skill path missing: ${line#NOTFOUND:}" ;;
esac
done <<< "$SKILL_PATH_ERRORS"
fi
else
error "composer.json not found"
fi
# --- plugin.json checks ---
PLUGIN_FILE="$REPO_DIR/.claude-plugin/plugin.json"
if [[ -f "$PLUGIN_FILE" ]]; then
success "plugin.json exists"
# Name matches SKILL.md name (only for single-skill repos)
PLUGIN_NAME=$(python3 - "$PLUGIN_FILE" <<'PYEOF' 2>/dev/null || echo ""
import json, sys
with open(sys.argv[1], 'r') as f:
print(json.load(f).get('name', ''))
PYEOF
)
SKILL_COUNT=$(python3 - "$PLUGIN_FILE" <<'PYEOF' 2>/dev/null || echo "1"
import json, sys
with open(sys.argv[1], 'r') as f:
print(len(json.load(f).get('skills', [])))
PYEOF
)
if [[ "$SKILL_COUNT" -le 1 ]]; then
if [[ -n "$NAME" ]] && [[ "$PLUGIN_NAME" == "$NAME" ]]; then
success "plugin.json name matches SKILL.md: $PLUGIN_NAME"
elif [[ -n "$NAME" ]]; then
error "plugin.json name '$PLUGIN_NAME' does not match SKILL.md name '$NAME'"
fi
else
success "plugin.json is multi-skill ($SKILL_COUNT skills), name check skipped"
fi
# Skills is array
SKILLS_TYPE=$(python3 - "$PLUGIN_FILE" <<'PYEOF' 2>/dev/null || echo "unknown"
import json, sys
with open(sys.argv[1], 'r') as f:
s = json.load(f).get('skills')
print('array' if isinstance(s, list) else type(s).__name__)
PYEOF
)
if [[ "$SKILLS_TYPE" == "array" ]]; then
success "plugin.json skills is array"
# Check each skill path exists as directory
MISSING_PATHS=$(python3 - "$PLUGIN_FILE" "$REPO_DIR" <<'PYEOF' 2>/dev/null || true
import json, os, sys
with open(sys.argv[1], 'r') as f:
data = json.load(f)
for path in data.get('skills', []):
full = os.path.join(sys.argv[2], path)
if not os.path.isdir(full):
print(path)
PYEOF
)
if [[ -z "$MISSING_PATHS" ]]; then
success "All plugin.json skill paths exist"
else
while IFS= read -r p; do
error "plugin.json skill path missing: $p"
done <<< "$MISSING_PATHS"
fi
else
error "plugin.json skills must be an array (got: $SKILLS_TYPE)"
fi
# Author URL
AUTHOR_URL=$(python3 - "$PLUGIN_FILE" <<'PYEOF' 2>/dev/null || echo ""
import json, sys
with open(sys.argv[1], 'r') as f:
print(json.load(f).get('author', {}).get('url', ''))
PYEOF
)
if [[ -z "$AUTHOR_URL" ]]; then
error "plugin.json author.url is missing or empty; it must be https://www.netresearch.de"
else
AUTHOR_URL_CLEAN="${AUTHOR_URL%/}"
if [[ "$AUTHOR_URL_CLEAN" == "https://www.netresearch.de" ]]; then
success "plugin.json author.url is correct"
else
error "plugin.json author.url must be https://www.netresearch.de (got: $AUTHOR_URL)"
fi
fi
else
error ".claude-plugin/plugin.json not found"
fi
# --- README.md quality checks (warnings only) ---
# Recommended level-2 headings (whole-line match) per skills/skill-repo/references/readme-template.md
README_REQUIRED_HEADINGS=(
"What this skill solves"
"Use when"
"Expected outputs"
"Context requirements"
"Example prompts"
"Related skills"
"Installation"
"Contributing"
"License"
)
if [[ -f "$REPO_DIR/README.md" ]]; then
if grep -q "Netresearch" "$REPO_DIR/README.md"; then
success "README.md contains Netresearch reference"
else
warning "README.md should contain Netresearch credits"
fi
for heading in "${README_REQUIRED_HEADINGS[@]}"; do
# Whole-line match only (avoids substring hits inside ### headings or prose)
if grep -Fxq "## ${heading}" "$REPO_DIR/README.md"; then
success "README.md has ## ${heading}"
else
warning "README.md missing recommended section (exact line ## ${heading}) — see skill-repo-skill skills/skill-repo/references/readme-template.md"
fi
done
# Every documented slash-command should be enumerated in the README so the
# command (and mode) list does not silently drift when one is added. Warning
# only: the name match is heuristic and a skill may intentionally omit one.
if [[ -d "$REPO_DIR/commands" ]]; then
for cmd_file in "$REPO_DIR"/commands/*.md; do
[[ -e "$cmd_file" ]] || continue
cmd_name="$(basename "$cmd_file" .md)"
# -F: match the command name literally (a filename with regex
# metacharacters can't break or mis-match). -w: require word
# boundaries, so `/work-update` does not match inside
# `commands/work-update.md` or a URL like `netresearch/work-update`.
if grep -qFw -- "/${cmd_name}" "$REPO_DIR/README.md"; then
success "README.md references /${cmd_name}"
else
warning "README.md does not mention command /${cmd_name} (commands/${cmd_name}.md) — keep the README command/mode list in sync when adding commands or modes"
fi
done
fi
fi
# --- Summary ---
echo ""
echo "========================================"
echo "Validation Summary"
echo "========================================"
echo -e "Errors: ${RED}$ERRORS${NC}"
echo -e "Warnings: ${YELLOW}$WARNINGS${NC}"
if [[ $ERRORS -eq 0 ]]; then
echo -e "${GREEN}Skill repository is valid!${NC}"
exit 0
else
echo -e "${RED}Skill repository has $ERRORS error(s) that must be fixed.${NC}"
exit 1
fi
# Copy to .github/workflows/npm-pack-smoke.yml in your skill repo (drop the
# .template suffix). This is a thin caller for the org-level reusable
# workflow at netresearch/skill-repo-skill — per SKILL.md, skill repos must
# delegate CI to reusable workflows rather than defining steps inline.
#
# What the reusable workflow asserts:
# 1. No internal leakage — repo-maintenance files (.github/, evals/,
# docs/, Build/, harness scripts like verify-harness/generate-dashboard/
# run-ab-evals, lint configs, .envrc) must NOT ship in the tarball.
# 2. Runtime-referenced dirs are present — every top-level dir referenced
# by skills/*/scripts/* via $ROOT/<dir> or ../<dir> MUST be in the
# tarball, otherwise installed scripts break at runtime.
#
# See skills/skill-repo/references/installation-methods.md for the
# files-allowlist rationale, and references/review-replies.md for canonical
# replies to reviewer comments about scripts/, AGENTS.md, and similar.
name: npm Pack Smoke
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
pack:
uses: netresearch/skill-repo-skill/.github/workflows/npm-pack-smoke.yml@main
name: Lint
# Skill validation (SKILL.md word cap, plugin.json schema, markdown lint, …)
# via the skill-repo-skill reusable. The reusable declares `permissions:
# contents: read` at top level; the calling job mirrors it explicitly so the
# grant is visible at the call site and independent of the repo default.
on:
push:
branches: [main]
pull_request:
permissions: {}
jobs:
validate:
name: Skill Validation
uses: netresearch/skill-repo-skill/.github/workflows/validate.yml@main
permissions:
contents: read
name: Auto-merge dependency PRs
on:
pull_request_target:
permissions: {}
jobs:
auto-merge:
uses: netresearch/.github/.github/workflows/auto-merge-deps.yml@main
permissions:
contents: write
pull-requests: write
{
"name": "netresearch/{repo-name}",
"description": "{Skill description}",
"type": "ai-agent-skill",
"license": "(MIT AND CC-BY-SA-4.0)",
"authors": [
{
"name": "Netresearch DTT GmbH",
"homepage": "https://www.netresearch.de/",
"role": "Manufacturer"
}
],
"require": {
"netresearch/composer-agent-skill-plugin": "*"
},
"extra": {
"ai-agent-skill": "SKILL.md"
}
}
#!/usr/bin/env bash
# pre-commit hook: validate skill repo structure
# Install: cp Build/hooks/pre-commit .git/hooks/pre-commit
# Or use: git config core.hooksPath Build/hooks
SCRIPT=""
for f in scripts/validate-skill.sh skills/*/scripts/validate-skill.sh; do
[[ -f "$f" ]] && SCRIPT="$f" && break
done
if [[ -z "$SCRIPT" ]]; then
echo "No local validate-skill.sh found, skipping validation"
exit 0
fi
bash "$SCRIPT" .