* feat(plugins): host-owned LLM access via ctx.llm
Plugins can now ask the host to run a one-shot chat or structured
completion against the user's active model and auth, without ever
seeing an OAuth token or API key. Closes the gap where plugins that
needed bounded structured inference (receipts, CRM extraction,
support classification) had to either bring their own provider keys
or register a tool the agent had to call.
New surface on PluginContext:
- ctx.llm.complete(messages, ...)
- ctx.llm.complete_structured(instructions, input, json_schema, ...)
- async siblings ctx.llm.acomplete / acomplete_structured
Backed by the existing auxiliary_client.call_llm pipeline — every
provider, fallback chain, vision routing, and timeout policy Hermes
already supports applies automatically.
Trust gate (fail-closed by default):
- plugins.entries.<id>.llm.allow_model_override
- plugins.entries.<id>.llm.allowed_models (allowlist; '*' = any)
- plugins.entries.<id>.llm.allow_agent_id_override
- plugins.entries.<id>.llm.allow_profile_override
Embedded model@profile shorthand goes through the same gate as
explicit profile=, so it can't bypass the auth-profile policy.
Conflicting explicit and embedded profiles fail closed.
Also lands:
- plugins/plugin-llm-example/ — reference plugin that registers
/receipt-extract, demonstrating image+text structured input,
jsonschema validation, and the trust-gate config.
- website/docs/developer-guide/plugin-llm-access.md — full API docs.
- 45 unit tests covering trust gates, JSON parsing, schema
validation, image encoding, async surface, and config loading.
Validation:
- 2628 tests pass in tests/agent/
- E2E: bundled plugin loaded with isolated HERMES_HOME, slash
command produced parsed JSON via stubbed call_llm
- response_format extra_body wired correctly for both json_object
and json_schema modes
* docs(plugin-llm): rewrite quickstart and framing
The quickstart now uses a meeting-notes-to-tasks example instead of
a receipt extractor, and the page leads with hook-time / gateway
pre-filter / scheduled-job framing rather than the OpenClaw
KB/support/CRM/finance/migration enumeration that the original
upstream PR used. Receipt example moved to a separate worked
example link so the docs page itself doesn't echo any of the
upstream framing.
Also clarifies where ctx.llm fits in the broader plugin surface
(table comparing register_tool / register_platform / register_hook
/ etc.) and what makes this lane different from auxiliary_client
internals.
No code change.
* docs(plugin-llm): reframe as any LLM call, not just structured output
The original draft leaned heavily on complete_structured() and made
the chat lane (complete() / acomplete()) feel like a footnote.
Restructure so:
- The page title and description say 'any LLM call.'
- The lead shows BOTH a plain chat call (error rewriter) AND a
structured call (triage scorer) up top.
- Quick start has two complete plugin examples — /tldr (chat) and
/paste-to-tasks (structured).
- New 'When to use which' table for choosing complete() vs
complete_structured() vs the async siblings.
- Trust-gate sections explicitly note 'all four methods,' and the
request-shaping list calls out chat-only fields (messages) and
structured-only fields (instructions, input, json_schema)
alongside each other.
- The 'Where this fits' section now says 'for any reason,
structured or not.'
The receipt-extractor reference plugin still exists under
plugins/plugin-llm-example/ — but the docs page no longer treats
it as the canonical surface example. It's now described as 'a third
worked example, this time with image input.'
No code change.
* feat(plugin-llm): split provider/model into independent explicit kwargs
The first cut accepted a single 'provider/model' slug on every method
and split it internally. That looked clean but broke under live test:
the model-override path tried to use the slug's vendor prefix as a
literal Hermes provider id, which silently switched the user off
their aggregator (e.g. plugin asks for 'openai/gpt-4o-mini' on a user
who routes through OpenRouter — host attempted to call the 'openai'
provider directly, failed because OPENAI_API_KEY wasn't set).
New shape mirrors the host's main config:
ctx.llm.complete(
messages=[...],
provider='openrouter', # gated, optional
model='openai/gpt-4o-mini', # gated, optional
profile='work', # gated, optional
...
)
Each is independently gated by its own allow_*_override flag.
Granting model-override does NOT auto-grant provider-override.
Allowlists are now per-axis (allowed_providers, allowed_models)
matched literally against whatever string the plugin sends.
Dropped 'model@profile' embedded-suffix shorthand entirely. Hermes
doesn't use that pattern anywhere else; profile= is its own kwarg.
Live E2E (against real OpenRouter via Teknium's config) confirms:
- zero-config call works
- default-deny blocks each override with a helpful error
- model-only override stays on user's active provider (the bug)
- provider+model override switches cleanly
- allowlist refuses non-listed entries
- structured output round-trip parses + schema-validates
Tests: 49 cases (up from 45); all green. Docs updated to match the
new shape, including a 'most plugins never need this section' callout
on the trust-gate config block.
* fix+cleanup(plugin-llm): real attribution, hook-mode coverage, move example out of core
Three integration fixes for the ctx.llm surface:
1. Attribution bug — result.provider and result.model now reflect
what call_llm actually used, not placeholder fallbacks ('auto',
'default'). New _resolve_attribution() helper:
- explicit overrides win (what the call targeted)
- response.model wins for the recorded model (provider
canonicalisation: 'gpt-4o' → 'gpt-4o-2024-08-06' etc.)
- falls back to _read_main_provider() / _read_main_model()
when no override is set, so audit logs reflect the user's
active main provider/model
- 'auto' / 'default' only when EVERYTHING is empty
Live verified: zero-config call now records
provider='openrouter', model='anthropic/claude-4.7-opus-20260416'
instead of provider='auto', model='default'.
2. Hook-mode coverage — TestHookMode confirms ctx.llm.complete
works from inside a registered post_tool_call callback. The
docs page promised hook integration; now there's a test that
exercises the lazy-import path through the real invoke_hook
machinery. Two cases: traceback-rewrite hook with conditional
ctx.llm.complete, and minimal hook regression for the
sync-hook + sync-llm path.
3. Reference plugin moved out of core. plugins/plugin-llm-example/
is gone from hermes-agent — it now lives in the new
NousResearch/hermes-example-plugins companion repo. The docs
page links there. Hermes' bundled plugins should be plugins
users actually run; reference / docs-companion plugins live
externally.
Test count: 56 (up from 49). Wider sweep on tests/hermes_cli/
+ tests/gateway/ + tests/tools/ + tests/agent/ shows 16770
passing; the 12 failures are all pre-existing on origin/main
(verified by stashing this branch's changes and re-running) —
kanban-boards, delegate-task, gateway-restart, tts-routing —
none touch the plugin_llm surface.
* chore(plugins): move all example plugins to companion repo
Reference / docs-companion plugins now live exclusively in
NousResearch/hermes-example-plugins, not bundled with the core repo:
- example-dashboard
- strike-freedom-cockpit
A new fourth example, plugin-llm-async-example, was added to that
repo demonstrating ctx.llm's async surface (acomplete()) with
asyncio.gather() — registers /translate <lang>: <text> which fires
forward translation + sentiment classifier in parallel, then a
back-translation for QA. Live-tested at 2.5s for three real
provider round-trips (would be ~5-6s sequential).
Docs updated:
- developer-guide/plugin-llm-access.md links both sync and async
examples in the Reference section
- user-guide/features/extending-the-dashboard.md repoints both demo
sections to the companion repo with corrected install paths
- user-guide/features/built-in-plugins.md drops the two demo rows
- AGENTS.md notes that example plugins live in the companion repo
Net: hermes-agent's plugins/ directory now contains only plugins
users actually run (memory providers, dashboard tabs that ship real
features, the disk-cleanup hook, platform adapters). All four
demo / reference plugins live externally where they can be cloned
on demand instead of inflating the core install.
16 KiB
| sidebar_position | sidebar_label | title | description |
|---|---|---|---|
| 12 | Built-in Plugins | Built-in Plugins | Plugins shipped with Hermes Agent that run automatically via lifecycle hooks — disk-cleanup and friends |
Built-in Plugins
Hermes ships a small set of plugins bundled with the repository. They live under <repo>/plugins/<name>/ and load automatically alongside user-installed plugins in ~/.hermes/plugins/. They use the same plugin surface as third-party plugins — hooks, tools, slash commands — just maintained in-tree.
See the Plugins page for the general plugin system, and Build a Hermes Plugin to write your own.
How discovery works
The PluginManager scans four sources, in order:
- Bundled —
<repo>/plugins/<name>/(what this page documents) - User —
~/.hermes/plugins/<name>/ - Project —
./.hermes/plugins/<name>/(requiresHERMES_ENABLE_PROJECT_PLUGINS=1) - Pip entry points —
hermes_agent.plugins
On name collision, later sources win — a user plugin named disk-cleanup would replace the bundled one.
plugins/memory/ and plugins/context_engine/ are deliberately excluded from bundled scanning. Those directories use their own discovery paths because memory providers and context engines are single-select providers configured through hermes memory setup / context.engine in config.
Bundled plugins are opt-in
Bundled plugins ship disabled. Discovery finds them (they appear in hermes plugins list and the interactive hermes plugins UI), but none load until you explicitly enable them:
hermes plugins enable disk-cleanup
Or via ~/.hermes/config.yaml:
plugins:
enabled:
- disk-cleanup
This is the same mechanism user-installed plugins use. Bundled plugins are never auto-enabled — not on fresh install, not for existing users upgrading to a newer Hermes. You always opt in explicitly.
To turn a bundled plugin off again:
hermes plugins disable disk-cleanup
# or: remove it from plugins.enabled in config.yaml
Currently shipped
The repo ships these bundled plugins under plugins/. All are opt-in — enable them via hermes plugins enable <name>.
| Plugin | Kind | Purpose |
|---|---|---|
disk-cleanup |
hooks + slash command | Auto-track ephemeral files and clean them on session end |
observability/langfuse |
hooks | Trace turns / LLM calls / tools to Langfuse |
spotify |
backend (7 tools) | Native Spotify playback, queue, search, playlists, albums, library |
google_meet |
standalone | Join Meet calls, live-caption transcription, optional realtime duplex audio |
image_gen/openai |
image backend | OpenAI gpt-image-2 image generation backend (alternative to FAL) |
image_gen/openai-codex |
image backend | OpenAI image generation via Codex OAuth |
image_gen/xai |
image backend | xAI grok-2-image backend |
hermes-achievements |
dashboard tab | Steam-style collectible badges generated from your real Hermes session history |
kanban/dashboard |
dashboard tab | Kanban board UI for the multi-agent dispatcher — tasks, comments, fan-out, board switching. See Kanban Multi-Agent. |
Memory providers (plugins/memory/*) and context engines (plugins/context_engine/*) are listed separately on Memory Providers — they're managed through hermes memory and hermes plugins respectively. The full per-plugin detail for the two long-running hooks-based plugins follows.
disk-cleanup
Auto-tracks and removes ephemeral files created during sessions — test scripts, temp outputs, cron logs, stale chrome profiles — without requiring the agent to remember to call a tool.
How it works:
| Hook | Behaviour |
|---|---|
post_tool_call |
When write_file / terminal / patch creates a file matching test_*, tmp_*, or *.test.* inside HERMES_HOME or /tmp/hermes-*, track it silently as test / temp / cron-output. |
on_session_end |
If any test files were auto-tracked during the turn, run the safe quick cleanup and log a one-line summary. Stays silent otherwise. |
Deletion rules:
| Category | Threshold | Confirmation |
|---|---|---|
test |
every session end | Never |
temp |
>7 days since tracked | Never |
cron-output |
>14 days since tracked | Never |
| empty dirs under HERMES_HOME | always | Never |
research |
>30 days, beyond 10 newest | Always (deep only) |
chrome-profile |
>14 days since tracked | Always (deep only) |
| files >500 MB | never auto | Always (deep only) |
Slash command — /disk-cleanup available in both CLI and gateway sessions:
/disk-cleanup status # breakdown + top-10 largest
/disk-cleanup dry-run # preview without deleting
/disk-cleanup quick # run safe cleanup now
/disk-cleanup deep # quick + list items needing confirmation
/disk-cleanup track <path> <category> # manual tracking
/disk-cleanup forget <path> # stop tracking (does not delete)
State — everything lives at $HERMES_HOME/disk-cleanup/:
| File | Contents |
|---|---|
tracked.json |
Tracked paths with category, size, and timestamp |
tracked.json.bak |
Atomic-write backup of the above |
cleanup.log |
Append-only audit trail of every track / skip / reject / delete |
Safety — cleanup only ever touches paths under HERMES_HOME or /tmp/hermes-*. Windows mounts (/mnt/c/...) are rejected. Well-known top-level state dirs (logs/, memories/, sessions/, cron/, cache/, skills/, plugins/, disk-cleanup/ itself) are never removed even when empty — a fresh install does not get gutted on first session end.
Enabling: hermes plugins enable disk-cleanup (or check the box in hermes plugins).
Disabling again: hermes plugins disable disk-cleanup.
observability/langfuse
Traces Hermes turns, LLM calls, and tool invocations to Langfuse — an open-source LLM observability platform. One span per turn, one generation per API call, one tool observation per tool call. Usage totals, per-type token counts, and cost estimates come out of Hermes' canonical agent.usage_pricing numbers, so the Langfuse dashboard sees the same breakdown (input / output / cache_read_input_tokens / cache_creation_input_tokens / reasoning_tokens) that appears in hermes logs.
The plugin is fail-open: no SDK installed, no credentials, or a transient Langfuse error — all turn into a silent no-op in the hook. The agent loop is never impacted.
Setup (interactive — recommended):
hermes tools # → Langfuse Observability → Cloud or Self-Hosted
The wizard collects your keys, pip installs the langfuse SDK, and adds observability/langfuse to plugins.enabled for you. Restart Hermes and the next turn ships a trace.
Setup (manual):
pip install langfuse
hermes plugins enable observability/langfuse
Then put the credentials in ~/.hermes/.env:
HERMES_LANGFUSE_PUBLIC_KEY=pk-lf-...
HERMES_LANGFUSE_SECRET_KEY=sk-lf-...
HERMES_LANGFUSE_BASE_URL=https://cloud.langfuse.com # or your self-hosted URL
How it works:
| Hook | Behaviour |
|---|---|
pre_api_request / pre_llm_call |
Open (or reuse) a per-turn root span "Hermes turn". Start a generation child observation for this API call with serialized recent messages as input. |
post_api_request / post_llm_call |
Close the generation, attach usage_details, cost_details, finish_reason, assistant output + tool calls. If no tool calls and non-empty content, close the turn. |
pre_tool_call |
Start a tool child observation with sanitized args. |
post_tool_call |
Close the tool observation with sanitized result. read_file payloads get summarized (head + tail + omitted-line count) so a huge file read stays under HERMES_LANGFUSE_MAX_CHARS. |
Session grouping keys off the Hermes session ID (or task ID for sub-agents) via langfuse.propagate_attributes, so everything in a single hermes chat session lives under one Langfuse session.
Verify:
hermes plugins list # observability/langfuse should show "enabled"
hermes chat -q "hello" # check the Langfuse UI for a "Hermes turn" trace
Optional tuning (in .env):
| Variable | Default | Purpose |
|---|---|---|
HERMES_LANGFUSE_ENV |
— | Environment tag on traces (production, staging, …) |
HERMES_LANGFUSE_RELEASE |
— | Release/version tag |
HERMES_LANGFUSE_SAMPLE_RATE |
1.0 |
Sampling rate passed to the SDK (0.0–1.0) |
HERMES_LANGFUSE_MAX_CHARS |
12000 |
Per-field truncation for message content / tool args / tool results |
HERMES_LANGFUSE_DEBUG |
false |
Verbose plugin logging to agent.log |
Hermes-prefixed and standard SDK env vars (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL) are both accepted — Hermes-prefixed wins when both are set.
Performance: the Langfuse client is cached after the first hook call. If credentials or SDK are missing, that decision is also cached — subsequent hooks fast-return without re-checking env vars or reloading config.
Disabling: hermes plugins disable observability/langfuse. The plugin module is still discovered, but no module code runs until you re-enable.
google_meet
Lets the agent join, transcribe, and participate in Google Meet calls — take notes on a meeting, summarize the back-and-forth after, follow up on specific points, and (optionally) speak replies back into the call via TTS.
What it adds:
- A headless virtual participant that joins a Meet URL using browser automation
- Live transcription of the meeting audio via the configured STT provider
- A
meet_summarize/meet_speak/meet_followuptoolset the agent invokes to act on what it heard - Post-meeting artifacts (transcript, speaker-attributed notes, action items) saved under
~/.hermes/cache/google_meet/<meeting_id>/
Setup:
hermes plugins enable google_meet
# Prompts you to sign in via the plugin's OAuth flow on first use —
# needs a Google account with Meet access. Host approval may be required
# if the meeting enforces "only invited participants can join".
Usage from chat:
"Join meet.google.com/abc-defg-hij and take notes. After the call, send me a summary with action items."
The agent kicks off the meeting join, streams the transcription back into its context as the call proceeds, and produces a structured summary when the meeting ends (or when you tell it to stop).
When to use it: recurring standups where you want a bot to transcribe + summarize for async attendees; deposition-style interviews where you want structured notes; any case where you'd otherwise need Fireflies / Otter / Grain. When you'd rather not have an AI listening in — don't enable it.
Disabling: hermes plugins disable google_meet. Any cached transcripts and recordings stay in ~/.hermes/cache/google_meet/ until you remove them.
hermes-achievements
Adds a Steam-style achievements tab to the dashboard — 60+ collectible, tiered badges generated from your real Hermes session history. Tool-chain feats, debugging patterns, vibe-coding streaks, skill/memory usage, model/provider variety, lifestyle quirks (weekend and night sessions). Originally authored by @PCinkusz as an external plugin; brought in-tree so it stays in lockstep with Hermes feature changes.
How it works:
- Scans your entire
~/.hermes/state.dbsession history on the dashboard backend - Per-session stats are cached by
(started_at, last_active)fingerprint, so only new or changed sessions re-analyze on subsequent scans - First-ever scan runs in a background thread — the dashboard never blocks waiting for it, even on databases with thousands of sessions
- Unlock state is persisted to
$HERMES_HOME/plugins/hermes-achievements/state.json
Tier progression: Copper → Silver → Gold → Diamond → Olympian. Each card exposes a "What counts" section listing the exact metric being tracked.
Achievement states:
| State | Meaning |
|---|---|
| Unlocked | At least one tier achieved |
| Discovered | Known achievement, progress visible, not yet earned |
| Secret | Hidden until Hermes detects the first related signal in your history |
API — routes mount under /api/plugins/hermes-achievements/:
| Endpoint | Purpose |
|---|---|
GET /achievements |
Full catalog with per-badge unlock state (returns a pending placeholder while the first cold scan is running) |
GET /scan-status |
State of the background scanner: idle / running / failed, last duration, run count |
GET /recent-unlocks |
Twenty most recently unlocked badges, newest first |
GET /sessions/{id}/badges |
Badges earned primarily in one specific session |
POST /rescan |
Manual synchronous rescan (blocks; use when the user clicks the rescan button) |
POST /reset-state |
Clear unlock history and cached snapshot |
State files — live under $HERMES_HOME/plugins/hermes-achievements/:
| File | Contents |
|---|---|
state.json |
Unlock history: which badges you've earned and when. Stable across Hermes updates. |
scan_snapshot.json |
Last completed scan payload (served immediately on dashboard load) |
scan_checkpoint.json |
Per-session stats cache keyed by fingerprint (makes warm rescans fast) |
Performance notes:
- Cold scan on ~8,000 sessions takes a few minutes. It runs in a background thread on first dashboard request; the UI sees a pending placeholder and polls
/scan-status. - Incremental results during a cold scan — the scanner publishes a partial snapshot every ~250 sessions so each dashboard refresh shows more badges unlocked as the scan progresses. No minute-long stare at zeros.
- Warm rescan reuses per-session stats for every session whose
started_at+last_activefingerprint matches the checkpoint — completes in seconds even on large histories. - The in-memory snapshot TTL is 120s; stale requests serve the old snapshot immediately and kick a background refresh. You never wait on a spinner just because TTL expired.
Enabling: Nothing to enable — hermes-achievements is a dashboard-only plugin (no lifecycle hooks, no model-visible tools). It auto-registers as a tab in hermes dashboard on first launch. The plugins.enabled config only gates lifecycle/tool plugins; dashboard plugins are discovered purely via their dashboard/manifest.json.
Opting out: Delete or rename plugins/hermes-achievements/dashboard/manifest.json, or override it with a user plugin of the same name in ~/.hermes/plugins/hermes-achievements/ that ships no dashboard. The plugin's state files under $HERMES_HOME/plugins/hermes-achievements/ survive — reinstalling preserves your unlock history.
Adding a bundled plugin
Bundled plugins are written exactly like any other Hermes plugin — see Build a Hermes Plugin. The only differences are:
- Directory lives at
<repo>/plugins/<name>/instead of~/.hermes/plugins/<name>/ - Manifest source is reported as
bundledinhermes plugins list - User plugins with the same name override the bundled version
A plugin is a good candidate for bundling when:
- It has no optional dependencies (or they're already
pip install .[all]deps) - The behaviour benefits most users and is opt-out rather than opt-in
- The logic ties into lifecycle hooks that the agent would otherwise have to remember to invoke
- It complements a core capability without expanding the model-visible tool surface
Counter-examples — things that should stay as user-installable plugins, not bundled: third-party integrations with API keys, niche workflows, large dependency trees, anything that would meaningfully change agent behaviour by default.