Claude Code Skill Anatomy
Claude Code skills — whether in .claude/skills/ or .claude/commands/ — use a similar but not identical structure. Here is a typical Claude Code skill file:
---
name: blog-writer
description: |
Writes long-form technical blog posts for the WOWHOW storefront.
Triggered when the user asks to write or publish a blog post.
trigger_conditions:
- user types /new-blog
- user asks to write a blog post
- user asks to publish an article
tools_required:
- Read
- Write
- Edit
- Bash
---
# Blog Writer Skill
## When to Use
Load this skill when the user asks to write, draft, or publish a blog post.
## Instructions
1. Read the reference file to understand the existing TypeScript format.
2. Determine content mode based on topic and instructions.
3. Write the TypeScript data file with the BlogPost schema.
4. Add the import and spread to blog-posts.ts.
## Quality Gates
- Word count must meet mode minimum
- All code blocks must have language specifiers
- Slug must be unique in POST_ORDER
Comparing the two formats side by side:
| Field |
Claude Code |
agentskills.io (Hermes) |
| Skill identifier |
name |
name |
| One-liner |
description (multi-line OK) |
description (one line) |
| Trigger |
trigger_conditions (prose) |
trigger_keywords (array of strings) |
| Version |
Not in spec |
version (required) |
| Tool refs |
tools_required (Claude tool names) |
No formal field (prose in body) |
| Body structure |
Free markdown |
Recommended: Procedure / Pitfalls / Verification |
The conversion is mostly mechanical. Extract keywords from trigger_conditions, truncate description to one line, add a version, restructure the body sections. That is what the conversion script does.
The Conversion Script
Here is the full script. It scans .claude/skills/ and .claude/commands/, converts each file to agentskills.io format, and writes the output to ~/.hermes/skills/. It handles missing frontmatter, missing fields, and keyword extraction from prose trigger conditions.
#!/usr/bin/env python3
"""
convert_skills.py — Convert Claude Code skills to agentskills.io (Hermes) format.
Usage: python3 convert_skills.py [--source-dir .claude] [--out-dir ~/.hermes/skills]
"""
import argparse
import os
import re
import sys
from pathlib import Path
import yaml # pip install pyyaml
# --- Configuration -----------------------------------------------------------
KEYWORD_EXTRACT_RE = re.compile(
r'(write|build|create|publish|deploy|audit|refactor|generate|port|convert|'
r'analyze|review|update|fix|check|run|install|add|remove|delete|list|show|get)',
re.IGNORECASE,
)
DEFAULT_VERSION = "1.0"
# --- Helpers -----------------------------------------------------------------
def load_frontmatter_and_body(path: Path) -> tuple[dict, str]:
"""Parse YAML frontmatter + body from a markdown file."""
text = path.read_text(encoding="utf-8")
if text.startswith("---"):
parts = text.split("---", 2)
if len(parts) >= 3:
try:
fm = yaml.safe_load(parts[1]) or {}
return fm, parts[2].strip()
except yaml.YAMLError:
pass
return {}, text.strip()
def extract_keywords(trigger_conditions: object) -> list[str]:
"""
Extract short keyword phrases from Claude Code trigger_conditions.
Accepts a string, a list of strings, or None.
"""
if not trigger_conditions:
return []
if isinstance(trigger_conditions, list):
raw = " ".join(str(c) for c in trigger_conditions)
else:
raw = str(trigger_conditions)
# Extract slash commands like /new-blog → "new blog"
slash_cmds = re.findall(r'/([a-z][a-z0-9-]+)', raw)
keywords = [cmd.replace("-", " ") for cmd in slash_cmds]
# Extract quoted phrases
quoted = re.findall(r'"([^"]{3,40})"', raw)
keywords.extend(quoted)
# Extract verb phrases (verb + 1-3 words)
for match in KEYWORD_EXTRACT_RE.finditer(raw):
start = match.start()
snippet = raw[start:start + 40].split("
")[0]
phrase = snippet.strip().rstrip(".,;")
if 3 < len(phrase) < 50:
keywords.append(phrase.lower())
# Deduplicate, preserve order
seen: set[str] = set()
unique: list[str] = []
for kw in keywords:
if kw not in seen:
seen.add(kw)
unique.append(kw)
return unique[:10] # cap at 10 keywords
def truncate_description(desc: object) -> str:
"""Collapse multi-line description to one line under 120 chars."""
if not desc:
return "Skill imported from Claude Code"
s = str(desc).replace("
", " ").strip()
return s[:120] if len(s) > 120 else s
def restructure_body(name: str, body: str, tools_required: list[str]) -> str:
"""
Ensure body has Procedure / Pitfalls / Verification sections.
If the body already has them, leave it alone.
"""
has_procedure = bool(re.search(r'^#+s*procedure', body, re.IGNORECASE | re.MULTILINE))
has_pitfalls = bool(re.search(r'^#+s*pitfall', body, re.IGNORECASE | re.MULTILINE))
has_verification = bool(re.search(r'^#+s*verif', body, re.IGNORECASE | re.MULTILINE))
out = body
if not has_procedure:
out = f"## Procedure
{out}"
if not has_pitfalls:
out += "
## Pitfalls
- Follow the procedure exactly; do not skip steps."
if not has_verification:
out += "
## Verification
- Confirm the task completed successfully before reporting done."
if tools_required:
tool_note = ", ".join(tools_required)
out += f"
"
return out.strip()
def convert_file(src: Path, out_dir: Path, dry_run: bool = False) -> bool:
"""Convert a single Claude Code skill file to agentskills.io format."""
fm, body = load_frontmatter_and_body(src)
name = fm.get("name") or src.stem
description = truncate_description(fm.get("description"))
version = fm.get("version") or DEFAULT_VERSION
trigger_conditions = fm.get("trigger_conditions") or fm.get("triggers") or ""
tools_required = fm.get("tools_required") or []
author = fm.get("author") or ""
tags = fm.get("tags") or []
keywords = extract_keywords(trigger_conditions)
if not keywords:
# Fall back to name tokens as keywords
keywords = name.replace("-", " ").split()
new_fm: dict = {
"name": name,
"version": str(version),
"description": description,
"trigger_keywords": keywords,
}
if author:
new_fm["author"] = author
if tags:
new_fm["tags"] = tags
new_body = restructure_body(name, body, tools_required)
out_text = f"---
{yaml.dump(new_fm, default_flow_style=False, sort_keys=False)}---
{new_body}
"
out_path = out_dir / f"{name}.md"
if dry_run:
print(f"[DRY RUN] Would write: {out_path}")
return True
out_dir.mkdir(parents=True, exist_ok=True)
out_path.write_text(out_text, encoding="utf-8")
print(f" Converted: {src.name} → {out_path}")
return True
# --- Main --------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Convert Claude Code skills to agentskills.io format")
parser.add_argument("--source-dir", default=".claude", help="Root of .claude directory (default: .claude)")
parser.add_argument("--out-dir", default="~/.hermes/skills", help="Output directory (default: ~/.hermes/skills)")
parser.add_argument("--dry-run", action="store_true", help="Print what would be written without writing")
args = parser.parse_args()
source_root = Path(args.source_dir).expanduser().resolve()
out_dir = Path(args.out_dir).expanduser().resolve()
scan_dirs = [
source_root / "skills",
source_root / "commands",
]
converted = 0
skipped = 0
for scan_dir in scan_dirs:
if not scan_dir.exists():
print(f" [skip] {scan_dir} not found")
continue
for md_file in sorted(scan_dir.glob("*.md")):
try:
if convert_file(md_file, out_dir, dry_run=args.dry_run):
converted += 1
except Exception as exc:
print(f" [error] {md_file.name}: {exc}", file=sys.stderr)
skipped += 1
print(f"
Done. Converted: {converted}, Skipped/errors: {skipped}")
print(f"Output: {out_dir}")
if __name__ == "__main__":
main()
Run it from the project root:
# Install dependency
pip install pyyaml
# Dry run first — see what would be generated
python3 convert_skills.py --source-dir .claude --out-dir ~/.hermes/skills --dry-run
# Execute
python3 convert_skills.py --source-dir .claude --out-dir ~/.hermes/skills
The script handles the three most common edge cases: skill files with no frontmatter at all (it treats the entire file as the body and derives the name from the filename), skill files where trigger_conditions is a multi-line prose block (it extracts verb phrases and slash commands), and skill files where description is a multi-paragraph block (it collapses it to one line).
5 Porting Patterns
Not all skills port cleanly from the mechanical conversion. Here are the five patterns I have encountered, with the approach for each.
Pattern 1: Direct Port
Most skills port 1:1. The skill has a clear name, a clear description, clear trigger conditions, and a procedure that does not reference Claude-specific tools. The conversion script handles these automatically with no manual intervention.
Example: a skill that generates a project directory structure, a skill that writes commit messages, a skill that formats TypeScript files. None of these reference agent-specific tooling. The procedure is universal. The script converts frontmatter and restructures the body. Done.
Indicator it is a direct port: The procedure section reads like a numbered list of steps that any agent with file read/write access could follow. No tool names appear in the procedure itself.
Pattern 2: Tool-Dependent Skills
Claude Code skills frequently reference specific built-in tools: Read, Write, Edit, Bash, Glob, Grep. Hermes does not have these as named primitives — it has MCP tools, and the tool names depend on your MCP configuration.
The approach: replace Claude tool references with generic action descriptions, then add a Hermes-specific note in the body listing the MCP equivalents.
Before (Claude Code):
## Procedure
1. Use Read to load the target file.
2. Use Grep to find all occurrences of the pattern.
3. Use Edit to apply the replacement.
4. Use Bash to run the test suite.
After (agentskills.io):
## Procedure
1. Load the target file using your file-read capability.
2. Search for all occurrences of the pattern using your search capability.
3. Apply the replacement using your file-edit capability.
4. Run the test suite using your shell-execution capability.
The comment block documents the intended tool mapping without making the skill dependent on a specific MCP configuration. If your Hermes setup uses different MCP tool names, update the comment block once rather than rewriting the procedure.
Pattern 3: Context-Dependent Skills
Some Claude Code skills encode knowledge about a specific project structure. The WOWHOW blog-writer skill, for example, references storefront/src/data/blog-posts/, the TypeScript BlogPost type, and the POST_ORDER array. A skill that portable to Hermes cannot hard-code these paths — Hermes might be running against a different working directory, a Docker container, or a remote codebase.
The approach: parameterize paths as variables the agent resolves at runtime, and add a Verification section that confirms the agent is in the right context before executing.
Before:
## Procedure
1. Read storefront/src/data/blog-posts/types.ts to understand the BlogPost schema.
2. Read an existing blog post file from storefront/src/data/blog-posts/ for format reference.
3. Create the new file at storefront/src/data/blog-posts/YYYY-MM-slug.ts.
After:
## Procedure
1. Locate the blog posts data directory. Look for a directory named `blog-posts` under `src/data/` relative to the project root. Confirm it exists before proceeding.
2. Read the types file in that directory to understand the BlogPost schema.
3. Read one existing post file for format reference.
4. Create the new file in the same directory following the naming convention of existing files.
## Verification
- Confirm the project root contains a `src/data/blog-posts/` directory before executing.
- If the directory is not found, stop and ask the user to confirm the project root.
This makes the skill project-agnostic. Hermes will resolve the directory at runtime. The verification step catches the case where Hermes is running against the wrong working directory.
Pattern 4: Composite Skills
Some Claude Code skills are large and cover multiple distinct operations. A deploy-checklist skill might cover pre-deploy checks, the deploy procedure itself, and post-deploy verification. In Claude Code, these are separate steps in one file because the operator reads it top-to-bottom. In Hermes, a large skill file increases context overhead without benefit — Hermes skill injection is additive, and a skill that covers three different operations will be loaded for any of the three, adding unnecessary context for the other two.
The approach: split composite skills into single-responsibility skills, each with focused trigger keywords.
One Claude Code file deploy-checklist.md becomes three Hermes files:
# ~/.hermes/skills/pre-deploy-check.md
---
name: pre-deploy-check
version: "1.0"
description: Run pre-deploy checks before pushing to production
trigger_keywords:
- pre-deploy
- before deploy
- deploy check
- check before push
---
# ~/.hermes/skills/deploy-execute.md
---
name: deploy-execute
version: "1.0"
description: Execute the production deploy procedure
trigger_keywords:
- deploy to production
- run deploy
- push to production
- execute deploy
---
# ~/.hermes/skills/post-deploy-verify.md
---
name: post-deploy-verify
version: "1.0"
description: Verify the deploy completed successfully
trigger_keywords:
- post-deploy
- after deploy
- verify deploy
- check deploy
---
The conversion script will not do this split automatically — it requires human judgment about which operations are genuinely distinct. But the split is worth doing for any skill file that covers more than one conceptual operation.
Pattern 5: Bidirectional Sync
For skills that are actively maintained — the blog-writer, the tool-builder, the deploy checklist — the right architecture is a shared canonical source in git with generated outputs for each agent format. Neither .claude/skills/ nor ~/.hermes/skills/ is the source of truth. A skills/canonical/ directory in the project root is.
The canonical format is agentskills.io (since it is the more constrained format — anything valid for Hermes is also valid for Claude Code). The Claude Code versions are generated by stripping or transforming only the fields that differ.
This is Pattern 5 because it requires committing to a directory structure and a generation step. The payoff is a single place to update operational knowledge, with no drift between agents.
Gotchas
Tool Name Differences
The most common conversion failure is a skill that references a Claude tool by name in its procedure body. Claude Code's Read, Edit, Bash, Glob, and Grep are not tool names that Hermes understands. A procedure that says "use Bash to run the test suite" will confuse Hermes — it will look for a tool named Bash in its MCP registry and not find it.
The conversion script adds a comment block mapping Claude tool names to MCP equivalents, but it does not rewrite the prose. Manually review any skill file that contains the strings Read, Write, Edit, Bash, Glob, or Grep as tool invocations (not as generic English words) and update the procedure to use capability descriptions instead.
Path Assumptions
Claude Code skills are typically run from the project root because Claude Code's working directory is the project. Hermes skills run from wherever Hermes is invoked, which on a VPS is often /root/ or a service user's home directory. A skill that hard-codes storefront/src/ as a relative path will fail silently on Hermes.
Audit every path reference in converted skills. Replace relative paths with runtime-resolved paths (see Pattern 3 above).
Model-Specific Instructions Baked In
Some Claude Code skills encode Claude-specific behavior: "use extended thinking for this step," "set thinking budget to 10000 tokens," "use claude-opus-4-7 for this task." These instructions are meaningless to Hermes running a different model.
The conversion script does not strip these. They will not cause errors in Hermes — the model will read the instruction and not know what to do with it, then proceed with the default behavior. But they add noise. During manual review, remove model-specific directives from skills that will run on Hermes with a different model backend.
skillListingBudgetFraction
Hermes has a configuration setting called skillListingBudgetFraction in hermes.config.toml. It controls what fraction of the model's context budget can be consumed by loaded skills. The default is 0.15 — 15% of the context window.
If you port 20+ skills and all of them have broad trigger keywords, many will match on every task, and Hermes will hit the skill budget cap on the first load. The cap causes Hermes to drop lower-priority skills silently. You will not see an error — you will just notice that some skills are not being applied.
Two mitigations: narrow your trigger keywords so fewer skills match on any given task, and set explicit priorities in the frontmatter (agentskills.io supports a priority field, integer 1-10, higher = loaded first when budget is tight).
---
name: blog-writer
version: "1.0"
description: Writes long-form technical blog posts
trigger_keywords:
- write blog post
- new blog
- publish article
priority: 8 # high priority — always load when matched
---
The Shared Source of Truth Pattern
Here is the directory structure I use for maintaining canonical skills with generated outputs for both agents:
project-root/
├── skills/
│ ├── canonical/ # Source of truth (agentskills.io format)
│ │ ├── blog-writer.md
│ │ ├── tool-builder.md
│ │ ├── deploy-check.md
│ │ └── graphify.md
│ ├── claude/ # Generated — do not edit directly
│ │ ├── blog-writer.md
│ │ ├── tool-builder.md
│ │ └── ...
│ └── hermes/ # Generated — do not edit directly
│ ├── blog-writer.md
│ ├── tool-builder.md
│ └── ...
├── scripts/
│ └── generate_skills.py # Reads canonical/, writes claude/ and hermes/
└── Makefile
The Makefile:
.PHONY: skills deploy-skills
# Generate both output formats from canonical source
skills:
python3 scripts/generate_skills.py --canonical skills/canonical --claude-out skills/claude --hermes-out skills/hermes
# Deploy to the live locations
deploy-skills: skills
cp -r skills/claude/* .claude/skills/
cp -r skills/hermes/* ${HOME}/.hermes/skills/
@echo "Skills deployed to .claude/skills/ and ~/.hermes/skills/"
The generate script is a modified version of the conversion script above, with one additional step: for the Claude output, it adds a trigger_conditions field (prose form, derived from the trigger_keywords array) and removes the version field (Claude Code does not use it). For the Hermes output, it passes through the canonical format unchanged, since canonical is already agentskills.io.
The workflow:
- Edit the canonical skill in
skills/canonical/.
- Run
make skills to regenerate both outputs.
- Run
make deploy-skills to copy to the live locations.
- Commit
skills/canonical/ and both generated directories to git.
The generated directories are committed so that the repository is fully self-contained — you can clone it on a new machine and run make deploy-skills without needing to regenerate from scratch. The generate step only needs to run when the canonical changes.
Where This Goes
agentskills.io is currently implemented by Hermes and a small number of open-source agent frameworks. It is not implemented by Cursor, GitHub Copilot, Codex, or Windsurf. Each of those has its own skill or instruction format: Cursor has .cursorrules, Codex has system prompts, Windsurf has Cascade instructions. None of them interoperate.
But the structural pattern is converging. Every major agent framework is moving toward YAML frontmatter over a markdown body as the format for agent instructions. The field names differ. The trigger mechanisms differ. But the underlying architecture — a collection of markdown files that encode operational knowledge, loaded selectively based on the current task — is becoming the de facto standard.
The bet I am making is that agentskills.io becomes the lingua franca for this format, or that a future standard emerges that is mechanically compatible with it. Either way, keeping operational knowledge in a canonical agentskills.io format and generating agent-specific outputs is more future-proof than maintaining separate files for each agent in each agent's native format.
Markdown is the portable format. YAML frontmatter is the portable metadata schema. The agents will converge on reading both. The question is just which field names win.
For now, the conversion script and the five patterns cover 90% of what you will encounter when porting a mature CLAUDE.md skill library to Hermes. The remaining 10% is the skills that are so tightly coupled to Claude's specific tool API that they cannot be ported without a rewrite — and those are worth identifying, because the tight coupling is usually a sign that the skill encodes agent-specific workarounds rather than durable operational knowledge.
The operational knowledge should be portable. The conversion script makes most of it so.
Comments · 0
No comments yet. Be the first to share your thoughts.