A fail-open Hermes directory plugin that classifies firewall lifecycle events with the Silmaril Security SDK. Shadow is silent, Warn adds one bounded warning to supported same-turn context, and Block vetoes malicious pre-tool calls. Completed tool or LLM output is never replaced; unsupported Block boundaries are replaced with fixed, content-free output at Hermes transform hooks.
Recommended install from GitHub:
hermes plugins install Silmaril-Security/HermesFirewallPlugin --enable
hermes gateway restartThe plugin requires the Silmaril Security SDK in the same Python environment that runs Hermes:
pip install silmaril-security-sdk==0.6.0Copy .env.example into your Hermes environment manager or shell profile and
replace placeholder values there. Do not commit real API keys.
Local development install on macOS/Linux:
git clone https://github.com/Silmaril-Security/HermesFirewallPlugin.git
cd HermesFirewallPlugin
hermes plugins install "file://$(pwd)" --force --enable
hermes gateway restartLocal development install on PowerShell:
$uri = (Get-Item .).FullName.Replace('\', '/')
hermes plugins install "file:///$uri" --force --enable
hermes gateway restartThe plugin registers seven hooks:
pre_llm_callclassifies the user message withHookLabel.USER_INPUT.pre_tool_callclassifies the tool name and arguments withHookLabel.TOOL_CALL. It returnsNoneby default so the call is allowed, or{"action": "block", "message": "..."}when the effective mode is Block and the classifier returns a malicious result.post_tool_callclassifies the tool result withHookLabel.TOOL_RESPONSEand remains observe-only.transform_tool_resultreuses the matchingpost_tool_callclassification. Warn appends bounded context; Block replaces the result before model reuse.transform_llm_outputclassifies the final assistant response withHookLabel.LLM_OUTPUTand replaces malicious Block output before delivery.subagent_startclassifies the child goal withHookLabel.USER_INPUTfor visibility.subagent_stopclassifies the child summary withHookLabel.LLM_OUTPUTfor visibility.
Each native Hermes event produces at most one classification. pre_llm_call
accepts the host's conversation_history argument for compatibility but ignores
it; conversation state is owned by the Firewall sequence cache.
The SDK client omits mode unless a pilot override is configured, so the backend selects the effective mode by default. SDK import, configuration, network, API, malformed payload, empty payload, and classification failures are logged and fail open. During a rolling backend upgrade, an explicit override remains authoritative and a mode-less legacy response preserves the plugin's observe-only default instead of escalating to Block.
Only the exact prediction MALICIOUS is enforceable. Scores, thresholds,
outcomes, missing predictions, and unknown predictions remain diagnostic. A
matching post/transform tool callback pair makes one SDK request, and changed
content receives a distinct content-sensitive identity. Child lifecycle events
use the child session as their conversation identity; other events use the
normal Hermes session. Large strings preserve both their head and tail when
sanitized for classification.
Each successful SDK call logs an sdk_result line containing event type, hook
label, tool name, tool call id when present, prediction, readable risk category,
and whether the SDK classified the event as blocked. Raw prompts, tool
arguments, tool outputs, assistant text, classifier scores, thresholds, detector
maps, and raw decision JSON are not emitted in structured logs or model-visible
context.
Every completed classification also writes one bounded LocalProtectionEventV1 JSON
record to the private local evidence spool. The record contains only redacted
metadata, opaque request/session fingerprints, decision facts, native action,
and plugin provenance. It never contains raw prompts, arguments, results,
assistant output, credentials, detector maps, or error bodies. Events always
report outcome=not_observed. Allowed and monitored actions report
evidenceTruth=plugin_reported; returned Hermes native vetoes report
evidenceTruth=native_response_returned. Neither value claims the downstream
consequence was independently prevented.
Writes are synchronous, per-event, and atomic, with 0700 directory and 0600
file permissions. Evidence failures log only the error type and never change
the Hermes hook return value. By default the plugin writes directly to:
~/Library/Application Support/Silmaril/Evidence/incoming
Required environment variables:
SILMARIL_API_KEY: API key for the Silmaril Firewall classify API.SILMARIL_API_URL: tenant/classifyendpoint for the Silmaril Firewall API.
Optional environment variables:
SILMARIL_ENDPOINT_IDis the canonical UUID v4 supplied by the Silmaril endpoint app.HERMES_FIREWALL_SDK_TIMEOUT_SECONDScontrols the SDK request timeout. Default:2.0.HERMES_FIREWALL_SDK_MAX_RETRIEScontrols SDK retries. Default:0.HERMES_FIREWALL_MAX_PAYLOAD_CHARScaps large string fields. Default:8000.SILMARIL_MODEoptionally overrides the backend withshadow,warn, orblock.HERMES_FIREWALL_BLOCK_MALICIOUSis the deprecated legacy mapping; true maps to Block and false maps to Shadow.SILMARIL_LOCAL_EVENT_DIRdirectly overrides the incoming evidence directory. It is primarily intended for testing and nonstandard installations.
Configuration precedence is Hermes-native and environment-based: the hook reads the process environment used by Hermes at call time. There is no local config file parser, credential service, or fallback that writes secrets into plugin state.
Every classifier request carries plugin-owned metadata.silmaril.provenance. If SILMARIL_ENDPOINT_ID is absent, the plugin continues with harness-only provenance.
Hermes supports pre-execution vetoes through pre_tool_call. Transform hooks
can deliver Warn context but never replace completed content in Block mode.
post_tool_call, subagent_start, and subagent_stop remain observe-only
because those Hermes hooks have no enforcement return channel. Unsafe delegation
is blocked at the nearest enforceable gate: the delegate_task tool call
(DELEGATION_TOOL_NAME in the plugin) is classified and vetoed by
pre_tool_call before the child agent starts. Child
agent prompts, tool calls, tool results, and final outputs are scanned through
the same normal hook path used for parent sessions.
By default, neither mode variable is set and the backend selects the effective mode.
To enable enforcement at supported boundaries:
SILMARIL_MODE=blockWhen enabled, malicious pre_tool_call classifications return the Hermes veto
shape with readable copy:
Silmaril Firewall blocked this tool call: Unsafe agent control attempt. Continue without using the blocked content.
Malicious Block decisions at transform hooks replace the original content with fixed, content-free text. Warn output is fixed and never includes raw content, arguments, results, secrets, scores, thresholds, detector maps, or hidden policy.
The demo launcher opens the hosted Silmaril Firewall UI at
https://app.silmaril.dev/demo/setup-complete. It does not serve a local UI,
start a credential proxy, or put SILMARIL_API_KEY in the URL, terminal output,
or chat.
Run it from the repository root:
python scripts/open_playground.py
python scripts/open_playground.py --open
python scripts/open_playground.py --route playground --jsonFor preview validation, override the hosted base URL:
SILMARIL_DEMO_BASE_URL="http://localhost:3001" python scripts/open_playground.pyThe JSON status output includes the configured API URL and a boolean
hasApiKey, but never the raw key.
python -m unittest discover -s tests -p 'test_*.py'
python -m py_compile firewall.py __init__.py
python -m py_compile scripts/open_playground.pyhermes plugins update hermes-firewall
hermes plugins disable hermes-firewall
hermes plugins remove hermes-firewallThis plugin is licensed under Apache-2.0. See LICENSE and NOTICE.