Files
hermes-agent/hermes_cli_analysis_report.md
Allegro 10271c6b44
Some checks failed
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Failing after 25s
Tests / test (pull_request) Failing after 24s
Docker Build and Publish / build-and-push (pull_request) Failing after 35s
security: fix command injection vulnerabilities (CVSS 9.8)
Replace shell=True with list-based subprocess execution to prevent
command injection via malicious user input.

Changes:
- tools/transcription_tools.py: Use shlex.split() + shell=False
- tools/environments/docker.py: List-based commands with container ID validation

Fixes CVE-level vulnerability where malicious file paths or container IDs
could inject arbitrary commands.

CVSS: 9.8 (Critical)
Refs: V-001 in SECURITY_AUDIT_REPORT.md
2026-03-30 23:15:11 +00:00

33 KiB

Hermes CLI Architecture Deep Analysis Report

Executive Summary

This report provides a comprehensive architectural analysis of the hermes_cli/ Python package, which serves as the command-line interface layer for the Hermes Agent system. The codebase consists of approximately 35,000+ lines of Python code across 35+ modules.


1. Architecture Diagram (Text Format)

┌─────────────────────────────────────────────────────────────────────────────────┐
│                           HERMES CLI ARCHITECTURE                                │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              ENTRY POINTS                                        │
├─────────────────────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐  │
│  │   hermes    │    │  hermes     │    │  hermes     │    │    hermes       │  │
│  │   chat      │    │  gateway    │    │  setup      │    │    status       │  │
│  │  (default)  │    │  (service)  │    │  (wizard)   │    │   (diagnostics) │  │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘    └─────────────────┘  │
│         │                  │                  │                                 │
│         └──────────────────┴──────────────────┘                                 │
│                            │                                                    │
│                    ┌───────┴───────┐                                            │
│                    │   main.py     │  ← CLI entry point, argument parsing       │
│                    └───────┬───────┘                                            │
└────────────────────────────┼────────────────────────────────────────────────────┘
                             │
┌────────────────────────────┼────────────────────────────────────────────────────┐
│                      CORE MODULES                                                │
├────────────────────────────┼────────────────────────────────────────────────────┤
│                            │                                                    │
│  ┌─────────────────────────┴─────────────────────────┐                          │
│  │              auth.py (2,365 lines)                │                          │
│  │  ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │                          │
│  │  │ OAuth Device │ │  API Key     │ │ External  │ │                          │
│  │  │ Code Flow    │ │ Providers    │ │ Process   │ │                          │
│  │  │ (Nous, Codex)│ │ (15+ prov)   │ │ (Copilot) │ │                          │
│  │  └──────────────┘ └──────────────┘ └───────────┘ │                          │
│  │         │                  │               │      │                          │
│  │         └──────────────────┼───────────────┘      │                          │
│  │                            ▼                      │                          │
│  │  ┌───────────────────────────────────────────┐   │                          │
│  │  │   ~/.hermes/auth.json (cross-process    │   │                          │
│  │  │   file locking, token refresh, minting) │   │                          │
│  │  └───────────────────────────────────────────┘   │                          │
│  └───────────────────────────────────────────────────┘                          │
│                                                                                  │
│  ┌───────────────────────────────────────────────────┐                          │
│  │           config.py (2,093 lines)                 │                          │
│  │  ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │                          │
│  │  │ ~/.hermes/   │ │   YAML       │ │   .env    │ │                          │
│  │  │   config.yaml│ │   Schema     │ │   Loader  │ │                          │
│  │  └──────────────┘ └──────────────┘ └───────────┘ │                          │
│  │         │                  │               │      │                          │
│  │         └──────────────────┼───────────────┘      │                          │
│  │                            ▼                      │                          │
│  │  ┌───────────────────────────────────────────┐   │                          │
│  │  │   DEFAULT_CONFIG dict (400+ settings)     │   │                          │
│  │  │   - model/agent settings                  │   │                          │
│  │  │   - terminal backends                     │   │                          │
│  │  │   - auxiliary models (vision, etc)        │   │                          │
│  │  │   - memory, TTS, STT, privacy             │   │                          │
│  │  └───────────────────────────────────────────┘   │                          │
│  └───────────────────────────────────────────────────┘                          │
│                                                                                  │
│  ┌───────────────────────────────────────────────────┐                          │
│  │          commands.py (737 lines)                  │                          │
│  │  ┌─────────────────────────────────────────────┐  │                          │
│  │  │   COMMAND_REGISTRY: 40+ slash commands      │  │                          │
│  │  │   - Session commands (/new, /retry, /undo)  │  │                          │
│  │  │   - Config commands (/config, /prompt)      │  │                          │
│  │  │   - Tool commands (/tools, /skills)         │  │                          │
│  │  │   - Gateway dispatch compatibility          │  │                          │
│  │  └─────────────────────────────────────────────┘  │                          │
│  └───────────────────────────────────────────────────┘                          │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
│                      SUBSYSTEM MODULES                                           │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
│  │  setup.py   │  │ gateway.py  │  │ models.py   │  │  status.py  │            │
│  │  (3,622)    │  │  (2,035)    │  │  (1,238)    │  │   (850)     │            │
│  │             │  │             │  │             │  │             │            │
│  │ Interactive │  │ Systemd/    │  │ Provider    │  │ Component   │            │
│  │ setup wizard│  │ Launchd/    │  │ model       │  │ health      │            │
│  │ (6 steps)   │  │ Windows svc │  │ catalogs    │  │ checks      │            │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘            │
│                                                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
│  │tools_config │  │ mcp_config  │  │ skills_hub  │  │  profiles   │            │
│  │  (1,602)    │  │   (645)     │  │   (620)     │  │   (380)     │            │
│  │             │  │             │  │             │  │             │            │
│  │ Toolset     │  │ MCP server  │  │ Skill       │  │ Profile     │            │
│  │ platform    │  │ lifecycle   │  │ install/    │  │ management  │            │
│  │ management  │  │ management  │  │ search      │  │ (~/.hermes) │            │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘            │
│                                                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
│  │   colors    │  │  banner.py  │  │   doctor    │  │  checklist  │            │
│  │   (22)      │  │   (485)     │  │   (620)     │  │   (210)     │            │
│  │             │  │             │  │             │  │             │            │
│  │ ANSI color  │  │ Update      │  │ Config/dep  │  │ Setup       │            │
│  │ utilities   │  │ notifications│ │ diagnostics │  │ completion  │            │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘            │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
│                      EXTERNAL DEPENDENCIES                                       │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
│  │   httpx     │  │    yaml     │  │prompt_toolki│  │ simple_term │            │
│  │  (HTTP)     │  │  (config)   │  │  (CLI TUI)  │  │   _menu     │            │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘            │
│                                                                                  │
│  ┌─────────────────────────────────────────────────────────────────────────┐   │
│  │                    PROJECT MODULES (../)                                 │   │
│  │  ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐  │   │
│  │  │   cli.py  │ │toolsets.py│ │  tools/   │ │  agent/   │ │ gateway/  │  │   │
│  │  │(main loop)│ │(tool reg) │ │(tool impl)│ │(LLM logic)│ │(messaging)│  │   │
│  │  └───────────┘ └───────────┘ └───────────┘ └───────────┘ └───────────┘  │   │
│  └─────────────────────────────────────────────────────────────────────────┘   │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

2. Dependency Graph Between Modules

                            ┌──────────────────┐
                            │   main.py        │
                            │   (entry point)  │
                            └────────┬─────────┘
                                     │
            ┌────────────────────────┼────────────────────────┐
            │                        │                        │
            ▼                        ▼                        ▼
   ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
   │    auth.py      │◄────│   config.py     │────►│  commands.py    │
   │                 │     │                 │     │                 │
   │ • OAuth flows   │     │ • Config I/O    │     │ • Command defs  │
   │ • Token refresh │     │ • Env loading   │     │ • Autocomplete  │
   │ • Provider reg  │     │ • Migration     │     │ • Gateway help  │
   └────────┬────────┘     └────────┬────────┘     └─────────────────┘
            │                       │
            │         ┌─────────────┼─────────────┐
            │         │             │             │
            ▼         ▼             ▼             ▼
   ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
   │   models.py     │ │   setup.py      │ │  gateway.py     │
   │                 │ │                 │ │                 │
   │ • Model catalogs│ │ • Setup wizard  │ │ • Service mgmt  │
   │ • Provider lists│ │ • Interactive UI│ │ • Systemd/launchd│
   └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
            │                   │                   │
            │                   │                   │
            ▼                   ▼                   ▼
   ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
   │  tools_config.py│ │    colors.py    │ │   status.py     │
   │  mcp_config.py  │ │    banner.py    │ │   doctor.py     │
   │  skills_hub.py  │ │    checklist.py │ │   profiles.py   │
   └─────────────────┘ └─────────────────┘ └─────────────────┘
            │                   │                   │
            └───────────────────┼───────────────────┘
                                │
                                ▼
                   ┌─────────────────────────┐
                   │     EXTERNAL MODULES    │
                   │  httpx, yaml, pathlib,  │
                   │  prompt_toolkit, etc    │
                   └─────────────────────────┘

Key Dependency Patterns:

  1. auth.py → config.py (get_hermes_home, get_config_path)
  2. config.py → hermes_constants (get_hermes_home re-export)
  3. main.py → auth.py, config.py, setup.py, gateway.py
  4. commands.py → (isolated - only prompt_toolkit)
  5. tools_config.py → config.py, colors.py
  6. mcp_config.py → config.py, tools/mcp_tool.py
  7. Most modules → colors.py (for terminal output)

3. Ten Specific Improvement Recommendations

3.1 CRITICAL: Refactor auth.py Token Storage Security

Location: auth.py lines 470-596 (_load_auth_store, _save_auth_store)

Issue: The auth.json file is created with 0600 permissions but there are race conditions between file creation and permission setting. Also, tokens are stored in plaintext.

Recommendation:

# Use atomic file operations with secure defaults
def _secure_save_auth_store(auth_store: Dict[str, Any]) -> Path:
    auth_file = _auth_file_path()
    auth_file.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    
    # Create temp file with restricted permissions from the start
    fd, tmp_path = tempfile.mkstemp(
        dir=auth_file.parent,
        prefix=f".{auth_file.name}.tmp.",
        suffix=".json"
    )
    try:
        os.fchmod(fd, 0o600)  # Set permissions BEFORE writing
        with os.fdopen(fd, 'w') as f:
            json.dump(auth_store, f, indent=2)
        os.replace(tmp_path, auth_file)
    except:
        os.unlink(tmp_path)
        raise

3.2 HIGH: Implement Config Schema Validation

Location: config.py lines 138-445 (DEFAULT_CONFIG)

Issue: No runtime validation of config.yaml structure. Invalid configs cause cryptic errors later.

Recommendation: Add Pydantic or attrs-based schema validation:

from pydantic import BaseModel, Field
from typing import Literal

class TerminalConfig(BaseModel):
    backend: Literal["local", "docker", "ssh", "modal", "daytona"] = "local"
    timeout: int = Field(default=180, ge=1, le=3600)
    container_memory: int = Field(default=5120, ge=256)
    # ... etc

class HermesConfig(BaseModel):
    model: Union[str, ModelConfig]
    terminal: TerminalConfig = Field(default_factory=TerminalConfig)
    # ... etc

3.3 HIGH: Add Async Support to Main CLI Loop

Location: main.py cmd_chat() function

Issue: The CLI runs synchronously, blocking on network I/O. This makes the UI unresponsive during API calls.

Recommendation: Refactor to use asyncio with prompt_toolkit's async support:

async def cmd_chat_async(args):
    # Enable concurrent operations during API waits
    # Show spinners, handle interrupts better
    # Allow background tasks (like update checks) to complete

3.4 MEDIUM: Implement Command Registry Plugin Architecture

Location: commands.py lines 46-135 (COMMAND_REGISTRY)

Issue: Commands are hardcoded in a list. Adding new commands requires modifying this central file.

Recommendation: Use entry_points for plugin discovery:

# In pyproject.toml
[project.entry-points."hermes_cli.commands"]
mycommand = "my_plugin.commands:register"

# In commands.py
import importlib.metadata

def load_plugin_commands():
    for ep in importlib.metadata.entry_points(group="hermes_cli.commands"):
        register_plugin_command(ep.load()())

3.5 MEDIUM: Add Comprehensive Logging Configuration

Location: All CLI modules

Issue: Inconsistent logging - some modules use logger, others use print(). No structured logging.

Recommendation: Implement structured JSON logging for machine parsing:

import structlog

logger = structlog.get_logger()
logger.info(
    "command_executed",
    command="gateway_start",
    provider="nous",
    duration_ms=2450,
    success=True
)

3.6 MEDIUM: Implement Connection Pooling for Auth Requests

Location: auth.py _refresh_access_token, _mint_agent_key

Issue: New httpx.Client created for every token operation. This is inefficient for high-throughput scenarios.

Recommendation: Use module-level connection pool with proper cleanup:

# At module level
_http_client: Optional[httpx.AsyncClient] = None

async def get_http_client() -> httpx.AsyncClient:
    global _http_client
    if _http_client is None:
        _http_client = httpx.AsyncClient(
            limits=httpx.Limits(max_connections=10),
            timeout=httpx.Timeout(30.0)
        )
    return _http_client

3.7 LOW: Add Type Hints to All Public Functions

Location: Throughout codebase

Issue: Many functions lack type hints, making IDE support and static analysis difficult.

Recommendation: Enforce mypy --strict compliance via CI:

# Add to CI
- name: Type check
  run: mypy --strict hermes_cli/

# Target: 100% type coverage for public APIs

3.8 LOW: Implement Config Hot-Reloading

Location: config.py

Issue: Config changes require process restart. Gateway and long-running CLI sessions don't pick up changes.

Recommendation: Add file watching with watchdog:

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class ConfigReloadHandler(FileSystemEventHandler):
    def on_modified(self, event):
        if event.src_path.endswith('config.yaml'):
            _config_cache.clear()
            logger.info("Config hot-reloaded")

Location: commands.py, integrate with cli.py

Issue: No persistent command history across sessions. No fuzzy matching for commands.

Recommendation: Use sqlite for persistent history with fuzzy finding:

# ~/.hermes/history.db
CREATE TABLE command_history (
    id INTEGER PRIMARY KEY,
    command TEXT NOT NULL,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    session_id TEXT
);

# Fuzzy search with sqlite FTS5

3.10 LOW: Implement Telemetry (Opt-in)

Location: New module telemetry.py

Issue: No visibility into CLI usage patterns, error rates, or performance.

Recommendation: Add opt-in telemetry with privacy-preserving metrics:

# Only if HERMES_TELEMETRY=1
metrics = {
    "command": "gateway_start",
    "provider_type": "nous",  # not the actual provider
    "duration_ms": 2450,
    "error_code": None,  # if success
}
# Send to telemetry endpoint with user consent

4. Five Potential Bug Locations

4.1 RACE CONDITION: Auth Store File Locking

Location: auth.py lines 480-536 (_auth_store_lock)

Risk: HIGH

Analysis: The file locking implementation has a race condition:

# Line 493-494
lock_path.parent.mkdir(parents=True, exist_ok=True)
# If parent dirs created by another process between check and lock acquisition,
# the lock may fail or be acquired by multiple processes.

Bug Scenario:

  1. Process A and B both try to acquire lock simultaneously
  2. Both create parent directories
  3. Both acquire locks on different file descriptors
  4. Both write to auth.json simultaneously
  5. Data corruption ensues

Fix: Use a single atomic mkdir with O_EXCL flag check.

4.2 TOKEN EXPIRATION: Clock Skew Not Handled

Location: auth.py lines 778-783 (_is_expiring)

Risk: HIGH

Analysis:

def _is_expiring(expires_at_iso: Any, skew_seconds: int) -> bool:
    expires_epoch = _parse_iso_timestamp(expires_at_iso)
    if expires_epoch is None:
        return True
    return expires_epoch <= (time.time() + skew_seconds)

Bug Scenario:

  • Client clock is 5 minutes fast
  • Token expires in 3 minutes (server time)
  • Client thinks token is valid for 8 more minutes
  • API calls fail with 401 Unauthorized

Fix: Add NTP sync check or server-time header parsing.

4.3 PATH TRAVERSAL: Config File Loading

Location: config.py load_config() function

Risk: MEDIUM

Analysis: The config loading doesn't validate path traversal:

# Line ~700 (estimated)
config_path = get_config_path()  # ~/.hermes/config.yaml
# If HERMES_HOME is set to something like "../../../etc/",
# config could be written outside intended directory

Bug Scenario:

HERMES_HOME=../../../etc hermes config set foo bar
# Writes to /etc/config.yaml

Fix: Validate HERMES_HOME resolves to within user's home directory.

4.4 SUBPROCESS INJECTION: Gateway Process Detection

Location: gateway.py lines 31-88 (find_gateway_pids)

Risk: MEDIUM

Analysis:

# Lines 65-67
result = subprocess.run(
    ["ps", "aux"],
    capture_output=True,
    text=True
)

Bug Scenario: If environment variables contain shell metacharacters in PATH, subprocess could execute arbitrary commands.

Fix: Use psutil library instead of shelling out to ps.

4.5 REGEX DoS: Command Argument Parsing

Location: commands.py line 250 (_PIPE_SUBS_RE)

Risk: LOW

Analysis:

_PIPE_SUBS_RE = re.compile(r"[a-z]+(?:\|[a-z]+)+")

Bug Scenario: A malformed command definition with excessive alternations could cause catastrophic backtracking:

args_hint = "a|a|a|a|a|a|a|a|a|a..." * 1000
# Regex engine hangs

Fix: Add length limit before regex matching, or use non-backtracking regex engine.


5. Security Audit Findings

5.1 SECURE: Credential Storage (GOOD)

Location: auth.py

Status: IMPLEMENTED WELL

Findings:

  • Auth file created with 0600 permissions (owner read/write only)
  • Uses atomic file replacement (write to temp, then rename)
  • Calls fsync() on file and directory for durability
  • Cross-process file locking prevents concurrent writes

5.2 SECURE: Environment Variable Handling (GOOD)

Location: config.py, env_loader.py

Status: IMPLEMENTED WELL

Findings:

  • API keys stored in ~/.hermes/.env, not config.yaml
  • .env file properly permissioned
  • Environment variable expansion is controlled

5.3 VULNERABILITY: Token Logging (MEDIUM RISK)

Location: auth.py lines 451-463 (_oauth_trace)

Status: ⚠️ PARTIAL EXPOSURE

Finding: Debug logging may leak token fingerprints:

def _oauth_trace(event: str, **fields: Any) -> None:
    # ... logs token fingerprints which could aid attackers
    payload.update(fields)
    logger.info("oauth_trace %s", json.dumps(payload))

Recommendation: Ensure HERMES_OAUTH_TRACE is never enabled in production, or hash values more aggressively.

5.4 VULNERABILITY: Insecure Deserialization (LOW RISK)

Location: auth.py lines 538-560 (_load_auth_store)

Status: ⚠️ REQUIRES REVIEW

Finding: Uses json.loads without validation:

raw = json.loads(auth_file.read_text())

Risk: If auth.json is compromised, malicious JSON could exploit known json.loads vulnerabilities (though rare in Python 3.9+).

Recommendation: Add schema validation before processing auth store.

5.5 VULNERABILITY: Certificate Validation Bypass

Location: auth.py lines 1073-1097 (_resolve_verify)

Status: ⚠️ USER-CONTROLLED RISK

Finding:

def _resolve_verify(insecure: Optional[bool] = None, ...):
    if effective_insecure:
        return False  # Disables SSL verification!

Risk: Users can disable SSL verification via env var or config, opening MITM attacks.

Recommendation: Add scary warning when insecure mode is used:

if effective_insecure:
    logger.warning("⚠️  SSL verification DISABLED - vulnerable to MITM attacks!")
    return False

5.6 SECURE: Input Sanitization (GOOD)

Location: commands.py

Status: IMPLEMENTED

Finding: Command parsing properly handles special characters and doesn't use shell=True in subprocess calls.

5.7 VULNERABILITY: Sensitive Data in Process List

Location: gateway.py, main.py

Status: ⚠️ EXPOSURE

Finding: Command-line arguments may contain API keys:

ps aux | grep hermes
# Shows: hermes chat --api-key sk-abc123...

Recommendation: Read API keys from environment or files only, never from command line arguments.


Summary Statistics

Metric Value
Total Lines of Code ~35,000+
Core Modules 35+
Entry Points 8
Supported Providers 15+
Slash Commands 40+
Test Coverage Unknown (tests exist in tests/hermes_cli/)

Conclusion

The Hermes CLI architecture is well-structured with clear separation of concerns:

Strengths:

  • Clean module organization
  • Comprehensive provider support
  • Good security practices for credential storage
  • Extensive configuration options
  • Strong backward compatibility

Areas for Improvement:

  • Race conditions in file locking need addressing
  • Type coverage could be improved
  • Async support would enhance UX
  • Plugin architecture would improve extensibility
  • Telemetry would help with debugging and optimization

The codebase shows signs of active development with regular additions for new providers and features. The security posture is generally good but has some edge cases around SSL verification and debug logging that should be addressed.