# 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**: ```python # 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: ```python 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: ```python 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: ```python # 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: ```python 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: ```python # 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: ```python # 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: ```python 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") ``` ### 3.9 LOW: Add Command History and Fuzzy Search **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: ```python # ~/.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: ```python # 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: ```python # 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**: ```python 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: ```python # 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**: ```bash 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**: ```python # 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**: ```python _PIPE_SUBS_RE = re.compile(r"[a-z]+(?:\|[a-z]+)+") ``` **Bug Scenario**: A malformed command definition with excessive alternations could cause catastrophic backtracking: ```python 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: ```python 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: ```python 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**: ```python 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: ```python 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: ```bash 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.