Skip to content

fix(skills): prune uninstalled skill from disabled config - #51422

Closed
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/skills-prune-disabled-on-uninstall
Closed

briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/skills-prune-disabled-on-uninstall

Conversation

@briandevans

Copy link
Copy Markdown

What does this PR do?

hermes skills uninstall <name> removed the skill from the hub lock file but left the skill's entry in skills.disabled / skills.platform_disabled in config.yaml. That config disable-state is stored and reconciled separately from the hub lock file — uninstall_skill() only pops the lock entry (tools/skills_hub.py), and nothing pruned the config side.

The orphaned entry has two user-visible consequences:

  1. A later re-install of the same skill name comes back silently disabled — the agent never loads it and there is no hint why.
  2. The hermes skills enabled/disabled summary count is wrong, because it still counts the uninstalled name as a disabled skill.

This prunes the uninstalled name from the global disabled list and from every platform_disabled[*] entry in do_uninstall, right after a successful uninstall. The prune is best-effort: an unreadable/locked config must never abort an otherwise-successful uninstall.

The fix lives in the CLI layer (hermes_cli/skills_hub.py:do_uninstall), which already owns config and already depends on tools — do_uninstall is the only production caller of uninstall_skill. The hub lock-file layer (tools/) is left untouched and gains no new dependency on hermes_cli.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/skills_hub.py — in do_uninstall, after a successful uninstall_skill, prune the uninstalled name from skills.disabled and every skills.platform_disabled[*] list in config.yaml, then save_config only if something changed. Wrapped best-effort so a config failure cannot break the uninstall.
  • tests/hermes_cli/test_skills_skip_confirm.py — regression tests covering (a) prune of both global and per-platform disabled entries while leaving unrelated entries intact, and (b) no-op when the uninstalled skill was never disabled.

How to Test

  1. Disable a skill globally and on a platform: it lands in skills.disabled and skills.platform_disabled[<platform>] in ~/.hermes/config.yaml.
  2. Uninstall that skill: hermes skills uninstall <name>.
  3. Inspect config.yaml — the name is gone from both skills.disabled and platform_disabled; unrelated disabled skills are untouched.
  4. Re-install the same skill — it is enabled, not silently disabled.

Regression test (fails before the fix, passes after):

pytest tests/hermes_cli/test_skills_skip_confirm.py -q

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/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15 (Python 3.11)

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 (no new config keys)
  • 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) — or N/A (pure config-list manipulation, no path/OS-specific code)
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

`hermes skills uninstall <name>` removed the skill from the hub lock file
but left its entry in skills.disabled / skills.platform_disabled in
config.yaml. That config disable-state is stored separately from the lock
file, which uninstall_skill already reconciles on its own. The orphaned
entry meant a later re-install of the same name came back silently
disabled (the agent never loaded it, with no hint why) and the
enabled/disabled count in the skills summary was wrong.

Prune the uninstalled name from the global disabled list and every
platform_disabled[*] in do_uninstall after a successful uninstall. The
prune is best-effort and never aborts a successful uninstall.
Copilot AI review requested due to automatic review settings June 23, 2026 15:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a CLI bug where hermes skills uninstall <name> removed a hub-installed skill from the hub lock, but left its disable-state behind in config.yaml (skills.disabled / skills.platform_disabled), which could cause a later reinstall to be silently disabled and skew hermes skills counts.

Changes:

  • After a successful uninstall, prunes the uninstalled skill name from skills.disabled and all skills.platform_disabled[*] lists.
  • Adds regression tests verifying the prune behavior and the no-op behavior when the skill was never disabled.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
hermes_cli/skills_hub.py Adds best-effort pruning of disable-state from config after uninstall.
tests/hermes_cli/test_skills_skip_confirm.py Adds regression tests covering the new prune behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/skills_hub.py Outdated
Comment on lines +1098 to +1118
try:
from hermes_cli.config import load_config, save_config
cfg = load_config()
skills_cfg = cfg.get("skills")
if isinstance(skills_cfg, dict):
changed = False
global_disabled = skills_cfg.get("disabled")
if isinstance(global_disabled, list) and name in global_disabled:
skills_cfg["disabled"] = [s for s in global_disabled if s != name]
changed = True
platform_disabled = skills_cfg.get("platform_disabled")
if isinstance(platform_disabled, dict):
for platform, names in list(platform_disabled.items()):
if isinstance(names, list) and name in names:
platform_disabled[platform] = [s for s in names if s != name]
changed = True
if changed:
save_config(cfg)
except Exception:
# Best-effort: a config prune must never abort a successful uninstall.
pass
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard tool/skills Skills system (list, view, manage) P3 Low — cosmetic, nice to have labels Jun 23, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Prune uninstalled skill from disabled config.

Looks Good

  • Good fix for a real issue: reinstalling a skill that was previously disabled comes back silently disabled
  • Properly handles both global and per-platform disabled lists
  • Best-effort pattern (catches exceptions, never aborts uninstall)
  • Good test coverage: tests both the prune and the no-op cases
  • Clean implementation that integrates with existing config system

Reviewed by Hermes Agent

The best-effort disable-state prune in do_uninstall read the config via
load_config(), which deep-merges DEFAULT_CONFIG and expands env refs.
Passing that result to save_config() rewrote config.yaml with the fully
expanded/merged tree, churning large amounts of unrelated config from a
narrow skill-uninstall cleanup.

Read the raw on-disk config with read_raw_config() so only the user's own
keys are touched (the documented read -> mutate -> save_config pattern),
and surface a warning when the prune fails instead of silently swallowing
the exception, so the "silently disabled on reinstall" symptom can't be
reintroduced without any trace.
@briandevans

Copy link
Copy Markdown
Author

@copilot Both findings addressed in commit 35563c4:

  • Switched the prune from load_config() to read_raw_config() so only the user's on-disk keys are touched — no more rewriting config.yaml with the deep-merged DEFAULT_CONFIG tree / expanded env refs from this narrow best-effort cleanup. This is the documented read → mutate → save_config() pattern.
  • The except no longer silently passes: it now prints a Warning: on failure so the "silently disabled on reinstall" symptom can't be reintroduced without any trace.

Full tests/hermes_cli/test_skills_skip_confirm.py (17 tests, incl. both prune-config tests) passes.

@briandevans

Copy link
Copy Markdown
Author

Draining our contribution queue to keep it lean and reviewable \u2014 closing this as stale (no reviewer traction in 2+ weeks). If the underlying issue is still live on current main, we'll re-file a fresh, focused fix.

@briandevans briandevans closed this Jul 5, 2026
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 tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants