
Skill Forge
- 33 installs
- 84 repo stars
- Updated April 10, 2026
- agricidaniel/skill-forge
skill-forge is a Claude Code skill that designs, scaffolds, builds, reviews, evolves, and publishes production-grade Claude Code skills.
About
skill-forge is a Claude Code skill that designs, scaffolds, builds, reviews, evolves, and publishes other Claude Code skills. It follows the Agent Skills open standard and a 3-layer architecture, and detects a skill complexity tier from single-file skills up to full ecosystems with sub-skills and subagents. It routes commands to specialized sub-skills for planning, building, reviewing, evaluating, and converting. A developer uses it as an end-to-end skill-authoring workbench.
- Designs, scaffolds, reviews, evolves, and publishes Claude Code skills
- Enforces the Agent Skills standard and 3-layer progressive-disclosure architecture
- Routes to sub-skills for plan, build, review, evolve, eval, benchmark, convert, publish
Skill Forge by the numbers
- 33 all-time installs (skills.sh)
- Ranked #397 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
skill-forge capabilities & compatibility
- Capabilities
- skill authoring · skill scaffolding · skill review · skill evaluation · skill publishing
- Use cases
- orchestration
- Pricing
- Free
What skill-forge says it does
Ultimate Claude Code skill creator and architect. Designs, scaffolds, builds,
following the Agent Skills open standard and 3-layer architecture (directive, orchestration,
### Tier 4: Full Ecosystem (orchestrator + sub-skills + agents + scripts)
npx skills add https://github.com/agricidaniel/skill-forge --skill skill-forgeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 84 |
| Last updated | April 10, 2026 |
| Repository | agricidaniel/skill-forge ↗ |
What it does
Design, scaffold, review, and publish production-grade Claude Code skills following the Agent Skills standard.
Who is it for?
Authoring, reviewing, and shipping production-grade Claude Code skills end to end
Skip if: General app coding unrelated to building agent skills
When should I use this skill?
user says create skill, build skill, skill creator, skill architecture, or publish skill
What you get
A scaffolded, validated, standard-compliant skill (or ecosystem) ready to eval and publish.
- scaffolded skill files
- SKILL.md
- sub-skills
By the numbers
- Detects 4 skill complexity tiers
- Routes 9 commands to specialized sub-skills
Files
Skill Forge — Ultimate Claude Code Skill Creator
Build production-grade Claude Code skills following the Agent Skills open standard, progressive disclosure architecture, and battle-tested patterns from high-performing skills like claude-seo and claude-ads.
Quick Reference
| Command | What it does |
|---|---|
/skill-forge | Interactive skill creation wizard |
/skill-forge plan <domain> | Architecture and design planning |
/skill-forge build <name> | Scaffold and build a skill from plan |
/skill-forge review <path> | Audit an existing skill for quality |
/skill-forge evolve <path> | Improve skill based on feedback/issues |
/skill-forge eval <path> | Run eval pipeline to test skill quality |
/skill-forge benchmark <path> | Benchmark skill with variance analysis |
/skill-forge publish <path> | Package and prepare for distribution |
/skill-forge convert <path> | Convert skill to Codex/Gemini/Antigravity/Cursor |
Orchestration Logic
Interactive Mode (/skill-forge)
Walk the user through the full skill creation lifecycle:
1. Discovery: Ask about the domain, use cases, and target users 2. Architecture: Determine skill complexity tier and design structure 3. Build: Generate all files following chosen template 4. Review: Validate structure, frontmatter, triggers, and quality 5. Eval: Run eval pipeline with assertions and grading 6. Benchmark: Measure pass rate, time, tokens with variance analysis 7. Iterate: Refine based on eval results and feedback
Command Routing
For specific commands, load the relevant sub-skill:
/skill-forge plan->skills/skill-forge-plan/SKILL.md/skill-forge build->skills/skill-forge-build/SKILL.md/skill-forge review->skills/skill-forge-review/SKILL.md/skill-forge evolve->skills/skill-forge-evolve/SKILL.md/skill-forge eval->skills/skill-forge-eval/SKILL.md/skill-forge benchmark->skills/skill-forge-benchmark/SKILL.md/skill-forge publish->skills/skill-forge-publish/SKILL.md/skill-forge convert->skills/skill-forge-convert/SKILL.md
Skill Complexity Tiers
Detect the appropriate tier based on user's description:
Tier 1: Single Skill (1 SKILL.md)
- Simple workflow or document generation
- No sub-skills or subagents needed
- Under 200 lines of instructions
- Template:
assets/templates/minimal.md
Tier 2: Skill + Scripts (SKILL.md + scripts/)
- Needs deterministic execution (validation, data processing)
- Python/Bash scripts for fragile operations
- Template:
assets/templates/workflow.md
Tier 3: Multi-Skill Orchestrator (main + sub-skills)
- Complex domain with multiple distinct workflows
- Main skill routes to specialized sub-skills
- Shared references across sub-skills
- Template:
assets/templates/multi-skill.md
Tier 4: Full Ecosystem (orchestrator + sub-skills + agents + scripts)
- Enterprise-grade skill with parallel subagent delegation
- Multiple execution scripts for deterministic tasks
- Industry templates and reference knowledge
- Template:
assets/templates/ecosystem.md
Core Principles (Enforce in ALL generated skills)
1. Progressive Disclosure (3 Levels)
- Level 1 (frontmatter): Always in system prompt. Name + description only (~50-100 tokens)
- Level 2 (SKILL.md body): Loaded on activation. Core instructions (<500 lines, <5000 tokens)
- Level 3 (references/scripts/assets): Loaded on-demand. Detailed knowledge and execution
2. Description is King
The description field determines when the skill activates. It MUST contain:
- WHAT the skill does (capabilities)
- WHEN to use it (trigger phrases users would say)
- Key domain keywords for matching
Read references/description-guide.md for the complete framework.
3. The 3-Layer Architecture
- Layer 1 (Directive): SKILL.md instructions, reference files = the "what"
- Layer 2 (Orchestration): Claude's routing and decision-making = the "how"
- Layer 3 (Execution): Scripts in scripts/ = the "do"
Push deterministic work into scripts. Keep probabilistic decisions in instructions.
4. Naming Conventions
- Skill folder:
kebab-case(lowercase + hyphens only) - Name field must match folder name exactly
- Sub-skills:
{parent}-{child}(e.g.,seo-audit,ads-google) - Agents:
agents/{skill}-{role}.md(e.g.,agents/seo-technical.md) - No "claude" or "anthropic" in skill names (reserved)
5. File Rules
- Required:
SKILL.md(exact case) - No
README.mdinside skill folders - No XML angle brackets in frontmatter
- Reference files: focused, small, loaded on-demand
- Scripts: atomic, testable, well-documented
Quality Gates
Before marking any generated skill as complete:
- [ ] SKILL.md exists with valid YAML frontmatter
- [ ] Name is valid kebab-case (1-64 chars)
- [ ] Description includes WHAT + WHEN + keywords (<1024 chars)
- [ ] No XML tags in frontmatter
- [ ] Instructions are specific and actionable (not vague)
- [ ] Error handling included for common failures
- [ ] Examples provided for key workflows
- [ ] SKILL.md body under 500 lines
- [ ] Reference files linked (not inlined) for detailed knowledge
- [ ] Scripts have docstrings, type hints, error handling
Run python scripts/validate_skill.py <path> to verify programmatically.
Reference Files
Load on-demand as needed -- do NOT load all at startup:
references/anatomy.md-- Skill file structure, naming rules, agent formatreferences/patterns.md-- Proven workflow patterns with examplesreferences/frontmatter-spec.md-- YAML frontmatter specification (skills)references/description-guide.md-- Writing trigger-optimized descriptionsreferences/testing-guide.md-- Testing methodology and checklistreferences/pro-agent.md-- 3-layer architecture deep divereferences/tools-reference.md-- All tool names, permission patterns, MCPreferences/hooks-reference.md-- Hook events, types, quality gate patternsreferences/skills-activation.md-- Skill discovery, activation, advanced featuresreferences/platforms.md-- Platform specs and conversion rules
Sub-Skills
This skill orchestrates 8 specialized sub-skills:
1. skill-forge-plan -- Architecture design and use case planning 2. skill-forge-build -- Scaffold and generate skill files 3. skill-forge-review -- Audit and validate existing skills 4. skill-forge-evolve -- Improve skills based on feedback 5. skill-forge-eval -- Run eval pipeline with assertions and grading 6. skill-forge-benchmark -- Benchmark performance with variance analysis 7. skill-forge-publish -- Package and prepare for distribution 8. skill-forge-convert -- Convert skills for Codex, Gemini CLI, Antigravity, Cursor
Template: Full Ecosystem (Tier 4)
Use for enterprise-grade skills with parallel subagent delegation, multiple scripts, industry templates, and comprehensive reference knowledge.
Structure
skill-name/ # Main orchestrator
SKILL.md
references/
ref-1.md
ref-2.md
ref-3.md
assets/
template-a.md
template-b.md
scripts/
fetch_data.py
parse_data.py
validate.py
skills/
skill-name-audit/ # Full audit with parallel delegation
SKILL.md
skill-name-sub1/
SKILL.md
skill-name-sub2/
SKILL.md
skill-name-sub3/
SKILL.md
...
agents/
skill-name-role1.md
skill-name-role2.md
skill-name-role3.md
install.shMain Orchestrator Template
Same as Tier 3 multi-skill template, PLUS:
## Subagents
For parallel analysis during full audits:
- `skill-name-role1` -- {{RESPONSIBILITY}}
- `skill-name-role2` -- {{RESPONSIBILITY}}
- `skill-name-role3` -- {{RESPONSIBILITY}}
## Industry Templates
Available in `assets/`:
- `assets/template-a.md` -- for {{INDUSTRY_A}}
- `assets/template-b.md` -- for {{INDUSTRY_B}}Audit Sub-Skill Template (with parallel delegation)
---
name: {{PARENT}}-audit
description: >
Full {{DOMAIN}} audit with parallel subagent delegation. {{CAPABILITIES}}.
Use when user says "audit", "full check", "analyze", or "health check".
---
# Full {{DOMAIN}} Audit
## Process
### Step 1: Gather Input
Collect URL/data from user.
### Step 2: Initial Analysis
Run: `python scripts/fetch_data.py {{INPUT}}`
### Step 3: Detect Context
Identify industry/type from initial data.
### Step 4: Parallel Delegation
Spawn subagents in parallel:
| Agent | Responsibility | Reference Files |
|-------|---------------|-----------------|
| {{PARENT}}-role1 | {{TASK}} | `references/{{REF}}.md` |
| {{PARENT}}-role2 | {{TASK}} | `references/{{REF}}.md` |
| {{PARENT}}-role3 | {{TASK}} | `references/{{REF}}.md` |
(If subagents unavailable, run inline sequentially)
### Step 5: Aggregate Results
Collect all agent outputs.
Calculate aggregate health score (0-100).
### Step 6: Generate Report
## Output: FULL-AUDIT-REPORT.md
{{DOMAIN}} Audit Report
URL: {{URL}} Date: {{DATE}} Health Score: {{SCORE}}/100
Executive Summary
{{2-3 SENTENCES}}
Scores by Category
| Category | Score | Status |
|---|---|---|
| {{CAT_1}} | {{SCORE}} | {{EMOJI}} |
| {{CAT_2}} | {{SCORE}} | {{EMOJI}} |
| {{CAT_3}} | {{SCORE}} | {{EMOJI}} |
Critical Issues
{{PRIORITIZED_LIST}}
Action Plan
Immediate (This Week)
{{ITEMS}}
Short-Term (This Month)
{{ITEMS}}
Long-Term (This Quarter)
{{ITEMS}}
Agent Definition Template
Agents use YAML frontmatter (different from skills). Body becomes system prompt.
---
name: {{PARENT}}-{{ROLE}}
description: >
{{WHAT_THIS_AGENT_ANALYZES}}. Scores {{CATEGORY}} on a 0-100 scale.
<example>User says: "{{EXAMPLE_TRIGGER}}"</example>
<example>User says: "{{EXAMPLE_TRIGGER}}"</example>
model: inherit
color: {{blue|cyan|green|yellow|magenta|red}}
tools:
- Read
- Grep
- Glob
---
You are a {{ROLE}} specialist.
## Your Role
{{WHAT_THIS_AGENT_ANALYZES_IN_THE_PARALLEL_WORKFLOW}}
## Process
1. Receive {{INPUT_DATA}} from the orchestrating audit skill
2. {{ANALYSIS_STEP_1}}
3. {{ANALYSIS_STEP_2}}
4. Score each item on a 0-10 scale
5. Calculate category score (0-100)
## Scoring Criteria
| Check | Weight | Pass | Fail |
|-------|--------|------|------|
| {{CHECK_1}} | {{WEIGHT}} | {{CRITERIA}} | {{CRITERIA}} |
## Output Format
Return structured markdown with score, findings table, and recommendations.
## Cross-References
- Load `references/{{REF}}.md` for {{DOMAIN_KNOWLEDGE}}
- Defer detailed {{TOPIC}} analysis to `{{PARENT}}-{{OTHER_SUB}}` sub-skillIndustry Template File
# {{INDUSTRY}} Template
## Overview
Key characteristics of {{INDUSTRY}} for this domain.
## Industry-Specific Checks
| Check | Importance | Details |
|-------|-----------|---------|
| {{CHECK_1}} | Critical | {{EXPLANATION}} |
| {{CHECK_2}} | High | {{EXPLANATION}} |
## Benchmarks
| Metric | Good | Average | Poor |
|--------|------|---------|------|
| {{METRIC_1}} | {{VALUE}} | {{VALUE}} | {{VALUE}} |
## Recommendations
{{INDUSTRY_SPECIFIC_RECOMMENDATIONS}}Checklist
- [ ] Main orchestrator under 150 lines
- [ ] Each sub-skill under 200 lines
- [ ] Agents have YAML frontmatter (name, description, model, tools)
- [ ] Agent descriptions use
<example>blocks - [ ] Scoring methodology defined with weights
- [ ] Industry templates cover target verticals
- [ ] Scripts for all deterministic operations
- [ ] install.sh tested (install + uninstall)
- [ ] Parallel delegation with sequential fallback
- [ ] Shared references (no duplication)
- [ ] Priority levels defined (Critical/High/Medium/Low)
Template: Minimal Skill (Tier 1)
Use this template for simple, single-file skills that don't need scripts or sub-skills.
Structure
skill-name/
SKILL.mdTemplate
---
name: {{SKILL_NAME}}
description: >
{{WHAT_IT_DOES}}. {{DETAILED_CAPABILITIES}}.
Use when user says "{{TRIGGER_1}}", "{{TRIGGER_2}}",
"{{TRIGGER_3}}", or "{{TRIGGER_4}}".
---
# {{SKILL_TITLE}}
{{ONE_LINE_OVERVIEW}}
## Instructions
### Step 1: {{FIRST_STEP_NAME}}
{{CLEAR_EXPLANATION}}
Expected outcome: {{WHAT_SUCCESS_LOOKS_LIKE}}
### Step 2: {{SECOND_STEP_NAME}}
{{CLEAR_EXPLANATION}}
### Step 3: {{THIRD_STEP_NAME}}
{{CLEAR_EXPLANATION}}
## Examples
### Example 1: {{COMMON_SCENARIO}}
User says: "{{EXAMPLE_INPUT}}"
Actions:
1. {{ACTION_1}}
2. {{ACTION_2}}
Result: {{EXPECTED_OUTPUT}}
### Example 2: {{ANOTHER_SCENARIO}}
User says: "{{EXAMPLE_INPUT}}"
Result: {{EXPECTED_OUTPUT}}
## Troubleshooting
### Error: {{COMMON_ERROR}}
**Cause:** {{WHY_IT_HAPPENS}}
**Solution:** {{HOW_TO_FIX}}
### Error: {{ANOTHER_ERROR}}
**Cause:** {{WHY_IT_HAPPENS}}
**Solution:** {{HOW_TO_FIX}}Checklist
- [ ] Description has WHAT + WHEN + keywords
- [ ] Instructions are specific (not "do it properly")
- [ ] At least 2 examples provided
- [ ] At least 1 troubleshooting entry
- [ ] Under 200 lines
Template: Multi-Skill Orchestrator (Tier 3)
Use for complex domains with multiple distinct workflows routed from a main skill.
Structure
skill-name/ # Main orchestrator
SKILL.md
references/
shared-ref-1.md
shared-ref-2.md
scripts/
shared_script.py
skills/
skill-name-sub1/
SKILL.md
skill-name-sub2/
SKILL.md
skill-name-sub3/
SKILL.mdMain Orchestrator Template
---
name: {{SKILL_NAME}}
description: >
{{COMPREHENSIVE_DESCRIPTION_OF_ENTIRE_DOMAIN}}.
{{LIST_KEY_CAPABILITIES}}.
Triggers on: "{{KEYWORD_1}}", "{{KEYWORD_2}}", "{{KEYWORD_3}}",
"{{KEYWORD_4}}", "{{KEYWORD_5}}", "{{KEYWORD_6}}".
allowed-tools:
- Read
- Grep
- Glob
- Bash
- WebFetch
---
# {{SKILL_TITLE}} -- {{TAGLINE}}
{{ONE_LINE_OVERVIEW}}
## Quick Reference
| Command | What it does |
|---------|-------------|
| `/{{NAME}}` | Interactive mode |
| `/{{NAME}} {{SUB1}}` | {{SUB1_DESCRIPTION}} |
| `/{{NAME}} {{SUB2}}` | {{SUB2_DESCRIPTION}} |
| `/{{NAME}} {{SUB3}}` | {{SUB3_DESCRIPTION}} |
## Orchestration Logic
When user invokes `/{{NAME}}`:
1. Parse command to identify sub-skill
2. If specific command, route to sub-skill directly
3. If no command, enter interactive mode:
- Ask what user wants to accomplish
- Detect context/industry type
- Route to appropriate sub-skill
## Context Detection
Detect context type from user input:
- **Type A**: {{SIGNALS}} -> route to {{SUB_SKILL}}
- **Type B**: {{SIGNALS}} -> route to {{SUB_SKILL}}
- **Type C**: {{SIGNALS}} -> route to {{SUB_SKILL}}
## Quality Gates
{{DOMAIN_SPECIFIC_HARD_RULES}}
## Scoring Methodology (if applicable)
### Health Score (0-100)
| Category | Weight |
|----------|--------|
| {{CAT_1}} | {{WEIGHT}}% |
| {{CAT_2}} | {{WEIGHT}}% |
| {{CAT_3}} | {{WEIGHT}}% |
## Reference Files
Load on-demand:
- `references/{{REF_1}}.md` -- {{DESCRIPTION}}
- `references/{{REF_2}}.md` -- {{DESCRIPTION}}
## Sub-Skills
1. **{{NAME}}-{{SUB1}}** -- {{DESCRIPTION}}
2. **{{NAME}}-{{SUB2}}** -- {{DESCRIPTION}}
3. **{{NAME}}-{{SUB3}}** -- {{DESCRIPTION}}Sub-Skill Template
---
name: {{PARENT}}-{{CHILD}}
description: >
{{FOCUSED_DESCRIPTION}}.
Use when user says "{{TRIGGER_1}}", "{{TRIGGER_2}}",
"{{TRIGGER_3}}", or "{{TRIGGER_4}}".
---
# {{CHILD_TITLE}}
## Process
### Step 1: {{ACTION}}
{{INSTRUCTIONS}}
### Step 2: {{ACTION}}
{{INSTRUCTIONS}}
## Output Format
{{DEFINE_STRUCTURED_OUTPUT}}
## Cross-References
- For {{RELATED_TOPIC}}, see `{{PARENT}}-{{OTHER_CHILD}}` sub-skill
- Load `references/{{REF}}.md` for {{KNOWLEDGE}}Checklist
- [ ] Main orchestrator has routing table
- [ ] Each sub-skill has focused responsibility
- [ ] Naming follows parent-child convention
- [ ] Shared knowledge in parent references/
- [ ] No duplicated content across sub-skills
- [ ] Main SKILL.md under 150 lines
- [ ] Sub-skills under 200 lines each
Template: Workflow Skill (Tier 2)
Use for skills that need deterministic scripts alongside instructions.
Structure
skill-name/
SKILL.md
scripts/
validate.py
process.py
references/
domain-guide.mdTemplate
---
name: {{SKILL_NAME}}
description: >
{{WHAT_IT_DOES}}. {{DETAILED_CAPABILITIES}}.
Use when user says "{{TRIGGER_1}}", "{{TRIGGER_2}}",
"{{TRIGGER_3}}", "{{TRIGGER_4}}", or "{{TRIGGER_5}}".
allowed-tools:
- Read
- Bash
- WebFetch
---
# {{SKILL_TITLE}}
{{ONE_LINE_OVERVIEW}}
## Process
### Step 1: {{GATHER_INPUTS}}
{{WHAT_TO_COLLECT_FROM_USER}}
Validate inputs before proceeding:
- {{VALIDATION_RULE_1}}
- {{VALIDATION_RULE_2}}
### Step 2: {{EXECUTE}}
Run the processing script:python scripts/process.py {{INPUT_ARGS}}
Expected output: {{DESCRIBE_OUTPUT_FORMAT}}
### Step 3: {{VALIDATE_RESULTS}}
Run validation:python scripts/validate.py {{OUTPUT_ARGS}}
If validation fails:
- {{ERROR_1}}: {{RECOVERY_ACTION_1}}
- {{ERROR_2}}: {{RECOVERY_ACTION_2}}
### Step 4: {{DELIVER}}
{{HOW_TO_PRESENT_RESULTS}}
## Quality Gates
Before delivering output:
- [ ] {{CHECK_1}}
- [ ] {{CHECK_2}}
- [ ] {{CHECK_3}}
## Reference Files
Load on-demand as needed:
- `references/domain-guide.md` -- {{WHAT_IT_CONTAINS}}
## Examples
### Example 1: {{SCENARIO}}
Input: {{INPUT}}
Script output: {{OUTPUT}}
Final result: {{DELIVERED_RESULT}}
## Troubleshooting
### Script fails with {{ERROR}}
**Cause:** {{REASON}}
**Solution:** {{FIX}}Checklist
- [ ] Scripts have docstrings and CLI interface
- [ ] Scripts output structured JSON
- [ ] Validation gates between steps
- [ ] Error handling with recovery paths
- [ ] References linked, not inlined
- [ ] Under 300 lines in SKILL.md
Skill Anatomy & Structure
The Only Required File: SKILL.md
Every skill is a folder containing at minimum a SKILL.md file. Everything else is optional.
SKILL.md Format
---
name: skill-name
description: >
What it does. When to use it. Keywords for matching.
---
# Skill Title
[Instructions in Markdown]Frontmatter Fields
| Field | Required | Constraints |
|---|---|---|
name | Yes | 1-64 chars, kebab-case, must match folder name |
description | Yes | 1-1024 chars, no XML tags, must include WHAT + WHEN |
argument-hint | No | Placeholder shown in UI (e.g., [url]) |
disable-model-invocation | No | If true, only user can invoke (not Claude) |
user-invocable | No | If false, hidden from slash command menu |
allowed-tools | No | List of pre-approved tools (e.g., Read, Bash, WebFetch) |
model | No | sonnet, opus, haiku, inherit |
context | No | fork runs skill in isolated subagent |
agent | No | Delegate to specific agent type |
hooks | No | Lifecycle hooks (PreToolUse, PostToolUse, Stop) |
license | No | License name or reference to LICENSE.txt |
compatibility | No | 1-500 chars, environment requirements |
metadata | No | Arbitrary key-value pairs (author, version, etc.) |
Body Content
No format restrictions on the body. Write whatever helps accomplish the task.
Size targets:
- Under 500 lines
- Under 5000 tokens
- Move detailed content to references/
Directory Structure
skill-name/ # Required: kebab-case folder
SKILL.md # Required: exact case
scripts/ # Optional: executable code
process.py
validate.sh
references/ # Optional: on-demand documentation
domain-guide.md
api-reference.md
assets/ # Optional: templates, data files
template.md
config.jsonProgressive Disclosure (3 Levels)
Level 1: Metadata (Always Loaded)
name+descriptionfrom YAML frontmatter- Injected into system prompt at startup
- ~50-100 tokens per skill
- Used to decide WHEN to activate
Level 2: Instructions (Loaded on Activation)
- Full SKILL.md body content
- Loaded when task matches description
- Target: <5000 tokens
- Core workflow instructions
Level 3: Resources (Loaded on Demand)
- Files in scripts/, references/, assets/
- Loaded only when explicitly referenced during execution
- No size limit (but keep individual files focused)
Multi-Skill Architecture
Main Orchestrator
skill-name/SKILL.md- Routes commands to sub-skills
- Contains shared configuration (scoring, quality gates)
- References shared knowledge in its own references/
Sub-Skills
skills/skill-name-sub1/SKILL.md
skills/skill-name-sub2/SKILL.md- One focused workflow each
- Can run independently OR be orchestrated
- Cross-reference shared refs via relative paths
Agents (for parallel delegation)
agents/skill-name-role1.md
agents/skill-name-role2.md- Subagent definitions for parallel execution
- Each has a clear role, input, and output format
- Used by audit/analysis sub-skills
Agent frontmatter format (different from skills):
| Field | Required | Constraints |
|---|---|---|
name | Yes | 3-50 chars, kebab-case |
description | Yes | CAN use <example> blocks (unlike skills) |
model | No | inherit, sonnet, opus, haiku |
color | No | blue, cyan, green, yellow, magenta, red |
tools | No | List of allowed tools (inherits all if omitted) |
disallowedTools | No | Explicitly denied tools |
permissionMode | No | default, acceptEdits, delegate, dontAsk, plan |
maxTurns | No | Maximum conversation turns |
skills | No | Skills available to the agent |
hooks | No | Lifecycle hooks (PreToolUse, PostToolUse, Stop) |
memory | No | user, project, or local scope |
Body after --- becomes the agent's system prompt (write in second person).
Scripts (deterministic execution)
scripts/script_name.py- One script, one responsibility
- CLI interface with structured JSON output
- Error handling and validation built in
File Naming Rules
| Type | Convention | Example |
|---|---|---|
| Skill folder | kebab-case | my-skill |
| Sub-skill folder | parent-child | my-skill-audit |
| SKILL.md | Exact case | SKILL.md (never skill.md) |
| Scripts | snake_case | fetch_data.py |
| References | kebab-case | api-guide.md |
| Agents | parent-role.md | my-skill-analyzer.md |
What NOT to Include
- No
README.mdinside skill folders (use repo-level README) - No
.envor credential files - No
node_modules/or__pycache__/ - No
claudeoranthropicin skill names - No XML angle brackets in frontmatter
Writing Effective Skill Descriptions
The description field is the single most important part of your skill. It determines when Claude activates your skill. Get this right.
The Framework
Every description needs three components:
[WHAT] + [CAPABILITIES] + [WHEN/TRIGGERS]Component 1: WHAT (capability statement)
One sentence explaining the skill's core purpose.
Component 2: CAPABILITIES (detailed features)
Key capabilities and domains covered.
Component 3: WHEN (trigger phrases)
Explicit phrases users would say, prefixed with "Use when user says" or "Triggers on:".
Examples: Good vs Bad
Example 1: SEO Skill
Bad:
description: Helps with SEO.Problems: Too vague, no triggers, Claude won't know when to activate.
Good:
description: >
Comprehensive SEO analysis for any website. Performs full site audits,
single-page analysis, technical SEO checks, schema markup detection,
content quality assessment, and sitemap analysis. Triggers on: "SEO",
"audit", "schema", "Core Web Vitals", "sitemap", "E-E-A-T",
"technical SEO", "content quality", "page speed", "structured data".Example 2: Code Review Skill
Bad:
description: Reviews code and suggests improvements.Problems: Too generic, overlaps with Claude's built-in ability.
Good:
description: >
Automated code review following team conventions and security best
practices. Checks code style, complexity metrics, security vulnerabilities
(OWASP Top 10), test coverage gaps, and documentation quality. Use when
user says "code review", "review this PR", "check for security issues",
"code quality audit", or "review my changes".Example 3: Data Pipeline Skill
Bad:
description: Processes data pipelines and ETL workflows.Problems: No trigger phrases, missing key capabilities.
Good:
description: >
Design, validate, and troubleshoot data pipelines and ETL workflows.
Covers schema validation, data quality checks, pipeline orchestration
(Airflow, Prefect, Dagster), transformation logic, and monitoring setup.
Use when user says "data pipeline", "ETL", "data quality", "schema
validation", "Airflow DAG", "data transformation", or "pipeline monitoring".Trigger Phrase Strategy
Include Multiple Formulations
Users say things differently. Cover:
- Formal: "perform SEO analysis"
- Casual: "check my site's SEO"
- Action-oriented: "audit this website"
- Domain-specific: "Core Web Vitals", "schema markup"
Include Tool/Technology Names
If your skill relates to specific tools:
- "Kubernetes", "Docker", "Terraform"
- "React", "Next.js", "Tailwind"
- "PostgreSQL", "Redis", "MongoDB"
Include File Types (if relevant)
- "PDF", "CSV", "JSON", "YAML"
- ".py files", ".tsx components"
Add Negative Triggers (if over-triggering risk)
Do NOT use for simple code formatting or syntax questions.
Not intended for general-purpose data analysis.Length Guidelines
| Skill Type | Description Length |
|---|---|
| Simple skill | 100-200 characters |
| Domain skill | 200-500 characters |
| Multi-skill orchestrator | 500-900 characters |
| Maximum allowed | 1024 characters |
Testing Your Description
After writing, test with these questions: 1. Would Claude activate for "help me [primary use case]"? -> Should be YES 2. Would Claude activate for "[specific trigger phrase]"? -> Should be YES 3. Would Claude activate for "help me with [unrelated task]"? -> Should be NO 4. Is the description specific enough to distinguish from similar skills? 5. Does it mention domain keywords that users would actually say?
The "When Would You Use This?" Test
Ask Claude: "When would you use the [skill-name] skill?"
Claude will quote the description back. If Claude's answer doesn't match your intended use cases, revise the description.
Common Pitfalls
1. Too vague: "Helps with projects" -- activate on EVERYTHING 2. Too narrow: "Processes Q4 2024 sales data" -- almost never activates 3. Missing triggers: Good explanation but no "use when" phrases 4. Jargon only: Technical terms without lay equivalents 5. No capabilities: Triggers without explaining what the skill does 6. Over 1024 chars: Will be rejected by the system
YAML Frontmatter Specification
Format
---
name: skill-name
description: >
Description text here.
---The --- delimiters are required. Frontmatter must be at the very top of SKILL.md.
Required Fields
name
| Property | Requirement |
|---|---|
| Type | String |
| Length | 1-64 characters |
| Format | kebab-case (lowercase letters, numbers, hyphens) |
| Restrictions | No leading, trailing, or consecutive hyphens |
| Must match | Parent directory name exactly |
| Reserved | Cannot contain "claude" or "anthropic" |
Valid examples:
name: seo-audit
name: data-pipeline
name: frontend-design
name: my-skill-v2Invalid examples:
name: SEO Audit # spaces and capitals
name: seo_audit # underscores
name: -seo-audit # leading hyphen
name: seo--audit # consecutive hyphens
name: claude-helper # reserved word "claude"description
| Property | Requirement |
|---|---|
| Type | String |
| Length | 1-1024 characters |
| Must include | WHAT it does + WHEN to use it |
| Forbidden | XML angle brackets (< >) |
| Recommended | 5-10 trigger phrases |
Structure framework:
[Capability statement]. [Detailed capabilities]. Use when user says
"[trigger 1]", "[trigger 2]", "[trigger 3]", or "[trigger 4]".YAML multiline options:
Folded scalar (recommended for long descriptions):
description: >
First line continues
on the next line as one paragraph.
Use when user says "trigger".Literal scalar (preserves newlines):
description: |
Line one.
Line two.
Line three.Quoted string:
description: "Short description. Use when user says trigger."Optional Fields
argument-hint
argument-hint: "[url] [options]"Placeholder text shown in the UI when the skill appears in the slash command menu. Helps users understand what arguments the skill expects.
disable-model-invocation
disable-model-invocation: trueBoolean. If true, only the user can invoke this skill via /skill-name. Claude cannot auto-invoke it based on description matching. Default: false.
user-invocable
user-invocable: falseBoolean. If false, the skill is hidden from the slash command menu. Useful for sub-skills that should only be invoked by a parent orchestrator. Default: true.
allowed-tools
allowed-tools:
- Read
- Bash
- WebFetch
- Glob
- GrepPre-approved tools the skill can use without additional permission. Supports patterns like Bash(git:*) for restricted bash access. Use "*" to grant access to all tools (use sparingly).
model
model: sonnetOverride the model used when this skill executes. Valid values: sonnet, opus, haiku, inherit. Default: inherits from session.
context
context: forkRuns the skill in an isolated subagent context, preventing heavy tool usage from polluting the main conversation. Useful for skills that do extensive exploration.
agent
agent: custom-agent-nameDelegate skill execution to a specific agent type defined in agents/.
hooks
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: prompt
prompt: "Validate the write operation"
once: trueHook definitions scoped to this skill's lifecycle. Supported events: PreToolUse, PostToolUse, Stop. The once: true flag makes a hook execute only once per skill activation.
license
license: MIT
# or
license: Complete terms in LICENSE.txtcompatibility
compatibility: "Requires Claude Code with Bash tool access. Needs Python 3.10+ and Node.js 18+."1-500 characters. Indicates environment requirements.
metadata
metadata:
author: YourName
version: 1.0.0
mcp-server: your-serviceArbitrary string key-value pairs.
Common Mistakes
| Mistake | Wrong | Correct |
|---|---|---|
| Missing delimiters | name: my-skill (no ---) | Wrap in --- delimiters |
| Unclosed quotes | description: "Does things | description: "Does things" |
| XML tags | description: Creates <div> | description: Creates HTML div |
| Invalid name | name: My Skill | name: my-skill |
| Spaces in name | name: my skill | name: my-skill |
Validation
Run: python scripts/validate_skill.py /path/to/skill
Checklist: --- present, name is kebab-case matching folder, description under 1024 chars with no < or >, optional fields follow constraints.
Hooks System Reference
Hooks execute shell commands, LLM prompts, or agents in response to Claude Code events. Use hooks to enforce quality gates, validate outputs, and automate workflows.
All 15 Hook Events
| Event | Trigger | Matcher | Key Capability |
|---|---|---|---|
PreToolUse | Before tool executes | Tool name | approve/deny/modify input |
PostToolUse | After tool succeeds | Tool name | inject feedback, suppress output |
PostToolUseFailure | After tool fails | Tool name | handle failures |
Stop | Before Claude stops | Stop reason | block until quality met |
SubagentStop | Before subagent stops | Stop reason | validate subagent work |
SubagentStart | Subagent launches | Agent type | inject context |
SessionStart | Session begins | * | set up environment |
SessionEnd | Session ends | * | cleanup |
UserPromptSubmit | User submits prompt | * | modify/validate input |
PreCompact | Before compaction | * | inject extra context |
Notification | Notification sent | * | log/forward |
PermissionRequest | Permission asked | Tool name | intercept prompts |
Setup | Via --init/--maintenance | * | first-run setup |
TeammateIdle | Agent team member idle | Agent type | assign work |
TaskCompleted | Delegated task done | Agent type | react to results |
Hook Types
Command Hook (type: "command")
{
"type": "command",
"command": "bash scripts/validate.sh",
"timeout": 30
}- Receives JSON via stdin with event data
- Exit 0 = success, Exit 2 = blocking error, other = warning
- stdout parsed as JSON, stderr shown on exit code 2
Prompt Hook (type: "prompt")
{
"type": "prompt",
"prompt": "Analyze: $TOOL_INPUT. Return 'approve' or 'deny'.",
"timeout": 15
}- LLM evaluates the prompt with event context
- Can reference
$TOOL_INPUT,$TOOL_INPUT.field_name,$TRANSCRIPT_PATH
Agent Hook (type: "agent")
{
"type": "agent",
"prompt": "Review the code changes and verify quality.",
"timeout": 60
}- Spawns a subagent with tool access
- Most powerful but most expensive
Configuration Locations
1. ~/.claude/settings.json -- user-level 2. .claude/settings.json -- project-level (team-shared) 3. .claude/settings.local.json -- project-level (personal) 4. Plugin hooks/hooks.json 5. Skill/agent YAML frontmatter hooks field
Matcher Patterns
| Pattern | Example | Matches |
|---|---|---|
| Exact | Write | Only Write tool |
| Pipe-separated | `Write\ | Edit` |
| Wildcard | * | Everything |
| Regex | mcp__.*__delete.* | MCP delete operations |
Hook Output Formats
Standard Output
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Additional context for Claude"
}PreToolUse Decision
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"updatedInput": {"command": "modified command"},
"additionalContext": "Extra context"
}
}Decisions: allow, deny, ask
Stop/SubagentStop Decision
{"decision": "approve"}or
{"decision": "block", "reason": "Tests not run"}Environment Variables in Hooks
| Variable | Available In |
|---|---|
$CLAUDE_PROJECT_DIR | All hooks |
$CLAUDE_PLUGIN_ROOT | Plugin hooks |
$CLAUDE_ENV_FILE | SessionStart only |
$TRANSCRIPT_PATH | Prompt hooks |
$TOOL_INPUT | Prompt hooks (PreToolUse) |
Hooks in Skill/Agent Frontmatter
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: prompt
prompt: "Validate write"
once: true
Stop:
- matcher: "*"
hooks:
- type: command
command: "bash scripts/check-quality.sh"Special flags:
once: true-- execute only once per activationasync: true-- run in background- Hooks for same event run in parallel
- Skill hooks scoped to skill lifecycle only
Common Patterns for Skill Creators
Quality Gate (block Stop until tests pass)
hooks:
Stop:
- matcher: "*"
hooks:
- type: command
command: "bash scripts/run-tests.sh"Validate Tool Input (pre-approve with modification)
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: prompt
prompt: "Is this command safe? $TOOL_INPUT"Post-Write Linting
hooks:
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: "bash scripts/lint.sh"Proven Skill Workflow Patterns
Pattern Selection Guide
| Pattern | Use When |
|---|---|
| Sequential Workflow | Steps must happen in order |
| Routing | Different inputs need different handling |
| Parallel Delegation | Independent tasks, need speed |
| Iterative Refinement | Quality improves with iteration |
| Context-Aware Selection | Same goal, different approaches by context |
| Scoring & Reporting | Need quantified analysis with priorities |
| Template Selection | Fill templates based on detected context |
| Hook-Enforced Gates | Must enforce hard rules automatically |
| Dynamic Context | Need real-time data at skill load time |
| Graceful Degradation | Tools/agents may be unavailable |
| Cross-Skill Delegation | One skill invokes another |
Pattern 1: Sequential Workflow
Steps flow in order. Output of step N feeds into step N+1. Each step has validation criteria and error recovery.
Real example: claude-seo page analysis (fetch -> parse -> analyze -> score)
Pattern 2: Command Routing (Orchestrator)
Main skill routes to sub-skills via routing table + orchestration logic.
## Quick Reference
| Command | Routes to |
|---------|-----------|
| /skill cmd1 | skills/skill-cmd1/SKILL.md |
## Orchestration Logic
- If command matches sub-skill, route directly
- If no command, enter interactive mode
- If ambiguous, ask for clarificationReal example: claude-seo routing 12 sub-skills (/seo audit, /seo page, etc.)
Pattern 3: Parallel Delegation (Fan-out/Fan-in)
Spawn multiple subagents for independent analysis, then aggregate.
1. Detect context type
2. Spawn subagents in parallel:
- agent-1: [responsibility]
- agent-2: [responsibility]
3. Collect results from all agents
4. Generate unified report with aggregate score
5. Create prioritized action planReal example: claude-seo full audit with 6 parallel agents -> Health Score (0-100)
Pattern 4: Iterative Refinement
Generate, evaluate, improve in a loop until threshold met or max iterations.
Real example: Anthropic's doc-coauthoring skill
- Stage 1: Context gathering
- Stage 2: Section-by-section refinement
- Stage 3: Reader testing with sub-agent verification
Pattern 5: Industry/Context Detection
Detect context type from signals, apply type-specific templates, thresholds, scoring weights, and recommendations.
Real example: claude-seo industry detection
- SaaS: pricing page, /features, "free trial"
- E-commerce: /products, /cart, product schema
- Local: phone number, address, service area
Pattern 6: Scoring & Reporting
Quantify analysis with weighted category scores and priority levels.
## Scoring (0-100)
| Category | Weight |
|----------|--------|
| Category A | 30% |
| Category B | 25% |
## Priority Levels
- Critical: fix immediately
- High: fix within 1 week
- Medium: fix within 1 month
- Low: backlogPattern 7: Template Selection
Select and fill templates based on detected context. Load from assets/templates/, fill with analysis data, validate completeness.
Real example: claude-seo plan with 6 industry templates
Pattern 8: Hook-Enforced Quality Gates
Use hooks to automatically enforce rules that instructions alone can't guarantee.
hooks:
Stop:
- matcher: "*"
hooks:
- type: command
command: "bash scripts/check-quality.sh"
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: "bash scripts/lint.sh"Real example: Anthropic's brand-guidelines (style enforcement), webapp-testing (visual QA checks before completion)
Pattern 9: Dynamic Context Injection
Inject real-time data into skill content at load time using ! backtick syntax.
Current branch: !`git branch --show-current`
Package version: !`cat package.json | jq -r '.version'`Executed before skill content is sent to Claude. Useful for git-aware skills.
Pattern 10: Graceful Degradation
Design for when tools or agents are unavailable. Always provide a sequential fallback path for parallel delegation.
Spawn subagents in parallel:
- agent-1: [task]
- agent-2: [task]
(If subagents unavailable, run analysis inline sequentially)Pattern 11: Cross-Skill Delegation
One skill invokes another via the Skill tool. Add Skill(other-skill) to allowed-tools. Useful for shared toolkits.
Real example: Anthropic's ms-office-suite (docx, pptx, xlsx) share an office/ scripts directory for common operations.
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|---|---|
| The Monolith | Everything inlined in one SKILL.md | Split into sub-skills + references |
| Vague Directive | "Analyze and provide insights" | Specific steps, criteria, output format |
| Over-Engineered Tier 1 | Unnecessary sub-skills for simple tasks | Start simple, evolve when needed |
| Copy-Paste Skill | Duplicated content across sub-skills | Shared references in parent's references/ |
| Silent Failure | No error handling or fallbacks | "If X fails, then Y" for each step |
| AI Slop | Generic fonts, purple gradients, cliches | Explicit style bans in instructions |
| Tool Assumption | Assumes all tools always available | Check availability, provide fallbacks |
Multi-Platform Skill Conversion Reference
Platform-specific paths, formats, and compatibility rules for converting Claude Code skills to OpenAI Codex, Google Gemini CLI, Google Antigravity, and Cursor.
Skill Storage Paths
| Platform | Project-Level | User-Level (Global) |
|---|---|---|
| Claude Code | .claude/skills/{name}/ | ~/.claude/skills/{name}/ |
| OpenAI Codex | .agents/skills/{name}/ | ~/.agents/skills/{name}/ |
| Gemini CLI | .gemini/skills/{name}/ | ~/.gemini/skills/{name}/ |
| Antigravity | .agent/skills/{name}/ | ~/.gemini/antigravity/skills/{name}/ |
| Cursor | .cursor/skills/{name}/ | ~/.cursor/skills/{name}/ |
All platforms use the same SKILL.md file as the entry point (Agent Skills standard).
Project Instruction Files
Each platform has a project-level instruction file that acts like a system prompt:
| Platform | File | Max Size | Hierarchy |
|---|---|---|---|
| Claude Code | CLAUDE.md | ~32 KiB | Root + parent dirs + CWD |
| OpenAI Codex | AGENTS.md | ~32 KiB | Root to CWD (hierarchical) |
| Gemini CLI | GEMINI.md | ~32 KiB | Project root |
| Antigravity | GEMINI.md | ~32 KiB | Workspace root |
| Cursor | .cursor/rules/*.mdc | ~32 KiB | Project root |
Cursor notes: Rules use .mdc extension with a 3-field YAML frontmatter (description, globs, alwaysApply). Cursor v2.4+ also reads SKILL.md natively.
Codex notes: Supports AGENTS.override.md per-directory overrides. Configurable fallback filenames via project_doc_fallback_filenames in config.toml.
Gemini notes: Configurable filename via settings.json -> context.fileName array. Can natively read AGENTS.md from Codex projects.
Frontmatter Field Compatibility
Portable Fields (keep on all platforms)
| Field | Notes |
|---|---|
name | Required everywhere (optional on Antigravity, defaults to dirname) |
description | Required everywhere; determines activation |
license | Standard field, universally supported |
compatibility | Environment requirements, universally supported |
metadata | Arbitrary key-value pairs, universally supported |
Adaptable Fields (platform-specific handling)
| Field | Codex | Gemini | Antigravity | Cursor |
|---|---|---|---|---|
allowed-tools | Keep (supported) | Strip + warn | Strip + warn | Strip + warn |
disable-model-invocation | Move to openai.yaml | Strip + warn | Strip + warn | Strip + warn |
argument-hint | Keep | Keep | Keep | Keep |
Claude-Only Fields (strip with warning)
These fields are Claude Code specific and must be removed for other platforms:
context(fork/isolated subagent mode)agent(delegate to agent type)hooks(PreToolUse, PostToolUse, Stop lifecycle)model(sonnet, opus, haiku, inherit)user-invocable(slash command visibility)skills(sub-skill references)memory(persistent memory paths)
Warning template: "Field '{field}' is Claude Code specific and was removed. {workaround}"
Codex note: Codex parses ONLY name and description; all other fields are silently ignored. This means adaptable fields like allowed-tools are supported in spec but effectively ignored by the parser.
Cursor Rule File Format
Cursor rules (.cursor/rules/*.mdc) use a minimal 3-field frontmatter:
---
description: "When to activate this rule"
globs: "*.ts,src/**/*.tsx"
alwaysApply: false
---
Rule content in markdown...| Field | Type | Notes |
|---|---|---|
description | string | Optional; used by agent for relevance matching |
globs | string | File patterns; triggers when matching files are in chat |
alwaysApply | boolean | If true, always loaded; globs are IGNORED when true |
Quirk: If both alwaysApply: true AND globs are set, globs are ignored.
Codex Platform Extension: openai.yaml
Codex supports an optional agents/openai.yaml file for platform-specific config:
interface:
display_name: "Skill Display Name"
policy:
# Maps from disable-model-invocation
allow_implicit_invocation: trueField Mapping to openai.yaml
| Claude Field | openai.yaml Location | Transform |
|---|---|---|
name | interface.display_name | Title case the kebab-case name |
disable-model-invocation | policy.allow_implicit_invocation | Invert boolean |
MCP Configuration Formats
Claude Code (.mcp.json)
{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "mcp-server"],
"env": { "API_KEY": "..." }
}
}
}Codex (config.toml)
[mcp_servers.server-name]
type = "stdio"
command = "npx"
args = ["-y", "mcp-server"]
[mcp_servers.server-name.env]
API_KEY = "..."Gemini CLI (settings.json)
{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "mcp-server"],
"env": { "API_KEY": "..." }
}
}
}Antigravity (mcp_config.json)
{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "mcp-server"]
}
}
}Cursor (mcp.json)
{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "mcp-server"],
"env": { "API_KEY": "..." }
}
}
}Key differences: Codex uses TOML with [mcp_servers] section. All others use JSON with mcpServers key. Cursor's format is identical to Claude's.
Antigravity Template Variables
Antigravity supports template variables in skill instructions:
{{SKILL_PATH}}-- resolves to the skill's directory path{{WORKSPACE_PATH}}-- resolves to the workspace root
When converting, replace hardcoded relative paths (e.g., ./scripts/) with {{SKILL_PATH}}/scripts/ for portability.
Hook System Comparison
| Aspect | Claude Code | Codex | Gemini/Antigravity | Cursor |
|---|---|---|---|---|
| Event count | 13 | 1 (notify) | Partial (CLI) | 6 (beta) |
| Blocking | Yes (exit 2) | No | No | Yes (allow/deny/ask) |
| Async support | Yes (v2.1) | No | No | No |
| Skill-scoped | Yes (v2.1) | No | No | No |
| Config file | settings.json | config.toml | settings.json | hooks.json |
Hook-dependent skills are the least portable feature across platforms.
Activation Modes
| Platform | How Skills Activate |
|---|---|
| Claude Code | LLM-routed (description matching) or /skill-name slash command |
| OpenAI Codex | LLM-routed (description matching) or $skill-name mention |
| Gemini CLI | LLM-routed or /skills command to list and select |
| Antigravity | LLM-routed based on description matching |
| Cursor | LLM-routed, rule auto-attachment via globs, or @rule-name mention |
Conversion impact: Trigger phrases in the description work across all platforms. The /slash-command syntax is Claude-specific but the underlying routing mechanism (description-based matching) is universal.
Subagent Comparison
| Platform | Subagent Support | Max Concurrent | Nesting |
|---|---|---|---|
| Claude Code | Task tool, built-in + custom types | 10 | No |
| OpenAI Codex | None (feature request) | N/A | N/A |
| Gemini CLI | Experimental sub-agents, A2A protocol | Unknown | Unknown |
| Antigravity | Agent Manager ("Mission Control") | Parallel | Unknown |
| Cursor | Background Agents (Ultra plan only) | Single-level | No |
Tier Conversion Notes
Tier 1: Single Skill (SKILL.md only)
- Converts cleanly to all platforms
- Just copy SKILL.md with frontmatter adjustments
- Generate platform instruction file from description
Tier 2: Skill + Scripts
- Converts well -- scripts are language-agnostic
- Copy SKILL.md + scripts/ directory
- Verify script paths in instructions match target structure
Tier 3: Multi-Skill Orchestrator
- Partial conversion -- routing table is Claude-specific
- Each sub-skill converts independently
- Main orchestrator needs manual adaptation per platform
- Codex: routing can use
$sub-skillmention syntax - Gemini: routing relies on description-based activation
- Cursor: relies on description-based matching
Tier 4: Full Ecosystem
- Manual review required for subagent delegation
- Agents (Task tool) are Claude Code specific
- Scripts and reference files convert cleanly
- Platform instruction files should aggregate key info
- MCP servers need config format conversion
Platform-Specific Warnings
Codex
allowed-toolsis in the spec but parser ignores it in practice- Agent/Task delegation has no direct equivalent
openai.yamlis optional but recommended for UI metadata- AGENTS.md supports hierarchical loading (root to CWD)
- AGENTS.override.md can override instructions per directory
Gemini CLI
- No
allowed-toolsequivalent -- all tools available by default - Experimental sub-agents available but not production-ready
- Skills discovered via
.gemini/skills/path scanning - GEMINI.md is the project instruction file
- Can natively read AGENTS.md via configurable filename setting
Antigravity
namefield is optional in frontmatter (defaults to dirname)- Workspace skills in
.agent/skills/, global in~/.gemini/antigravity/skills/ - Supports
{{SKILL_PATH}}and{{WORKSPACE_PATH}}template variables - Agent Manager provides multi-agent orchestration
- No
allowed-toolsor skill-scoped hooks
Cursor
- Native SKILL.md support since v2.4 (January 2026)
- Rules (
.cursor/rules/*.mdc) have 3-field frontmatter only - Background Agents require Ultra/Teams/Enterprise plan
- Single-level subagents only (no nesting)
- 6 hook events (beta) with blocking support
globs+alwaysApply: truequirk: globs are ignored- MCP config format identical to Claude's
.mcp.json
3-Layer Architecture for Skills
Why This Architecture?
LLMs are probabilistic. Business logic is deterministic. The 3-layer architecture bridges this gap by separating concerns:
- 90% accuracy per step = 59% success over 5 steps
- Push deterministic complexity into code, keep decisions in the LLM
The Three Layers
Layer 1: Directive (The "What")
In skills: SKILL.md instructions and reference files
What to do, in what order, with what quality criteria. Written in natural language Markdown.
Contains:
- Goals and success criteria
- Process steps and decision points
- Quality gates and validation rules
- Edge cases and error handling
- Domain knowledge and thresholds
Layer 2: Orchestration (The "How")
In skills: Claude's reasoning and routing
The intelligent glue between intent and execution.
Responsibilities: 1. Parse user intent 2. Read the relevant skill instructions 3. Plan execution order 4. Call tools and scripts 5. Handle results and errors 6. Learn and adapt
Layer 3: Execution (The "Do")
In skills: Scripts in scripts/
Deterministic code that does reliable, repeatable work.
Properties:
- Same input -> same output
- One script, one responsibility
- Testable independently
- Clear error messages
- Well-documented
When to Use Each Layer
| Task Type | Layer |
|---|---|
| "Analyze this and tell me what you think" | L1 (directive) + L2 (orchestration) |
| "Parse this HTML and extract all links" | L3 (script) |
| "Decide which workflow to use" | L2 (orchestration) |
| "Validate this YAML frontmatter" | L3 (script) |
| "Generate a report with recommendations" | L1 (directive) + L2 (orchestration) |
| "Calculate a score from these metrics" | L3 (script) if complex formula |
Rule of Thumb
Use a script (L3) when:
- The operation is fragile (XML manipulation, file packaging)
- Exact format matters (ZIP creation, validation)
- Math/calculations are involved
- Same operation happens repeatedly
- Errors would be hard to diagnose
Use instructions (L1) when:
- Judgment is required
- Output varies by context
- Creative or analytical work
- Multiple valid approaches exist
Self-Annealing
When things break: 1. Read the error 2. Diagnose root cause 3. Fix (script if deterministic, instruction if judgment) 4. Test the fix 5. Update the skill with the learning
The system gets stronger with each failure.
Applying to Skill Design
Tier 1-2 Skills
- L1: SKILL.md body
- L2: Claude's built-in reasoning
- L3: Optional scripts for fragile ops
Tier 3-4 Skills
- L1: SKILL.md + sub-skill instructions + reference files
- L2: Orchestrator routing + subagent coordination
- L3: Multiple scripts for validation, processing, analysis
The key insight: Don't try to do everything in instructions. If something can go wrong, make it a script. If it requires judgment, keep it in instructions.
Skill Discovery & Activation Reference
How Claude Code finds, loads, and activates skills. Critical knowledge for writing descriptions that trigger correctly.
Skill Locations (Priority Order)
1. Enterprise (highest priority) 2. Personal: ~/.claude/skills/*/SKILL.md 3. Project: .claude/skills/*/SKILL.md 4. Additional directories: via --add-dir (auto-loaded) 5. Plugin: skills/*/SKILL.md within installed plugins 6. Nested: Auto-discovered from .claude/skills in subdirectories (monorepo)
How Activation Works
1. Metadata always loaded: Name + description always in context (~100 words each) 2. Character budget: 2% of context window (override: SLASH_COMMAND_TOOL_CHAR_BUDGET) 3. SKILL.md body: Loaded when skill triggers (<5k words recommended) 4. Resources: Loaded on-demand by Claude (unlimited via scripts)
Activation triggers:
- User invokes via
/skill-nameor/skill-name args - Claude auto-invokes when description matches user's intent
- Recently/frequently used skills get priority
- Skills without extra permissions allowed without approval
Complete Skill Frontmatter
---
# Required
name: kebab-case-name # 1-64 chars, must match folder
description: > # 1-1024 chars, determines activation
This skill should be used when...
# Optional - UI/Invocation
argument-hint: "[target]" # Placeholder shown in UI
disable-model-invocation: false # Prevent Claude auto-invoking
user-invocable: true # Show in slash command menu
# Optional - Permissions
allowed-tools: # Pre-approved tools
- Read
- Write
- Bash(npm:*)
# Optional - Execution
model: inherit # sonnet | opus | haiku | inherit
context: fork # Run in isolated subagent
agent: custom-agent-name # Delegate to specific agent
# Optional - Hooks (scoped to skill lifecycle)
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: prompt
prompt: "Validate"
once: true
# Optional - Metadata
license: MIT
compatibility: "Requires Python 3.10+"
metadata:
author: YourName
version: 1.0.0
---String Substitutions in Skills
| Substitution | Description |
|---|---|
$ARGUMENTS | All arguments passed to the skill |
$ARGUMENTS[0], $ARGUMENTS[1] | Individual arguments |
$0, $1, $2 | Positional shorthand |
${CLAUDE_SESSION_ID} | Current session UUID |
${CLAUDE_PLUGIN_ROOT} | Plugin root directory |
Dynamic Context Injection
Execute bash at skill load time with ! backtick syntax:
Current branch: !`git branch --show-current`
Recent commits: !`git log --oneline -5`Output injected into skill content before sending to Claude.
Context Fork
context: forkRuns skill in isolated subagent context. Prevents heavy tool usage from polluting the main conversation. Useful for exploration-heavy skills.
Permission Model
Permission Levels (Priority Order)
1. Managed policy (organization-enforced, highest) 2. User settings: ~/.claude/settings.json 3. Project settings: .claude/settings.json 4. Local settings: .claude/settings.local.json 5. Skill frontmatter: allowed-tools
Permission Decisions
| Decision | Effect |
|---|---|
allow | Tool executes without prompting |
deny | Tool blocked entirely |
ask | User prompted for approval |
allowed-tools Behavior
- Pre-approves listed tools when skill is active
- Supports YAML list or JSON array format
${CLAUDE_PLUGIN_ROOT}substituted in plugin context"*"grants access to ALL tools
Skill Hot-Reload
Skills created or modified in ~/.claude/skills or .claude/skills are immediately available without restarting the session.
Hidden Features for Skill Creators
Automatic Continue
When response hits output token limit, Claude automatically continues. Skills generating long output benefit from this.
Large Bash Output
Large command outputs saved to disk with file path reference (not truncated). Skills can run commands with arbitrarily large output.
Skill Character Budget Scaling
Budget scales with context window (2% of context). Larger context = more skill descriptions visible without truncation.
Plan Mode
/plan command enters plan mode. Shift+Tab selects "auto-accept edits".
Subagent Transcripts
Stored at ~/.claude/projects/{project}/{sessionId}/subagents/. Useful for debugging or chaining workflows.
File References
@path/to/fileincludes file inline in prompts@autocomplete shows icons for different file types
Skill Testing & Validation Guide
Three Testing Areas
1. Triggering Tests
Does the skill activate at the right times?
Should trigger (test 5-10 queries):
- Obvious task match
- Paraphrased requests
- Domain-specific keywords
- Casual phrasing
Should NOT trigger (test 5-10 queries):
- Unrelated topics
- Adjacent but different domains
- General questions Claude handles natively
Edge cases (test 3-5 queries):
- Ambiguous requests
- Multi-domain queries
- Partial keyword matches
2. Functional Tests
Does the skill produce correct outputs?
Test each workflow:
Test: [Workflow Name]
Given: [Input/context]
When: Skill executes workflow
Then:
- [Expected output 1]
- [Expected output 2]
- [No errors]Test error handling:
- Invalid inputs
- Missing dependencies
- Network failures (for web-fetching skills)
- Edge cases (empty data, very large data)
3. Performance Comparison
Is the skill better than no skill?
Baseline (without skill):
- How many messages to complete task?
- How many errors/retries?
- Token usage?
- Output quality?
With skill:
- Should reduce messages
- Should reduce errors
- Should reduce token usage
- Should improve consistency
Testing in Claude Code
Manual Testing
# Test triggering
> [type a query that should trigger]
> [observe if skill activates]
# Test full workflow
> /skill-name [command]
> [observe output quality]Scripted Testing
Create test cases as markdown:
## Test Case 1: Basic Trigger
Input: "Help me audit my website's SEO"
Expected: seo skill activates, asks for URL
Pass criteria: Skill loads and follows workflow
## Test Case 2: Negative Trigger
Input: "What's the weather in London?"
Expected: seo skill does NOT activate
Pass criteria: Skill stays dormant
## Test Case 3: Full Workflow
Input: "/seo page https://example.com"
Expected:
- Page fetched successfully
- SEO elements analyzed
- Score generated (0-100)
- Recommendations provided
Pass criteria: All 4 outputs present and accurateQuality Metrics
Quantitative
| Metric | Target |
|---|---|
| Trigger accuracy | 90%+ on relevant queries |
| False positive rate | <5% on unrelated queries |
| Workflow completion | 95%+ without user correction |
| Error recovery | 80%+ of errors handled gracefully |
Qualitative
- Users don't need to prompt Claude about next steps
- Workflows complete without user correction
- Consistent results across sessions
- Output matches domain expert expectations
Pre-Publish Checklist
Structure
- [ ] SKILL.md exists (exact case)
- [ ] Folder name = kebab-case
- [ ] Name field matches folder name
- [ ] No README.md inside skill folder
Frontmatter
- [ ] Valid YAML with --- delimiters
- [ ] Name: kebab-case, 1-64 chars
- [ ] Description: WHAT + WHEN + keywords
- [ ] Description: under 1024 chars
- [ ] No XML tags (< >)
Instructions
- [ ] Specific and actionable
- [ ] Error handling included
- [ ] Examples provided
- [ ] Under 500 lines
- [ ] References linked, not inlined
Scripts (if present)
- [ ] Docstrings with purpose/input/output
- [ ] CLI interface
- [ ] Structured JSON output
- [ ] Error handling
- [ ] Can run independently
Testing
- [ ] 5+ trigger queries pass
- [ ] 5+ negative queries don't trigger
- [ ] Each workflow tested end-to-end
- [ ] Error handling tested
- [ ] Cross-references verified
Iteration Signals
Under-triggering (skill doesn't load)
- Add more trigger phrases to description
- Add domain keywords
- Make description more specific (counterintuitively)
- Add common paraphrases
Over-triggering (skill loads incorrectly)
- Add negative triggers ("Do NOT use for...")
- Narrow the description scope
- Add disambiguation phrases
Execution issues
- Add validation gates between steps
- Add "if X fails, then Y" paths
- Consider scripts for fragile operations
- Add more specific instructions (replace vague language)
Tools Reference for Skill Creators
Complete list of tool names for allowed-tools frontmatter and permission patterns.
Core Tools
| Tool | allowed-tools Name | Purpose |
|---|---|---|
| Read | Read | Read files (text, images, PDFs, notebooks) |
| Write | Write | Create or overwrite files |
| Edit | Edit | Exact string replacement in files |
| Bash | Bash | Execute shell commands |
| Glob | Glob | Find files by pattern |
| Grep | Grep | Search file contents (ripgrep) |
| WebFetch | WebFetch | Fetch and process web content |
| WebSearch | WebSearch | Search the web |
| Task | Task | Spawn subagents |
| NotebookEdit | NotebookEdit | Edit Jupyter notebook cells |
| Skill | Skill(name) | Invoke other skills |
| TodoWrite | TodoWrite | Task list management |
| AskUserQuestion | AskUserQuestion | Prompt user for input |
| LSP | LSP | Language server queries |
| MCPSearch | MCPSearch | Search MCP tool descriptions |
| TaskOutput | TaskOutput | Read background task output |
| TaskStop | TaskStop | Stop running tasks |
Bash Permission Patterns
Restrict Bash access with patterns in allowed-tools:
| Pattern | Matches |
|---|---|
Bash | All commands |
Bash(git:*) | Git commands only |
Bash(npm:*) | npm commands only |
Bash(npm test:*) | npm test only |
Bash(npm *) | npm with any subcommand |
Bash(* install) | Commands ending with "install" |
Bash(git * main) | Git commands containing "main" |
Bash(*) | Equivalent to Bash |
Task Permission Patterns
Restrict which subagents can be spawned:
| Pattern | Matches |
|---|---|
Task | All subagent types |
Task(AgentName) | Only the named agent |
Skill Permission Patterns
| Pattern | Matches |
|---|---|
Skill(name) | Specific skill only |
Skill(name *) | Skill with any arguments |
MCP Tool Naming
MCP tools follow the pattern mcp__<server>__<tool>:
| Pattern | Matches |
|---|---|
mcp__server__tool | Specific MCP tool |
mcp__server__* | All tools from server |
mcp__plugin_name_server__* | Plugin-provided tools |
Wildcard
Use "*" in allowed-tools to grant access to ALL tools (use sparingly).
Key Tool Capabilities
Read
- Up to 2000 lines by default, with offset/limit for large files
- Reads images visually (multimodal), PDFs (use
pagesfor >10 pages) - Reads Jupyter notebooks with all cell outputs
Write
- Requires file to have been Read first if it exists
- Creates intermediate directories
- Blocked from writing to
.claude/skillsin sandbox mode
Edit
- Requires file to have been Read first
old_stringmust be unique (or usereplace_all: true)- Preserves exact indentation
Bash
- Working directory persists between calls; shell state does not
- Default timeout: 2 minutes (max 10 minutes)
run_in_background: truefor async execution- Large outputs saved to disk (not truncated)
WebFetch
- Converts HTML to markdown, processes with AI
- FAILS for authenticated/private URLs
- 15-minute cache for repeated access
WebSearch
- Domain filtering via
allowed_domains/blocked_domains - Only available in the US
Task (Subagents)
- Spawns independent subagent with own context window
- Custom agent types from
agents/directory - Background support with
run_in_background
Built-in Agent Types
| Agent | Model | Purpose |
|---|---|---|
Explore | Haiku | Fast read-only code exploration |
Plan | Inherit | Planning and analysis |
general-purpose | Inherit | Full task delegation |
Bash | Inherit | Command execution |
MCP Server Types
| Type | Transport | Best For |
|---|---|---|
| stdio | Process stdin/stdout | Local tools, NPM packages |
| SSE | HTTP + Server-Sent Events | Cloud services, OAuth |
| HTTP | REST request/response | REST APIs, stateless |
| WebSocket | Persistent bidirectional | Real-time, low-latency |
Configure in .mcp.json, settings.json, or plugin mcpServers field.
Environment Variables
| Variable | Purpose |
|---|---|
CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS | Override file read token limit |
SLASH_COMMAND_TOOL_CHAR_BUDGET | Override skill character budget |
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 | Enable agent teams |
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS | Disable background tasks |
#!/usr/bin/env python3
"""
Purpose: Aggregate eval results into a benchmark report with pass rate, time, and tokens.
Input: Path to an iteration workspace directory containing eval-*/with_skill/ and eval-*/baseline/
Output: benchmark.json and benchmark.md with aggregated metrics
Usage: python scripts/aggregate_benchmark.py /path/to/iteration-N --skill-name my-skill
Reads grading.json and timing.json from each eval run directory and produces:
- Per-eval pass rate, avg tokens, avg duration
- Overall summary with improvement ratios
- Variance analysis across trials
- Comparison with previous iteration (if --previous provided)
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def load_json(path: Path) -> dict[str, Any] | None:
"""Safely load a JSON file, returning None on failure."""
try:
return json.loads(path.read_text())
except (FileNotFoundError, json.JSONDecodeError):
return None
def discover_eval_dirs(iteration_path: Path) -> list[Path]:
"""Find all eval-* directories in an iteration workspace."""
eval_dirs: list[Path] = []
for entry in sorted(iteration_path.iterdir()):
if entry.is_dir() and re.match(r'^eval-\d+$', entry.name):
eval_dirs.append(entry)
return eval_dirs
def collect_run_data(run_path: Path) -> dict[str, Any] | None:
"""Collect grading and timing data from a run directory."""
if not run_path.is_dir():
return None
grading = load_json(run_path / "grading.json")
timing = load_json(run_path / "timing.json")
if not grading:
return None
result: dict[str, Any] = {
"pass_rate": grading.get("pass_rate", 0.0),
"assertions": grading.get("assertions", []),
}
if timing:
result["total_tokens"] = timing.get("total_tokens", 0)
result["duration_seconds"] = timing.get("total_duration_seconds", 0.0)
else:
result["total_tokens"] = 0
result["duration_seconds"] = 0.0
return result
def collect_trial_data(eval_dir: Path, run_type: str) -> list[dict[str, Any]]:
"""Collect data across multiple trials for a run type."""
trials: list[dict[str, Any]] = []
# Check for single run (with_skill/ or baseline/)
single_run = eval_dir / run_type
if single_run.is_dir():
data = collect_run_data(single_run)
if data:
trials.append(data)
# Check for numbered trials (with_skill_0/, with_skill_1/, etc.)
for i in range(20):
trial_dir = eval_dir / f"{run_type}_{i}"
if trial_dir.is_dir():
data = collect_run_data(trial_dir)
if data:
trials.append(data)
return trials
def calculate_stats(values: list[float]) -> dict[str, float]:
"""Calculate mean and standard deviation for a list of values."""
if not values:
return {"mean": 0.0, "std": 0.0, "min": 0.0, "max": 0.0}
n = len(values)
mean = sum(values) / n
if n > 1:
variance = sum((x - mean) ** 2 for x in values) / (n - 1)
std = variance ** 0.5
else:
std = 0.0
return {
"mean": round(mean, 4),
"std": round(std, 4),
"min": round(min(values), 4),
"max": round(max(values), 4),
}
def aggregate_benchmark(
iteration_path: str,
skill_name: str,
previous_path: str | None = None,
) -> dict[str, Any]:
"""Aggregate all eval results into a benchmark report."""
path = Path(iteration_path).resolve()
if not path.is_dir():
return {"error": f"Not a directory: {iteration_path}"}
eval_dirs = discover_eval_dirs(path)
if not eval_dirs:
return {"error": f"No eval-* directories found in {path}"}
# Extract iteration number from path
iteration_match = re.search(r'iteration-(\d+)', path.name)
iteration = int(iteration_match.group(1)) if iteration_match else 1
per_eval: list[dict[str, Any]] = []
all_with_pass_rates: list[float] = []
all_baseline_pass_rates: list[float] = []
all_with_tokens: list[float] = []
all_baseline_tokens: list[float] = []
all_with_durations: list[float] = []
all_baseline_durations: list[float] = []
for eval_dir in eval_dirs:
eval_id_match = re.match(r'^eval-(\d+)$', eval_dir.name)
eval_id = int(eval_id_match.group(1)) if eval_id_match else 0
# Load eval metadata
metadata = load_json(eval_dir / "eval_metadata.json")
eval_name = metadata.get("eval_name", eval_dir.name) if metadata else eval_dir.name
with_trials = collect_trial_data(eval_dir, "with_skill")
baseline_trials = collect_trial_data(eval_dir, "baseline")
with_pass_rates = [t["pass_rate"] for t in with_trials]
baseline_pass_rates = [t["pass_rate"] for t in baseline_trials]
with_tokens = [t["total_tokens"] for t in with_trials if t["total_tokens"] > 0]
baseline_tokens = [t["total_tokens"] for t in baseline_trials if t["total_tokens"] > 0]
with_durations = [t["duration_seconds"] for t in with_trials if t["duration_seconds"] > 0]
baseline_durations = [t["duration_seconds"] for t in baseline_trials if t["duration_seconds"] > 0]
eval_result: dict[str, Any] = {
"eval_id": eval_id,
"eval_name": eval_name,
"trials": max(len(with_trials), len(baseline_trials)),
}
if with_pass_rates:
eval_result["with_skill"] = {
"pass_rate": calculate_stats(with_pass_rates)["mean"],
"pass_rate_std": calculate_stats(with_pass_rates)["std"],
"avg_tokens": calculate_stats(with_tokens)["mean"] if with_tokens else 0,
"avg_duration_seconds": calculate_stats(with_durations)["mean"] if with_durations else 0,
}
all_with_pass_rates.extend(with_pass_rates)
all_with_tokens.extend(with_tokens)
all_with_durations.extend(with_durations)
if baseline_pass_rates:
eval_result["baseline"] = {
"pass_rate": calculate_stats(baseline_pass_rates)["mean"],
"pass_rate_std": calculate_stats(baseline_pass_rates)["std"],
"avg_tokens": calculate_stats(baseline_tokens)["mean"] if baseline_tokens else 0,
"avg_duration_seconds": calculate_stats(baseline_durations)["mean"] if baseline_durations else 0,
}
all_baseline_pass_rates.extend(baseline_pass_rates)
all_baseline_tokens.extend(baseline_tokens)
all_baseline_durations.extend(baseline_durations)
per_eval.append(eval_result)
# Calculate overall summary
with_stats = calculate_stats(all_with_pass_rates)
baseline_stats = calculate_stats(all_baseline_pass_rates)
with_token_mean = calculate_stats(all_with_tokens)["mean"] if all_with_tokens else 0
baseline_token_mean = calculate_stats(all_baseline_tokens)["mean"] if all_baseline_tokens else 0
with_duration_mean = calculate_stats(all_with_durations)["mean"] if all_with_durations else 0
baseline_duration_mean = calculate_stats(all_baseline_durations)["mean"] if all_baseline_durations else 0
# Ratios: >1 means with_skill is better for pass rate, <1 means with_skill uses fewer tokens/time
# Handle edge cases: no baseline data or zero baseline values
if baseline_stats["mean"] > 0:
improvement_ratio = round(with_stats["mean"] / baseline_stats["mean"], 2)
elif with_stats["mean"] > 0:
improvement_ratio = 999.99 # Skill passes but baseline doesn't (capped for JSON)
else:
improvement_ratio = 1.0 # Both zero — no improvement
token_savings = round(with_token_mean / baseline_token_mean, 2) if baseline_token_mean > 0 else 1.0
time_savings = round(with_duration_mean / baseline_duration_mean, 2) if baseline_duration_mean > 0 else 1.0
benchmark: dict[str, Any] = {
"skill_name": skill_name,
"iteration": iteration,
"timestamp": datetime.now(timezone.utc).isoformat(),
"summary": {
"total_evals": len(per_eval),
"with_skill": {
"pass_rate": with_stats["mean"],
"pass_rate_std": with_stats["std"],
"avg_tokens": with_token_mean,
"avg_duration_seconds": with_duration_mean,
},
"baseline": {
"pass_rate": baseline_stats["mean"],
"pass_rate_std": baseline_stats["std"],
"avg_tokens": baseline_token_mean,
"avg_duration_seconds": baseline_duration_mean,
},
"improvement_ratio": improvement_ratio,
"token_savings_ratio": token_savings,
"time_savings_ratio": time_savings,
},
"per_eval": per_eval,
}
# Compare with previous iteration
if previous_path:
prev_benchmark = load_json(Path(previous_path) / "benchmark.json")
if prev_benchmark:
prev_summary = prev_benchmark.get("summary", {})
prev_with = prev_summary.get("with_skill", {})
benchmark["comparison"] = {
"previous_iteration": prev_benchmark.get("iteration", 0),
"pass_rate_delta": round(
with_stats["mean"] - prev_with.get("pass_rate", 0), 4
),
"token_delta": round(
with_token_mean - prev_with.get("avg_tokens", 0), 2
),
"duration_delta": round(
with_duration_mean - prev_with.get("avg_duration_seconds", 0), 2
),
}
# Write outputs
benchmark_json_path = path / "benchmark.json"
benchmark_json_path.write_text(json.dumps(benchmark, indent=2))
# Generate markdown report
md_lines = [
f"# Benchmark Report: {skill_name}",
f"",
f"**Iteration**: {iteration} ",
f"**Timestamp**: {benchmark['timestamp']} ",
f"**Total Evals**: {len(per_eval)}",
f"",
f"## Summary",
f"",
f"| Metric | With Skill | Baseline | Ratio |",
f"|--------|-----------|----------|-------|",
f"| Pass Rate | {with_stats['mean']:.0%} (std {with_stats['std']:.2f}) | {baseline_stats['mean']:.0%} (std {baseline_stats['std']:.2f}) | {improvement_ratio}x |",
f"| Avg Tokens | {with_token_mean:,.0f} | {baseline_token_mean:,.0f} | {token_savings}x |",
f"| Avg Time | {with_duration_mean:.1f}s | {baseline_duration_mean:.1f}s | {time_savings}x |",
f"",
f"## Per-Eval Results",
f"",
f"| Eval | With Skill | Baseline | Trials |",
f"|------|-----------|----------|--------|",
]
for e in per_eval:
w_rate = e.get("with_skill", {}).get("pass_rate", 0)
b_rate = e.get("baseline", {}).get("pass_rate", 0)
trials = e.get("trials", 0)
md_lines.append(f"| {e['eval_name']} | {w_rate:.0%} | {b_rate:.0%} | {trials} |")
md_lines.append("")
benchmark_md_path = path / "benchmark.md"
benchmark_md_path.write_text("\n".join(md_lines))
return {
"status": "success",
"benchmark_json": str(benchmark_json_path),
"benchmark_md": str(benchmark_md_path),
"summary": benchmark["summary"],
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Aggregate eval results into a benchmark report"
)
parser.add_argument("path", help="Path to iteration-N workspace directory")
parser.add_argument(
"--skill-name", required=True,
help="Name of the skill being benchmarked"
)
parser.add_argument(
"--previous", "-p",
help="Path to previous iteration directory for comparison"
)
args = parser.parse_args()
if not os.path.isdir(args.path):
print(json.dumps({"error": f"Not a directory: {args.path}"}), file=sys.stderr)
sys.exit(1)
result = aggregate_benchmark(args.path, args.skill_name, args.previous)
print(json.dumps(result, indent=2))
if "error" in result:
sys.exit(1)
#!/usr/bin/env python3
"""
Purpose: Convert Claude Code skills to work on OpenAI Codex, Gemini CLI, Antigravity, and Cursor.
Input: Path to a skill directory, target platforms, optional output directory
Output: JSON conversion report with generated files and compatibility scores
Usage: python scripts/convert_skill.py /path/to/skill --target codex,gemini,antigravity,cursor [--output dist/] [--dry-run] [--include-mcp]
"""
import argparse
import json
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Any
# --- Field Classification ---
PORTABLE_FIELDS = {"name", "description", "license", "compatibility", "metadata", "argument-hint"}
ADAPTABLE_FIELDS = {
"allowed-tools": {
"codex": "keep",
"gemini": "strip",
"antigravity": "strip",
"cursor": "strip",
},
"disable-model-invocation": {
"codex": "move_to_openai_yaml",
"gemini": "strip",
"antigravity": "strip",
"cursor": "strip",
},
}
CLAUDE_ONLY_FIELDS = {
"context": "No equivalent. Subagent isolation is Claude Code specific.",
"agent": "No equivalent. Agent delegation is Claude Code specific.",
"hooks": "No equivalent. Lifecycle hooks are Claude Code specific.",
"model": "No equivalent. Model selection is Claude Code specific.",
"user-invocable": "Skills are always discoverable on other platforms.",
"skills": "No equivalent. Sub-skill references are Claude Code specific.",
"memory": "No equivalent. Persistent memory is Claude Code specific.",
}
# Platform skill paths
PLATFORM_PATHS = {
"codex": {"project": ".agents/skills", "user": "~/.agents/skills"},
"gemini": {"project": ".gemini/skills", "user": "~/.gemini/skills"},
"antigravity": {"project": ".agent/skills", "user": "~/.gemini/antigravity/skills"},
"cursor": {"project": ".cursor/skills", "user": "~/.cursor/skills"},
}
# Platform instruction files
PLATFORM_INSTRUCTION_FILES = {
"codex": "AGENTS.md",
"gemini": "GEMINI.md",
"antigravity": "GEMINI.md",
"cursor": "rules", # .cursor/rules/<name>.mdc
}
# MCP config file names per platform
PLATFORM_MCP_FILES = {
"codex": "config.toml",
"gemini": "settings.json",
"antigravity": "mcp_config.json",
"cursor": "mcp.json",
}
# --- Frontmatter Parsing (reused from validate_skill.py) ---
def parse_frontmatter(content: str) -> tuple[dict[str, Any] | None, str, list[str]]:
"""Parse YAML frontmatter from SKILL.md content.
Returns (frontmatter_dict, body, errors).
Uses basic parsing to avoid PyYAML dependency.
"""
errors: list[str] = []
if not content.startswith('---'):
return None, content, ["Missing opening '---' delimiter"]
parts = content.split('---', 2)
if len(parts) < 3:
return None, content, ["Missing closing '---' delimiter"]
yaml_text = parts[1].strip()
body = parts[2].strip()
if not yaml_text:
return None, body, ["Empty frontmatter"]
frontmatter: dict[str, Any] = {}
current_key = ""
current_value = ""
in_multiline = False
in_list = False
current_list: list[str] = []
for line in yaml_text.split('\n'):
stripped = line.strip()
if in_multiline:
if stripped and not re.match(r'^[a-z_-]+:', stripped):
current_value += " " + stripped
continue
else:
frontmatter[current_key] = current_value.strip()
in_multiline = False
if in_list:
if stripped.startswith('- '):
current_list.append(stripped[2:].strip())
continue
else:
frontmatter[current_key] = current_list
in_list = False
current_list = []
match = re.match(r'^([a-z_-]+):\s*(.*)', stripped)
if match:
current_key = match.group(1)
value = match.group(2).strip()
if value == '>':
in_multiline = True
current_value = ""
elif value == '|':
in_multiline = True
current_value = ""
elif value == '':
in_list = True
current_list = []
else:
frontmatter[current_key] = value.strip('"').strip("'")
if in_multiline:
frontmatter[current_key] = current_value.strip()
if in_list and current_list:
frontmatter[current_key] = current_list
return frontmatter, body, errors
# --- Tier Detection ---
def detect_skill_tier(skill_path: Path) -> int:
"""Detect skill complexity tier (1-4) from directory structure."""
has_scripts = (skill_path / "scripts").is_dir()
has_references = (skill_path / "references").is_dir()
has_agents = False
has_sub_skills = False
# Check parent directory for agents/ and skills/ folders
parent = skill_path.parent
skill_name = skill_path.name
agents_dir = parent / "agents"
if agents_dir.is_dir():
has_agents = any(agents_dir.glob(f"{skill_name}-*.md"))
skills_dir = parent / "skills"
if skills_dir.is_dir():
has_sub_skills = any(skills_dir.glob(f"{skill_name}-*/"))
if has_agents and has_sub_skills:
return 4 # Full ecosystem
if has_sub_skills:
return 3 # Multi-skill orchestrator
if has_scripts or has_references:
return 2 # Skill + scripts/references
return 1 # Single skill
# --- Field Classification ---
def classify_frontmatter_fields(fm: dict[str, Any]) -> dict[str, list[str]]:
"""Classify frontmatter fields into portable, adaptable, and claude_only."""
result: dict[str, list[str]] = {
"portable": [],
"adaptable": [],
"claude_only": [],
}
for key in fm:
if key in PORTABLE_FIELDS:
result["portable"].append(key)
elif key in ADAPTABLE_FIELDS:
result["adaptable"].append(key)
elif key in CLAUDE_ONLY_FIELDS:
result["claude_only"].append(key)
else:
result["claude_only"].append(key)
return result
def calculate_compatibility_score(classification: dict[str, list[str]]) -> int:
"""Calculate platform compatibility score (0-100)."""
total = sum(len(v) for v in classification.values())
if total == 0:
return 100
portable_count = len(classification["portable"])
adaptable_count = len(classification["adaptable"])
claude_only_count = len(classification["claude_only"])
# Portable fields = full weight, adaptable = half weight, claude_only = 0
score = ((portable_count * 1.0 + adaptable_count * 0.5) / total) * 100
return min(100, max(0, round(score)))
# --- Body Content Adaptation ---
# Patterns to find and replace in skill body text per platform
BODY_REPLACEMENTS: dict[str, list[tuple[str, str]]] = {
"codex": [
(r'~/\.claude/skills/', '~/.agents/skills/'),
(r'\.claude/skills/', '.agents/skills/'),
(r'~/\.claude/', '~/.agents/'),
(r'\.claude/', '.agents/'),
(r'CLAUDE\.md', 'AGENTS.md'),
],
"gemini": [
(r'~/\.claude/skills/', '~/.gemini/skills/'),
(r'\.claude/skills/', '.gemini/skills/'),
(r'~/\.claude/', '~/.gemini/'),
(r'\.claude/', '.gemini/'),
(r'CLAUDE\.md', 'GEMINI.md'),
],
"antigravity": [
(r'~/\.claude/skills/', '~/.gemini/antigravity/skills/'),
(r'\.claude/skills/', '.agent/skills/'),
(r'~/\.claude/', '~/.gemini/antigravity/'),
(r'\.claude/', '.agent/'),
(r'CLAUDE\.md', 'GEMINI.md'),
(r'\./scripts/', '{{SKILL_PATH}}/scripts/'),
(r'\./references/', '{{SKILL_PATH}}/references/'),
],
"cursor": [
(r'~/\.claude/skills/', '~/.cursor/skills/'),
(r'\.claude/skills/', '.cursor/skills/'),
(r'~/\.claude/', '~/.cursor/'),
(r'\.claude/', '.cursor/'),
(r'CLAUDE\.md', '.cursor/rules/'),
],
}
# Patterns that can't be auto-replaced -- just warn about them
BODY_WARNING_PATTERNS: list[tuple[str, str]] = [
(r'`/[a-z][\w-]+(?:\s+[a-z][\w-]+)*`', 'Slash command syntax is Claude Code specific. Codex uses $mention, Gemini uses description-based activation.'),
(r'\bTask\s+tool\b', 'Task tool (subagent delegation) is Claude Code specific. No direct equivalent on other platforms.'),
(r'\bspawn\s+(?:a\s+)?subagent', 'Subagent spawning is Claude Code specific.'),
(r'\bcontext:\s*fork\b', 'Forked context is Claude Code specific.'),
(r'\bhooks?\s*:', 'Lifecycle hooks are Claude Code specific.'),
]
def adapt_body_content(body: str, target: str) -> tuple[str, list[str]]:
"""Adapt skill body content for a target platform.
Replaces Claude-specific paths, file references, and config names.
Returns (adapted_body, warnings) where warnings list things that
need manual review.
"""
adapted = body
warnings: list[str] = []
# Apply automatic replacements
replacements = BODY_REPLACEMENTS.get(target, [])
for pattern, replacement in replacements:
new_text = re.sub(pattern, replacement, adapted)
if new_text != adapted:
warnings.append(f"Auto-replaced '{pattern.replace(chr(92), '')}' -> '{replacement}' in body text.")
adapted = new_text
# Scan for patterns that need manual attention
for pattern, message in BODY_WARNING_PATTERNS:
matches = re.findall(pattern, body)
if matches:
unique = list(set(matches))[:3] # Show up to 3 examples
examples = ", ".join(f"'{m.strip()}'" for m in unique)
warnings.append(f"Manual review needed: {message} Found: {examples}")
return adapted, warnings
# --- Frontmatter Generation ---
def strip_claude_fields(fm: dict[str, Any], target: str) -> tuple[dict[str, Any], list[str]]:
"""Remove Claude-only and platform-incompatible fields. Returns (cleaned_fm, warnings)."""
cleaned: dict[str, Any] = {}
warnings: list[str] = []
for key, value in fm.items():
if key in PORTABLE_FIELDS:
cleaned[key] = value
elif key in ADAPTABLE_FIELDS:
action = ADAPTABLE_FIELDS[key].get(target, "strip")
if action == "keep":
cleaned[key] = value
elif action == "move_to_openai_yaml":
warnings.append(f"Field '{key}' moved to openai.yaml (Codex platform extension)")
else:
workaround = ""
if key == "allowed-tools":
workaround = "All tools available by default on this platform."
warnings.append(f"Field '{key}' removed: not supported on {target}. {workaround}".strip())
elif key in CLAUDE_ONLY_FIELDS:
warnings.append(f"Field '{key}' removed: {CLAUDE_ONLY_FIELDS[key]}")
else:
warnings.append(f"Field '{key}' removed: unknown field, may be Claude Code specific.")
return cleaned, warnings
def generate_frontmatter_text(fm: dict[str, Any]) -> str:
"""Generate YAML frontmatter text from a dict."""
lines = ["---"]
for key, value in fm.items():
if isinstance(value, list):
lines.append(f"{key}:")
for item in value:
lines.append(f" - {item}")
elif isinstance(value, dict):
lines.append(f"{key}:")
for k, v in value.items():
lines.append(f" {k}: {v}")
elif isinstance(value, str) and (len(value) > 80 or '\n' in value):
lines.append(f"{key}: >")
# Wrap long descriptions
words = value.split()
current_line = " "
for word in words:
if len(current_line) + len(word) + 1 > 80:
lines.append(current_line)
current_line = " " + word
else:
current_line += (" " if len(current_line) > 2 else "") + word
if current_line.strip():
lines.append(current_line)
elif isinstance(value, bool):
lines.append(f"{key}: {'true' if value else 'false'}")
else:
lines.append(f"{key}: {value}")
lines.append("---")
return "\n".join(lines)
# --- Platform-Specific Generators ---
def generate_openai_yaml(fm: dict[str, Any]) -> str:
"""Generate Codex openai.yaml platform extension."""
name = fm.get("name", "skill")
display_name = name.replace("-", " ").title()
lines = [
"interface:",
f' display_name: "{display_name}"',
"",
"policy:",
]
# Map disable-model-invocation to allow_implicit_invocation (inverted)
disable_invocation = fm.get("disable-model-invocation", "false")
allow_implicit = "false" if disable_invocation == "true" else "true"
lines.append(f" allow_implicit_invocation: {allow_implicit}")
return "\n".join(lines) + "\n"
def generate_instruction_file(fm: dict[str, Any], body: str, platform: str) -> str:
"""Generate AGENTS.md or GEMINI.md from skill content.
Extracts the body content and re-titles it to avoid duplicate H1 headings.
Demotes body headings if they conflict with the generated structure.
"""
name = fm.get("name", "skill")
description = fm.get("description", "")
display_name = name.replace("-", " ").title()
lines = [
f"# {display_name}",
"",
description,
"",
]
if body:
# Strip any leading H1 from body to avoid duplicate top-level heading
body_lines = body.split('\n')
start_idx = 0
for i, line in enumerate(body_lines):
stripped = line.strip()
if stripped.startswith('# ') and not stripped.startswith('## '):
# Skip this H1 -- we already have one
start_idx = i + 1
# Also skip blank lines right after the removed H1
while start_idx < len(body_lines) and not body_lines[start_idx].strip():
start_idx += 1
break
elif stripped:
# First non-empty line isn't H1, keep everything
break
trimmed_body = '\n'.join(body_lines[start_idx:]).strip()
if trimmed_body:
lines.append(trimmed_body)
return "\n".join(lines) + "\n"
def generate_codex_output(
skill_path: Path,
fm: dict[str, Any],
body: str,
output_dir: Path,
) -> dict[str, Any]:
"""Generate Codex-compatible skill output."""
cleaned_fm, warnings = strip_claude_fields(fm, "codex")
manual_steps: list[str] = []
# Adapt body content for Codex
adapted_body, body_warnings = adapt_body_content(body, "codex")
warnings.extend(body_warnings)
skill_name = fm.get("name", skill_path.name)
target_dir = output_dir / "codex" / skill_name
# Create directory structure
target_dir.mkdir(parents=True, exist_ok=True)
files_created: list[str] = []
# Generate SKILL.md with cleaned frontmatter and adapted body
skill_md_content = generate_frontmatter_text(cleaned_fm) + "\n\n" + adapted_body
(target_dir / "SKILL.md").write_text(skill_md_content)
files_created.append(f"{skill_name}/SKILL.md")
# Generate openai.yaml
agents_dir = target_dir / "agents"
agents_dir.mkdir(exist_ok=True)
openai_yaml = generate_openai_yaml(fm)
(agents_dir / "openai.yaml").write_text(openai_yaml)
files_created.append(f"{skill_name}/agents/openai.yaml")
# Generate AGENTS.md with adapted body
agents_md = generate_instruction_file(fm, adapted_body, "codex")
(target_dir / "AGENTS.md").write_text(agents_md)
files_created.append(f"{skill_name}/AGENTS.md")
# Copy scripts if present
src_scripts = skill_path / "scripts"
if src_scripts.is_dir():
dst_scripts = target_dir / "scripts"
shutil.copytree(src_scripts, dst_scripts, dirs_exist_ok=True)
for f in dst_scripts.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "codex")))
# Copy references if present
src_refs = skill_path / "references"
if src_refs.is_dir():
dst_refs = target_dir / "references"
shutil.copytree(src_refs, dst_refs, dirs_exist_ok=True)
for f in dst_refs.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "codex")))
# Tier 3-4 notes
tier = detect_skill_tier(skill_path)
if tier >= 3:
manual_steps.append("Routing table uses Claude Code slash commands. Adapt to Codex $mention syntax.")
if tier >= 4:
manual_steps.append("Subagent delegation (Task tool) has no direct Codex equivalent. Consider breaking into separate skills.")
classification = classify_frontmatter_fields(fm)
return {
"output_dir": str(target_dir),
"files_created": files_created,
"compatibility_score": calculate_compatibility_score(classification),
"warnings": warnings,
"manual_steps": manual_steps,
}
def generate_gemini_output(
skill_path: Path,
fm: dict[str, Any],
body: str,
output_dir: Path,
) -> dict[str, Any]:
"""Generate Gemini CLI compatible skill output."""
cleaned_fm, warnings = strip_claude_fields(fm, "gemini")
manual_steps: list[str] = []
# Adapt body content for Gemini
adapted_body, body_warnings = adapt_body_content(body, "gemini")
warnings.extend(body_warnings)
skill_name = fm.get("name", skill_path.name)
target_dir = output_dir / "gemini" / skill_name
target_dir.mkdir(parents=True, exist_ok=True)
files_created: list[str] = []
# Generate SKILL.md with cleaned frontmatter and adapted body
skill_md_content = generate_frontmatter_text(cleaned_fm) + "\n\n" + adapted_body
(target_dir / "SKILL.md").write_text(skill_md_content)
files_created.append(f"{skill_name}/SKILL.md")
# Generate GEMINI.md with adapted body
gemini_md = generate_instruction_file(fm, adapted_body, "gemini")
(target_dir / "GEMINI.md").write_text(gemini_md)
files_created.append(f"{skill_name}/GEMINI.md")
# Copy scripts if present
src_scripts = skill_path / "scripts"
if src_scripts.is_dir():
dst_scripts = target_dir / "scripts"
shutil.copytree(src_scripts, dst_scripts, dirs_exist_ok=True)
for f in dst_scripts.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "gemini")))
# Copy references if present
src_refs = skill_path / "references"
if src_refs.is_dir():
dst_refs = target_dir / "references"
shutil.copytree(src_refs, dst_refs, dirs_exist_ok=True)
for f in dst_refs.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "gemini")))
tier = detect_skill_tier(skill_path)
if tier >= 3:
manual_steps.append("Routing table uses Claude Code slash commands. Gemini relies on description-based activation.")
if tier >= 4:
manual_steps.append("Subagent delegation (Task tool) has no Gemini CLI equivalent.")
classification = classify_frontmatter_fields(fm)
return {
"output_dir": str(target_dir),
"files_created": files_created,
"compatibility_score": calculate_compatibility_score(classification),
"warnings": warnings,
"manual_steps": manual_steps,
}
def generate_antigravity_output(
skill_path: Path,
fm: dict[str, Any],
body: str,
output_dir: Path,
) -> dict[str, Any]:
"""Generate Antigravity compatible skill output."""
cleaned_fm, warnings = strip_claude_fields(fm, "antigravity")
manual_steps: list[str] = []
# Adapt body content for Antigravity
adapted_body, body_warnings = adapt_body_content(body, "antigravity")
warnings.extend(body_warnings)
skill_name = fm.get("name", skill_path.name)
target_dir = output_dir / "antigravity" / skill_name
target_dir.mkdir(parents=True, exist_ok=True)
files_created: list[str] = []
# Generate SKILL.md (name is optional on Antigravity but we keep it) with adapted body
skill_md_content = generate_frontmatter_text(cleaned_fm) + "\n\n" + adapted_body
(target_dir / "SKILL.md").write_text(skill_md_content)
files_created.append(f"{skill_name}/SKILL.md")
# Generate GEMINI.md with adapted body
gemini_md = generate_instruction_file(fm, adapted_body, "antigravity")
(target_dir / "GEMINI.md").write_text(gemini_md)
files_created.append(f"{skill_name}/GEMINI.md")
# Copy scripts if present
src_scripts = skill_path / "scripts"
if src_scripts.is_dir():
dst_scripts = target_dir / "scripts"
shutil.copytree(src_scripts, dst_scripts, dirs_exist_ok=True)
for f in dst_scripts.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "antigravity")))
# Copy references if present
src_refs = skill_path / "references"
if src_refs.is_dir():
dst_refs = target_dir / "references"
shutil.copytree(src_refs, dst_refs, dirs_exist_ok=True)
for f in dst_refs.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "antigravity")))
tier = detect_skill_tier(skill_path)
if tier >= 3:
manual_steps.append("Routing table is Claude Code specific. Antigravity uses description-based activation.")
if tier >= 4:
manual_steps.append("Subagent delegation (Task tool) has no Antigravity equivalent.")
classification = classify_frontmatter_fields(fm)
return {
"output_dir": str(target_dir),
"files_created": files_created,
"compatibility_score": calculate_compatibility_score(classification),
"warnings": warnings,
"manual_steps": manual_steps,
}
def generate_cursor_rule(fm: dict[str, Any], body: str) -> str:
"""Generate a .cursor/rules/<name>.mdc rule file.
Cursor rules use a 3-field YAML frontmatter: description, globs, alwaysApply.
"""
description = fm.get("description", "")
# Escape double quotes to produce valid YAML
escaped_description = description.replace('"', '\\"')
lines = [
"---",
f"description: \"{escaped_description}\"",
"alwaysApply: false",
"---",
"",
]
if body:
# Strip any leading H1 from body
body_lines = body.split('\n')
start_idx = 0
for i, line in enumerate(body_lines):
stripped = line.strip()
if stripped.startswith('# ') and not stripped.startswith('## '):
start_idx = i + 1
while start_idx < len(body_lines) and not body_lines[start_idx].strip():
start_idx += 1
break
elif stripped:
break
trimmed_body = '\n'.join(body_lines[start_idx:]).strip()
if trimmed_body:
lines.append(trimmed_body)
return "\n".join(lines) + "\n"
def generate_cursor_output(
skill_path: Path,
fm: dict[str, Any],
body: str,
output_dir: Path,
) -> dict[str, Any]:
"""Generate Cursor-compatible skill output."""
cleaned_fm, warnings = strip_claude_fields(fm, "cursor")
manual_steps: list[str] = []
# Adapt body content for Cursor
adapted_body, body_warnings = adapt_body_content(body, "cursor")
warnings.extend(body_warnings)
skill_name = fm.get("name", skill_path.name)
target_dir = output_dir / "cursor" / skill_name
target_dir.mkdir(parents=True, exist_ok=True)
files_created: list[str] = []
# Generate SKILL.md with cleaned frontmatter and adapted body
skill_md_content = generate_frontmatter_text(cleaned_fm) + "\n\n" + adapted_body
(target_dir / "SKILL.md").write_text(skill_md_content)
files_created.append(f"{skill_name}/SKILL.md")
# Generate .cursor/rules/<name>.mdc rule file
rules_dir = target_dir / "rules"
rules_dir.mkdir(exist_ok=True)
cursor_rule = generate_cursor_rule(fm, adapted_body)
(rules_dir / f"{skill_name}.mdc").write_text(cursor_rule)
files_created.append(f"{skill_name}/rules/{skill_name}.mdc")
# Copy scripts if present
src_scripts = skill_path / "scripts"
if src_scripts.is_dir():
dst_scripts = target_dir / "scripts"
shutil.copytree(src_scripts, dst_scripts, dirs_exist_ok=True)
for f in dst_scripts.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "cursor")))
# Copy references if present
src_refs = skill_path / "references"
if src_refs.is_dir():
dst_refs = target_dir / "references"
shutil.copytree(src_refs, dst_refs, dirs_exist_ok=True)
for f in dst_refs.rglob("*"):
if f.is_file():
files_created.append(str(f.relative_to(output_dir / "cursor")))
tier = detect_skill_tier(skill_path)
if tier >= 3:
manual_steps.append("Routing table is Claude Code specific. Cursor uses description-based activation.")
if tier >= 4:
manual_steps.append("Cursor has single-level subagents only (Background Agents, Ultra plan). Task tool delegation needs manual adaptation.")
classification = classify_frontmatter_fields(fm)
return {
"output_dir": str(target_dir),
"files_created": files_created,
"compatibility_score": calculate_compatibility_score(classification),
"warnings": warnings,
"manual_steps": manual_steps,
}
# --- MCP Config Conversion ---
def convert_mcp_json_to_toml(mcp_json_path: Path) -> str | None:
"""Convert .mcp.json (Claude format) to TOML (Codex format).
Returns TOML string or None if file not found.
"""
if not mcp_json_path.exists():
return None
try:
data = json.loads(mcp_json_path.read_text())
except (json.JSONDecodeError, OSError):
return None
servers = data.get("mcpServers", {})
if not servers:
return None
lines: list[str] = []
for name, config in servers.items():
lines.append(f"[mcp_servers.{name}]")
command = config.get("command", "")
if command:
lines.append(f'command = "{command}"')
server_type = config.get("type", "stdio")
lines.append(f'type = "{server_type}"')
args = config.get("args", [])
if args:
args_str = ", ".join(f'"{a}"' for a in args)
lines.append(f"args = [{args_str}]")
env = config.get("env", {})
if env:
lines.append(f"")
lines.append(f"[mcp_servers.{name}.env]")
for key, value in env.items():
lines.append(f'{key} = "{value}"')
lines.append("")
return "\n".join(lines)
def convert_mcp_json_for_json_platform(mcp_json_path: Path, platform: str) -> str | None:
"""Convert .mcp.json to JSON format for Gemini, Antigravity, or Cursor.
Gemini uses settings.json, Antigravity uses mcp_config.json,
Cursor uses mcp.json. All use the same mcpServers JSON schema.
"""
if not mcp_json_path.exists():
return None
try:
data = json.loads(mcp_json_path.read_text())
except (json.JSONDecodeError, OSError):
return None
servers = data.get("mcpServers", {})
if not servers:
return None
output = {"mcpServers": servers}
return json.dumps(output, indent=2)
# --- Multi-Platform Install Script ---
def generate_multiplatform_install(skill_name: str, platforms: list[str]) -> str:
"""Generate a multi-platform install.sh that detects the agent platform."""
lines = [
'#!/usr/bin/env bash',
f'# Multi-platform installer for {skill_name}',
f'# Supports: Claude Code, {", ".join(p.title() for p in platforms)}',
'# Usage: bash install.sh [--platform claude|codex|gemini|antigravity|cursor]',
'',
'set -euo pipefail',
'',
'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
'PLATFORM="${1:-auto}"',
'',
'detect_platform() {',
' if [ -d "$HOME/.claude" ]; then',
' echo "claude"',
]
if "codex" in platforms:
lines += [
' elif [ -d "$HOME/.agents" ]; then',
' echo "codex"',
]
if "cursor" in platforms:
lines += [
' elif [ -d "$HOME/.cursor" ]; then',
' echo "cursor"',
]
if "gemini" in platforms or "antigravity" in platforms:
lines += [
' elif [ -d "$HOME/.gemini" ]; then',
' echo "gemini"',
]
lines += [
' else',
' echo "claude" # Default',
' fi',
'}',
'',
'if [ "$PLATFORM" = "auto" ] || [ "$PLATFORM" = "--auto" ]; then',
' PLATFORM=$(detect_platform)',
' echo "Detected platform: $PLATFORM"',
'fi',
'',
'# Strip -- prefix if provided as flag',
'PLATFORM="${PLATFORM#--}"',
'PLATFORM="${PLATFORM#--platform=}"',
'',
'case "$PLATFORM" in',
' claude)',
f' SKILL_DIR="$HOME/.claude/skills"',
' ;;',
]
if "codex" in platforms:
lines += [
' codex)',
f' SKILL_DIR="$HOME/.agents/skills"',
' ;;',
]
if "gemini" in platforms:
lines += [
' gemini)',
f' SKILL_DIR="$HOME/.gemini/skills"',
' ;;',
]
if "antigravity" in platforms:
lines += [
' antigravity)',
f' SKILL_DIR="$HOME/.gemini/antigravity/skills"',
' ;;',
]
if "cursor" in platforms:
lines += [
' cursor)',
f' SKILL_DIR="$HOME/.cursor/skills"',
' ;;',
]
lines += [
' *)',
' echo "Unknown platform: $PLATFORM"',
' echo "Supported: claude, ' + ", ".join(platforms) + '"',
' exit 1',
' ;;',
'esac',
'',
'echo "Installing to: $SKILL_DIR"',
'mkdir -p "$SKILL_DIR"',
'',
f'# Copy skill for the target platform',
'if [ -d "$SCRIPT_DIR/$PLATFORM" ]; then',
f' cp -r "$SCRIPT_DIR/$PLATFORM"/* "$SKILL_DIR/"',
'elif [ -d "$SCRIPT_DIR/claude" ]; then',
' # Fallback to claude version',
f' cp -r "$SCRIPT_DIR/claude"/* "$SKILL_DIR/"',
'fi',
'',
'echo ""',
f'echo "{skill_name} installed for $PLATFORM!"',
'',
]
return "\n".join(lines) + "\n"
# --- Main Conversion ---
def convert_skill(
skill_path: str,
targets: list[str],
output_dir: str,
dry_run: bool = False,
include_mcp: bool = False,
) -> dict[str, Any]:
"""Convert a Claude Code skill to target platforms."""
path = Path(skill_path).resolve()
out = Path(output_dir).resolve()
# Validate input
skill_md = path / "SKILL.md"
if not skill_md.exists():
return {"status": "error", "message": f"SKILL.md not found in {path}"}
content = skill_md.read_text()
fm, body, parse_errors = parse_frontmatter(content)
if not fm:
return {"status": "error", "message": f"Invalid frontmatter: {'; '.join(parse_errors)}"}
skill_name = fm.get("name", path.name)
tier = detect_skill_tier(path)
classification = classify_frontmatter_fields(fm)
# Dry run: just report compatibility
if dry_run:
platform_scores: dict[str, Any] = {}
for target in targets:
_, warnings = strip_claude_fields(fm, target)
_, body_warnings = adapt_body_content(body, target)
warnings.extend(body_warnings)
manual_steps: list[str] = []
if tier >= 3:
manual_steps.append(f"Tier {tier} skill: routing and orchestration need manual adaptation for {target}.")
if tier >= 4:
manual_steps.append(f"Subagent delegation needs manual adaptation for {target}.")
platform_scores[target] = {
"compatibility_score": calculate_compatibility_score(classification),
"warnings": warnings,
"manual_steps": manual_steps,
"fields_portable": classification["portable"],
"fields_adaptable": classification["adaptable"],
"fields_claude_only": classification["claude_only"],
}
return {
"status": "dry_run",
"skill_name": skill_name,
"tier": tier,
"platforms": platform_scores,
}
# Full conversion
out.mkdir(parents=True, exist_ok=True)
platform_results: dict[str, Any] = {}
generators = {
"codex": generate_codex_output,
"gemini": generate_gemini_output,
"antigravity": generate_antigravity_output,
"cursor": generate_cursor_output,
}
for target in targets:
if target in generators:
platform_results[target] = generators[target](path, fm, body, out)
# MCP config conversion
if include_mcp:
mcp_json = path / ".mcp.json"
if not mcp_json.exists():
mcp_json = path.parent / ".mcp.json"
if mcp_json.exists():
# Codex: convert to TOML
if "codex" in targets:
toml_content = convert_mcp_json_to_toml(mcp_json)
if toml_content:
codex_dir = out / "codex" / skill_name
codex_dir.mkdir(parents=True, exist_ok=True)
(codex_dir / "config.toml").write_text(toml_content)
if "codex" in platform_results:
platform_results["codex"]["files_created"].append(f"{skill_name}/config.toml")
# Gemini, Antigravity, Cursor: convert to platform-specific JSON
json_platforms = {"gemini": "settings.json", "antigravity": "mcp_config.json", "cursor": "mcp.json"}
for platform, filename in json_platforms.items():
if platform in targets:
json_content = convert_mcp_json_for_json_platform(mcp_json, platform)
if json_content:
platform_dir = out / platform / skill_name
platform_dir.mkdir(parents=True, exist_ok=True)
(platform_dir / filename).write_text(json_content)
if platform in platform_results:
platform_results[platform]["files_created"].append(f"{skill_name}/{filename}")
# Generate multi-platform install script
install_script_path = out / "install-multiplatform.sh"
install_content = generate_multiplatform_install(skill_name, targets)
install_script_path.write_text(install_content)
os.chmod(install_script_path, 0o755)
return {
"status": "success",
"skill_name": skill_name,
"tier": tier,
"platforms": platform_results,
"install_script": str(install_script_path),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Convert Claude Code skills to other platforms"
)
parser.add_argument("path", help="Path to skill directory")
parser.add_argument(
"--target", "-t", default="all",
help="Comma-separated targets: codex,gemini,antigravity,cursor,all (default: all)"
)
parser.add_argument(
"--output", "-o", default="./dist",
help="Output directory (default: ./dist)"
)
parser.add_argument(
"--dry-run", action="store_true",
help="Analyze compatibility without generating files"
)
parser.add_argument(
"--include-mcp", action="store_true",
help="Convert .mcp.json config files for target platforms"
)
args = parser.parse_args()
if not os.path.isdir(args.path):
print(json.dumps({"status": "error", "message": f"Not a directory: {args.path}"}), file=sys.stderr)
sys.exit(1)
targets = args.target.split(",")
if "all" in targets:
targets = ["codex", "gemini", "antigravity", "cursor"]
valid_targets = {"codex", "gemini", "antigravity", "cursor"}
invalid = set(targets) - valid_targets
if invalid:
print(json.dumps({"status": "error", "message": f"Invalid targets: {', '.join(invalid)}"}), file=sys.stderr)
sys.exit(1)
result = convert_skill(args.path, targets, args.output, args.dry_run, args.include_mcp)
if result["status"] == "error":
print(json.dumps(result, indent=2), file=sys.stderr)
sys.exit(1)
print(json.dumps(result, indent=2))
#!/usr/bin/env python3
"""
Purpose: Generate a starter eval set for a Claude Code skill based on its SKILL.md.
Input: Path to a skill directory containing SKILL.md
Output: JSON eval set with should-trigger and should-not-trigger queries
Usage: python scripts/generate_eval_set.py /path/to/skill [--output evals.json]
Analyzes the skill's description and instructions to produce:
- 10 should-trigger queries (various phrasings, edge cases)
- 10 should-not-trigger queries (near-misses, adjacent domains)
- Basic assertions for functional testing
"""
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
# Use shared parser when available; inline fallback for standalone execution
try:
from skill_utils import parse_frontmatter_simple as parse_frontmatter
except ImportError:
def parse_frontmatter(content: str) -> tuple[dict[str, Any] | None, str]: # type: ignore[misc]
"""Parse YAML frontmatter from SKILL.md content (inline fallback)."""
if not content.startswith('---'):
return None, content
parts = content.split('---', 2)
if len(parts) < 3:
return None, content
yaml_text, body = parts[1].strip(), parts[2].strip()
frontmatter: dict[str, Any] = {}
current_key, current_value, in_multiline = "", "", False
for line in yaml_text.split('\n'):
stripped = line.strip()
if in_multiline:
if stripped and not re.match(r'^[a-z_-]+:', stripped):
current_value += " " + stripped
continue
frontmatter[current_key] = current_value.strip()
in_multiline = False
match = re.match(r'^([a-z_-]+):\s*(.*)', stripped)
if match:
current_key, value = match.group(1), match.group(2).strip()
if value in ('>', '|'):
in_multiline, current_value = True, ""
elif value:
frontmatter[current_key] = value.strip('"').strip("'")
if in_multiline:
frontmatter[current_key] = current_value.strip()
return frontmatter, body
def extract_trigger_phrases(description: str) -> list[str]:
"""Extract quoted trigger phrases from the description."""
phrases = re.findall(r'"([^"]+)"', description)
return phrases
def extract_keywords(description: str) -> list[str]:
"""Extract domain keywords from the description."""
stop_words = {
'use', 'when', 'user', 'says', 'the', 'and', 'for', 'with', 'that',
'this', 'from', 'are', 'was', 'were', 'been', 'have', 'has', 'had',
'will', 'would', 'could', 'should', 'may', 'might', 'can', 'does',
'not', 'but', 'also', 'more', 'into', 'than', 'then', 'its', 'all',
'any', 'each', 'both', 'such', 'only', 'own', 'same', 'other',
}
words = re.findall(r'[a-z]+(?:-[a-z]+)*', description.lower())
keywords = [w for w in words if len(w) > 3 and w not in stop_words]
# Deduplicate while preserving order
seen: set[str] = set()
unique: list[str] = []
for w in keywords:
if w not in seen:
seen.add(w)
unique.append(w)
return unique[:20]
def extract_headings(body: str) -> list[str]:
"""Extract section headings from the SKILL.md body."""
headings = re.findall(r'^#{1,3}\s+(.+)$', body, re.MULTILINE)
return headings
def generate_trigger_evals(
name: str,
description: str,
trigger_phrases: list[str],
keywords: list[str],
) -> list[dict[str, Any]]:
"""Generate should-trigger eval entries."""
evals: list[dict[str, Any]] = []
eval_id = 0
# Direct trigger phrases from description
for phrase in trigger_phrases[:5]:
evals.append({
"eval_id": eval_id,
"eval_name": f"trigger-direct-{eval_id}",
"prompt": f"I need to {phrase}",
"input_files": [],
"assertions": [
{
"name": "skill-activated",
"check": f"The {name} skill activated and began its workflow",
"weight": 1.0,
}
],
"should_trigger": True,
})
eval_id += 1
# Casual paraphrases
casual_templates = [
"hey can you help me {keyword} something?",
"I've got this {keyword} task I need done",
"so my boss wants me to {keyword} — can you handle that?",
"quick question: how do I {keyword} with this tool?",
"need to {keyword} asap, what's the best approach?",
]
for i, template in enumerate(casual_templates):
if i < len(keywords):
evals.append({
"eval_id": eval_id,
"eval_name": f"trigger-casual-{eval_id}",
"prompt": template.format(keyword=keywords[i]),
"input_files": [],
"assertions": [
{
"name": "skill-activated",
"check": f"The {name} skill activated and began its workflow",
"weight": 1.0,
}
],
"should_trigger": True,
})
eval_id += 1
return evals[:10]
def generate_negative_evals(
name: str,
keywords: list[str],
) -> list[dict[str, Any]]:
"""Generate should-not-trigger eval entries.
Best practice: negative evals should be near-misses that share keywords
with the skill but need different tools. Avoid obviously irrelevant
queries like "what's the weather" — those don't test real disambiguation.
"""
# Near-miss templates that share domain keywords but need different tools
near_miss_templates = [
"Can you explain what {keyword} means in general terms?",
"Write documentation about the concept of {keyword} for my README",
"I'm learning about {keyword} -- what are some good tutorials?",
"What's the difference between {keyword} and similar approaches?",
"Search GitHub for open-source projects related to {keyword}",
"Debug this error I'm getting -- it mentions {keyword} in the stack trace",
"Refactor this function that handles {keyword} logic to be more readable",
"Add unit tests for the {keyword} module in my project",
"Review this PR that changes how we handle {keyword}",
"Set up a CI/CD pipeline that includes {keyword} validation steps",
]
evals: list[dict[str, Any]] = []
for i, template in enumerate(near_miss_templates[:10]):
keyword = keywords[i % len(keywords)] if keywords else "this"
evals.append({
"eval_id": 100 + i,
"eval_name": f"no-trigger-near-miss-{i}",
"prompt": template.format(keyword=keyword),
"input_files": [],
"assertions": [
{
"name": "skill-not-activated",
"check": f"The {name} skill did NOT activate",
"weight": 1.0,
}
],
"should_trigger": False,
})
return evals
def generate_eval_set(skill_path: str, output_path: str | None = None) -> dict[str, Any]:
"""Generate a complete eval set for a skill."""
path = Path(skill_path).resolve()
skill_md = path / "SKILL.md"
if not skill_md.exists():
return {"error": f"SKILL.md not found at {path}"}
content = skill_md.read_text()
frontmatter, body = parse_frontmatter(content)
if not frontmatter:
return {"error": "Could not parse SKILL.md frontmatter"}
name = frontmatter.get("name", path.name)
description = frontmatter.get("description", "")
trigger_phrases = extract_trigger_phrases(description)
keywords = extract_keywords(description)
headings = extract_headings(body)
trigger_evals = generate_trigger_evals(name, description, trigger_phrases, keywords)
negative_evals = generate_negative_evals(name, keywords)
eval_set = {
"skill_name": name,
"skill_path": str(path),
"generated_from": "description + instructions",
"evals": trigger_evals + negative_evals,
"metadata": {
"trigger_phrases_found": len(trigger_phrases),
"keywords_found": len(keywords),
"headings_found": len(headings),
"total_evals": len(trigger_evals) + len(negative_evals),
"should_trigger_count": len(trigger_evals),
"should_not_trigger_count": len(negative_evals),
},
}
if output_path:
out = Path(output_path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(eval_set, indent=2))
return eval_set
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate a starter eval set for a Claude Code skill"
)
parser.add_argument("path", help="Path to skill directory containing SKILL.md")
parser.add_argument(
"--output", "-o",
help="Output file path for eval set JSON (default: stdout)"
)
args = parser.parse_args()
if not Path(args.path).is_dir():
print(json.dumps({"error": f"Not a directory: {args.path}"}), file=sys.stderr)
sys.exit(1)
result = generate_eval_set(args.path, args.output)
if "error" in result:
print(json.dumps(result), file=sys.stderr)
sys.exit(1)
if not args.output:
print(json.dumps(result, indent=2))
else:
print(json.dumps({
"status": "success",
"output": args.output,
"total_evals": result["metadata"]["total_evals"],
}, indent=2))
#!/usr/bin/env python3
"""
Purpose: Shared utilities for skill-forge scripts.
Input: N/A (library module)
Output: N/A (library module)
Usage: from skill_utils import parse_frontmatter
"""
import re
from typing import Any
def parse_frontmatter(content: str) -> tuple[dict[str, Any] | None, str, list[str]]:
"""Parse YAML frontmatter from SKILL.md content.
Returns (frontmatter_dict, body, errors).
Uses basic parsing to avoid PyYAML dependency.
"""
errors: list[str] = []
if not content.startswith('---'):
return None, content, ["Missing opening '---' delimiter"]
parts = content.split('---', 2)
if len(parts) < 3:
return None, content, ["Missing closing '---' delimiter"]
yaml_text = parts[1].strip()
body = parts[2].strip()
if not yaml_text:
return None, body, ["Empty frontmatter"]
frontmatter: dict[str, Any] = {}
current_key = ""
current_value = ""
in_multiline = False
in_list = False
current_list: list[str] = []
for line in yaml_text.split('\n'):
stripped = line.strip()
if in_multiline:
if stripped and not re.match(r'^[a-z_-]+:', stripped):
current_value += " " + stripped
continue
else:
frontmatter[current_key] = current_value.strip()
in_multiline = False
if in_list:
if stripped.startswith('- '):
current_list.append(stripped[2:].strip())
continue
else:
frontmatter[current_key] = current_list
in_list = False
current_list = []
match = re.match(r'^([a-z_-]+):\s*(.*)', stripped)
if match:
current_key = match.group(1)
value = match.group(2).strip()
if value == '>':
in_multiline = True
current_value = ""
elif value == '|':
in_multiline = True
current_value = ""
elif value == '':
in_list = True
current_list = []
else:
frontmatter[current_key] = value.strip('"').strip("'")
if in_multiline:
frontmatter[current_key] = current_value.strip()
if in_list and current_list:
frontmatter[current_key] = current_list
return frontmatter, body, errors
def parse_frontmatter_simple(content: str) -> tuple[dict[str, Any] | None, str]:
"""Simplified parse_frontmatter that returns (frontmatter, body) without errors.
Convenience wrapper for scripts that don't need error details.
"""
frontmatter, body, _ = parse_frontmatter(content)
return frontmatter, body
Related skills
FAQ
What standard does skill-forge follow?
The Agent Skills open standard with a 3-layer architecture (directive, orchestration, execution).
How many complexity tiers does it detect?
Four, from a single SKILL.md up to a full ecosystem with sub-skills, subagents, and scripts.