Skip to content

Bug: symlinked skills under ~/.hermes/skills are omitted from skills_list and bare-name skill_view lookup #8293

@x-hansong

Description

@x-hansong

Summary

Directory symlink skills placed under ~/.hermes/skills/<category>/<name> can be directly readable on disk, but Hermes discovery misses them in multiple places.

Observed behavior:

  • skills_list omits the skill
  • skill_view("bare-name") returns not found
  • skill_view("category/name") can still work
  • direct file reads like ~/.hermes/skills/social-media/follow-builders/SKILL.md work fine

This creates a confusing partial-failure mode where the skill exists and is readable, but normal discovery and bare-name lookup behave as if it does not exist.

Reproduction

  1. Create a real skill directory outside Hermes, for example:
    • ~/skill_hub/follow-builders/SKILL.md
  2. Symlink it into the local skills tree:
    mkdir -p ~/.hermes/skills/social-media
    ln -s ~/skill_hub/follow-builders ~/.hermes/skills/social-media/follow-builders
  3. Confirm the target file is readable:
    test -f ~/.hermes/skills/social-media/follow-builders/SKILL.md && echo ok
  4. In Hermes:
    • call skills_list
    • call skill_view("follow-builders")
    • call skill_view("social-media/follow-builders")

Actual behavior

  • skills_list does not include follow-builders
  • skill_view("follow-builders") returns not found
  • skill_view("social-media/follow-builders") succeeds

Expected behavior

A skill symlinked into ~/.hermes/skills/... should behave like a first-class local skill:

  • it should appear in skills_list
  • bare-name lookup should work when the name is unique
  • discovery should be consistent with direct path loading

Why this happens

There are at least two discovery paths that do not follow directory symlinks:

  1. tools/skills_tool.py

    • _find_all_skills() scans with scan_dir.rglob("SKILL.md")
    • bare-name fallback in skill_view() also scans with search_dir.rglob("SKILL.md")
  2. agent/skill_utils.py

    • iter_skill_index_files() uses os.walk(skills_dir) without followlinks=True

On Python 3.11+, these scans do not recurse into directory symlinks, so symlinked skills are skipped during recursive discovery even though direct path access still works.

Symptom pattern

This is easy to miss because some code paths still succeed:

  • direct path access to the symlinked directory works
  • skill_view("category/name") can work because the direct-path branch checks search_dir / name
  • but recursive scans miss the same skill

So the user sees an inconsistent system where the skill both exists and does not exist depending on the lookup path.

Workaround

Using skills.external_dirs pointing at the real source directory works reliably. For example:

skills:
  external_dirs:
    - /absolute/path/to/skill_hub

After switching to external_dirs, the same skill becomes discoverable via skills_list and skill_view("follow-builders").

Possible fixes

A few possible approaches:

  1. Make recursive discovery explicitly follow directory symlinks where appropriate

    • for os.walk, use followlinks=True
    • for rglob, replace with a traversal that follows symlinked directories intentionally
  2. Centralize skill discovery so list/view/prompt-builder/gateway all share one traversal implementation

  3. Preserve safety by following only symlinks that remain within trusted skill roots, or document the intended policy clearly if symlinked local skills are unsupported

Related issue

This seems adjacent to, but distinct from, #4759.

#4759 is about skill_manage not handling external_dirs consistently.

This issue is about ordinary local discovery under ~/.hermes/skills/ failing when the skill directory itself is a symlink.

Environment

  • Hermes Agent main branch as of 2026-04-12
  • Python 3.11
  • macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havesweeper:implemented-on-mainSweeper: behavior already present on current maintool/skillsSkills system (list, view, manage)type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions