fix(profile): preserve user-added skills and cron jobs on install --force / update#44386
Conversation
…orce / update hermes profile install --force and hermes profile update both advertise "user data preserved", but _copy_dist_payload rmtree'd and recopied every directory the distribution ships. Anything the user added inside skills/ or cron/ after install — including skills installed with hermes skills install, which writes into the same tree — was silently deleted. Replace the wholesale rmtree+copytree with a merge: * dist-shipped skill dirs (containing SKILL.md) still replace wholesale, so renamed/dropped files don't linger * other shipped dirs merge recursively (covers category folders like skills/coding/ shared by distribution and user) * shipped files overwrite; entries the distribution doesn't ship are left untouched The copytree ignore= lambda this removes was dead code: it only filtered when the visited dir equaled the staged root, but copytree starts one level below the root, so the predicate never fired. Top-level filtering happens via the USER_OWNED_EXCLUDE guard above. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Code Review — Positive Verification Reviewed the full diff across What was checked:
Verdict: Clean. The merge-instead-of-mirror approach prevents data loss for users who install additional skills via |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Summary
Fixes hermes update/install --force to preserve user-added skills and cron jobs that were installed after the distribution was first deployed. Previously, distribution updates would wipe user additions inside distribution-owned directories.
Changes
- Adds
_replace_dir_merging_user_entries()that recursively merges directories, preserving user entries while replacing distribution-owned skill roots wholesale. - Distribution directories containing
SKILL.mdare replaced wholesale; other directories are merged recursively.
Review
- Good fix with clear documentation of the preservation semantics.
- No security concerns.
- Clean approach that distinguishes skill roots from general category folders.
Reviewed by Hermes Agent (batch cron)
The §4a extras-preservation block in install.sh works around hermes-agent deleting user-added skills on 'profile install --force'. That is fixed upstream by NousResearch/hermes-agent#44386 (issue #25120); note in both the block and its probe that §4a can be removed once the deployed hermes-agent includes the fix, verified by the probe passing with §4a reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes #25120
What
hermes profile install --forceandhermes profile updateboth advertise "user data preserved" in their help text, but_copy_dist_payload()rmtree'd + recopied every directory the distribution ships. Anything the user added insideskills/after install was silently deleted — including skills installed withhermes skills install, which writes into the same tree. User-addedcron/jobs were deleted the same way.This is the destructive replacement reported in #25120, implemented along the lines of its "Option B — additive merge", with one refinement for distribution-owned skills (below).
How
A new
_replace_dir_merging_user_entries()replaces the wholesalermtree+copytreein_copy_dist_payload():SKILL.md) still replaces its counterpart wholesale, so files dropped or renamed by the new version don't linger inside distribution-owned skills;skills/devops/shared by the distribution andhermes skills install --category devops(the exact layout in Bug: hermes profile install/update destructively replaces local skills despite distribution_owned flag #25120's repro);One semantics change to be aware of: a skill the distribution removes in a new version now lingers in the profile instead of being deleted as a side effect of the mirror. If cleanup of dist-removed skills matters, the follow-up would be payload bookkeeping in the installed manifest (record shipped entries at install; delete previously-shipped-now-absent ones on update). Happy to iterate in that direction if you prefer.
Also removed: the
ignore=lambda previously passed tocopytree, which was dead code — it only filtered names when the visited directory equaled the staged root, but thatcopytreestarted one level below the root, so the predicate never fired. Top-level filtering already happens via thename in USER_OWNED_EXCLUDEguard at the top of the loop;TestNestedUserOwnedExcludeNotFilteredpasses unchanged.The module docstring's "Update semantics" section and the
profile updateparser description are updated to state the merge behavior, which makes the existing "(user data preserved)" help text on--forceandupdateaccurate.How to test
New tests in
tests/hermes_cli/test_profile_distribution.py::TestUserAdditionsSurviveReinstall— written first, red onmain, green with this change:test_force_install_preserves_user_added_skilltest_update_preserves_user_added_skill— dist's own skill still receives its new contenttest_update_preserves_user_skill_in_shared_category_dir— Bug: hermes profile install/update destructively replaces local skills despite distribution_owned flag #25120'sskills/<category>/<custom-skill>casetest_update_preserves_user_added_cron_jobtest_update_still_mirrors_dist_shipped_skill_dir— guard: stale files inside a dist-shipped skill must NOT linger (passes before and after)End-to-end repro (fails on stock
mainat e71d746, fixed with this branch):repro script
Before this change:
BUG: user-added skill was DELETED. After:OK: user-added skill preserved.Platforms
macOS (Darwin 25.2.0), Python 3.11.15. Full
scripts/run_tests.sh tests/hermes_cli/run: the only failures (11 tests across 4 gateway/service test files, plus 2 provider files with collection errors) are identical on a pristinemainworktree in this environment — pre-existing and unrelated to this change. The code path touched here is platform-independent (pathlib/shutilonly).🤖 Generated with Claude Code