Hermes AI Usage Monitor
August 27, 2026 · View on GitHub
A read-only extension for Hermes Agent that separates three facts people often blur together:
- Official provider quota — only when Hermes can obtain a provider-backed snapshot.
- Local Hermes usage — token and call counters recorded in Hermes
state.db. - Unavailable data — displayed honestly instead of converted from a guessed allowance.
The plugin supports the native Hermes Desktop app and the Hermes Web Dashboard. It never reads prompt or message content.
Current plugin version: v0.7.5.
Merging or publishing v0.7.5 distributes artifacts only; neither action deploys, restarts, upgrades, or changes any Hermes installation. Installation or migration remains a separate, explicit operator action.
Tested baseline (not a minimum-support claim): Hermes v0.20.5, local source authority
981101239a064c020a9d18fc3b1060ae306934ed, tested 2026-08-25.
Features
- account-quota windows, remaining percentage, and reset time for providers supported
by Hermes core (
openai-codex, Anthropic OAuth, and OpenRouter); - Desktop status-bar indicator and detailed page;
- Web Dashboard tab at
/ai-usage; - active-session token and context counters in Hermes Desktop;
- selectable 24-hour, 7-day, 30-day, and 90-day container-aware UTC chart that retains every returned hourly/daily bucket and scrolls only when buckets reach their minimum width;
- selected-period token composition with exact and percentage breakdowns, a multi-color linear timeline, and hatched reasoning visibly nested within output rather than double counted;
- session history with provider, model, safe surface/workload enums, strict profile slug, validated duration/active state, calls, five visible token-consumption bands, and a short reference that can be searched in retained Hermes logs;
- all-profile history initially selected in Desktop and Web Dashboard, with an explicit Current-profile selector and per-profile totals that disclose incomplete or truncated reads;
- separate non-cache-read, cache-read, and secondary raw totals across summaries, profile attribution, period composition, and recent sessions;
- a compact, keyboard-scrollable per-profile table showing five data rows at a time;
- fixed-coordinate quota scales whose visible red, orange, yellow, and green portion represents remaining allowance, with a text legend and the used/reset details retained;
- French and English UI;
- no independent credential handling, browser storage, analytics, or third-party scripts.
Important semantic boundary
For openai-codex, the percentage is the Codex allowance attached to the
ChatGPT subscription. It is not a universal meter for ordinary ChatGPT
conversations. Provider quota and token counters are deliberately displayed
separately: model choice, cache, reasoning, tools, images, service tier, and
rolling windows make a direct conversion misleading.
The UI uses one neutral client-side metric:
non_cache_read_tokens = input_tokens + output_tokens + cache_write_tokens, after each
component is normalized to a finite non-negative bound. Cache-read tokens are shown
separately. Raw total is the secondary sum of non-cache-read and cache-read tokens. These
local counters do not represent spend, billing, provider quota, or provider allocation.
Security posture
The repository is intentionally small and adds zero plugin runtime dependencies: it reuses Hermes' FastAPI, SQLite state, provider adapters, Desktop SDK, and Dashboard SDK.
Key controls:
- provider credentials remain inside Hermes adapters;
- account snapshot cache is scoped by Hermes home/profile and provider;
- per-key single-flight prevents duplicate concurrent provider snapshot fetches and uses bounded waiter liveness with cleanup even when an owner aborts;
- SQLite URI
mode=roplusPRAGMA query_only=ON; - static parameterized SQL; no mutation statements;
- response allowlisting and bounded display strings;
- exact session-surface enums and fail-closed optional-schema handling; raw source, title, paths, chat metadata, and lineage identifiers are never returned;
- no complete session identifiers, prompt content, messages, tool payloads, or raw exception text; only bounded log-searchable session suffixes are returned;
- no
innerHTML,eval, browser storage, custom auth headers, or direct browserfetch; - CI with locked development dependencies, Ruff, Bandit, pip-audit, CodeQL, Gitleaks, dependency review, zizmor, and OpenSSF Scorecard;
- GitHub Actions pinned to immutable commit SHAs.
Read SECURITY.md, the threat model, and the privacy model before deploying in a shared environment.
Repository layout
plugin.yaml Hermes plugin manifest (`ai-usage-monitor`)
__init__.py Inert agent entry point
desktop/plugin.js Native Hermes Desktop extension
dashboard/manifest.json Web Dashboard manifest
dashboard/dist/index.js Web Dashboard UI bundle
dashboard/dist/style.css Theme-aware dashboard styles
dashboard/plugin_api.py Read-only FastAPI router
tests/ Backend and frontend smoke tests
scripts/security_invariants.py Privacy/security regression gate
docs/ Architecture, installation, privacy, threat model
Installation
See docs/INSTALLATION.md for complete installation, verification, upgrade, and removal instructions.
Before the primary Git install, select the profile with hermes profile show PROFILE_NAME, copy
its exact home, and run this fail-closed preflight. Any legacy standalone Desktop tree or
non-Git/manual unified destination must use
Legacy split-tree migration or deterministic manual fallback
instead; do not delete either tree or unrelated plugins.
export HERMES_HOME="/exact/Path/from-profile-show"
test "$HERMES_HOME" = "$(realpath -e -- "$HERMES_HOME")"
test -f "$HERMES_HOME/config.yaml"
PLUGINS_ROOT="$HERMES_HOME/plugins"
DESKTOP_PLUGINS_ROOT="$HERMES_HOME/desktop-plugins"
for PLUGIN_ROOT in "$PLUGINS_ROOT" "$DESKTOP_PLUGINS_ROOT"; do
if [ -L "$PLUGIN_ROOT" ]; then
printf '%s\n' "Symlinked plugin root found; use the migration procedure." >&2
exit 1
fi
if [ -e "$PLUGIN_ROOT" ]; then
test -d "$PLUGIN_ROOT" &&
test "$PLUGIN_ROOT" = "$(realpath -e -- "$PLUGIN_ROOT")" || {
printf '%s\n' "Plugin root is not a canonical directory; use the migration procedure." >&2
exit 1
}
fi
done
LEGACY_DESKTOP="$HERMES_HOME/desktop-plugins/ai-usage-monitor"
UNIFIED="$HERMES_HOME/plugins/ai-usage-monitor"
if [ -e "$LEGACY_DESKTOP" ] || [ -L "$LEGACY_DESKTOP" ]; then
printf '%s\n' "Legacy Desktop tree found; use the migration procedure." >&2
exit 1
fi
if [ -e "$UNIFIED" ] || [ -L "$UNIFIED" ]; then
test -d "$UNIFIED" && test ! -L "$UNIFIED" || exit 1
test "$(git -C "$UNIFIED" rev-parse --show-toplevel)" = "$(realpath -e -- "$UNIFIED")" || {
printf '%s\n' "Existing unified tree is not its own Git clone; use the migration procedure." >&2
exit 1
}
fi
The complete guide then resolves v0.7.5 to an exact commit, installs it with
hermes plugins install ... --enable, and verifies the installed clone's HEAD. That command
enables the agent/Dashboard half only. For both a fresh Git install and a v0.7.2/v0.7.3
split-tree migration, open Hermes Desktop Settings → Plugins, find AI Usage Monitor under
Desktop plugins, choose Rescan if it is not inventoried yet, and confirm its switch is
off. If an upgrade shows it on, switch it off first: the old standalone copy's enablement state
is not authority for the new unified root. Then switch it on explicitly. No Desktop reload or
restart is required; the tested host activates Desktop contributions live. Only then verify the
AI Usage page and status-bar indicator.
Git clone installation is not covered by archive SHA256SUMS or attestation. The manual
migration verifies those controls, requires positive unified-host-contract evidence, swaps the
two historical trees transactionally, preserves rollback evidence and configuration, and never
removes unrelated plugins.
A pinned exact-SHA install is intentionally immutable:
hermes plugins update ai-usage-monitor is expected to fail closed rather than move it. For a
Git version transition, preserve the old exact SHA and recovery prerequisites before removal,
resolve the published version to a new exact 40-character commit SHA, disable and remove only
ai-usage-monitor with supported commands, then install that SHA with the scanner enabled. Use
a bounded interactive review of every CAUTION finding and stop on BLOCK. Never use --force
or disable the scanner. Split-tree/manual users must use the transactional release installer,
not this Git lifecycle. This transition must not delete or edit unrelated plugins or
configuration. The complete commands and rollback path are in
docs/INSTALLATION.md.
Development
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --require-hashes -r requirements-dev.txt
make check
The plugin's production runtime is provided by Hermes. requirements-dev.txt
contains only pinned CI and review tools.
API
Hermes mounts the router under /api/plugins/ai-usage-monitor:
GET /healthGET /snapshot?provider=autoGET /history?days=7&limit=200&bucket_start=<UTC epoch>&scope=current(daysis bounded to 1–90;bucket_startis optional and must be a UTC bucket boundary within the requested range, with the immediately preceding bucket also accepted as clock grace)
Hermes Dashboard authentication protects these routes. The plugin does not create
another auth mechanism and should not be exposed independently. session_ref is a
12-character suffix (extended on collisions), not the complete session ID; it can be
searched in local Hermes logs only while the corresponding logs are retained.
Bucket-specific results remain capped at 200 rows and expose row_count plus
rows_truncated so the UI never implies that a partial list is complete.
History rows expose surface, workload_type, profile, duration_seconds, and
is_active. The legacy source field is only an alias of the safe surface enum.
The backend API retains scope=current as its request default and reads only the active
profile when scope is omitted. Desktop and Web Dashboard initially request scope=all, and
their selector preserves the chosen scope across period changes, bucket selection, and
refresh. Hermes documents the Dashboard as a machine-level management surface that can manage every
local profile. Accordingly, every authenticated Dashboard principal must be treated as a
trusted machine operator, and first-party Desktop/Web clients initially select All profiles.
Shared low-privilege Dashboard access is unsupported. Operators requiring distinct exposure
must use separate authentication and --isolated per-profile Dashboard servers. See the
official Dashboard documentation
and official profile documentation.
The plugin does not invent a separate per-profile ACL. All-profile responses include partial, totals_complete,
profile_failures, profiles_considered, profiles_succeeded, and profiles_truncated.
Failures contain only profile slugs and fixed codes. Duplicate physical databases are read
once. If distinct databases contain an equal full session ID, every affected database is
excluded and the response is marked partial because exact ownership cannot be proven.
Provider quota remains account-level/shared and is never apportioned to profiles.
Raw-volume bands are fixed by raw total: Low below 10k, Moderate below 50k, Elevated below
100k, High below 250k, and Extreme at 250k or above. They appear only beside the explicitly
labelled secondary raw total, and visible text accompanies color.
Roadmap
See ROADMAP.md. Per-turn attribution will only be labelled exact when provider data and concurrency permit it; otherwise it will be labelled estimated or confounded. The project will not derive an official quota percentage from guessed local token allowances.
Contributing and security
- General changes: CONTRIBUTING.md
- Security reports: SECURITY.md — never use a public issue
- Support: SUPPORT.md
Licensed under Apache-2.0.