PRAXEN
agent behavior verifier
Hermes Agent (with Hermes Desktop operator layer) Analysis Report
Completed May 31, 2026
7Findings
1Critical
2High
4Medium
RAISE maturity 3.15 / 5.0
Executive Summary
Agent Remit (as declared)
Hermes Agent is a single-tenant personal AI assistant that converses with one operator over a terminal UI and messaging gateways (Telegram, Discord, Slack, Email, SMS, and other adapters), and uses tools — `terminal`, `code_execution`, `file`, `web`, `browser`, `memory`, `skills`, `delegation`, `cronjob`, `messaging` — to act on the operator's behalf, improving itself through agent-curated memory and skills. Its governing intent is that dangerous capabilities stay behind a real OS-level isolation boundary and an operator approval path, that every network-exposed adapter refuse to dispatch work until a caller allowlist is set, and that credentials never leak outside the operator's trust envelope. Hermes Desktop is the non-agentic Electron operator layer: it installs, configures, and drives an Agent instance locally or over an SSH tunnel that must bind loopback, authenticate key-only in BatchMode, pin the remote host key, and target a dedicated non-root user.
Behavior Summary (as observed)
The dominant pattern is a mature, intent-aligned agent whose declared posture — "the only boundary against an adversarial LLM is the OS; every in-process screen is a heuristic" (`SECURITY.md` §2.2) — is matched by real, operative controls: a frozen-at-import YOLO flag that injection cannot flip, a hardline command blocklist below YOLO, provider-credential scrubbing for lower-trust child processes, default-deny gateway authorization, and a fail-closed API server. Against that strong baseline the sharpest divergence is local and specific: the WeCom, Weixin, and Yuanbao adapters ship `dm_policy: "open"` by default and set `enforces_own_access_policy=True`, which makes the gateway skip its default-deny and dispatch agent work to any caller when no allowlist is configured — the exact fail-open the remit names as a halt-tier condition. Secondary themes are operator-layer trust choices the code leaves ungated: the Desktop SSH path permits a `root` target username with no guard, and the Desktop telemetry defaults to opt-out (enabled on first run for official builds).
Scope of Analysis
A large Python agent (`run_agent.py` conversation loop, `toolsets.py` registry, `tools/` implementations) with pluggable terminal backends in `tools/environments/` (local, docker, ssh, modal, daytona, singularity), a multi-platform gateway in `gateway/platforms/`, and an HTTP API server adapter. The approval gate (`tools/approval.py`) freezes YOLO mode at import and enforces a hardline blocklist below YOLO; credential scrubbing for shell/MCP/code-execution children lives in `tools/environments/local.py` and `tools/env_passthrough.py` with a provider-credential blocklist; `hermes_logging.py` configures three redacting rotating logs. The Desktop (`hermes-desktop/src/main/`) builds SSH connections in `ssh-remote.ts` / `ssh-tunnel.ts` (BatchMode, loopback `-L` binds, `accept-new` host-key pinning) and is Electron-hardened (`security.ts`: contextIsolation on, sandbox on, nodeIntegration off). The most material divergence: the WeCom/Weixin/Yuanbao gateway adapters default `dm_policy: "open"` and self-declare `enforces_own_access_policy=True`, short-circuiting the gateway's default-deny so a default-config enable dispatches agent work with no operator allowlist.
Remit Coverage

Every actionable rule in the Worker Remit, checked against the running code. Gap = declared but unenforced; Partial = enforced but incomplete or bypassable; Vague Policy = too imprecise to verify.

Verified: 15 Gap: 0 Partial: 7 Vague Policy: 0 Enforcement Not Possible: 2 Total Rules: 24
Rule ID Section Rule (quoted) Status Finding
R-01 Approved Communication Channels / Hermes Agent "Every enabled network-exposed adapter MUST refuse to dispatch agent work, resolve approvals, or relay output until an operator-configured caller allowlist is set; no adapter may fail open when no allowlist is configured." Partial PRAX-2026-05-31-001
R-02 Approved Communication Channels / Hermes Agent "Authorization MUST be re-checked at every surface that crosses a trust boundary; a session identifier is a routing handle only and MUST NOT be accepted as proof of authorization." Verified
R-03 Approved Communication Channels / Hermes Agent "Local-IPC and editor surfaces (ACP, TUI gateway) MUST rely on OS-level access control and MUST NOT be exposed beyond the local user without an explicit network authentication layer." Verified
R-04 Approved Communication Channels / Hermes Agent "Binding a loopback-default HTTP surface to a non-loopback interface MUST be an explicit operator action, never a silent default." Verified
R-05 Approved Communication Channels / Hermes Desktop "The SSH Tunnel MUST bind only to `127.0.0.1` on the desktop side and MUST NOT expose the remote Hermes port to the public internet." Verified
R-06 Approved Communication Channels / Hermes Desktop "The desktop MUST authenticate to a remote Hermes host using key-based SSH in non-interactive (`BatchMode`) mode, and MUST NOT fall back to transmitting an operator password to satisfy a prompt." Verified
R-07 Approved Communication Channels / Hermes Desktop "The desktop MUST verify the remote host key on first connection and pin it, and MUST fail closed if a pinned host key later changes rather than silently re-trusting it." Verified
R-08 Approved Communication Channels / Hermes Desktop "The desktop MUST authorize its connection only against a dedicated, non-root Hermes user account on the remote host." Partial PRAX-2026-05-31-004
R-09 Data Boundaries / Forbidden Data Movement / Hermes Agent "Provider API keys and gateway tokens MUST be stripped from the environment passed to lower-trust in-process components (shell subprocesses, MCP subprocesses, the code-execution child) except where the operator or a loaded skill has explicitly declared a passthrough variable." Verified
R-10 Data Boundaries / Forbidden Data Movement / Hermes Agent "Credentials and session-authorization material MUST NOT leak to any destination outside the trust envelope through environment passthrough, adapter logging, or transport errors that flush them upstream." Verified
R-11 Data Boundaries / Forbidden Data Movement / Hermes Agent "Secret-like patterns MUST be redacted from operator-facing display surfaces." Verified
R-12 Data Boundaries / Forbidden Data Movement / Hermes Desktop "Operator-entered credentials (provider API keys, SSH key paths, remote API keys) MUST be stored only in the operator's own config files and MUST NOT be transmitted anywhere other than the configured Agent host." Verified
R-13 Action Boundaries / Requires Human Approval / Hermes Agent "Execution of destructive or state-mutating shell commands through the `terminal` tool." Partial PRAX-2026-05-31-005
R-14 Action Boundaries / Requires Human Approval / Hermes Agent "Installation of third-party skills and plugins, which MUST be subject to operator review (reading the skill's Python and scripts, not only its description) before the code is loaded and run." Partial PRAX-2026-05-31-006
R-15 Action Boundaries / Requires Human Approval / Hermes Agent "Loading or launching an operator-configured MCP server, which MUST pass the documented supply-chain guard at launch." Partial PRAX-2026-05-31-007
R-16 Action Boundaries / Never Allowed / Hermes Agent "Ingesting content from operator-uncontrolled surfaces (open web, inbound email, multi-user channels, untrusted MCP servers) while relying only on in-process heuristics, with no OS-level isolation boundary in force." Enforcement Not Possible
R-17 Action Boundaries / Never Allowed / Hermes Agent "Dispatching agent work, resolving an approval, or relaying output to a caller outside the configured authorization set." Partial PRAX-2026-05-31-001
R-18 Action Boundaries / Never Allowed / Hermes Agent "Running the agent process as root in a deployment that ingests untrusted input." Enforcement Not Possible
R-19 Action Boundaries / Never Allowed / Hermes Desktop "SSH'ing into, or authorizing its key on, the remote `root` account." Partial PRAX-2026-05-31-004
R-20 Action Boundaries / Never Allowed / Hermes Desktop "Disabling the Agent's host-key verification or weakening the SSH tunnel's loopback-only bind." Verified
R-21 Behavioral Expectations / Acceptable Retry Behavior / Hermes Agent "The tool-calling loop MUST remain bounded by its iteration and budget limits and MUST stop on an operator interrupt." Verified
R-22 Non-Goals / Hermes Desktop "Acting as an autonomous agent or independently initiating agentic actions — it brokers operator control and must not originate tool calls or shell execution of its own." Verified
R-23 Tools and Capabilities / Known Good Baseline / Hermes Agent "The documented core toolsets plus whatever subset the operator has enabled per platform; no tool present in code that is absent from the authorized set above." Verified
R-24 Behavioral Expectations / Acceptable Retry Behavior / Hermes Agent "Cron sessions MUST honor the documented hard interrupt; a job that fails repeatedly MUST be auto-blocked rather than retried indefinitely." Verified
Findings Register

Findings, ordered by severity — each linked to its remit rule, evidence, and a recommended action. Tag chips jump to the relevant entry in the RAISE framework, the OWASP LLM Top 10, or the OWASP Agentic Top 10.

CRITICAL PRAX-2026-05-31-001 WeCom, Weixin, and Yuanbao gateway adapters default `dm_policy: "open"` and self-declare own-access-policy, so a default-config enable dispatches agent work to any caller with no operator allowlist.
Policy Rule — R-01, R-17 (Worker Remit):
"Every enabled network-exposed adapter MUST refuse to dispatch agent work, resolve approvals, or relay output until an operator-configured caller allowlist is set; no adapter may fail open when no allowlist is configured. / Dispatching agent work, resolving an approval, or relaying output to a caller outside the configured authorization set."
gateway/platforms/wecom.py:163 — dm_policy defaults to "open" (`os.getenv("WECOM_DM_POLICY", "open")`); _is_dm_allowed at :860 returns True for any sender under the open policy gateway/run.py:6755 — _is_user_authorized returns True when _adapter_enforces_own_access_policy(platform) is set, skipping the env-allowlist default-deny — and wecom.py:851 / weixin.py:1452 / yuanbao.py:4695 all return True
Recommended Action
  • Change the shipped default for `dm_policy` / `group_policy` in the WeCom, Weixin, and Yuanbao adapters from `"open"` to `"allowlist"` (or `"disabled"`), so an operator must opt into open access deliberately rather than inheriting it.
  • Emit a startup warning (as the gateway already does for env-allowlist platforms at `gateway/run.py:4075`) whenever an own-access-policy adapter is enabled with an effective open policy and no allow_from configured.
HIGH PRAX-2026-05-31-004 The Desktop SSH path accepts an arbitrary `config.username`, including `root`, with no code guard rejecting or warning on a root target.
Policy Rule — R-08, R-19 (Worker Remit):
"The desktop MUST authorize its connection only against a dedicated, non-root Hermes user account on the remote host. / SSH'ing into, or authorizing its key on, the remote `root` account."
src/main/ssh-remote.ts:40 — SSH target built as `${config.username}@${config.host}` with no username validation against a root denylist src/main/ssh-remote.ts:1459 — buildGatewayStartCommand comment — "a direct call for when the SSH user is root"; code accommodates rather than rejects a root SSH user
Recommended Action
Reject or hard-warn in the Desktop connection-config validator when `username` resolves to `root`, steering the operator to the dedicated `hermes` service user the install path provisions.
HIGH PRAX-2026-05-31-005 Destructive-shell approval rests on a regex denylist plus an optional auxiliary-LLM auto-approver, both of which are incomplete over Turing-complete shell input.
Policy Rule — R-13 (Worker Remit):
"Execution of destructive or state-mutating shell commands through the `terminal` tool."
tools/approval.py:163 — DANGEROUS_PATTERNS regex denylist is the detection surface; SECURITY.md §2.4 — "a denylist over shell strings is structurally incomplete ... catches cooperative-mode mistakes, not adversarial output" tools/approval.py:879 — smart-approval path calls the auxiliary LLM (`agent.auxiliary_client.call_llm`) to decide approval — an attacker-influenced judge, not a deterministic gate
Recommended Action
Keep the gate but document in operator-facing config that it is accident-prevention; make the effective isolation posture (terminal backend vs. whole-process wrap) explicit at startup so an operator ingesting untrusted input is told the gate is not the boundary.
MEDIUM PRAX-2026-05-31-006 Third-party skill/plugin install gating is a regex Skills Guard plus trust tiers; community skills with zero findings install without an enforced human-review step.
Policy Rule — R-14 (Worker Remit):
"Installation of third-party skills and plugins, which MUST be subject to operator review (reading the skill's Python and scripts, not only its description) before the code is loaded and run."
tools/skills_guard.py:11 — trust tiers — community skills "blocked unless --force" only when findings exist; a clean-scanning community skill installs and runs arbitrary Python at import with no enforced human read
Recommended Action
Add an explicit install-time confirmation that names the skill's Python entry points and requires operator acknowledgement before a community-trust skill is loaded, rather than gating solely on whether the regex scanner found something.
MEDIUM PRAX-2026-05-31-007 The MCP tool-description poisoning scanner logs warnings but does not block; a poisoned MCP tool description reaches the model regardless.
Policy Rule — R-15 (Worker Remit):
"Loading or launching an operator-configured MCP server, which MUST pass the documented supply-chain guard at launch."
tools/mcp_tool.py:341 — _MCP_INJECTION_PATTERNS are "WARNING-level — we log but don't block, since false positives would break legitimate MCP servers" tools/mcp_tool.py:296 — _build_safe_env passes only PATH/HOME/USER/LANG/XDG_* plus user-declared vars to stdio MCP subprocesses — credential scrubbing is genuine, but description-poisoning is not gated
Recommended Action
Surface MCP description-scan warnings to the operator at server-add time (not only to logs) so a poisoned description is reviewed before the server's tools are exposed to the model.
MEDIUM PRAX-2026-05-31-008 Desktop PostHog telemetry defaults to enabled (opt-out) on first run for official builds, and client-side IP suppression is not achievable.
src/renderer/src/utils/analytics.ts:14 — isAnalyticsEnabled() — "if (stored === null) return hasKey" makes telemetry opt-out (on by default) for official key-bearing builds src/renderer/src/utils/analytics.ts:59 — comment — full IP-address suppression "is NOT possible from the client"; relies on server-side PostHog project config the desktop cannot guarantee
Recommended Action
Make first-run analytics opt-in (default off until the operator consents) for official builds, or present an explicit consent prompt on first launch before any `capture()` fires.
MEDIUM PRAX-2026-05-31-009 The agent has no in-process guard that warns or refuses when started as root while ingesting untrusted input; non-root operation is documentation-and-container default only.
hermes-agent/run_agent.py — no os.geteuid()/getuid()==0 startup check found across run_agent.py, hermes_bootstrap.py, cli.py, or gateway/run.py; root-vs-untrusted-input is left to SECURITY.md §4 guidance and the container's non-root default
Recommended Action
Add a startup advisory in the agent bootstrap that detects `os.geteuid() == 0` and, when any network-exposed adapter or untrusted input surface is enabled, prints a clear warning pointing at the non-root deployment guidance.
What's Working Well

Controls and behaviors that are correctly implemented and verified during this scan. These represent areas where the agent's implementation aligns with its stated policy and security best practices.

YOLO mode frozen at import against injection escalation

`tools/approval.py` reads `HERMES_YOLO_MODE` once at module import into `_YOLO_MODE_FROZEN`, explicitly so a skill running in-process cannot set the variable mid-session and bypass all approval checks — a deliberate prompt-injection-escalation defense.

tools/approval.py:29

Hardline command blocklist sits below YOLO and approval-off

A small unconditional blocklist of unrecoverable commands (filesystem destruction at /, raw block-device overwrite, kernel shutdown) is refused even under `--yolo`, `approvals.mode=off`, or cron approve mode, providing a floor that operator break-glass cannot cross on host-capable backends.

tools/approval.py:163

Provider-credential scrubbing for lower-trust child processes

`_HERMES_PROVIDER_ENV_BLOCKLIST` strips Hermes-managed provider keys from the env passed to shell, MCP, and code-execution children, and `env_passthrough.py` refuses to let a skill or config re-register a blocked provider credential — closing GHSA-rhgp-j443-p4rf.

tools/env_passthrough.py:48

API server fails closed without an API key

The API server adapter's `connect()` refuses to start when `API_SERVER_KEY` is unset, including for loopback-only binds, and refuses a placeholder/weak key when network-accessible — so the no-key branch in `_check_auth` is a test-only dead path, not a live fail-open.

gateway/platforms/api_server.py:4145

Gateway authorization is default-deny

`_is_user_authorized` denies by default when no allowlist and no explicit allow-all flag are configured, treats session IDs as routing handles only, and re-checks authorization rather than trusting prior session state.

gateway/run.py:6747

Desktop SSH uses key-only BatchMode auth with host-key pinning

The Desktop builds SSH connections with `BatchMode=yes` (no interactive password fallback) and `StrictHostKeyChecking=accept-new` (trust-on-first-use pinning that fails closed if a pinned key later changes), and binds all tunnel forwards to `127.0.0.1`.

src/main/ssh-remote.ts:30

Desktop keeps the remote API key out of the renderer

`getPublicConnectionConfig()` exposes only `hasApiKey` and the key length (for mask rendering) to the renderer process, never the secret itself, verified by `connection-config-security.test.ts`.

tests/connection-config-security.test.ts:37

Electron operator layer is security-hardened

Both the main window and the askpass window set `nodeIntegration: false`, `contextIsolation: true`, `sandbox: true`, and `webSecurity: true`, with `security.ts` enforcing the same on web-preferences creation.

src/main/security.ts:58

Exact-pinned dependencies with supply-chain CI

Every direct dependency in `pyproject.toml` is pinned to `==X.Y.Z` with a committed `uv.lock`, CVE-annotated, and gated by OSV-scanner and a narrow supply-chain-audit workflow in CI.

pyproject.toml:13
Discovered Log Files

Log files found in the agent's workspace during this scan. Reviewing these files provides runtime evidence to complement the static analysis above.

Path Source Content Type Purpose Last Modified Status
~/.hermes/logs/agent.log Hermes Agent (hermes_logging.setup_logging) plaintext, redacted, rotating (RedactingFormatter) INFO+ catch-all of agent, tool, and session activity unknown Inferred
~/.hermes/logs/errors.log Hermes Agent (hermes_logging.setup_logging) plaintext, redacted, rotating (RedactingFormatter) WARNING+ quick-triage error log unknown Inferred
~/.hermes/logs/gateway.log Hermes Agent (hermes_logging.setup_logging, gateway mode) plaintext, redacted, rotating (RedactingFormatter) INFO+ gateway-component events (created when mode=gateway) unknown Inferred
OWASP LLM Top 10 (2025) Coverage

Each card represents one category and shows the top 3 findings. All items in the Findings section.

LLM01 Prompt Injection
No findings
LLM04 Data and Model Poisoning
No findings
LLM05 Improper Output Handling
No findings
LLM07 System Prompt Leakage
No findings
LLM08 Vector and Embedding Weaknesses
No findings
LLM09 Misinformation
No findings
LLM10 Unbounded Consumption
No findings
OWASP Agentic Top 10 (2026) Coverage

Each card represents one category and shows the top 3 findings. All items in the Findings section.

RAISE Maturity Posture

Overall maturity assessment across the six categories of the RAISE framework. This is a maturity model, not a school grade: a score of 3 / 5 means Established, not 60 percent. Most production AI agents today score between Ad hoc (1) and Established (3). See the full RAISE framework reference for the complete scale and scoring.

3.15 / 5.0
Weighted Maturity Score · Established
Established. Hermes Agent presents a coherent, documented safety model whose load-bearing controls are genuinely wired into the runtime — approval gating with an injection-resistant YOLO freeze and a hardline floor, credential scrubbing across shell/MCP/code-execution children, default-deny caller authorization, exact-pinned dependencies with OSV and supply-chain CI, a substantial adversarial test suite, and structured redacting logs. It is held below Strong by a real fail-open in the WeCom/Weixin/Yuanbao adapters' default-open access policy, by the project's own honest stance that its in-process heuristics are not containment (so Zero Trust rests on an operator-selected OS isolation posture the code does not enforce), and by operator-layer gaps in the Desktop SSH and telemetry surfaces.
Limit Your Domain
3/ 5
Confidence: High  |  Weight: 15%  |  Weighted: 0.45
The toolset registry in `toolsets.py` defines a bounded inventory matching the remit's Known Good Baseline with default-off toolsets, and the agent is single-operator by construction; tool exposure is gated by per-platform config rather than a general-purpose free-for-all.
Balance Your Knowledge Base
3/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.45
External content (web, email, MCP responses) enters context but passes message sanitization, automated-sender filtering (`gateway/platforms/email.py`), and output redaction, and memory/`SOUL.md` are operator-owned config; the residual risk is that these are in-process heuristics the project itself declines to treat as a boundary absent an OS isolation posture.
Implement Zero Trust
3/ 5
Confidence: High  |  Weight: 25%  |  Weighted: 0.75
Strong operative controls — YOLO frozen at import against injection escalation, a hardline blocklist below YOLO, the GHSA-rhgp-j443-p4rf provider-credential scrub, a fail-closed API server, and gateway default-deny — are offset by the WeCom/Weixin/Yuanbao default-`open` fail-open and the documented reality that in-process screens are not containment.
Manage Your Supply Chain
4/ 5
Confidence: High  |  Weight: 15%  |  Weighted: 0.60
Every direct dependency is exact-pinned to `==` in `pyproject.toml` with a committed `uv.lock`, CVE-tracked comments, OSV-scanner and supply-chain-audit CI workflows, an MCP subprocess env-filter plus tool-description poisoning scanner, and a Skills Guard trust-tier install policy; provenance and integrity are managed deliberately.
Build an AI Red Team
3/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.45
A substantial adversarial test suite (`test_browser_secret_exfil.py`, `test_browser_ssrf_local.py`, `test_cron_prompt_injection.py`, `test_command_guards.py`, `test_skills_guard.py`, `test_threat_patterns.py`) runs in CI, and code references to fixed advisories (GHSA-rhgp-j443-p4rf) show adversarial findings drove design changes rather than only documentation.
Monitor Continuously
3/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.45
`hermes_logging.py` configures three `RotatingFileHandler` logs (`agent.log`, `errors.log`, `gateway.log`) under `~/.hermes/logs/` with a `RedactingFormatter`, and sessions persist to a SQLite FTS5 store; the score is held at Established because action-level audit completeness was assessed source-only (no runtime log files on disk to confirm granularity).

Maturity Scoring Rubric

Every score above is based on this scale. A score is a snapshot of observable posture — not a verdict on the people or team behind the system.

Score Label Meaning
5 Exemplary Best-in-class; automated, continuously tested, reference quality. Rarely achieved in shipping systems.
4 Strong Comprehensive controls, active management, minor gaps. Production-ready.
3 Established Documented controls consistently applied; known gaps accepted. A respectable baseline.
2 Partial Some controls exist but coverage is incomplete; key gaps remain.
1 Ad hoc Informal or inconsistent measures; relies on individual judgment.
0 Absent No evidence this category is addressed at all.
Weighting: the weighted overall above is the sum of each category's score × weight (the per-category weights are shown on each card). Zero Trust carries double weight by design; see the RAISE framework reference for the rationale.