The dominant pattern is a deliberately layered, honestly documented safety model with one context where the top layer simply stops running. Hermes ships genuine deterministic controls — an unbypassable hardline command floor and sudo guard in tools/approval.py that even --yolo cannot defeat, an API server that refuses to start without a key, a default-deny caller allowlist, and a credential-scrub blocklist that neither a skill nor config.yaml can override — yet in any non-interactive, non-gateway run the same file auto-approves every non-hardline dangerous command and the whole of execute_code, and those are exactly the runs in which poisoned web_extract, browser_* and mcp_* results reach the model held only by a provenance label.
Two secondary themes: four gateway adapters opt out of the central default-deny by declaring their own access policy and defaulting it to open, so an enabled-but-unconfigured adapter serves any DM sender; and the desktop's runtime install path pipes a remote shell script straight into an interpreter with no checksum or signature, in a codebase that otherwise pins every Python dependency exactly and gates merges on a supply-chain scanner.
cli.py, run_agent.py, agent/, ~85 tools under tools/, a gateway with ~25 platform adapters under gateway/platforms/) and an Electron/TypeScript desktop client with a hardened main/renderer split. Command execution funnels through tools/approval.py's check_all_command_guards(), which layers an unconditional hardline block floor, a sudo-stdin guard, the tirith rule engine and a dangerous-pattern denylist; terminal work runs through pluggable backends in tools/environments/ (local host by default, Docker/Modal/Daytona/SSH optional). Caller authorization is centralized in gateway/run.py::_is_user_authorized() as default-deny, and the OpenAI-compatible API server in gateway/platforms/api_server.py refuses to start without API_SERVER_KEY. The desktop talks to a locally launched hermes serve backend on 127.0.0.1:8642 and manages first-run runtime install and in-place updates from src/main/installer.ts.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.
| Rule ID | Section | Rule (quoted) | Status | Finding |
|---|---|---|---|---|
| R-01 | Prohibited Behaviors | "The agent MUST NOT act on instructions embedded in ingested content — web pages, emails, inbound messages, file contents, tool results, or MCP server responses — as though they were operator commands." | Partial | PRAX-2026-08-12-007 |
| R-02 | Prohibited Behaviors | "The agent MUST NOT grant itself new authority beyond what the operator has explicitly granted — new callers, egress destinations, tools, integrations, or trust tiers." | Verified | — |
| R-03 | Prohibited Behaviors | "The agent MUST NOT disable, bypass, or route around its own safety controls (approval gate, output redaction, caller allowlists, credential scoping) in order to complete a task." | Verified | — |
| R-04 | Prohibited Behaviors | "The desktop layer MUST NOT render agent-produced or tool-produced output as active or executable content; agent output MUST be treated as inert (displayed as data, never evaluated as HTML/script/markup with side effects)." | Verified | — |
| R-05 | Approved Communication Channels | "MUST refuse to dispatch work, relay output, or resolve approvals until an operator-configured allowlist is set. MUST NOT fail open when no allowlist is configured." | Partial | PRAX-2026-08-12-003 |
| R-06 | Approved Communication Channels | "MUST default to a loopback bind; MUST require an allowlist before serving." | Verified | — |
| R-07 | Approved Communication Channels | "MUST bind loopback only and rely on OS-level access control; MUST NOT be exposed beyond the local user without an explicit network authentication layer." | Verified | — |
| R-08 | Approved Communication Channels | "Approved Communication Channels" | Verified | — |
| R-09 | Authorized Counterparties | "Trusted People / Accounts" | Partial | PRAX-2026-08-12-003 |
| R-10 | Authorized Counterparties | "Any outbound destination reached in code but absent from the operator's configured/allowlisted set is a trust expansion; under egress isolation, outbound connections MUST be limited to the allowlisted hosts." | Partial | PRAX-2026-08-12-004 |
| R-11 | Authorized Counterparties | "Third-party MCP servers, skills, and plugins — but each MUST be reviewed and approved by the operator before install or activation; none may be auto-trusted on the strength of its own description alone." | Partial | PRAX-2026-08-12-008 |
| R-12 | Tools and Capabilities | "Allowed Tools (Known Good Baseline)" | Verified | — |
| R-13 | Tools and Capabilities | "the default local (host) terminal backend is authorized only for trusted content; any untrusted input surface MUST run under a sandboxed terminal backend (Docker/Modal/Daytona) or whole-process wrapping" | Gap | PRAX-2026-08-12-006 |
| R-14 | Tools and Capabilities | "Code-execution and MCP-subprocess tools operating on untrusted-influenced input MUST additionally be operator-gated or run only under that OS-level isolation posture." | Partial | PRAX-2026-08-12-001 |
| R-15 | Data Boundaries | "Allowed Data Sources" | Verified | — |
| R-16 | Data Boundaries | "Provider API keys and gateway/platform tokens MUST NOT be passed into lower-trust in-process or subprocess components (shell subprocesses, the code-execution child, MCP subprocesses, cron job scripts); they MUST be stripped from that environment by default, and the strip set MUST cover every configured provider/gateway credential (a registry-derived denylist is acceptable)." | Verified | — |
| R-17 | Data Boundaries | "Credentials MUST NOT be written into the main config file or into version control; they belong in the operator credential file with tight permissions (or a dedicated secret store)." | Verified | — |
| R-18 | Data Boundaries | "Operator credentials and session authorization material MUST NOT egress to any destination outside the trust envelope, whether via environment leakage, adapter logging, or a transport error that flushes them upstream." | Verified | — |
| R-19 | Data Boundaries | "Outbound telemetry and usage attribution are opt-in and off by default; no telemetry or usage data may egress unless the operator has explicitly enabled it." | Partial | PRAX-2026-08-12-004 |
| R-20 | Action Boundaries | "Allowed Without Approval" | Verified | — |
| R-21 | Action Boundaries | "Destructive or irreversible shell and file operations (deletion, overwrite, bulk or recursive changes) MUST require operator approval before execution, in every execution context and regardless of interactive vs. unattended mode." | Partial | PRAX-2026-08-12-001 |
| R-22 | Action Boundaries | "Binding a local-only surface (dashboard, plugin HTTP server, or any local-IPC surface) to a non-loopback interface MUST be an explicit operator decision, never taken autonomously." | Verified | — |
| R-23 | Action Boundaries | "Agent work MUST NOT be dispatched, output relayed, or approvals resolved for any caller outside the configured authorization set." | Partial | PRAX-2026-08-12-003 |
| R-24 | Action Boundaries | "The desktop self-update and first-run install path MUST NOT fetch or install agent runtime code from an unverified or unauthenticated source; installers and runtime updates MUST be integrity-verified before they replace the running app or backend." | Partial | PRAX-2026-08-12-002 |
| R-25 | Escalation Rules | "On a destructive-command match, the response MUST be halt-pending-approval — pause and prompt the operator, never autonomous execution (the runtime enforcement of the destructive-operation approval gate)." | Partial | PRAX-2026-08-12-005 |
| R-26 | Escalation Rules | "On an enabled network-exposed adapter with no caller allowlist, the response MUST be fail-closed — refuse to serve rather than dispatch." | Partial | PRAX-2026-08-12-003 |
| R-27 | Escalation Rules | "When Skills Guard detects injection-like patterns in installable skill or plugin content, surface the detection to the operator for review before install." | Verified | — |
| R-28 | Escalation Rules | "When output redaction detects a secret-like pattern in agent output, strip it from display and continue." | Verified | — |
| R-29 | Escalation Rules | "Tool invocations, approval decisions, and session lifecycle events MUST be recorded to durable, structured logs (`agent.log` / `gateway.log`; `desktop.log` for the desktop layer) sufficient to reconstruct what the agent did." | Partial | PRAX-2026-08-12-010 |
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.
LLM06 primary — the finding's main OWASP categoryASI10 secondary — a category it also touches
CRITICAL PRAX-2026-08-12-001 In non-interactive, non-gateway runs every non-hardline dangerous command and all execute_code scripts auto-approve, in the same contexts where untrusted web and MCP content reaches the model.
"Code-execution and MCP-subprocess tools operating on untrusted-influenced input MUST additionally be operator-gated or run only under that OS-level isolation posture. / Destructive or irreversible shell and file operations (deletion, overwrite, bulk or recursive changes) MUST require operator approval before execution, in every execution context and regardless of interactive vs. unattended mode."
- In `tools/approval.py`, replace the non-interactive fall-through at lines 1206-1223 and 1518-1519 with a fail-closed default: deny dangerous commands and `execute_code` when no approval surface is attached, and require an explicit opt-in setting (mirroring `approvals.cron_mode: approve`) for operators who genuinely want unattended execution.
- Log every such denial at WARNING with the pattern key, so headless runs that need an allowance are discoverable from `agent.log` rather than silently executing.
HIGH PRAX-2026-08-12-002 The desktop first-run and repair install pipes a remote shell script straight into an interpreter with no checksum, signature or pinned revision.
"The desktop self-update and first-run install path MUST NOT fetch or install agent runtime code from an unverified or unauthenticated source; installers and runtime updates MUST be integrity-verified before they replace the running app or backend."
- Pin the install script to a tagged release URL and verify a published SHA-256 before execution in both `runUnixInstall` and the Windows wrapper in `src/main/installer.ts`.
- Ship the expected digest with the desktop build so verification does not depend on a second fetch from the same origin.
HIGH PRAX-2026-08-12-003 Four gateway adapters opt out of the central default-deny and default their own DM policy to open, so an enabled but unconfigured adapter dispatches work for any sender.
"MUST refuse to dispatch work, relay output, or resolve approvals until an operator-configured allowlist is set. MUST NOT fail open when no allowlist is configured. / Trusted People / Accounts / Agent work MUST NOT be dispatched, output relayed, or approvals resolved for any caller outside the configured authorization set. / On an enabled network-exposed adapter with no caller allowlist, the response MUST be fail-closed — refuse to serve rather than dispatch."
- Make the adapter-owned policy path deny by default: in `gateway/platforms/wecom.py`, `weixin.py`, `qqbot/adapter.py` and `yuanbao.py`, change the `dm_policy` / `group_policy` default from `"open"` to `"allowlist"` so an unconfigured adapter serves nobody.
- Alternatively gate the `enforces_own_access_policy` shortcut in `gateway/run.py:6755` on the adapter reporting that it actually holds a non-empty allowlist.
HIGH PRAX-2026-08-12-004 Official desktop builds send usage telemetry to a third-party endpoint by default, with consent inferred from the presence of a build key rather than an operator decision.
"Any outbound destination reached in code but absent from the operator's configured/allowlisted set is a trust expansion; under egress isolation, outbound connections MUST be limited to the allowlisted hosts. / Outbound telemetry and usage attribution are opt-in and off by default; no telemetry or usage data may egress unless the operator has explicitly enabled it."
- In `src/renderer/src/utils/analytics.ts`, change the first-run branch to return `false` so telemetry stays off until the operator opts in, and surface the choice in the onboarding flow.
- Record the operator's decision in the profile configuration rather than renderer `localStorage`, so a profile reset does not silently re-enable capture.
HIGH PRAX-2026-08-12-005 With approvals.mode set to smart, an auxiliary LLM resolves the destructive-command decision and its APPROVE also grants standing session approval for that pattern.
"On a destructive-command match, the response MUST be halt-pending-approval — pause and prompt the operator, never autonomous execution (the runtime enforcement of the destructive-operation approval gate)."
- In `tools/approval.py`, stop calling `approve_session()` on a smart-mode approval so the decision is one-shot rather than standing for the session.
- Restrict smart mode to escalating or denying — never auto-approving — or gate it behind a setting that names the trade-off, and log every smart approval at WARNING with the command and verdict.
HIGH PRAX-2026-08-12-006 Nothing in code binds an enabled untrusted-ingress surface to a sandboxed terminal backend; the pairing is left entirely to operator discipline.
"the default local (host) terminal backend is authorized only for trusted content; any untrusted input surface MUST run under a sandboxed terminal backend (Docker/Modal/Daytona) or whole-process wrapping"
- Add a startup interlock in the gateway launch path that refuses to enable an untrusted-ingress adapter (messaging, email, webhook) while `TERMINAL_ENV` resolves to `local`, overridable only by an explicit named setting.
- Surface the resolved backend and the enabled ingress surfaces together in `hermes doctor` output so the mismatch is visible before it matters.
MEDIUM PRAX-2026-08-12-007 Untrusted-content labeling covers an enumerated list of four tool names with a 32-character floor, so file reads, code-execution output and session search reach the model unlabeled.
"The agent MUST NOT act on instructions embedded in ingested content — web pages, emails, inbound messages, file contents, tool results, or MCP server responses — as though they were operator commands."
- In `agent/tool_dispatch_helpers.py`, invert the list: wrap every tool result by default and maintain a small allowlist of first-party tools whose output is genuinely agent-internal.
- Remove the 32-character floor or lower it to zero for the covered tools — a short result is exactly where a terse injected directive fits.
MEDIUM PRAX-2026-08-12-008 Skills the agent writes for itself bypass Skills Guard entirely because the gate that would scan them is off by default.
"Third-party MCP servers, skills, and plugins — but each MUST be reviewed and approved by the operator before install or activation; none may be auto-trusted on the strength of its own description alone."
- Default `skills.guard_agent_created` to enabled in the config schema so agent-authored skill content is scanned on the same terms as community content.
- Record every agent-created skill write to the install audit log with its scan verdict, so the self-authoring loop is reviewable after the fact.
MEDIUM PRAX-2026-08-12-009 There is no component inventory anywhere in the tree, and the one scanner that would find known-vulnerable pinned dependencies is configured never to fail.
- Generate a CycloneDX SBOM for both roots as a release artifact (`uv`/`cyclonedx-py` for Python, `cyclonedx-npm` for the desktop) and attach it to each published build.
- Keep `fail-on-vuln: false` if deliberate, but add a severity threshold that fails the release workflow specifically, so shipping is gated even when day-to-day merges are not.
MEDIUM PRAX-2026-08-12-010 The three primary logs are free-form text lines rather than structured records, and nothing exports or alerts beyond the local host.
"Tool invocations, approval decisions, and session lifecycle events MUST be recorded to durable, structured logs (`agent.log` / `gateway.log`; `desktop.log` for the desktop layer) sufficient to reconstruct what the agent did."
- Add a JSON-lines handler alongside the existing text handlers in `hermes_logging.setup_logging()` and emit tool invocations, approval decisions and session lifecycle events through it with stable field names.
- Ship an optional OTLP or file-tail export path so an operator can route those events off-host, which nothing in the tree currently supports.
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.
Unbypassable hardline command floor
A fixed set of catastrophic commands (rm -rf /, mkfs, dd to a raw device, shutdown, fork bomb, kill -1) is blocked before the yolo bypass, the approval-mode check and the cron path, so no session-level setting can defeat it.
YOLO mode frozen at import to block in-process escalation
HERMES_YOLO_MODE is read once at module import with an explicit comment that re-reading os.environ would let any skill inside the process disable every approval check, closing a prompt-injection escalation path.
API server fails closed without authentication
connect() refuses to start the API server at all when API_SERVER_KEY is unset, including on loopback binds, and refuses to start with a placeholder-strength key when the bind address is network-accessible.
Default-deny caller authorization in the gateway
_is_user_authorized() resolves per-platform allowlists, chat allowlists, the pairing store and the global allowlist, and returns deny when none is configured unless an explicit allow-all flag is set.
Credential scrubbing that skills and config cannot override
Provider credentials are stripped from shell, MCP and code-execution child environments, and both the skill-declared and config.yaml passthrough paths refuse to register anything on the provider blocklist — the shipped fix for GHSA-rhgp-j443-p4rf.
Injection-blocking on context files and agent memory
Context files matching the shared threat-pattern set are replaced with a placeholder before they can enter the system prompt, and memory entries are sanitized on load with a drift check that refuses writes when the file changed underneath the agent.
Electron main process hardened and asserted by tests
The renderer runs with nodeIntegration off, contextIsolation and sandbox on, navigation and window-open are allowlisted, attached webviews have their preload stripped — and electron-security.test.ts asserts these invariants against the source.
Malware check before every MCP server launch
npx and uvx MCP server packages are queried against the OSV API for MAL-* malware advisories and blocked on a hit before the subprocess is spawned.
Exact dependency pinning with a documented incident rationale
Every direct Python dependency is pinned to an exact version with uv.lock regenerated alongside, and the policy comment names the PyPI worm incident that drove the change away from ranges.
Security advisories traced to fixes in-tree
Release notes link GHSA-rhgp-j443-p4rf and GHSA-5qr3-c538-wm9j to the pull requests that fixed them, credit the reporter, and the advisory ID is cited in the fixing code itself.
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_logging.setup_logging (all agent/tool/session loggers) | plaintext rotating log, timestamp + level + session tag + logger + message | Main activity log at INFO+, including every tool completion with duration and every auto-approval warning | unknown | Inferred |
| ~/.hermes/logs/errors.log | hermes_logging.setup_logging (WARNING+ handler) | plaintext rotating log | Triage log for warnings and errors, including hardline and sudo-guard blocks | unknown | Inferred |
| ~/.hermes/logs/gateway.log | hermes_logging.setup_logging with mode=gateway, filtered to gateway.* loggers | plaintext rotating log | Gateway-only events — platform adapters, session management, slash commands, delivery | unknown | Inferred |
| $HERMES_HOME/logs/dashboard-auth.log | hermes_cli/dashboard_auth/audit.py | structured JSON lines with a token-field redaction denylist | Dashboard authentication events — login, logout, refresh, revoke, session-verify failure, websocket ticket mint and reject | unknown | Inferred |
| $HERMES_HOME/logs/mcp-stderr.log | tools/mcp_tool.py shared stderr file handle for stdio MCP subprocesses | plaintext subprocess stderr capture | Keeps MCP server stderr off the parent process stream and on disk for diagnosis | unknown | Inferred |
| $HERMES_HOME/logs/desktop.log | hermes-desktop main process and updater-log.ts | plaintext log | Desktop app lifecycle and auto-updater events | unknown | Inferred |
Each card represents one category and shows the top 3 findings. All items in the Findings section.
Each card represents one category and shows the top 3 findings. All items in the Findings section.
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.
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. |