- install: catalog names resolve to the pinned SHA (ref= checkout),
print tier + capability summary before the enable prompt, and write a
.hermes-catalog.json provenance sidecar; raw git URLs get a
'custom (unreviewed) source' banner; --allow-removed loudly bypasses
the removed blocklist (skip_removed_check=True)
- update: sidecar installs compare against the current catalog pin and
force-reinstall at the new SHA (enabled state preserved); plain git
installs keep the git-pull flow
- list: catalog:<tier>@<shaShort> annotation + red 'REMOVED from
catalog' lines for blocklisted installs (table and --json)
- search/browse/info: live-index catalog tables and full entry detail
with removed-list warnings
- validate: human ✓/✗ output, --json for CI, exit 0/1
- doctor: per-plugin manifest/enabled/load-error/env/requires_hermes/
provenance/pin/removed diagnosis, compact table or single-name detail
Builds on memosr's sink-level opt-in gate (#29249). Enabling a
non-bundled plugin now surfaces the privileged allow_tool_override
decision at `hermes plugins enable` time instead of leaving the
operator to discover the config key after a runtime rejection.
- `hermes plugins enable <name>` prompts for non-bundled plugins:
'Allow this plugin to replace built-in tools?' Default is deny
(blank Enter / non-interactive stdin / EOF all fail closed).
- --allow-tool-override / --no-allow-tool-override flags for
non-interactive and scripted use (and a future desktop checkbox).
- Bundled plugins are trusted: never prompted, no entry written.
- Writes plugins.entries.<key>.allow_tool_override, the same key the
sink gate reads (manifest.key == discovery key), so consent and
enforcement compose end to end.
Subcommands whose handler was a closure defined inside main() — memory, acp,
tools, insights, skills, pairing, plugins, mcp, claw — have their handler
promoted to a top-level function and their parser block extracted into
hermes_cli/subcommands/<name>.py (build_<name>_parser, injected handler).
These 9 had zero closure-over-main-locals, so promotion is a pure relocation.
acp/mcp parser blocks use the shared add_accept_hooks_flag helper.
main() 1798 -> 954 LOC (71% below the 3297 Phase-2 starting point);
add_parser calls in main.py 89 -> 28.
Deferred: sessions, computer-use, secrets handlers reference <name>_parser
(for a no-subcommand print_help fallback) — left in place to avoid the
_self_parser indirection; minority, low value.
Behavior-neutral: all 9 subcommands' --help (incl nested subactions) byte-
identical to pre-extraction (diff-verified). tests/hermes_cli/ 6519 passed /
0 failed; new test_subcommands_followup.py covers the 9 builders.