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
- Create a real skill directory outside Hermes, for example:
~/skill_hub/follow-builders/SKILL.md
- 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
- Confirm the target file is readable:
test -f ~/.hermes/skills/social-media/follow-builders/SKILL.md && echo ok
- 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:
-
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")
-
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:
-
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
-
Centralize skill discovery so list/view/prompt-builder/gateway all share one traversal implementation
-
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
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_listomits the skillskill_view("bare-name")returns not foundskill_view("category/name")can still work~/.hermes/skills/social-media/follow-builders/SKILL.mdwork fineThis 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
~/skill_hub/follow-builders/SKILL.mdskills_listskill_view("follow-builders")skill_view("social-media/follow-builders")Actual behavior
skills_listdoes not includefollow-buildersskill_view("follow-builders")returns not foundskill_view("social-media/follow-builders")succeedsExpected behavior
A skill symlinked into
~/.hermes/skills/...should behave like a first-class local skill:skills_listWhy this happens
There are at least two discovery paths that do not follow directory symlinks:
tools/skills_tool.py_find_all_skills()scans withscan_dir.rglob("SKILL.md")skill_view()also scans withsearch_dir.rglob("SKILL.md")agent/skill_utils.pyiter_skill_index_files()usesos.walk(skills_dir)withoutfollowlinks=TrueOn 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:
skill_view("category/name")can work because the direct-path branch checkssearch_dir / nameSo the user sees an inconsistent system where the skill both exists and does not exist depending on the lookup path.
Workaround
Using
skills.external_dirspointing at the real source directory works reliably. For example:After switching to
external_dirs, the same skill becomes discoverable viaskills_listandskill_view("follow-builders").Possible fixes
A few possible approaches:
Make recursive discovery explicitly follow directory symlinks where appropriate
os.walk, usefollowlinks=Truerglob, replace with a traversal that follows symlinked directories intentionallyCentralize skill discovery so list/view/prompt-builder/gateway all share one traversal implementation
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_managenot handlingexternal_dirsconsistently.This issue is about ordinary local discovery under
~/.hermes/skills/failing when the skill directory itself is a symlink.Environment