fix(skills): rmtree scope guard + default pre_update_backup to true - #48340
fix(skills): rmtree scope guard + default pre_update_backup to true#48340Kewe63 wants to merge 1 commit into
Conversation
…ousResearch#48200) Defense-in-depth fix for the silent wipe of ~/.hermes/ documented in NousResearch#48200. A `hermes update --yes` run silently destroyed a user's .env, MEMORY.md, kanban.db, custom skills, and scripts. Two changes: 1. `_rmtree_writable` in tools/skills_sync.py now refuses to rmtree anything outside SKILLS_DIR (the HERMES_HOME/skills/ root). All five call sites pass paths under SKILLS_DIR, so the guard is a no-op for current code and a loud, recoverable failure for any future regression (bad path join, malicious bundled manifest, stale path in scope after an exception). 2. The default `updates.pre_update_backup` flips from false to true in hermes_cli/config.py. A few minutes of zip per update is negligible compared to silent total data loss. Still overridable; --no-backup still works for one-off opt-out. Five new tests in TestRmtreeWritableScopeGuard (root path, hermes home, sibling dir, skills root itself, subdir) plus a flipped `test_default_enabled_creates_backup` in test_backup.py. 178/178 tests pass in the two affected files. Public method signatures unchanged, no test-stub blast radius. Closes NousResearch#48200
|
Related: #48200 (the data-loss incident this Closes), #48271 (open scope-guard PR for the same This PR is a superset of #48271: it implements the same Verified against main: |
|
Merged via #48423... |
|
Correction to my previous comment: merged via #48424 (not #48423). Your commit landed on current main with authorship preserved (f1254c8). I added one hardening commit on top — the guard now refuses SKILLS_DIR itself, not just out-of-tree paths, since no caller passes the root and a dest collapsing to it was the residual footgun. Thanks for the fast turnaround on a P0 data-loss fix. |
Summary
A
hermes update --yesrun on an Ubuntu VPS silently wiped the user's entire~/.hermes/directory:.env(Telegram bot token, OpenRouter key, Granola APIkey, GitHub PAT),
MEMORY.md,SOUL.md,kanban.db, custom skills, scripts,and the rest of the install. Recovery required restoring a Hetzner snapshot
from 12 hours earlier (#48200).
The fix is defense in depth — two changes that together prevent the silent-wipe
failure mode:
_rmtree_writablenow refuses to remove anything outsideSKILLS_DIR. A path-computation error (bad path join, maliciousbundled-manifest entry, stale path after an exception) becomes a loud
ValueErrorinstead of silent data loss.updates.pre_update_backupdefaults totrue. A fewminutes of zip time per update is negligible compared to silent total data
loss. The flag is still overridable;
hermes update --no-backupstill worksfor one-off opt-out.
Changes
tools/skills_sync.py—_rmtree_writable: resolves the target path andchecks it is the skills root itself or strictly under it; raises
ValueErrorreferencing hermes update --yes wiped entire ~/.hermes/ directory (.env, MEMORY.md, kanban.db, skills, scripts — all gone) #48200 otherwise. All five existing call sites pass paths under
SKILLS_DIR, so this is a no-op for current code.hermes_cli/config.py: changedupdates.pre_update_backupdefault fromfalsetotrue.hermes_cli/main.py—_run_pre_update_backup: tightened comment toreflect the new default and explain fallback semantics (older custom configs
that omit the field still get the safe default).
How to Test
The 5 new
TestRmtreeWritableScopeGuardtests patchSKILLS_DIRto a tempdirectory and verify: (a) the guard raises
ValueErrorfor/, the hermeshome itself, and a sibling of the skills root, and (b) allows the skills root
and any strict subdirectory.
test_default_enabled_creates_backup(renamed fromtest_default_disabled_is_silent) drives the new default.test_config_disabled_is_silentstill proves that an explicitpre_update_backup: falseopt-out is honored.Checklist
ruff check— PASS, 0 warningsRisk & Impact
Low. The scope guard is a fail-safe — the existing five call sites already
follow the contract, so current behavior is unchanged. Future violations become
a loud
ValueErrorinstead of silent data loss. The default-backup change isopt-out;
hermes update --no-backupstill works for one-off cases.Type: 🐛 Bug fix (data-loss prevention)
Closes: #48200