Skip to content

Integration and support for Hermes agent #428

Description

What is Hermes Agent

Hermes Agent is Nous Research's open-source, self-hosted, self-improving agent (curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash, global hermes command). Unlike a plain chat model, it runs persistent sessions with long-term memory (~/.hermes/MEMORY.md), a skill curator that writes and reuses its own skills, tool/code execution, browser automation, MCP servers, and delivery through 16+ messaging platforms. That autonomy and persistence is exactly why it's worth having as a first-class red-team target.

Proposed approach: CLI-driven provider, same shape as Claude Code

Hermes has no OpenAI-compatible HTTP endpoint, but it does have a documented one-shot headless mode:

hermes -z "prompt"        # final response only, no decorations — ideal for automation
hermes chat -q "prompt"   # one-shot, includes tool-call transcript

This is the same shape as claude -p in router/providers/claude.py, so the integration should follow that provider almost line for line rather than invent a new pattern:

  • Add HERMES = "HERMES" to AgentTypeEnum in router/types.py, plus common aliases ("HERMES_AGENT", "HERMES_CLI") in its _missing_ alias table.
  • New router/providers/hermes.py:
    • A per-instance litellm.CustomLLM (_HermesCustomLLM) registered under hackagent_hermes_<id>, exactly like _ClaudeCodeCustomLLMcompletion() shells out to hermes -z, feeds the prompt via stdin (never argv, so adversarial payloads starting with - aren't parsed as flags), and maps stdout/exit code onto ModelResponse.
    • HermesAgent(Agent) with ADAPTER_TYPE = "HermesAgent", constructed like ClaudeCodeAgent: validates the hermes binary is on PATH (shutil.which) at construction time, builds the argv, registers the custom provider, exposes litellm_model = f"{provider_name}/{model}".
  • Wire it into AGENT_TYPE_TO_ADAPTER_MAP in router/router.py (AgentTypeEnum.HERMES: HermesAgent), next to CLAUDE_CODE and CODEX.

Config surface (HermesAgent.__init__)

key maps to notes
name (required) -m <model> overrides the configured default model for this run only
binary (default hermes) argv[0] checked with shutil.which at construction
timeout (default e.g. 300s) subprocess.run(..., timeout=) Hermes can trigger tool/browser use, so this likely needs a higher default than Claude Code's
provider --provider <provider> optional per-run provider override
cwd subprocess.run(..., cwd=) working dir Hermes operates in (skills, worktrees, file tools)
extra_args appended raw flags escape hatch, same as Claude Code

Reproducibility and isolation — the part that's different from Claude Code

Claude Code is stateless per invocation; Hermes is explicitly not — it has persistent memory, a background skill curator, and session continuation. Left on defaults, red-teaming a real Hermes install would (a) let it "learn" from being probed, biasing later attack turns, and (b) pollute the user's actual ~/.hermes state. The provider should default to flags that neutralize this rather than leave it to the caller to discover:

  • Always pass --ignore-user-config (defaults + .env credentials only, skip ~/.hermes/config.yaml) and never pass -r/--resume/-c/--continue, so every attack turn is a fresh, isolated session by default.
  • Consider --safe-mode (disables all customizations) as an opt-in for maximum isolation, and/or driving attacks through hermes profile so the target under test never touches the operator's real profile/memory/skills.
  • --pass-session-id / --source hackagent are worth passing so Hermes-side logs are attributable to hackagent runs during debugging.

Output parsing

hermes -z gives bare text on stdout with no structured metadata (no session id, cost, exit reason) — simpler than Claude Code's --output-format json, but it means the adapter can't currently distinguish "the agent declined" from "the agent errored" the way _extract_result_text does for Claude Code. Needs investigation before implementation: does any one-shot mode support a --json-style result envelope, or does the provider need to rely on exit codes only (0 success, 1 delivery/backend failure, 2 usage error per the CLI reference)? If not, non-zero exit + non-empty stdout should still be captured as a content-level response (mirroring the Claude Code refusal-capture logic), not treated as a hard failure.

Out of scope for the first pass

hermes serve exposes a headless backend over JSON-RPC/WebSocket (rather than plain HTTP), which would let hackagent target a remotely deployed Hermes instance the way ADKAgent does over HTTP. That's a reasonable phase 2 once the local CLI-driven path is proven, but is a different transport (needs a WS/JSON-RPC client, not just subprocess) and shouldn't block this issue.

Acceptance criteria

  • AgentTypeEnum.HERMES + aliases added
  • router/providers/hermes.py with HermesAgent + _HermesCustomLLM, isolation flags on by default
  • Wired into AGENT_TYPE_TO_ADAPTER_MAP
  • Missing-binary and timeout paths raise the same AdapterConfigurationError/AdapterInteractionError shape as the other CLI providers
  • Test coverage mirroring tests/unit/router/test_claude_agent.py (mock subprocess.run, assert argv construction and isolation flags, assert refusal/error handling)
  • Example under hackagent/examples/ showing a Hermes target

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions