-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
The plugin registers on Hermes startup via register(ctx). During registration, it:
- Reads
~/.hermes/custom-dangerous-patterns.yaml(configurable path) - Compiles each user-defined regex pattern
- Appends
(pattern, description)tuples totools.approval.DANGEROUS_PATTERNS - Appends compiled regexes to
tools.approval.DANGEROUS_PATTERNS_COMPILED - Monkey-patches
detect_dangerous_command()to check allow patterns first
Hermes startup:
1. cli.py / run_agent.py starts
2. Plugin discovery → register(ctx) runs
→ reads config, compiles patterns
→ appends to DANGEROUS_PATTERNS / DANGEROUS_PATTERNS_COMPILED
→ monkey-patches detect_dangerous_command() for allow patterns
3. Tool discovery → terminal_tool.py imports approval.py
4. approval.py builds DANGEROUS_PATTERNS_COMPILED from DANGEROUS_PATTERNS
(our patterns are already in the list)
5. Agent runs → detect_dangerous_command() matches our patterns
6. Built-in approval flow handles once/session/always/deny
| Context | Behavior |
|---|---|
| CLI | Interactive [o]nce/[s]ession/[a]lways/[d]eny prompt |
| Gateway (Telegram/Discord/etc.) |
/approve and /deny commands, async approval queue |
| Session persistence | "Session" choice survives for the session duration |
| Permanent allowlist | "Always" choice persists to command_allowlist in config.yaml
|
| Smart mode | If approvals.mode: smart, auxiliary LLM assesses custom patterns |
| Cron | Respects approvals.cron_mode (deny by default) |
--yolo |
Custom patterns bypassed (they're DANGEROUS_PATTERNS, not HARDLINE) |
hermes-custom-dangerous-patterns-plugin/
├── plugin.yaml # Hermes plugin manifest
├── __init__.py # register(ctx) — injects patterns, monkey-patches detection
├── config.py # YAML loading, validation, caching
├── patterns.py # Pattern compilation and allow-pattern matching
├── examples/
│ └── custom-dangerous-patterns.yaml # Example config
├── README.md # User-facing documentation
├── SPEC.md # Design spec
├── LICENSE # MIT
└── .gitignore
| Module | Responsibility |
|---|---|
__init__.py |
Plugin entry point. Calls register(ctx) to inject patterns and monkey-patch detection. |
config.py |
Loads and validates ~/.hermes/custom-dangerous-patterns.yaml. Caches result per-process. |
patterns.py |
Compiles raw config patterns into (compiled_regex, description) tuples. Provides is_allow_pattern() for the monkey-patch. |
from .config import load_config # correct
from config import load_config # fails — Python can't find top-level moduleHermes loads plugins as hermes_plugins.<slug> packages. Absolute imports against plugin-local modules will raise ModuleNotFoundError.
The built-in detect_dangerous_command() doesn't have an allow-pattern concept. Without the monkey-patch, the plugin could inject block patterns but couldn't exempt commands from them. The alternative — registering an approval hook — wouldn't work because:
-
pre_approval_requestis observer-only (return values ignored) - By the time the hook fires, the command is already flagged as dangerous
The monkey-patch is clean: it wraps the original, checks allow patterns first, and falls through to the original for everything else.
The plugin doesn't register any pre_tool_call or post_tool_call hooks. All work happens at startup via pattern injection and monkey-patching. This is simpler and avoids the hook allowlisting ceremony.
patterns.py tries to import tools.ansi_strip.strip_ansi for ANSI normalization, falling back to a regex if running outside Hermes. config.py tries hermes_constants.get_hermes_home(), falling back to Path.home() / ".hermes".
The plugin never crashes the agent on bad config.
The module-level _config_cache in config.py avoids re-reading and re-validating the YAML on repeated calls. The force=True parameter exists only for testing — mid-session config edits are silently ignored.
The plugin's injected patterns participate in Hermes's existing approval system. There is no custom persistence logic — it's all handled by tools/approval.py:
_session_approved: dict[str, set]:
- Keyed by
session_key(derived from gateway session or CLI process) - Each key maps to a
setofpattern_keystrings (the human-readable description) - Populated by
approve_session()when user chooses[s]ession - Lives only in process memory — cleared when the session ends
- Thread-safe via
_lock
_permanent_approved: set:
- Process-global
setofpattern_keystrings - Populated by
approve_permanent()when user chooses[a]lways - Persisted to
~/.hermes/config.yamlundercommand_allowlist: [...] - Reloaded at startup
- Survives restarts; entries are keyed by
pattern_key(description string)
# When user approves "vultr instance delete":
# pattern_key = "Vultr destructive instance/snapshot command" (the description)
# This key is stored in _session_approved[session_key] and/or _permanent_approved
# Future calls to detect_dangerous_command for the same pattern return the same keyThe approval system also exposes:
-
pre_approval_request(command, description, pattern_key, pattern_keys, session_key, surface)— fired when an approval is first requested -
post_approval_response(..., choice)— fired after user responds withonce/session/always/deny/timeout
These are observer-only — return values are ignored; plugins cannot veto.
| Scenario | Behavior |
|---|---|
| Config file missing | Plugin loads silently, no patterns injected, log message at INFO |
| Config file invalid YAML | Log WARNING, plugin loads with empty pattern list |
| Invalid regex in pattern | Log WARNING for that pattern, skip it, load valid ones |
| Pattern matches but allow also matches | Allow wins — no prompt |
--yolo mode |
Custom patterns bypassed (they're DANGEROUS_PATTERNS, not HARDLINE) |
approvals.mode: off |
Custom patterns bypassed |
approvals.mode: smart |
Custom patterns assessed by auxiliary LLM |
Cron session + cron_mode: deny
|
Custom patterns blocked in cron |
| Container backend (docker, etc.) | All approval checks skipped (container is sandboxed) |
command_allowlist "always" choice |
Persisted to config.yaml — survives restarts |
| Plugin loads after approval.py | Patterns not injected (import order dependency — plugins load before tools) |