Skip to content

fix(profiles): preserve symlinks when cloning and exporting profiles#11573

Open
mvanhorn wants to merge 1 commit into
NousResearch:mainfrom
mvanhorn:fix/11560-profile-clone-symlink-recursion
Open

fix(profiles): preserve symlinks when cloning and exporting profiles#11573
mvanhorn wants to merge 1 commit into
NousResearch:mainfrom
mvanhorn:fix/11560-profile-clone-symlink-recursion

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

What does this PR do?

shutil.copytree is called in three places in hermes_cli/profiles.py without symlinks=True, so it follows symlinks instead of copying them as-is. A symlink inside ~/.hermes that points at a parent directory (easy to create by accident - @Opfour hit one while cloning profiles) sends shutil._copytree into infinite recursion and hermes profile create --clone-all crashes with RecursionError. The export paths would crash the same way if a user ever triggered them on such a profile.

Pass symlinks=True to all three call sites. symlinks=True copies symlinks verbatim rather than walking their target, which is the right semantic when cloning or exporting a user-owned profile tree. The existing ignore= callbacks at the export sites are preserved.

Related Issue

Fixes #11560

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/profiles.py:437 - add symlinks=True to the --clone-all shutil.copytree call (the crash site named in the issue).
  • hermes_cli/profiles.py:802 - add symlinks=True to the default-profile export path. Same latent bug.
  • hermes_cli/profiles.py:815 - add symlinks=True to the named-profile export path. Same latent bug. Existing ignore= callbacks (credential files, _default_export_ignore) are untouched and still run.
  • tests/hermes_cli/test_profiles.py - add test_clone_all_preserves_recursive_symlink using the existing profile_env fixture. The test creates a source profile with a loop -> .. symlink, calls create_profile(clone_all=True), and asserts the call returns, the destination exists, and the symlink survives as a symlink.

How to Test

  1. Reproduce (pre-fix): mkdir -p /tmp/h/profiles/src && ln -s .. /tmp/h/profiles/src/loop && HERMES_HOME=/tmp/h hermes profile create dup --clone-all --clone-from src --no-alias -> RecursionError: maximum recursion depth exceeded from shutil._copytree.
  2. Post-fix: same commands succeed. The new dup/ profile contains the loop entry as a symlink (ls -la shows loop -> ..).
  3. Targeted test: pytest tests/hermes_cli/test_profiles.py -v - 88 passed including the new regression test.

Demo: https://vhs.charm.sh/vhs-2QQWVciEMvWZk9M4DRyHnF.gif

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/hermes_cli/test_profiles.py -v and all tests pass (88 passed)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15.3

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guidesymlinks=True works identically on Linux/macOS; on Windows, Python's stdlib shutil.copytree falls back per documented behavior.
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

This contribution was developed with AI assistance (Codex).

shutil.copytree defaults to following symlinks, so a symlink inside
~/.hermes pointing at a parent directory causes infinite recursion
during `hermes profile create <name> --clone-all` (reported traceback
bottoms out in shutil._copytree hitting the recursion limit).

Pass symlinks=True to all three profile copytree call sites in
hermes_cli/profiles.py so symlinks are copied as symlinks rather than
dereferenced. This fixes the clone-all crash and preempts the same
failure in the export paths for the default and named profiles.

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

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hermes profile --clone-all crashes with RecursionError on recursive symlink

2 participants