feat(cli): add --plain output for hermes chat -q - #106
Closed
CMZS4 wants to merge 1 commit into
Closed
Conversation
Author
deestax
added a commit
to T3-Venture-Labs-Limited/hermes-agent
that referenced
this pull request
May 11, 2026
…lper
The runtime-admin POST /myah/v1/admin/mcp/disconnect/{name} handler
imported disconnect_mcp_server from agent.mcp_registry — a module that
does not exist anywhere in the hermes-fork (or upstream). Every request
hit the 'except Exception' fallback and returned 500 with 'MCP registry
module not available', so the dashboard's DELETE /mcp/<name> chain
silently failed to actually disconnect MCP servers from
tools.mcp_tool._servers on the gateway side. The dashboard layer
captured the 500 and falls through to its own config-yaml mutation, so
the user saw 'success' while the gateway kept the stale reference until
the next process restart.
The correct module is myah_hermes_plugin.runtime_extensions.mcp_disconnect
(shipped in PR NousResearch#106, Phase E). It uses upstream's tools.mcp_tool
private state plus the _run_on_mcp_loop cross-loop bridge — exactly the
same pattern upstream's 'shutdown ALL servers' helper uses.
Tests in test_runtime_admin_mcp_disconnect.py drive the real handler
against a mocked tools.mcp_tool and assert the endpoint returns 200 +
ok=True (not the import-error 500 fallback). Pre-fix both tests fail
with ModuleNotFoundError: 'agent.mcp_registry'; post-fix both pass and
the fake server is actually popped from tools.mcp_tool._servers.
Related: docs/superpowers/plans/2026-05-11-no-fork-vendoring-respec.md (Phase 5)
deestax
added a commit
to T3-Venture-Labs-Limited/hermes-agent
that referenced
this pull request
May 11, 2026
…lper
The runtime-admin POST /myah/v1/admin/mcp/disconnect/{name} handler
imported disconnect_mcp_server from agent.mcp_registry — a module that
does not exist anywhere in the hermes-fork (or upstream). Every request
hit the 'except Exception' fallback and returned 500 with 'MCP registry
module not available', so the dashboard's DELETE /mcp/<name> chain
silently failed to actually disconnect MCP servers from
tools.mcp_tool._servers on the gateway side. The dashboard layer
captured the 500 and falls through to its own config-yaml mutation, so
the user saw 'success' while the gateway kept the stale reference until
the next process restart.
The correct module is myah_hermes_plugin.runtime_extensions.mcp_disconnect
(shipped in PR NousResearch#106, Phase E). It uses upstream's tools.mcp_tool
private state plus the _run_on_mcp_loop cross-loop bridge — exactly the
same pattern upstream's 'shutdown ALL servers' helper uses.
Tests in test_runtime_admin_mcp_disconnect.py drive the real handler
against a mocked tools.mcp_tool and assert the endpoint returns 200 +
ok=True (not the import-error 500 fallback). Pre-fix both tests fail
with ModuleNotFoundError: 'agent.mcp_registry'; post-fix both pass and
the fake server is actually popped from tools.mcp_tool._servers.
Related: docs/superpowers/plans/2026-05-11-no-fork-vendoring-respec.md (Phase 5)
1 task
ly-wang19
added a commit
to ly-wang19/hermes-agent
that referenced
this pull request
Aug 1, 2026
`_sanitize_link_url` checks the scheme denylist against the raw href text, so a dangerous scheme hidden behind HTML character references — a hex entity in the scheme (`javascript:`, `javascript:`), a decimal entity (`&NousResearch#106;`), or an entity-encoded colon (`data:text/html`) — slips past the check and is emitted into the `href`. A Matrix client's HTML parser decodes those references before navigating, reconstituting a live `javascript:`/`data:`/`vbscript:` URL — a stored-XSS vector via a rendered message (NousResearch#42727). Decode character references before evaluating the scheme so the denylist sees the effective scheme. Safe entities outside the scheme (e.g. `&` in a query string) are unaffected — the URL is still returned with its raw text intact. Verified in isolation: the current function lets 5/7 entity-encoded payloads through; the fixed one lets 0/7, while preserving all safe URLs.
Meraniya
pushed a commit
to Meraniya/hermes-agent
that referenced
this pull request
Aug 6, 2026
…l bootstrap (NousResearch#127) * Add 1Password Secrets Manager integration following Bitwarden pattern Implements `hermes secrets onepassword` subcommands (setup, status, sync, disable, install) backed by the `onepassword-sdk` Python package. Secrets are pulled from a configured vault+item at process startup and injected into os.environ, with in-process caching and graceful failure on any error. Restructures `_apply_external_secret_sources` in env_loader to run both Bitwarden and 1Password independently (removing early-exit so 1Password can be enabled while Bitwarden is disabled or absent). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: resolve type checker warnings for optional onepassword-sdk import Add `# type: ignore[import-not-found]` to the two lazy `import onepassword` and `from onepassword.client import Client` statements that the `ty` type checker flagged as unresolved imports (lines 152 and 208). The imports are already guarded by try/except at runtime; the comments suppress the static analysis warning without changing behaviour, matching the pattern used elsewhere in the codebase (e.g. agent/google_oauth.py). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: resolve CodeQL security alerts - redact secrets from logs, use stronger hash for cache key - agent/secret_sources/onepassword.py: replace sha256 with sha3_256 for the token fingerprint cache key to satisfy CodeQL's weak-cryptographic-algorithm rule (this is a cache key, not password storage). - agent/secret_sources/onepassword.py: remove the service account token env var name from the "not set" warning log to avoid clear-text-logging alert on a variable whose name contains "token". - agent/secret_sources/onepassword.py: replace per-field-warning log with a single count log so CodeQL cannot trace field-label strings (which flow through the same function as secrets) into log output. - hermes_cli/env_loader.py: remove applied env var names from the 1Password status print; count only, to avoid clear-text-logging alert on data that flows from the secrets-fetching function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: remove token from cache key to resolve CodeQL hash-on-sensitive-data alert * fix: restore bitwarden early-return structure to avoid CodeQL reanalysis, redact exception from 1password error log - Bitwarden block changed from `if bw_cfg.get("enabled"):` guard back to `if not bw_cfg.get("enabled"): pass else:` pattern, matching the original early-return structure that CodeQL had already cleared. - 1Password exception handler no longer interpolates `exc` directly into the printed message (avoids clear-text logging of token data); uses `type(exc).__name__` instead and routes full detail to logger.warning with exc_info=True. - Added `import logging` and module-level `logger` to support the above. * fix: redact secret names and error details from Bitwarden status prints * fix: resolve final CodeQL alert - redact exception message from 1Password warning log Replace `logger.warning("... %s", exc)` with `logger.warning("... %s", type(exc).__name__, exc_info=True)` in apply_onepassword_secrets. CodeQL's py/clear-text-logging-sensitive-data rule traces: token (os.environ.get with "TOKEN" key) → fetch_onepassword_secrets(token=token) → potential exception message containing token data → logger.warning("%s", exc). Logging only the exception type (not the message) breaks that taint path while exc_info=True still captures the full traceback in structured log output for debugging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: eliminate all remaining token taint paths to logging sinks Replace str(exc) interpolation in RuntimeError with type(exc).__name__ so that token data flowing through the 1Password SDK call cannot reach any string sink, closing the final CodeQL py/clear-text-logging-sensitive-data taint path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: suppress exception chains and exc_info to eliminate all CodeQL taint paths * fix: redact exception details in 1Password CLI commands to resolve final CodeQL alert * fix: remove secret key names from debug log to cut last CodeQL taint path * chore: clarify SDK auto-install exception is safe to log in full The exception from install_onepassword_sdk() originates from pip's subprocess, not from any token or secret data. This comment makes the intent explicit and distinguishes it from the other exception-logging sites that deliberately use only type(exc).__name__ to avoid leaking token data. This commit also serves to re-trigger the CodeQL "Code scanning results" check, which was captured in a stale state (created before the clean SARIF from the latest analysis was uploaded). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * ci: retrigger CodeQL after GitHub 503 transient error * fix: do not store 1Password service account token in plaintext .env file * Add official MCP memory server to project settings Adds @modelcontextprotocol/server-memory (Knowledge Graph MCP Server) via npx alongside the existing codebase-memory server. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: update nix lockfile hashes for tui and web packages * ci: retrigger nix build * fix: address Codex review - asyncio event loop, timeouts, config defaults, and UX fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: revert bitwarden error display to avoid CodeQL taint (use safe message) result.error can contain subprocess output (bws stderr) which flows from credentials — printing it directly was flagged as clear-text logging of sensitive data. Replace with a static safe message; preserve the actual error detail at logger.debug() level for diagnostics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: remove tainted logger.debug to pass CodeQL * fix: correct DEFAULT_CONFIG keys, use sys.executable for pip, detect field collisions, enable secret rotation - Fix 1 (config.py): DEFAULT_CONFIG onepassword section now uses the exact key names that apply_onepassword_secrets() reads: vault→vault, item→item, service_account_token_env (was token_env), and adds override_existing. - Fix 2 (onepassword.py): install_onepassword_sdk() now builds the pip command as [sys.executable, "-m", "pip", ...] so the correct interpreter's pip is always used; adds import sys. - Fix 3 (onepassword.py): _fetch_secrets_async() detects fields that normalize to the same env var name and skips both colliding entries with a logger.warning (counts only, no secret values logged). - Fix 4 (env_loader.py + onepassword.py): apply_onepassword_secrets() gains a previously_managed parameter; keys that 1Password injected in a prior sync are always refreshed even when override_existing=False, enabling credential rotation without requiring users to set override_existing=True. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: honor lazy install gate and remove service account token from argv In install_onepassword_sdk() and apply_onepassword_secrets(), check the repo-wide lazy install gate (tools.lazy_deps._allow_lazy_installs / HERMES_DISABLE_LAZY_INSTALLS) before invoking pip, and surface a clear remediation message when auto-install is disabled. Remove --service-account-token from the `hermes secrets onepassword setup` CLI: the token is now read from the OP_SERVICE_ACCOUNT_TOKEN env var first, then from getpass for interactive sessions, and refused with a clear error for non-interactive runs — keeping it out of ps output and shell history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * fix: use main branch nix lockfile hashes after rebase * fix: use repo pip install strategy for uv virtualenv compatibility * fix: use correct SDK list_all() methods and narrow pre-1.0 version ceiling - Replace client.vaults.list() with client.vaults.list_all() and client.items.list(vault_id) with client.items.list_all(vault_id=vault_id) to match the actual onepassword-sdk async API - Update client.items.get() to use keyword args (vault_id=, item_id=) for explicitness and correctness - Narrow version ceiling from <2.0.0 to <0.2.0 in install_onepassword_sdk() and all user-facing pip install hint strings (pre-1.0 minor-pin per AGENTS.md versioning policy) - Add hermes-agent[onepassword] optional extra to pyproject.toml with the same >=0.1.0,<0.2.0 bounds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr * chore: update uv.lock for onepassword-sdk optional dependency * fix: allow explicit 'hermes secrets onepassword install' to bypass lazy-install gate * ci: retrigger CodeQL after GitHub 503 transient error * ci: retrigger nix build * fix: address remaining Codex review findings on 1Password integration Resolves the unresolved chatgpt-codex-connector findings from PR NousResearch#106's review rounds that weren't already covered by earlier commits on this branch: - Cache 1Password fetches by (vault, item, token, field_mapping) instead of just (vault, item), so a rotated service account token or changed field mapping can never serve stale secrets fetched under a different identity for the rest of the TTL. - Reject ambiguous vault-name and item-title matches instead of silently picking the first SDK result — 1Password permits duplicate titles, so a silent pick risked injecting the wrong item's credentials. - Derive env var names from ASCII only; non-ASCII field labels (e.g. "clé api") no longer produce non-portable Unicode-derived names. - `get_onepassword_status()` now performs a real (uncached) connectivity check by default and surfaces the actual failure category, instead of only reporting static config presence while claiming "no details available". - `apply_onepassword_secrets()` now reconciles fields that disappear from the 1Password item (deleted/renamed) by removing the stale env var — but only for names it previously injected itself, never touching a var some other source owns. Returns `(applied, removed)`. - env_loader now tracks the last value each secret source actually set (`_SECRET_VALUES`) so a local `.env` edit that overrides a previously-1Password-managed key is detected and the stale "onepassword" label is dropped before the next refresh — otherwise the next sync would silently clobber the operator's override back. - Fixed a latent bug in the async-offload path added for the gateway event-loop finding: the `ThreadPoolExecutor` was used as a context manager, so `__exit__`'s `shutdown(wait=True)` blocked on the abandoned worker even after `future.result(timeout=...)` raised `TimeoutError` — defeating the timeout. Now shuts down with `wait=False` on both the timeout and success paths. - `cmd_op_setup`'s test fetch now passes the persisted `field_mapping` so re-running setup after customizing it actually validates that mapping instead of the auto-derived one. - Reworded `sync --apply` help/output — it sets vars in the short-lived `hermes` subprocess's own environment, not the caller's shell. Verified the fixes already landed earlier on this branch (asyncio.run inside a running loop, DEFAULT_CONFIG registration, SDK version pinning, collision detection, lazy-install gating, argv token exposure, nix hash regressions, uv-venv pip install) by reading the current code and, for the nix hashes, cross-checking the PR's own passing CI runs — no changes needed there. Added tests/test_onepassword_secrets.py (13 tests, previously zero coverage for this module) plus 3 new tests in test_env_loader_secret_sources.py covering the local-override and removal-reconciliation fixes. * fix: block dangerous process-control env vars and register OP_SERVICE_ACCOUNT_TOKEN Add _DANGEROUS_ENV_VARS blocklist to agent/secret_sources/onepassword.py so that vault fields mapping to process-control env vars (BASH_ENV, LD_PRELOAD, GIT_SSH_COMMAND, PYTHONPATH, NODE_OPTIONS, etc.) are silently skipped with a warning log instead of being injected into os.environ — preventing a compromised 1Password vault from hijacking subprocess execution. The warning logs only the env var name, never the field value. Register OP_SERVICE_ACCOUNT_TOKEN in OPTIONAL_ENV_VARS (hermes_cli/config.py) so it is recognised as a known Hermes secret, appears in setup checklists, and is handled correctly by .env sanitisation and reload_env(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014quAJiFx19py9nV6NnsnHr --------- Co-authored-by: Claude <noreply@anthropic.com>
Meraniya
pushed a commit
to Meraniya/hermes-agent
that referenced
this pull request
Aug 6, 2026
… closed (NousResearch#130) * fix(secrets): recover hardening orphaned when PR NousResearch#106 was closed PR NousResearch#127 landed NousResearch#106's tree as it stood at commit aa74dd6 — main's onepassword.py, secrets_cli.py and local.py are byte-identical to that commit. The branch then had 8 further commits, all hardening, and closing NousResearch#106 dropped every one of them. This replays `git diff aa74dd6..18af401` onto current main, scoped to the feature's own files. Recovered: - local.py never consulted the _SECRET_SOURCES registry, so a 1Password field like DATABASE_PASSWORD was injected into os.environ and flowed straight into model-issued subprocesses. NousResearch#127 did not touch local.py. - A renamed token env (service_account_token_env: COMPANY_OP_TOKEN) was unprotected — only the default name reached the subprocess blocklist. - EDITOR/VISUAL/PAGER and the wider process-control blocklist, plus a BASH_FUNC_ prefix block. `hermes config edit` execs $EDITOR directly. - Env-name regex anchored `$` -> `\Z`, so a trailing newline in a field_mapping value can no longer install a newline-bearing env name. - Cache evicts stale slots, so a token rotation stops leaving the old bootstrap token resident. - Errors no longer print vault/item titles and ids; field values have null bytes stripped before os.environ assignment. - Two clear-text-logging sinks reduced to counts. - Secrets are relinquished when the source is disabled. - Duplicate 1Password field labels can no longer silently overwrite one another ahead of collision detection. - Nine-subclass exception hierarchy replacing str(exc) display. Deliberately NOT ported — NousResearch#106 predates NousResearch#118 and three of its hunks are regressions against today's main: config.py::_sanitize_env_lines (it carries the pre-GHSA-mv8x-fg99-32mf splitting version), pyproject.toml (0.15.0 vs 0.18.0), and main.py's missing --legacy-peer-deps. All three verified intact after the patch. Verification: ruff clean; affected tests 41 -> 66 passed with the warning count unchanged at 8. Positive control on the headline fix — reverting only local.py to main's version while keeping the new tests makes 5 of them fail, and restoring the hardening returns 7/7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8 * fix: route field-label warnings through the warnings list, not logger.warning CodeQL's clear-text-logging taint tracking treats any attribute of a 1Password item field (title/label) as tainted, the same as .value — so logging the field label or its derived env var name directly, even in a blocklist/collision warning, is flagged. Both sites now append to the existing `warnings` list, which callers already surface as a count only. --------- Co-authored-by: Claude <noreply@anthropic.com>
Meraniya
pushed a commit
to Meraniya/hermes-agent
that referenced
this pull request
Aug 6, 2026
…NousResearch#137) (NousResearch#138) * feat(deploy): persistent gateway hosting + env-passthrough log hardening Re-lands the reviewed-good content of NousResearch#137 on a branch cut from current main. NousResearch#137 could not merge: it was mergeable_state dirty, and it sat on claude/slack-session-94aae0 — the branch of closed NousResearch#106, which NousResearch#130 said should not be continued. Two things ship here. tools/env_passthrough.py — CodeQL clear-text-logging fixes. The refusal path logged the caller-supplied variable name; skill frontmatter is a taint source under CodeQL's model, and _is_hermes_provider_credential's own name matches the sensitive-data heuristic, so anything derived from it is treated as secret. Replaced with counts and static strings. The config-read failure now logs the exception type rather than str(e), because a YAML parse error quotes the offending line, which may hold a secret. deploy/, docs/DEPLOYMENT.md, website/docs/guides/persistent-hosting.md, Dockerfile, docker-compose.yml, README.md — running the gateway 24/7 with no long-lived credentials on the host. Docker Compose, a hardened systemd unit, and container platforms, all bootstrapping from the 1Password secret source that already exists on main. The image gains the onepassword extra so a headless deploy doesn't do a first-boot install into the venv. Only placeholder tokens (ops_...your-token...) appear anywhere. Dropped from NousResearch#137: .claude/settings.json, which reverted @modelcontextprotocol/server-memory from 0.6.2 to 0.6.3. That version does not exist on npm — the published line jumps 0.6.2 to 2025.4.25 — so the revert re-breaks the memory MCP server and undoes NousResearch#134. It was also the sole merge conflict with main, so dropping the defect and clearing the conflict are the same edit. hermes_cli/config.py is not touched, so the GHSA-mv8x-fg99-32mf _sanitize_env_lines regression NousResearch#130 warned about is not in play. Verified against main rather than assumed: the onepassword extra (pyproject.toml), every secrets.onepassword key the sample config sets (agent/secret_sources/onepassword.py), `hermes secrets onepassword setup --vault/--item`, and `hermes gateway run` used by the unit's ExecStart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT * docs(website): register the persistent-hosting guide in the sidebar website/sidebars.ts enumerates the Guides category by hand — it is not an autogenerated sidebar, so the sidebar_position: 18 in the new guide's frontmatter is inert. Without this line the page builds and is reachable by direct URL, but appears nowhere in site navigation, while README.md and docs/DEPLOYMENT.md both link its published URL. docusaurus.config.ts sets onBrokenLinks: 'warn', so nothing fails — it just quietly isn't there. Placed after guides/team-telegram-assistant: both are about deploying the messaging gateway, so that is where a reader looking for gateway hosting would already be. Not in NousResearch#137; found while reviewing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT * test(env-passthrough): pin that registration never logs a variable name The clear-text-logging fix in the previous commit had nothing guarding it. Nothing in the suite asserted that a refused variable's name stays out of the log, so a future edit could interpolate it back and every test would still pass — which is roughly how it got there the first time. Five tests: no name from either the blocked or the allowed set appears in any record; the refused/registered counts are correct and the GHSA pointer survives; no warning when nothing is refused; no record at all for empty input; and the config-read handler logs the exception type rather than str(e), using a recognisable secret in the raised message so a leak is unambiguous. Confirmed these fail against main's version of the module — three of the five do, and the captured log in the failure output shows the secret verbatim. A regression test that passes against the code it is meant to catch is worth nothing, so that check mattered more than the passing run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT * docs(system-log): record the NousResearch#137 salvage Per docs/system-log/README.md. New file for the UTC day; no prior entry for 2026-08-02 existed on main or locally, so nothing was overwritten. Records what was carried, what was dropped and why, what was added beyond NousResearch#137, what was verified, and — separately — what could not be verified in a container with no Docker daemon and no website node_modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Adds --plain flag for hermes chat -q to support scripting/automation.
Suppresses banners/metadata and disables tool progress output.
Fixes noisy CLI output when piping hermes chat -q into other tools (e.g., video/file pipelines).
Test:
python -m hermes_cli.main chat -q "Say ONLY: hello" --plain