Bug Description
agent/skill_utils.py:440-451 — iter_skill_index_files() uses os.walk(..., followlinks=True) without any cycle detection:
def iter_skill_index_files(skills_dir: Path, filename: str):
matches = []
for root, dirs, files in os.walk(skills_dir, followlinks=True):
dirs[:] = [d for d in dirs if d not in EXCLUDED_SKILL_DIRS]
if filename in files:
matches.append(Path(root) / filename)
for path in sorted(matches, key=lambda p: str(p.relative_to(skills_dir))):
yield path
Python's os.walk(followlinks=True) does no cycle detection. If a symlink in skills_dir creates a cycle (e.g., a subdirectory symlink pointing to an ancestor), the walk recurses indefinitely until the OS rejects the path with OSError (ENAMETOOLONG or ELOOP).
EXCLUDED_SKILL_DIRS only filters .git, .github, .hub, .archive — it provides zero protection against symlink cycles.
Steps to Reproduce
# Setup a skills dir with a cyclic symlink
SKILLS_DIR=~/.hermes/skills
mkdir -p "$SKILLS_DIR/test-cycle"
ln -s "$SKILLS_DIR" "$SKILLS_DIR/test-cycle/circular"
# Trigger the walk (e.g., via skills listing or /model command)
# The agent will loop infinitely
Expected Behavior
The function should detect symlink cycles and terminate cleanly.
Actual Behavior
Infinite recursion until OSError (path too long or too many symlink levels). This blocks the agent's startup or skill discovery path.
Affected Component
agent/skill_utils.py:440-451 — iter_skill_index_files()
Called from:
agent/skill_commands.py
agent/prompt_builder.py
tools/skills_tool.py
Debug Report
N/A — no crash or stack trace. The bug manifests as an infinite loop that eventually raises OSError when the path exceeds OS limits.
Proposed Fix
Track resolved real paths with a visited set:
def iter_skill_index_files(skills_dir: Path, filename: str):
matches = []
visited = set()
for root, dirs, files in os.walk(skills_dir, followlinks=True):
real_root = os.path.realpath(root)
if real_root in visited:
dirs[:] = []
continue
visited.add(real_root)
dirs[:] = [d for d in dirs if d not in EXCLUDED_SKILL_DIRS]
if filename in files:
matches.append(Path(root) / filename)
for path in sorted(matches, key=lambda p: str(p.relative_to(skills_dir))):
yield path
Operating System
All (behavior is inherent to Python's os.walk on any OS with symlink support).
Bug Description
agent/skill_utils.py:440-451—iter_skill_index_files()usesos.walk(..., followlinks=True)without any cycle detection:Python's
os.walk(followlinks=True)does no cycle detection. If a symlink inskills_dircreates a cycle (e.g., a subdirectory symlink pointing to an ancestor), the walk recurses indefinitely until the OS rejects the path withOSError(ENAMETOOLONG or ELOOP).EXCLUDED_SKILL_DIRSonly filters.git,.github,.hub,.archive— it provides zero protection against symlink cycles.Steps to Reproduce
Expected Behavior
The function should detect symlink cycles and terminate cleanly.
Actual Behavior
Infinite recursion until
OSError(path too long or too many symlink levels). This blocks the agent's startup or skill discovery path.Affected Component
agent/skill_utils.py:440-451—iter_skill_index_files()Called from:
agent/skill_commands.pyagent/prompt_builder.pytools/skills_tool.pyDebug Report
N/A — no crash or stack trace. The bug manifests as an infinite loop that eventually raises
OSErrorwhen the path exceeds OS limits.Proposed Fix
Track resolved real paths with a
visitedset:Operating System
All (behavior is inherent to Python's
os.walkon any OS with symlink support).