Skip to content

[Bug]: os.walk(followlinks=True) in iter_skill_index_files may infinite-loop on cyclic symlinks #18809

@SimbaKingjoe

Description

@SimbaKingjoe

Bug Description

agent/skill_utils.py:440-451iter_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-451iter_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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havecomp/agentCore agent loop, run_agent.py, prompt buildertool/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