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 _ClaudeCodeCustomLLM — completion() 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
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, globalhermescommand). 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:
This is the same shape as
claude -pinrouter/providers/claude.py, so the integration should follow that provider almost line for line rather than invent a new pattern:HERMES = "HERMES"toAgentTypeEnuminrouter/types.py, plus common aliases ("HERMES_AGENT","HERMES_CLI") in its_missing_alias table.router/providers/hermes.py:litellm.CustomLLM(_HermesCustomLLM) registered underhackagent_hermes_<id>, exactly like_ClaudeCodeCustomLLM—completion()shells out tohermes -z, feeds the prompt via stdin (never argv, so adversarial payloads starting with-aren't parsed as flags), and maps stdout/exit code ontoModelResponse.HermesAgent(Agent)withADAPTER_TYPE = "HermesAgent", constructed likeClaudeCodeAgent: validates thehermesbinary is onPATH(shutil.which) at construction time, builds the argv, registers the custom provider, exposeslitellm_model = f"{provider_name}/{model}".AGENT_TYPE_TO_ADAPTER_MAPinrouter/router.py(AgentTypeEnum.HERMES: HermesAgent), next toCLAUDE_CODEandCODEX.Config surface (
HermesAgent.__init__)name(required)-m <model>binary(defaulthermes)shutil.whichat constructiontimeout(default e.g. 300s)subprocess.run(..., timeout=)provider--provider <provider>cwdsubprocess.run(..., cwd=)extra_argsReproducibility 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
~/.hermesstate. The provider should default to flags that neutralize this rather than leave it to the caller to discover:--ignore-user-config(defaults +.envcredentials only, skip~/.hermes/config.yaml) and never pass-r/--resume/-c/--continue, so every attack turn is a fresh, isolated session by default.--safe-mode(disables all customizations) as an opt-in for maximum isolation, and/or driving attacks throughhermes profileso the target under test never touches the operator's real profile/memory/skills.--pass-session-id/--source hackagentare worth passing so Hermes-side logs are attributable to hackagent runs during debugging.Output parsing
hermes -zgives 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_textdoes 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 (0success,1delivery/backend failure,2usage 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 serveexposes a headless backend over JSON-RPC/WebSocket (rather than plain HTTP), which would let hackagent target a remotely deployed Hermes instance the wayADKAgentdoes 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 justsubprocess) and shouldn't block this issue.Acceptance criteria
AgentTypeEnum.HERMES+ aliases addedrouter/providers/hermes.pywithHermesAgent+_HermesCustomLLM, isolation flags on by defaultAGENT_TYPE_TO_ADAPTER_MAPAdapterConfigurationError/AdapterInteractionErrorshape as the other CLI providerstests/unit/router/test_claude_agent.py(mocksubprocess.run, assert argv construction and isolation flags, assert refusal/error handling)hackagent/examples/showing a Hermes target