Here is how Hermes Agent’s skills harness works end to end: what gets loaded when, how execution happens, and how you turn skills on or off.
Skills are markdown playbooks (SKILL.md + optional references/, scripts/, etc.), not executable plugins. The harness does three jobs:
- Discover skills on disk and build a cheap catalog.
- Expose that catalog to the model (system prompt + tools + slash commands).
- Load full content on demand when the user or the model explicitly asks.
There is no separate “skill matcher” that scans chat text and auto-riggers skills. For normal chat, activation is either explicit (/skill-name, bundles, preload) or model-driven (the model reads the index and calls skill_view).
flowchart TB
subgraph disk ["On disk"]
SK["~/.hermes/skills/**/SKILL.md"]
EXT["skills.external_dirs"]
BUND["~/.hermes/skill-bundles/*.yaml"]
end
subgraph discovery ["Discovery"]
SCAN["scan_skill_commands()"]
INDEX["build_skills_system_prompt()"]
FIND["_find_all_skills()"]
end
subgraph exposure ["Exposure to agent"]
SYS["System prompt: name + description index"]
TOOLS["skills_list / skill_view / skill_manage"]
SLASH["/skill-name slash commands"]
end
subgraph activation ["Activation"]
USER_SLASH["Slash / bundle → user message"]
MODEL["Model calls skill_view"]
PRELOAD["CLI --skill → system prompt chunk"]
end
subgraph execution ["Execution"]
LLM["Agent loop follows SKILL.md"]
TERM["terminal / other tools per skill prose"]
end
SK --> SCAN
SK --> INDEX
SK --> FIND
EXT --> INDEX
BUND --> USER_SLASH
SCAN --> SLASH
INDEX --> SYS
FIND --> TOOLS
SYS --> MODEL
SLASH --> USER_SLASH
MODEL --> execution
USER_SLASH --> execution
PRELOAD --> execution
TOOLS --> execution
Canonical storage: ~/.hermes/skills/ (profile-aware via get_hermes_home()). Bundled repo skills are copied/synced there; hub installs and agent-created skills live there too (tools/skills_tool.py documents this).
Tool surface (only if the skills toolset is enabled):
"skills": {
"description": "Access, create, edit, and manage skill documents with specialized instructions and knowledge",
"tools": ["skills_list", "skill_view", "skill_manage"],
"includes": []
},| Tier | Mechanism | What the model sees |
|---|---|---|
| 0 – Catalog | build_skills_system_prompt() in agent/system_prompt.py |
Category + name: description lines inside <available_skills> |
| 1 – List tool | skills_list() |
JSON metadata (name, description, category) |
| 2 – Full skill | skill_view(name) |
Full SKILL.md body + linked file hints + setup notes |
| 3 – Sub-files | skill_view(name, file_path="references/...") |
One reference/template/script file |
The system prompt block is built only when the agent has skills tools:
has_skills_tools = any(name in agent.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage'])
if has_skills_tools:
avail_toolsets = {
toolset
for toolset in (
_r.get_toolset_for_tool(tool_name) for tool_name in agent.valid_tool_names
)
if toolset
}
skills_prompt = _r.build_skills_system_prompt(
available_tools=agent.valid_tool_names,
available_toolsets=avail_toolsets,
)
else:
skills_prompt = ""
if skills_prompt:
stable_parts.append(skills_prompt)Instructions tell the model to err on loading relevant skills via skill_view (agent/prompt_builder.py around the ## Skills (mandatory) section).
At startup / first use, scan_skill_commands() walks SKILL.md files and registers /slug → skill metadata (agent/skill_commands.py). Slugs are normalized (lowercase, hyphens; underscores accepted in input).
CLI: process_command() sees /foo-bar, calls build_skill_invocation_message(), and queues the result on _pending_input as a user message (not system prompt — preserves prompt cache per AGENTS.md):
elif base_cmd in skill_commands:
user_instruction = cmd_original[len(base_cmd):].strip()
msg = build_skill_invocation_message(
base_cmd, user_instruction, task_id=self.session_id
)
if msg:
skill_name = skill_commands[base_cmd]["name"]
print(f"\n⚡ Loading skill: {skill_name}")
if hasattr(self, '_pending_input'):
self._pending_input.put(msg)Gateway: Same idea — rewrite event.text to the loaded skill payload, then run the normal agent turn (gateway/run.py ~7961–8021). Per-platform disabled skills are checked again at dispatch time (scan cache is global; platform overrides are applied here).
Skill bundles (agent/skill_bundles.py): YAML in ~/.hermes/skill-bundles/ loads multiple skills in one message; bundles win over a skill with the same slash name.
A plain message like “use the arxiv skill” does not run code that loads arxiv. The model is expected to:
- See the skill in the system index (or remember a prior
skill_view). - Call
skill_view("arxiv")(or follow slash-command text if the user typed/arxiv).
Whether it actually loads the skill depends on the model and the mandatory-skills guidance in the prompt — not on Hermes keyword matching.
build_preloaded_skills_prompt() loads skills at session start and merges them into cli.system_prompt (different from slash injection):
if parsed_skills:
skills_prompt, loaded_skills, missing_skills = build_preloaded_skills_prompt(
parsed_skills,
task_id=cli.session_id,
)
...
if skills_prompt:
cli.system_prompt = "\n\n".join(
part for part in (cli.system_prompt, skills_prompt) if part
).strip()/skills install,hermes skills install— install to disk; usually effective next session unless--nowinvalidates the skills prompt cache (hermes_cli/skills_hub.py).- Cron jobs —
skills/skillfields on jobs load named skills before the run (cron/jobs.py). - Kanban workers — per-task forced skills via board config (plugin/docs).
- Background review — post-turn fork may update memory/skills using only memory + skills toolsets (
agent/background_review.py).
| Gate | Effect |
|---|---|
skills toolset disabled |
No skills tools, no skills index in system prompt |
skills.disabled in config.yaml |
Hidden from index, slash map, skills_list, skill_view |
skills.platform_disabled.<platform> |
Same, per Telegram/CLI/etc. |
platforms: in SKILL.md |
Hidden on wrong OS (skill_matches_platform) |
| Conditional metadata | metadata.hermes.requires_toolsets, fallback_for_toolsets, etc. filter the index only (_skill_should_show in agent/prompt_builder.py) |
| Optional hub skills not installed | Not on disk → not discoverable until hermes skills install |
| Setup / env | Skill may load with setup_needed notes; gateway won’t prompt for secrets in chat |
Disabled list resolution (agent/skill_utils.py):
def get_disabled_skill_names(platform: str | None = None) -> Set[str]:
...
if resolved_platform:
platform_disabled = (skills_cfg.get("platform_disabled") or {}).get(
resolved_platform
)
if platform_disabled is not None:
return _normalize_string_set(platform_disabled)
return _normalize_string_set(skills_cfg.get("disabled"))skill_view also refuses disabled skills with an error JSON payload.
Important: Hermes does not run a skill interpreter. “Execution” means:
- Load content into the conversation context.
- Let the agent loop follow the markdown (calling
terminal,patch, MCP tools, etc. as the skill describes).
build_skill_invocation_message() → _load_skill_payload() → skill_view(..., preprocess=False) → _build_skill_message():
- Prepends an activation note (“user invoked X skill”).
- Injects full
SKILL.mdbody (with optional template vars / inline shell per config). - Adds skill directory path, resolved
skills.config.*values, supporting-file listing, optional user text after the command.
That string becomes a user turn, then run_conversation() runs like any other message.
skill_view in tools/skills_tool.py:
- Resolves skill by path, category/name, or
plugin:skillqualified names. - Enforces path trust, disabled state, platform, injection warnings.
- Optionally preprocesses (
agent/skill_preprocessing.py:${HERMES_SKILL_DIR},!backtick`` shell snippets ifskills.inline_shellis on). - Prompts for missing env vars in local CLI; gateway gets a hint to use setup/
.env. - Registers declared env vars with
tools/env_passthrough.pysoterminal/execute_codecan see them when set.
After load, the model executes steps using normal tools — the skill is instructions, not a VM.
skill_manage creates/edits/patches/deletes skills under ~/.hermes/skills/, with optional security scanning for agent-created skills (skills.guard_agent_created). Curator (agent/curator.py) manages agent-created skill lifecycle separately (stale/archive), not bundled/hub skills.
skills:
disabled: [skill-a, skill-b] # global
platform_disabled:
telegram: [skill-c]
cli: []- UI:
hermes skills→hermes_cli/skills_config.py(curses checklist, global or per-platform). - Effect: Removed from slash commands, system index, and tool listings;
skill_viewerrors if called.
hermes tools or tools.<platform>.enabled / disabled in config.yaml. Without the skills toolset, the agent cannot list/view/manage skills and does not get the skills block in the system prompt.
- Bundled under repo
skills/→ synced into~/.hermes/skills/. - Optional under
optional-skills/→ install via hub/hermes skills install official/.... - Uninstall/reset via
hermes skills//skills uninstall|reset.
Installing/disabling skills usually affects new sessions. /skills install --now (or hermes skills install ... --now) invalidates the skills prompt cache so the index updates without /reset (documented in skills hub — trades token cost for immediacy).
/reload-skills rescans slash commands but does not invalidate the system-prompt skills cache (agent/skill_commands.reload_skills docstring) — by design for caching.
platforms: [macos, linux] in frontmatter — automatic hide on unsupported OS (index, slash, tools).
metadata:
hermes:
fallback_for_toolsets: [web]
requires_toolsets: [terminal]Handled in _skill_should_show() — skill may be “off” in the index when e.g. web_search is available, but still loadable if the model knows the name and calls skill_view (unless also disabled in config).
Session start
→ If skills toolset enabled: build_skills_system_prompt() → stable system prefix (cached)
→ scan_skill_commands() → /slash map for CLI/gateway
User: "Help me fine-tune with axolotl"
→ No automatic loader
→ Model may call skill_view("axolotl") after reading index
→ Tool returns JSON content → tool result message in history
→ Agent follows SKILL.md using terminal, etc.
User: "/axolotl use QLoRA on my dataset"
→ build_skill_invocation_message → user message with full skill body
→ Same agent loop; bump_use() for curator telemetry
- User-message injection for slash skills keeps the system prompt stable for prompt caching (AGENTS.md policy); only preload and
--nowcache invalidation change the stable prefix aggressively. - Progressive disclosure keeps ~hundreds of skill descriptions cheap; full bodies are paid only when loaded.
- Trust boundaries: skills dirs +
external_dirsare trusted roots; traversal and injection patterns are guarded inskill_view. - Plugins can ship skills as
namespace:skillvia the plugin manager, parallel to disk skills.
If you want to go deeper on one path (e.g. gateway Telegram slash normalization, skills hub install/sync, or curator + skill_manage), say which surface you care about and we can trace that file chain only.