Skill System — Skills
Responsibility
A Skill is a higher-order capability unit than a Tool: a reusable, frontmatter-annotated flow (similar to the agentskills.io spec), categorized by domain under skills/<category>/. Unlike tools' "static functions", skills can be created, rewritten, and refined by the learning loop — how the agent "gets smarter the more you use it". Skills are exposed to the main loop via skills_tool.
Design Motivation
Why organize skills by category directory instead of a flat layout? The skill count grows with usage (user-added, hub-synced, learning-loop-generated all land in ~/.hermes/skills/), and a flat list returned by skills_list quickly becomes noise. Categories let progressive disclosure's first layer expose "here is a group of research skills" rather than 50 concrete skill names. Each SKILL.md is a single self-contained definition (with frontmatter), paired with files under skills/<category>/<skill>/references/ for on-demand loading, so tokens do not blow up at once.
Bundles are another layer of packaging: they load multiple skills with one slash command, because a scenario like "backend development" needs code review, TDD, and PR skills all attached at once — /skill one by one is too fragmented.
Key Files
skills/ directory— grouped by domain (apple, computer-use, research, software-development, yuanbao...). Each skill looks like this:
skills/
├── my-skill/
│ ├── SKILL.md # main instructions (required)
│ ├── references/ # supporting docs
│ │ ├── api.md
│ │ └── examples.md
│ ├── templates/ # output templates
│ └── assets/ # attachments (agentskills.io standard)
└── category/
└── another-skill/
└── SKILL.mdThe top of SKILL.md is YAML frontmatter (name/description/version/platforms/prerequisites...), followed by the body instructions. name is capped at 64 chars, description at 1024; these two fields are the only thing the first progressive-disclosure layer of skills_list returns, to save tokens.
skills_tool— exposes skills as a tool to the main loop. It registers two tools:
registry.register(
name="skills_list",
toolset="skills",
schema=SKILLS_LIST_SCHEMA,
handler=lambda args, **kw: skills_list(
category=args.get("category"), task_id=kw.get("task_id")
),
check_fn=check_skills_requirements,
emoji="📚",
)
registry.register(
name="skill_view",
toolset="skills",
schema=SKILL_VIEW_SCHEMA,
handler=_skill_view_with_bump,
check_fn=check_skills_requirements,
emoji="📚",
)skills_list returns only name + description + category (first progressive-disclosure layer); skill_view loads the full SKILL.md (second layer); reference/template files are pulled on demand via skill_view(name, file_path="references/api.md") (third layer). _skill_view_with_bump calls bump_view / bump_use on success to update usage counters, which the curator's stale timer uses to clean up unused skills.
skill_manager_tool— skill management (list/view/enable)skills_hub— skill-hub sync (agentskills.io compatible)skills_sync— local ↔ remote skill syncskill_bundles— skill bundle packaging, injected into the prompt. A bundle is a small file under~/.hermes/skill-bundles/*.yaml:
name: backend-dev
description: Backend feature work — code review, testing, PR workflow.
skills:
- github-code-review
- test-driven-development
- github-pr-workflow
instruction: |
Optional extra guidance to inject above the skill bodies./<bundle-name> calls build_bundle_invocation_message to splice every member skill's SKILL.md into a single user message. When a bundle and a skill share a name, the bundle wins — if the user named them the same, they want to override the colliding skill. Missing member skills do not raise; the result just notes which ones were skipped.
skill_preprocessing— skill content preprocessing. Replaces${HERMES_SKILL_DIR}/${HERMES_SESSION_ID}template variables, and runs inline shell snippets like!date +%Y-%m-%d``. A failed inline shell returns an[inline-shell error: ...]marker instead of raising, so one bad snippet does not tank the whole skill message.skill nodes & edges:125-193—build_skill_nodes/build_edges, organizing skills into a learning graph
Data Flow
- At startup,
skill_preprocessing(agent/skill_preprocessing.py) scansskills/<category>/and reads frontmatter. skill_bundles(agent/skill_bundles.py) packages relevant skills and injects them into this turn's prompt viabuild_turn_context:268.- The provider decides to invoke a skill → tool_call
skills→tools/skills_tool.pyloads and executes that skill's flow. - The execution result is backfilled to the main loop.
- The learning loop reviews this turn in the background and may:
- create a new skill (
learning_mutations) - register the new skill into the learning graph
agent/learning_graph.py:125 - sync it to the skill hub via
tools/skills_sync.py
- create a new skill (
Boundaries and Failure
- Path traversal: a skill name is concatenated onto
~/.hermes/skills/to locate files, so_skill_lookup_path_errorrejects..segments, absolute paths, and Windows drive letters. Otherwisename="../outside"could read files outside the skills directory. - Prompt injection: SKILL.md content containing patterns like "ignore previous instructions", "system prompt:", or "]]>" is flagged by
_INJECTION_PATTERNS. Third-party skills (synced from the hub) are untrusted content; the main loop must sanitize them on load. - Bundle missing members:
build_bundle_invocation_messageonly records missing/disabled member skills intomissing/disabledlists and does not raise — bundles load leniently, and a few missing members do not affect the rest. - Cache staleness: scanning
skills/is O(#dirs) stat, with a signature that includes directory mtime + the disabled set + platform. But in-place edits to a singleSKILL.mdonly bump its own mtime, which the directory signature cannot see, so an additional 30s TTL acts as a fallback.
Summary
Skill = an evolvable higher-order flow, described by frontmatter, defined by a file, rewritable by the learning loop. Its relation to tools: tools are the atoms called within a skill flow; skills are knowledge about "how to combine tools to accomplish a class of tasks". See the learning loop for how skills evolve.