Hermes Agent: Deep Technical Research
A source-code-level analysis of NousResearch's self-evolving AI agent | NousResearch/hermes-agent | MIT | v0.7.0 (2026-04-03)
1. Executive Summary
Hermes Agent by Nous Research is not another IDE copilot. It's a persistent, self-improving AI agent with a unique closed learning loop — it autonomously creates skills from experience, improves them during use, curates persistent memory, searches its own past conversations, and builds a deepening model of its user across sessions.
Key metrics:
- ~30K GitHub Stars | 242+ Contributors | 7 releases in 5 weeks
- Core:
run_agent.py(9,423 LOC) |cli.py(8,615 LOC) - 47 built-in tools | 70+ bundled skills | 14+ messaging platforms | 6 execution backends | 18+ LLM providers
This report is based on direct source code reading, not documentation alone.
2. Architecture Overview
2.1 Three-Tier Design
┌──────────────────────────────────────────────────┐
│ User Interface Layer │
│ CLI (cli.py) │ Gateway (gateway/run.py) │ ACP │
│ Batch Runner │ Cron Scheduler │ API Server │
├──────────────────────────────────────────────────┤
│ Core Agent Layer │
│ AIAgent (run_agent.py) — Central Orchestrator │
│ ├── _build_system_prompt() Prompt Assembly │
│ ├── run_conversation() Main Agent Loop │
│ ├── _spawn_background_review() BG Learning Loop │
│ ├── ContextCompressor Context Management │
│ └── IterationBudget Resource Control │
├──────────────────────────────────────────────────┤
│ Tool & Execution Layer │
│ ToolRegistry (tools/registry.py) │
│ 47 tools × 6 execution backends │
│ (Local/Docker/SSH/Modal/Daytona/Singularity) │
└──────────────────────────────────────────────────┘
2.2 The Core Loop: run_conversation() (run_agent.py:6882)
This is the heart of the system. From source code analysis:
User Message → Surrogate sanitization
→ Restore primary runtime (if fallback occurred last turn)
→ System prompt assembly (cached, built only once)
→ Preflight context compression (if history exceeds threshold)
→ Plugin pre_llm_call hooks
→ External memory provider prefetch
╔══════════════════════════════════╗
║ Main Loop (while budget > 0) ║
║ 1. Check interrupt requests ║
║ 2. Consume iteration budget ║
║ 3. Inject ephemeral context ║
║ 4. Apply Anthropic cache control ║
║ 5. Sanitize orphaned tool_calls ║
║ 6. API call (with retry logic) ║
║ 7. Process tool_calls ║
║ 8. Append results to messages ║
║ 9. Check context compression need ║
╚══════════════════════════════════╝
→ External memory sync + queue_prefetch
→ Background memory/skill review
→ Plugin on_session_end hooks
→ Return result
Notable implementation details:
- Thread-safe iteration budget —
IterationBudgetusesthreading.Lock. Parent agents get 90 iterations, child agents get 50. - System prompt built exactly once, cached on
_cached_system_prompt. For continued sessions, it's loaded from SessionDB rather than rebuilt — preserving Anthropic prefix cache. - Oversized tool results (>100K chars) are saved to temp files with a 1,500-char inline preview.
- Parallel tool execution has strict safety checks —
_should_parallelize_tool_batch()validates against a whitelist of parallel-safe tools and checks for path overlaps on file operations.
3. The Four-Layer Memory Architecture (Core Innovation)
This is what sets Hermes apart from every other agent framework.
3.1 Overview
| Layer | Storage | Behavior | Role |
|---|---|---|---|
| Prompt Memory | MEMORY.md + USER.md | Always injected into system prompt, capped at 3,575 chars | Semantic (what matters) |
| Session Search | SQLite FTS5 | On-demand retrieval with LLM summarization | Episodic (what happened) |
| Skills | ~/.hermes/skills/ dirs | Progressive disclosure — names+summaries by default, full content on demand | Procedural (how to do it) |
| Honcho | External dialectic engine | Passive user modeling across 12 identity layers | Relational (who you are) |
3.2 Prompt Memory Implementation (tools/memory_tool.py)
The most critical memory layer. Key design decisions from source:
class MemoryStore:
def __init__(self, memory_char_limit=2200, user_char_limit=1375):
# Total cap: 3,575 chars ≈ ~900 tokens
# Deliberate constraint: forces curation over accumulation
Frozen Snapshot Pattern:
- At session start, MEMORY.md and USER.md are loaded and a snapshot is captured for system prompt injection
- Mid-session writes persist to disk immediately but do not modify the system prompt
- Reason: preserving prefix cache stability. A precise trade-off between recency and cost efficiency
Security scanning on every write:
_MEMORY_THREAT_PATTERNS = [
(r'ignore\s+(previous|all|above|prior)\s+instructions', "prompt_injection"),
(r'curl\s+[^
]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)', "exfil_curl"),
(r'authorized_keys', "ssh_backdoor"),
# ... more patterns
]
Memory content is injected into the system prompt, making it a critical security boundary. Every write is scanned for injection and exfiltration patterns before persistence.
3.3 Session Search (tools/session_search_tool.py)
FTS5-powered retrieval with LLM summarization:
FTS5 search → Group by session → Top 3 unique sessions
→ Load conversation → Truncate to ~100K chars around matches
→ Auxiliary LLM summarization → Return structured summaries
This retrieve-then-summarize pattern avoids stuffing raw conversation history into the context window.
3.4 Periodic Memory/Skill Nudge Mechanism
The nudge system is the engine of the learning loop:
# run_agent.py
self._memory_nudge_interval = 10 # Every 10 user turns → memory review
self._skill_nudge_interval = 10 # Every 10 tool iterations → skill review
When triggered, _spawn_background_review():
- Creates a full AIAgent fork (shared memory store, own iteration budget)
- Uses current conversation history as context
- Appends a review prompt as the "user message"
- Runs in a background daemon thread — never blocks user interaction
- The review agent has access to memory and skill_manage tools
- On completion, surfaces a compact summary: "💾 Memory updated · Skill created"
4. Skills System (Procedural Memory)
4.1 Skill Structure
~/.hermes/skills/
├── systematic-debugging/
│ └── SKILL.md # YAML frontmatter + Markdown instructions
├── software-development/
│ ├── test-driven-development/
│ │ └── SKILL.md
│ └── subagent-driven-development/
│ └── SKILL.md
└── category/skill-name/
├── SKILL.md
├── references/ # Reference materials
├── templates/ # Template files
├── scripts/ # Scripts
└── assets/ # Assets
Skills use the agentskills.io open standard — a strategic bet on cross-agent portability.
4.2 Progressive Disclosure
Skills are NOT loaded all at once:
# agent/prompt_builder.py - build_skills_system_prompt()
# Default: only skill names + description summaries (dozens of bytes per skill)
# Full content loaded only when the agent determines relevance
# Token usage stays flat regardless of skill count (70+ skills, no bloat)
4.3 Self-Improving Skills
Skills are mutable. The agent can modify them through 6 actions:
# tools/skill_manager_tool.py
# Actions: create, edit, patch, delete, write_file, remove_file
# Default: patch (targeted find-and-replace) over edit (full rewrite)
Trigger conditions (from SKILLS_GUIDANCE):
- Complex task completion (5+ tool calls)
- Tricky error resolution
- Non-obvious workflow discovery
Security: Agent-created skills undergo the same security scanning as community skills (skills_guard.py), detecting exfiltration, injection, destructive commands, and persistence patterns.
5. Tool System
5.1 Registry Pattern (tools/registry.py)
class ToolRegistry:
"""Singleton registry collecting schemas + handlers from tool files"""
def register(self, name, toolset, schema, handler, check_fn=None, ...):
"""Called at module-import time by each tool file"""
def get_definitions(self, tool_names, quiet=False):
"""Returns OpenAI-format tool schemas (only tools passing check_fn)"""
def dispatch(self, name, args, **kwargs):
"""Execute tool handler by name (auto-bridges async handlers)"""
Self-registration decouples tool implementations from orchestration logic. Each tool file calls registry.register() at import time — no central manifest to maintain.
5.2 47 Built-in Tools (20 Toolsets)
| Toolset | Key Tools | Description |
|---|---|---|
| terminal | terminal | Command execution across 6 backends |
| browser | browser_* (11 tools) | Playwright + Camofox anti-detection browser |
| web | web_search, web_extract | Search and content extraction |
| file | read_file, write_file, patch, search_files | File operations |
| code_execution | execute_code | Sandboxed code execution |
| delegation | delegate_task | Subagent spawning |
| memory | memory | Persistent memory management |
| skills | skills_list, skill_view, skill_manage | Skill system |
| session_search | session_search | Historical session search |
| vision | vision_analyze | Image analysis |
| image_generation | image_generate | Image generation (fal.ai) |
| tts | text_to_speech | TTS (edge-tts / ElevenLabs) |
| mcp | dynamic | MCP protocol extension |
| cron | cron_* | Scheduled tasks |
5.3 Dangerous Command Approval System (tools/approval.py)
DANGEROUS_PATTERNS = [
(r'\brm\s+-[^\s]*r', "recursive delete"),
(r'\bchmod\s+(-[^\s]*\s+)*(777|666)', "world-writable permissions"),
(r'\bDROP\s+(TABLE|DATABASE)\b', "SQL DROP"),
(r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:', "fork bomb"),
(r'\b(curl|wget)\b.*\|\s*(ba)?sh\b', "pipe remote content to shell"),
# ... 30+ patterns
]
Key features:
- Command normalization: Strips ANSI escapes, null bytes, Unicode fullwidth characters before matching — prevents bypass via encoding tricks
- Three-tier approval: once / session / always (permanent allowlist in config.yaml)
- Gateway async approval: Threading.Event queues enable cross-thread blocking approval in gateway mode
- Smart approval: Optional auxiliary LLM evaluates low-risk commands for auto-approval
6. Execution Backends
6.1 Strategy Pattern (tools/environments/base.py)
class BaseEnvironment(ABC):
"""Common interface for all Hermes execution backends"""
@abstractmethod
def execute(self, command, cwd="", *, timeout=None, stdin_data=None) -> dict:
"""Execute command, return {"output": str, "returncode": int}"""
@abstractmethod
def cleanup(self):
"""Release backend resources"""
Six backends behind a single interface, switchable via configuration:
| Backend | Use Case |
|---|---|
| Local | Direct host execution (default, fastest) |
| Docker | Isolated containers (read-only root, dropped capabilities) |
| SSH | Remote server execution |
| Modal | Serverless cloud sandboxes (hibernates when idle) |
| Daytona | Managed workspace environments |
| Singularity | HPC/research environments |
7. Messaging Gateway (14+ Platforms)
A single gateway process manages adapters for: Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email (IMAP/SMTP), SMS, DingTalk, Feishu/Lark, WeCom, Home Assistant, and Webhooks.
Conversation context persists across platforms — start on Telegram, continue on Discord, finish via Email.
Key gateway hardening in v0.7.0: race condition fixes, flood control, stuck session recovery, approval routing, and compression death spiral prevention.
8. Prompt Caching Strategy
8.1 Anthropic Prefix Caching (agent/prompt_caching.py)
def apply_anthropic_cache_control(api_messages, cache_ttl="5m"):
"""system_and_3 strategy: system prompt + last 3 non-system messages"""
# Anthropic allows max 4 cache_control breakpoints
# 1. System prompt (stable across all turns)
# 2-4. Last 3 non-system messages (rolling window)
Why is the system prompt never modified mid-session?
This is a hard architectural rule, enforced throughout the codebase:
_cached_system_promptis built once, reused thereafter- Continued sessions load stored system prompts from SessionDB (not rebuilt)
- External memory context injected into user messages, not system prompt
- Plugin context also injected into user messages
Result: ~75% reduction in input token costs.
9. Context Compression (agent/context_compressor.py)
class ContextCompressor:
"""Algorithm:
1. Prune old tool results (cheap, no LLM call)
2. Protect head messages (system prompt + first exchange)
3. Protect tail messages by token budget (~20K tokens)
4. Summarize middle turns with structured LLM prompt
5. Iteratively update previous summary on subsequent compactions
"""
Key parameters:
threshold_percent = 0.50— triggers at 50% context usage_SUMMARY_RATIO = 0.20— summary gets 20% of compressed content's token count_SUMMARY_TOKENS_CEILING = 12,000— absolute cap on summary tokens- Structured summary template: Goal, Progress, Decisions, Files, Next Steps
10. Subagent Delegation (tools/delegate_tool.py)
DELEGATE_BLOCKED_TOOLS = frozenset([
"delegate_task", # No recursive delegation
"clarify", # No user interaction
"memory", # No writes to shared MEMORY.md
"send_message", # No cross-platform side effects
"execute_code", # Children should reason step-by-step
])
MAX_CONCURRENT_CHILDREN = 3
MAX_DEPTH = 2 # parent(0) → child(1) → grandchild rejected
DEFAULT_MAX_ITERATIONS = 50
Design: fresh context (no parent history), restricted toolsets, independent iteration budget. Parent only sees the delegation call and summary result — never intermediate steps.
11. Security Architecture
11.1 Defense in Depth
| Layer | Mechanism | Implementation |
|---|---|---|
| Input | Prompt injection scanning | prompt_builder.py — 10 threat patterns + invisible char detection |
| Memory | Content scanning on write | memory_tool.py — injection/exfil pattern detection |
| Skills | Pre-install security scan | skills_guard.py — trust levels + threat patterns |
| Commands | Dangerous command approval | approval.py — 30+ patterns + command normalization |
| Tools | Output size limits | 100K char threshold, overflow saved to file |
| Execution | Container isolation | Docker: read-only root, dropped capabilities |
| Output | Secret exfiltration detection | Browser URLs and LLM responses scanned |
11.2 Invisible Character Detection
_CONTEXT_INVISIBLE_CHARS = {
'\u200b', # Zero-width space
'\u200c', # Zero-width non-joiner
'\u200d', # Zero-width joiner
'\u2060', # Word joiner
'\ufeff', # Zero-width no-break space (BOM)
'\u202a'-'\u202e', # Bidi override characters
}
These characters can be used to hide prompt injection payloads. They're detected and blocked in context files, memory entries, and skill content.
12. Credential Pool (agent/credential_pool.py)
STRATEGY_FILL_FIRST = "fill_first"
STRATEGY_ROUND_ROBIN = "round_robin"
STRATEGY_RANDOM = "random"
STRATEGY_LEAST_USED = "least_used" # default
EXHAUSTED_TTL_429_SECONDS = 60 * 60 # Rate limit: 1 hour cooldown
EXHAUSTED_TTL_DEFAULT_SECONDS = 24 * 60 * 60 # Other failures: 24 hour cooldown
Multiple API keys per provider with thread-safe least_used rotation and automatic 401 failover. Production-grade credential management for high-throughput deployments.
13. Tech Stack
Core Language: Python 3.11+ (93.4%)
Package Manager: uv (Astral)
Database: SQLite + FTS5
CLI UI: Rich + prompt_toolkit + KawaiiSpinner
LLM SDKs: openai + anthropic
Web: httpx + requests + exa-py + firecrawl-py
Browser: Playwright + Camoufox
TTS: edge-tts (free) + ElevenLabs (premium)
STT: faster-whisper
Config: YAML (pyyaml)
MCP: mcp >= 1.2.0
Testing: pytest (3,000+ test cases)
Build: Nix Flake (reproducible builds)
14. Competitive Analysis
| Dimension | Hermes Agent | OpenClaw (OpenHands) | Claude Code |
|---|---|---|---|
| Focus | Self-evolving learning agent | Broad ecosystem integration | Developer coding assistant |
| Memory | 4-layer (prompt/episodic/procedural/dialectic) | Basic context | File-backed notes |
| Skills | agentskills.io portable standard, self-creating+improving | Plugin-based | Tool-use only |
| Messaging | 14 platforms, single gateway | Limited | Telegram/Discord |
| Execution | 6 backends | Primarily local | Local |
| Cost | LLM API only; runs on $5 VPS | $59/mo or self-host | Subscription |
| Security | Command approval + injection scanning + Docker sandbox | Standard | Permission prompts |
| Open Source | MIT | Open source | Proprietary |
15. Key Design Insights
15.1 The 3,575-Character Prompt Memory Cap
memory_char_limit=2200 + user_char_limit=1375 = 3,575 characters ≈ ~900 tokens.
Why so small? This is a deliberate constraint that forces curation over accumulation. Unbounded growth turns memory into a garbage dump. The cap forces the agent to retain only genuinely valuable information — making every character count.
15.2 Frozen Snapshot vs. Real-Time Updates
System prompt memory is a snapshot from session start. Mid-session writes update disk but don't change the active system prompt. This sacrifices memory recency for 75% cache cost savings. For most use cases, this is the right trade-off.
15.3 Background Review, Not Inline Review
Memory and skill review runs in background daemon threads via _spawn_background_review(). Users never wait for review to complete. Review results only take effect in the next session (due to frozen snapshot). This is architecturally clean: the learning loop is decoupled from the task execution loop.
15.4 The agentskills.io Bet
Skills use YAML frontmatter + Markdown, portable across compatible agent frameworks. If widely adopted, this becomes the "npm for agent skills" — a significant ecosystem play.
15.5 Full-Chain Prompt Injection Defense
From context file loading → memory writes → skill installation → command execution, every layer has independent injection detection. This isn't a security afterthought — it's woven into the architecture from the ground up.
16. Conclusion
Hermes Agent's core innovation is not any single feature, but a complete self-evolution loop:
Task Execution → Experience Accumulation → Memory Curation → Skill Creation
↑ ↓
← ← ← ← Skill Improvement ← Skill Retrieval ← ← ← ←
Every component of this loop has careful engineering:
- Memory has capacity constraints and security scanning
- Skills have progressive disclosure and self-improvement
- Review runs in background without blocking users
- Caching strategy permeates the entire architecture
7 releases in 5 weeks, 30K stars, 242 contributors — this project has found a real need in the open-source AI agent space: a general-purpose agent framework that isn't tied to a specific IDE, isn't locked to a specific LLM, and actually learns from use.
Based on v0.7.0 source code (commit 2026-04-03) deep reading analysis | Analysis date: 2026-04-07