Skip to content

Instantly share code, notes, and snippets.

@khandar-william
Last active June 1, 2026 09:03
Show Gist options
  • Select an option

  • Save khandar-william/32cefcedaa609470c35fbb250f506545 to your computer and use it in GitHub Desktop.

Select an option

Save khandar-william/32cefcedaa609470c35fbb250f506545 to your computer and use it in GitHub Desktop.
Hermes Agent exploration

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.

Mental model

Skills are markdown playbooks (SKILL.md + optional references/, scripts/, etc.), not executable plugins. The harness does three jobs:

  1. Discover skills on disk and build a cheap catalog.
  2. Expose that catalog to the model (system prompt + tools + slash commands).
  3. 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).


Architecture (layers)

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
Loading

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": []
    },

Progressive disclosure (token budget)

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


When a chat message triggers a skill (and when it does not)

1. Explicit slash commands (deterministic)

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.

2. Natural language (non-deterministic, model-driven)

A plain message like “use the arxiv skill” does not run code that loads arxiv. The model is expected to:

  1. See the skill in the system index (or remember a prior skill_view).
  2. 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.

3. CLI preload (--skill / multiple skills)

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()

4. Other entry points

  • /skills install, hermes skills install — install to disk; usually effective next session unless --now invalidates the skills prompt cache (hermes_cli/skills_hub.py).
  • Cron jobs — skills / skill fields 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).

What prevents a skill from appearing or loading

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.


How skills are “executed”

Important: Hermes does not run a skill interpreter. “Execution” means:

  1. Load content into the conversation context.
  2. Let the agent loop follow the markdown (calling terminal, patch, MCP tools, etc. as the skill describes).

Slash / preload path

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.md body (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.

Tool path (skill_view)

skill_view in tools/skills_tool.py:

  • Resolves skill by path, category/name, or plugin:skill qualified names.
  • Enforces path trust, disabled state, platform, injection warnings.
  • Optionally preprocesses (agent/skill_preprocessing.py: ${HERMES_SKILL_DIR}, !backtick`` shell snippets if skills.inline_shell is 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.py so terminal / execute_code can see them when set.

After load, the model executes steps using normal tools — the skill is instructions, not a VM.

Authoring / maintenance (skill_manage)

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.


Turning skills on and off

Per-skill enable/disable (config)

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_view errors if called.

Toolset enable/disable (session capability)

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.

Install vs bundled

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

Cache-aware changes

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.

OS gating

platforms: [macos, linux] in frontmatter — automatic hide on unsupported OS (index, slash, tools).

Conditional visibility (not quite on/off)

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


End-to-end flow (typical chat)

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

Design choices worth remembering

  1. User-message injection for slash skills keeps the system prompt stable for prompt caching (AGENTS.md policy); only preload and --now cache invalidation change the stable prefix aggressively.
  2. Progressive disclosure keeps ~hundreds of skill descriptions cheap; full bodies are paid only when loaded.
  3. Trust boundaries: skills dirs + external_dirs are trusted roots; traversal and injection patterns are guarded in skill_view.
  4. Plugins can ship skills as namespace:skill via 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.

Here is a map of prompts and prompt-like text in Hermes that relate to agent skills, grouped by where they land in the stack.


1. Stable system prompt (every turn, if skills toolset is on)

Assembled in agent/system_prompt.py from agent/prompt_builder.py:

Constant / builder Role
HERMES_AGENT_HELP_GUIDANCE Tells the model to load hermes-agent via skill_view for Hermes setup/config questions.
MEMORY_GUIDANCE When memory tools are on: procedural workflows belong in skills, not memory; mentions saving approaches as skills.
SKILLS_GUIDANCE When skill_manage is available: save/patch skills after hard tasks; patch outdated skills immediately.
build_skills_system_prompt() Large block: ## Skills (mandatory) + instructions to scan <available_skills> and call skill_view(name) + embedded catalog (name: description per category).

The mandatory block text starts around:

        result = (
            "## Skills (mandatory)\n"
            "Before replying, scan the skills below. If a skill matches or is even partially relevant "
            "to your task, you MUST load it with skill_view(name) and follow its instructions. "
            ...
            "<available_skills>\n"
            + "\n".join(index_lines) + "\n"
            "</available_skills>\n"
            ...
            "Only proceed without loading a skill if genuinely none are relevant to the task."
        )

Not separate prompts, but filter what appears in that index: get_disabled_skill_names(), skill_matches_platform(), and _skill_should_show() (conditional metadata.hermes.requires_* / fallback_for_*).


2. Tool schemas (model-facing “prompts” for tool use)

Registered in tools/skills_tool.py and tools/skill_manager_tool.py:

Tool Prompt surface
skills_list Schema description: list names/descriptions; use skill_view for full content.
skill_view Schema description + parameter docs for name and optional file_path.
skill_manage SKILL_MANAGE_SCHEMA["description"] — long guidance on create/patch/edit/delete, when to save skills, absorbed_into, pinned skills, etc.

These are part of the tools array sent to the API, not the system string, but they shape skill behavior the same way instructions do.


3. Per-invocation user-turn payloads (skill content, not Hermes-authored)

Built in agent/skill_commands.py via _build_skill_message():

Wrapper text When
Slash activation '[IMPORTANT: The user has invoked the "{skill_name}" skill...]'
CLI preload '[IMPORTANT: The user launched this CLI session with the "{skill_name}" skill preloaded...]'
Optional blocks [Skill directory: …], [Skill config: …], setup notes, supporting-file hints, user text after /command

Underlying body is the skill’s own SKILL.md (and optional preprocessing: ${HERMES_SKILL_DIR}, !backtick`` shell if enabled).

Skill bundles (agent/skill_bundles.py): optional YAML instruction: prepended above multiple loaded skill bodies.

Cron (cron/scheduler.py): same activation framing as slash commands when a job lists skills:.

Webhooks (gateway/platforms/webhook.py): build_skill_invocation_message() replaces the route prompt when skills: is set on a route.


4. CLI / session one-shot notes (user role, not system)

Source Text
/reload-skills _pending_skills_reload_note: [USER INITIATED SKILLS RELOAD: + added/removed skill lines + “Use skills_list…” (cli.py).
Cron skipped skills Notice prepended when listed skills are missing (cron/scheduler.py).

5. Background / maintenance agent prompts (forked agents)

Prompt File Purpose
_SKILL_REVIEW_PROMPT agent/background_review.py Post-turn fork: actively update skill library (skill_view / skill_manage), class-level umbrellas, protected bundled/hub skills.
_COMBINED_REVIEW_PROMPT same Memory + skills review in one fork.
CURATOR_REVIEW_PROMPT agent/curator.py Curator fork: umbrella consolidation, archive rules, skill_manage / terminal usage.
CURATOR_DRY_RUN_BANNER same Prepended on dry-run curator passes (report only).

run_agent.py exposes these on AIAgent as _SKILL_REVIEW_PROMPT, _COMBINED_REVIEW_PROMPT, etc. The skill review fork is triggered when agent._skill_nudge_interval is exceeded (default 10 tool iterations without skill_manage; configurable via skills.creation_nudge_interval in agent init — see agent/agent_init.py).

Memory-only fork uses _MEMORY_REVIEW_PROMPT (mentions skills only indirectly).


6. Skill documents themselves (~/.hermes/skills/**/SKILL.md)

Hundreds of bundled/optional/user skills are the largest skill-related prompts. Each has:

  • YAML frontmatter (description, platforms, metadata.hermes.*, env/config declarations)
  • Markdown body (## When to Use, procedures, pitfalls, etc.)

Hermes does not centralize those strings; they are loaded by skill_view / slash / cron / webhooks.

Plugin skills (plugin:namespace/skill): served with an optional banner in _serve_plugin_skill() inside tools/skills_tool.py.


7. Related but indirect

Item Relation to skills
KANBAN_GUIDANCE System prompt for kanban workers; separate from the kanban-worker / kanban-orchestrator skills (those skills add depth when loaded).
file_safety denial message Points agents at skills_list / skill_view instead of reading .hub caches.
Cron injection scanners _scan_cron_skill_assembled vs _scan_cron_prompt — not instructional prompts; gate assembled skill+cron text.
insights / onboarding Telemetry or setup copy; not agent instruction prompts.
Docs site Generated from SKILL.md; not loaded at runtime unless the model reads docs or a skill says to.

Quick reference: what hits the model when

System (stable, cached)
  ├─ HERMES_AGENT_HELP_GUIDANCE
  ├─ MEMORY_GUIDANCE (if memory tools)
  ├─ SKILLS_GUIDANCE (if skill_manage)
  └─ build_skills_system_prompt() → "## Skills (mandatory)" + <available_skills>

Tools JSON
  ├─ skills_list / skill_view / skill_manage descriptions

User turns (dynamic)
  ├─ /skill-name, bundles, cron, webhook, --skill preload
  ├─ skill_view / skill_manage tool results (SKILL.md + JSON wrappers)
  └─ /reload-skills note, cron skip notices

Forked agents (separate sessions)
  ├─ _SKILL_REVIEW_PROMPT / _COMBINED_REVIEW_PROMPT
  └─ CURATOR_REVIEW_PROMPT (+ dry-run banner)

If you want a single file to start from for Hermes-authored skill prompts, use agent/prompt_builder.py (runtime catalog + guidance) and agent/background_review.py / agent/curator.py (maintenance loops). For user-facing skill instructions, everything under ~/.hermes/skills/ (and repo skills/ / optional-skills/ as sources) is the catalog.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment