Upstream sets process.title="openclaw-gateway" in the gateway run loop, so
the real daemon has comm "openclaw-gatewa" (15-char truncation) and no
`node … openclaw` argv for the script probe to match. Add the exact comm
probe; substring matching stays out.
The runtime already registers nothing for an explicit empty include list
(fb1ec36a4b), but every CLI reader still coerced `[]` to "no filter":
`hermes mcp list` printed "all", `hermes mcp configure` and `hermes tools`
pre-checked every tool (so confirming the picker silently re-enabled all of
them), and a catalog reinstall pre-checked the manifest defaults over the
user's zero-tool choice.
`_tool_filters` now returns the list whenever the key holds a list; only an
absent/non-list key is None. The pickers and list output branch on `is not
None`, matching `tools/mcp_tool_registration.py`.
Fixes#12865. Builds on #13096 (@dingn42) and #52874 (@Bartok9).
`hermes plugins install owner/private-repo` failed for every private repository
even when the same user could `git clone` it from their shell: the hardened
`noninteractive_git_env` disables credential helpers, askpass and global config
(so a hostile repo can't make our plumbing prompt or hang), which also blocks
the user's own stored credential. The result was "could not read Username" or a
60s hang on a GUI askpass, with no hint about how to authenticate.
New `hermes_cli/git_credentials.py` resolves a credential up front from sources
the user already owns — GITHUB_TOKEN/GH_TOKEN (profile-scoped), `gh auth token`,
then `git credential fill` against their configured helpers with prompting
disabled (any host) — and hands it to git as a one-shot
`http.<origin>/.extraheader` via the GIT_CONFIG_* env block. Nothing lands in
the URL, `.git/config` or install metadata. The same path covers
`plugins update`, catalog MCP git installs and profile-distribution staging,
which share the same hardened env and the same failure.
A private-repo clone with no credential now fails fast with an actionable hint.
The probe-fail path ignored prior state entirely: with no manifest
default_enabled it wrote include=None, which pops the whole tools
block. For exclude-mode manifests default_enabled is necessarily unset,
and for the 30+ OAuth entries the entry rewrite precedes first auth —
so the common reinstall-while-unreachable case removed the curated
excludes and enabled every tool on next connect. The fallback order is
now: prior include > prior exclude > manifest default > no filter.
The "Probing '<name>' for available tools..." line printed before the
exclude-mode short-circuit, which deliberately never probes (the test
suite asserts _probe_tools must not run there). Move the announcement
next to the actual probe call so install output matches what happened.
Review blockers (independent reviewer on #94513):
1. Reinstalling an exclude-mode catalog entry wiped the user's edited
tools.exclude, replacing it with manifest defaults. install_entry now
reads the prior exclude (like it already did for include) and re-writes
it verbatim on reinstall. Regression test added + sabotage-verified
(fails on old behavior); include-priority test added too.
2. aws-knowledge: exclude aws___retrieve_skill — vendor SKILL.md loader is
a vendor skill layer (live tools/list confirmed the tool exists).
3. betterstack: exclude list rewritten to cover the snake_case wire names
(vendor's own header examples show remove_dashboard) via globs alongside
the doc display-labels; caveat documented in the manifest — server is
OAuth-gated so pre-auth enumeration is impossible.
4. railway: exclude railway-agent (opaque server-side agent delegation,
acts outside Hermes's per-tool approval loop).
5. twelve-data: exclude oauth plumbing pseudo-tools + quota probe.
6. betterstack post_install no longer claims a fully-checked checklist —
exclude-mode bypasses the checklist; text now describes the applied
exclude list.
Live E2E: fresh temp HERMES_HOME — install applies manifest excludes,
user edit survives reinstall. 33/33 catalog tests green.
The cloudflare entry's 3,320-endpoint surface is ~43% product families a
personal/dev account never touches (Zero Trust org-fleet suite, Magic
Transit/WAN, Cloudforce One, Radar analytics, API Shield, legacy
migration surfaces). Ship a 34-pattern curated exclude list in the
manifest: 3,320 -> 1,905 tools kept, and everything Cloudflare adds
later stays enabled by default.
Mechanism, two small extensions:
- tools/mcp_tool.py: tools.include/exclude entries containing glob
metacharacters now match via fnmatch (plain names stay exact-match),
so a product family is one pattern instead of hundreds of stale
literals.
- hermes_cli/mcp_catalog.py: manifests may declare
tools.default_excluded (mutually exclusive with default_enabled);
install writes it to tools.exclude and skips the probe/checklist —
a 3,320-row curses checklist is not a UX. Prior user include
selections still win on reinstall.
Verified by replaying the real filter functions over the live-probed
3,320-tool list: 1,415 excluded, zero overmatch against a per-product
target audit; DNS/Workers/R2/D1/tunnels/Access/AI kept.
The desktop app carried its own hardcoded list of 17 vendor MCP endpoints
(apps/desktop/src/lib/mcp-directory.ts) powering the composer suggestion
pills — a second PR-reviewed vendor list, overlapping and drifting from the
Nous-approved MCP catalog (optional-mcps/).
This makes the catalog the single source of truth:
- manifest schema: optional `suggest:` block (keywords + hosts), parsed,
validated, and normalized in mcp_catalog.py
- 15 new URL-only hosted-remote catalog entries (atlassian, sentry, datadog,
notion, stripe, vercel, supabase, netlify, hugging_face, asana, intercom,
airtable, webflow, paypal, square); figma + linear manifests gain suggest
blocks
- GET /api/mcp/catalog now serves the suggest metadata
- desktop suggestion provider builds its match index from the catalog;
the static directory remains only as a compatibility rung for older
backends without suggest metadata
- setup card source line prefers the catalog entry's transport URL
GitHub stays out of the catalog on purpose: its hosted MCP rejects generic
DCR and the bundled github/* skills (gh CLI) are the stronger integration.
New desktop `github` suggestion provider offers the github-auth skill
instead — gated on a new cached GET /api/git/gh-auth probe so already-
authenticated users never see the pill.
Simplify-pass follow-up on the #70782 salvage: _bearer_auth_headers
hard-emits ${MCP_<NAME>_API_KEY} but install_entry only persists
auth.env-declared vars — a manifest naming its key differently (the
shipped n8n style) would install cleanly yet send a literal-placeholder
header at connect time (silent 401, the #37792 bug class). Enforce the
naming contract at parse time. Also pins the secret-stays-in-.env
property in the install test (raw config.yaml carries the template,
never the secret). Mutation-checked: validation disabled -> guard test
fails.
When an optional-mcps manifest declares transport.type=http with
auth.type=api_key, install_entry() prompts for the key and saves it to
.env, but _build_server_config() only handled the oauth case — the
api_key case produced a bare url entry with no headers, so every
request to the server was unauthenticated (-> 401).
Reuse _bearer_auth_headers(entry.name) from mcp_config.py so the
catalog path emits the same 'Authorization: Bearer ${MCP_..._API_KEY}'
template as the manual 'hermes mcp add --url' path.
Salvaged from #70782 (production hunk applied clean; tests re-anchored
onto current main). Credit: JonthanaHanh.
Port from openai/codex#34540 / #34612 ("detach non-interactive
subprocesses from stdin"): internal git invocations that run with nobody
attached — MCP catalog installs, plugin install/update, profile
distribution staging, worktree base fetches, and the desktop review
pane's git/gh backend — could hang on a credential prompt when a remote
is private, misconfigured, or requires auth. git prompts on the
inherited terminal (or via Git Credential Manager on Windows), so the
operation silently waits until its timeout, or forever at sites without
one (mcp_catalog clones have no timeout at all and inherit the parent
terminal).
- Add noninteractive_git_env() to hermes_cli/_subprocess_compat.py:
GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=Never on a copy of the
environment; GIT_ASKPASS/SSH_ASKPASS deliberately preserved so
working non-interactive auth still succeeds.
- Wire it + stdin=DEVNULL into: mcp_catalog._do_git_install (clone/
checkout), plugins_cmd (clone + pull), profile_distribution._git_clone,
web_git._git/_gh (gh also gets GH_PROMPT_DISABLED=1), and cli.py's
worktree base fetch helper.
- Tests: env contract, a real-git E2E against a local 401 Basic-auth
HTTP server proving fail-fast ("terminal prompts disabled") instead
of a hang, and per-call-site plumbing assertions. Sabotage-verified:
removing the env from web_git._git fails the site test.
Catalog entries now follow the same supply-chain rules as pyproject
dependencies:
- n8n: install.ref main -> full commit SHA 7a9ae007 (2026-05-23,
branches/tags can be moved by the upstream owner; SHAs cannot)
- new contract test: every shipped manifest must pin exactly —
git installs need a 40-char SHA, uvx/npx-style launchers need
pkg==X / pkg@X with a digit-leading version (rejects bare names,
ranges, and npm dist-tags like @latest)
- module docstring documents the pin policy (exact version, 2-week
cooldown)
unreal-engine and linear are http transports (server runs elsewhere)
so there is nothing to pin at the transport layer.
Verified: unpinning blender-mcp in the manifest makes the contract
test fail with a named diagnostic; restoring the pin passes.
Adds optional-mcps/blender (ahujasid/blender-mcp, stdio via uvx). The
server advertises 22 tools; 18 front optional asset services with no
upstream trim mechanism, so tools.default_enabled pins the install to
the core surface (scene/object info, viewport screenshot, code exec)
and the rest stay opt-in through 'hermes mcp configure blender'.
Manifests can now declare transport.env (static, non-secret subprocess
env vars), parsed/validated in _parse_manifest and written by
_build_server_config — used here to ship DISABLE_TELEMETRY=true per
the no-telemetry-without-opt-in policy. Runtime already honored
per-server env; manifests just couldn't declare it.
* fix(windows): stop subprocess console-window popups + add CI guard
The single biggest source of Windows 'terminal popup' bug reports was bare
subprocess.run/Popen calls spawning a console window. The compat helpers
(windows_hide_flags / windows_detach_popen_kwargs) already existed but the
footgun checker had no rule to stop new bare calls from reintroducing the flash.
- scripts/check-windows-footguns.py: new AST-based rule flagging subprocess
calls that can create a new console — output-redirection-aware (capture/
redirect/check_output exempt) and POSIX-only-program-aware (launchctl/
systemctl/brew/etc. exempt). Comprehensive on real popups, no annotation
burden on calls that can't flash.
- Swept all genuine window-spawning sites through windows_hide_flags()/
windows_detach_popen_kwargs(); marked intentionally-visible launches
(editor/terminal/foreground re-exec) with '# windows-footgun: ok'.
- tests/scripts/test_windows_footgun_subprocess_rule.py: behavior-contract
tests + full-repo cleanliness invariant.
- CONTRIBUTING.md: documents the rule + the helper pattern.
* test: accept creationflags kwarg in psutil_android fake_subprocess_run
The Windows no-window sweep added creationflags=windows_hide_flags() to
install_psutil_android.py's subprocess.run call; the test's fake stub had a
fixed (cmd) signature and raised TypeError on the new kwarg.
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.
- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
unused in their defining module are kept with explicit # noqa:
F401 (gateway/run.py load_dotenv; run_agent re-exports from
agent.message_sanitization, agent.context_compressor,
agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
selected); this is a one-time cleanup, not a config change
Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
module still resolves
* feat(mcp): Nous-approved MCP catalog with interactive picker
Adds an optional-mcps/ directory mirroring optional-skills/: curated,
Nous-approved MCP servers shipped with the repo but disabled by default.
Presence in optional-mcps/ = approval. No community tier, no trust signals.
Entries are added by merging a PR.
New surface:
hermes mcp Interactive catalog picker (default)
hermes mcp catalog Plain-text list, scriptable
hermes mcp install <name> Install a catalog entry
Picker behavior:
not installed -> install (clone/bootstrap if needed, prompt for creds)
installed/off -> enable
installed/on -> menu (disable / uninstall / reinstall)
Manifest schema (manifest_version: 1) supports:
- transport: stdio (command/args, ${INSTALL_DIR} substitution) or http (url)
- install: optional git clone + bootstrap commands (for repos that need
local venv setup, like the n8n bridge); omit for npx/uvx servers
- auth: api_key (prompts -> ~/.hermes/.env), oauth (provider-mediated
or native MCP), or none
Catalog entries are never auto-updated. Users re-run `hermes mcp install`
to refresh. Credentials always go to ~/.hermes/.env (the .env-is-for-secrets
rule), never to per-server env blocks.
Ships n8n as the reference manifest (https://github.com/CyberSamuraiX/hermes-n8n-mcp).
Tests: 19 catalog tests + E2E install/uninstall round-trip via the shipped
manifest.
* feat(mcp): tool-selection checklist + Linear catalog entry
Adds install-time tool selection so users only enable the MCP tools they
actually want, and ships Linear as a second reference catalog entry to
demonstrate the http+oauth path alongside n8n's stdio+api_key+git-bootstrap.
Tool selection flow:
install (clone/auth/credentials) ->
probe server for available tools ->
curses checklist with pre-checked rows ->
write mcp_servers.<name>.tools.include
Pre-check priority:
1. user's prior tools.include (reinstall preserves selection)
2. manifest's tools.default_enabled (curated subset)
3. all probed tools (default)
Probe-failure fallback (server unreachable, OAuth not yet complete,
backing service offline):
- manifest declared default_enabled -> applied directly
- no default declared -> no filter written (all-on when reachable)
- both cases point user at hermes mcp configure <name>
Manifest schema additions:
tools:
default_enabled: [list, of, tool, names] # optional
Updates:
- optional-mcps/linear/manifest.yaml -- new reference entry (http+oauth)
- optional-mcps/n8n/manifest.yaml -- tools.default_enabled set to the
8 read-mostly tools; mutating tools (activate/deactivate, container_logs)
pruned by default
- docs: new 'Tool selection at install time' section in features/mcp.md
Tests: 7 new tests in TestToolSelection covering probe-success / probe-fail
matrix, manifest-default filtering, reinstall-preserves-selection, and
invalid-default-enabled rejection. 26 catalog tests + 32 existing
mcp_config tests passing.
* feat(mcp): polish — picker unification, include-mode convergence, hardening
Addresses review findings on PR #30870. Lands all improvements that
belong in this PR before merge; defers separate cleanup (consolidating
two probe implementations, change-detector tests) to follow-ups.
Picker UX (mcp_picker.py)
- Unifies catalog + custom (user-added) MCPs in one view with distinct
status badges (available / enabled / installed (disabled) /
custom — enabled / custom — disabled)
- Adds 'Configure tools (probe server + re-pick)' action to both the
catalog-installed and custom-row submenus — the existing
hermes mcp configure flow was previously unreachable from the picker
- Loops until ESC/q so the user can manage several entries in one
session instead of having to re-launch
- Uninstall message now mentions .env credentials are preserved with a
pointer to clean them up manually if no longer needed
- Surfaces a 'requires a newer Hermes' warning per future-manifest
entry instead of silently hiding it
Catalog (mcp_catalog.py)
- catalog_diagnostics() exposes which manifests were skipped and why
(future_manifest vs invalid) so UIs can give actionable feedback
- _do_git_install detects SHA-shaped refs (regex /[0-9a-f]{7,40}/)
and skips the doomed 'git clone --branch <sha>' attempt — clone --branch
only accepts branches/tags, so SHAs always failed noisily before
falling back to the full-clone path
- Probe-success all-tools-enabled message now mentions that new tools
the server adds later will be auto-enabled (no-filter mode)
Convergence (tools_config.py)
- _configure_mcp_tools_interactive now writes tools.include (whitelist)
instead of tools.exclude (blacklist), matching the catalog flow and
hermes mcp configure. The on-disk config shape no longer depends on
which UI the user touched last
- Two existing tests updated to assert the new include-mode contract
Discoverability
- Setup wizard final step now prints 'Browse curated MCPs: hermes mcp'
- Three tip-corpus entries pointing at the new catalog
- Docs updated with: trust model (manifests run code locally, gated by
PR review, but read before installing), runtime ${ENV_VAR} substitution
semantics, and the manifest_version forward-compat behavior
Tests
- 7 new tests covering future-manifest diagnostics, custom MCP picker
rows, SHA-ref git-install path, branch-ref git-install path, and the
tools_config include-mode write contract
- 80 MCP-related tests passing across test_mcp_catalog.py,
test_mcp_config.py, test_mcp_tools_config.py
* fix(mcp): drop setup-wizard catalog hint to satisfy supply-chain scanner
The wizard line 'Browse curated MCPs: hermes mcp' triggered the
CI supply-chain scanner because it pattern-matches on edits to any
file named hermes_cli/setup.py — that filename matches the Python
'install-hook file' heuristic even though this setup.py is the
user-facing 'hermes setup' wizard, not a packaging install hook.
The catalog is already surfaced via three tip-corpus entries in
hermes_cli/tips.py (which the scanner doesn't flag), so dropping the
wizard mention loses no discoverability. Worth revisiting after a
scanner allowlist for this specific file lands.