DEV Community

Anup Karanjkar
Anup Karanjkar

Posted on • Originally published at wowhow.cloud

Auto-Porting Your CLAUDE.md Skills to Hermes Agent: The agentskills.io Open Standard Nobody Is Using Yet

I run Claude Code with 20+ custom skills spread across .claude/skills/ and .claude/commands/. I also run Hermes Agent on my VPS. For months, I maintained two copies of the same operational knowledge — one formatted for Claude, one for Hermes. Then I found out about the agentskills.io open standard, which Hermes implements natively, and realized I was doing unnecessary work.

The structural gap between a Claude Code skill file and a Hermes skill file is smaller than it looks. Both use YAML frontmatter over a markdown body. Both encode a trigger condition, a name, and a procedure. The field names differ, the trigger syntax differs, and the tool references differ — but the conversion is mechanical. You can automate it. Almost nobody has.

This is the full story: what agentskills.io is, what the conversion script looks like, five porting patterns covering the cases you will actually encounter, and the gotchas that will bite you if you do not know about them.

The Problem: Two Islands of Operational Knowledge

A typical WOWHOW-style CLAUDE.md setup ends up with skills in at least two places. The .claude/skills/ directory holds reusable skill files — things like a blog-writer skill, a tool-builder skill, a deploy-invariants checker. The .claude/commands/ directory holds slash-command handlers — things like /new-blog, /graphify, /build-tool. Each file teaches Claude how to do something in a particular way, often encoding hard-won knowledge from production incidents.

Hermes Agent has its own skill directory at ~/.hermes/skills/. Each file there teaches Hermes how to respond to a trigger. The Hermes skill runner loads relevant skills based on keyword matching against the task prompt, injects them into the agent's context, and lets the model follow the procedure.

The result is two islands. You fix a bug in your blog-writer workflow, update .claude/skills/blog-writer.md, and then remember (or forget) to update ~/.hermes/skills/blog-writer.md. The islands drift. One agent knows the right procedure; the other is running an outdated one.

The solution is a single canonical source with generated outputs for each agent format. The agentskills.io standard is what makes the generation straightforward.

The agentskills.io Standard

agentskills.io is an open standard for agent skill format. It specifies a YAML frontmatter schema and a markdown body structure that any agent framework can implement. Hermes adopted it as its native skill format in v0.11.0. The standard is intentionally minimal — it specifies only what is needed for a skill runner to load and invoke a skill, leaving the body content to the author.

A compliant agentskills.io skill file looks like this:

---
name: blog-writer
version: "1.2"
description: "Writes long-form technical blog posts for the WOWHOW storefront"
trigger_keywords:
  - blog post
  - write post
  - new blog
  - publish article
author: Anup Karanjkar
tags:
  - content
  - seo
  - writing
---

## Procedure

1. Read the blog-posts data directory to understand the existing format.
2. Select the content mode: PILLAR (8k-12k words), STANDARD (1.5k-2.5k), or QUICK (600-900).
3. Write the post following answer-first structure: thesis + data point + structure preview in the first 150 words.
4. Generate the TypeScript data file following the existing BlogPost schema.
5. Add the import and spread to blog-posts.ts.

## Pitfalls

- Never use blanket noindex. Use conditional quality gates.
- All ${VAR} in code blocks must be escaped with a backslash.
- Do not push to git unless explicitly asked.

## Verification

- Check that the TypeScript file compiles: `npx tsc --noEmit`
- Confirm the slug is added to POST_ORDER in blog-posts.ts
Enter fullscreen mode Exit fullscreen mode

The frontmatter fields the standard defines:

  • name (required): Machine-readable skill identifier, lowercase, hyphens allowed.

  • version (required): SemVer string. The skill runner uses this to detect staleness.

  • description (required): Human-readable one-liner. Also used in skill listings.

  • trigger_keywords (required): Array of strings. The skill runner matches these against the task prompt. If any keyword appears in the prompt, the skill is loaded into context.

  • author (optional): String.

  • tags (optional): Array of strings. Used for skill browsing and filtering.

The markdown body is free-form, but the standard recommends three sections: Procedure (the steps the agent should follow), Pitfalls (common mistakes to avoid), and Verification (how to confirm the skill executed correctly). Hermes renders all three sections to the model. Claude Code uses the full file body as context.

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
Enter fullscreen mode Exit fullscreen mode

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 |
Enter fullscreen mode Exit fullscreen mode

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  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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
---
Enter fullscreen mode Exit fullscreen mode

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
---
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Edit the canonical skill in skills/canonical/.

  2. Run make skills to regenerate both outputs.

  3. Run make deploy-skills to copy to the live locations.

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

Originally published at wowhow.cloud

Top comments (0)