Start the per-user service daemon, then submit notifications via CLI. The service manages the full lifecycle: UI launch, deferrals, and deadlines.
# Start the per-user service daemon (see Architecture for autostart setup)
hermes serve
# Send a notification (blocks until user responds)
hermes notify '{"heading":"Restart Required","message":"Please restart."}'
# Or use --config flag on root command
hermes --config notification.json
# List active notifications
hermes list
# Cancel a notification
hermes cancel <notification-id>
# View notification history
hermes inbox
# Print history as JSON
hermes inbox --jsonCompleted notifications are automatically saved to the history bucket. The inbox lets you review past notifications and their outcomes.
hermes inbox # Opens the inbox UI
hermes inbox --json # Prints history as JSON to stdout
hermes inbox --db my.db # Read directly from a bolt DB file (skip service)The inbox connects to the running service via gRPC. If the service is unreachable, it falls back to reading the bolt database directly. History is auto-pruned on service startup: records older than 30 days or exceeding 50 entries are removed.
For testing or single-session use, render directly without the service:
hermes --local '{"heading":"Test","message":"Local test."}'
hermes --local notification.json
echo '{"heading":"..."}' | hermes --localhermes accepts a single JSON or YAML config with these fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
heading |
string | yes | Bold heading text | |
message |
string | yes | Body text below the heading | |
buttons |
array | no | [] |
Button definitions (see below) |
timeout |
int | no | 300 |
Seconds until auto-action |
timeout_value |
string | no | "" |
Value returned on timeout |
esc_value |
string | no | "" |
Value returned on ESC (defaults to timeout_value when set, otherwise empty) |
title |
string | no | IT Department |
Small uppercase label at the top |
accent_color |
string | no | #D4A843 |
Theme accent color (hex) |
help_url |
string | no | "" |
"Need help?" link URL |
id |
string | no | auto-generated | Unique notification ID for the service |
defer_deadline |
string | no | "" |
Max deferral window (e.g., "24h", "7d") |
max_defers |
int | no | 0 |
Max number of deferrals (0 = unlimited) |
images |
array | no | [] |
HTTPS URLs or data:image/ URIs for a carousel (max 5, no SVG data URIs) |
watch_paths |
array | no | [] |
Filesystem paths to monitor for changes (max 5, no .. traversal) |
dnd |
string | no | "respect" |
Do Not Disturb behavior: "respect", "ignore", or "skip" |
priority |
int | no | 5 |
Delivery priority (0-10). Higher = shown first in queue drain |
escalation |
array | no | [] |
Progressive urgency steps applied after repeated deferrals (see below) |
result_actions |
object | no | {} |
Maps response values to automatic actions (max 10 entries; action chaining, see below) |
quiet_hours |
object | no | null |
Time-based delivery suppression (see below) |
heading_localized |
object | no | {} |
Locale → heading text map for i18n |
message_localized |
object | no | {} |
Locale → message text map for i18n |
depends_on |
string | no | "" |
ID of notification that must complete first (sequential workflows) |
Simple button:
{"label": "Restart Now", "value": "restart", "style": "primary"}Dropdown button (fly-out menu above the button):
{
"label": "Defer",
"style": "secondary",
"dropdown": [
{"label": "1 Hour", "value": "defer_1h"},
{"label": "4 Hours", "value": "defer_4h"},
{"label": "1 Day", "value": "defer_1d"}
]
}Styles: primary (accent color), secondary (dark with border), danger (red).
Button values prefixed with url: open the URI in the default handler instead of closing the notification. Hermes supports platform-specific settings URIs alongside standard web URLs:
| Scheme | Platform | Example |
|---|---|---|
https: |
All | url:https://example.com/kb/update |
http: |
All | url:http://intranet.corp/install |
ms-settings: |
Windows | url:ms-settings:windowsupdate |
x-apple.systempreferences: |
macOS | url:x-apple.systempreferences:com.apple.Software-Update-Settings.extension |
Settings URIs are only allowed on their native platform. A ms-settings: button on macOS is silently blocked (and vice versa). Include both in a shared config -- hermes filters at runtime.
{"label": "Windows Update", "value": "url:ms-settings:windowsupdate", "style": "primary"}
{"label": "Software Update", "value": "url:x-apple.systempreferences:com.apple.Software-Update-Settings.extension", "style": "primary"}
{"label": "FileVault", "value": "url:x-apple.systempreferences:com.apple.preference.security?FileVault", "style": "secondary"}macOS pane IDs follow the pattern com.apple.preference.<name> or com.apple.<Name>-Settings.extension. Append ?Anchor for sub-panes (e.g. ?FileVault, ?Privacy_AllFiles). Linux has no standard settings URI scheme.
Button values prefixed with cmd: execute a shell command when clicked. The command runs through the platform shell (cmd /C on Windows, sh -c on Unix). Arguments, pipes, and shell features are supported.
{"label": "Restart Now", "value": "cmd:shutdown /r /t 0", "style": "primary"}
{"label": "Reboot", "value": "cmd:sudo shutdown -r now", "style": "danger"}Commands are also re-executable from the inbox history view. Empty commands (cmd: with no argument) are blocked. Only one primary button per notification is recommended -- the Enter key triggers the first primary button.
When using the service daemon, configure deferrals to control how long and how many times a user can defer:
{
"heading": "System Restart Required",
"message": "Your computer needs to restart to apply security updates.",
"timeout": 300,
"timeout_value": "restart",
"defer_deadline": "24h",
"max_defers": 5,
"buttons": [
{"label": "Defer 1h", "value": "defer_1h", "style": "secondary"},
{"label": "Defer 4h", "value": "defer_4h", "style": "secondary"},
{"label": "Restart Now", "value": "restart", "style": "primary"}
]
}Defer values must match the pattern defer_Xh, defer_Xd, defer_Xm or defer_Xs (hours, days, minutes, seconds). The service parses these to schedule re-notification. Deferral state is persisted to disk so notifications survive service restarts (see Architecture — Persistence).
When max_defers is reached or defer_deadline has passed, hermes automatically hides any buttons (or dropdown options) that trigger a deferral. If a button has no other action (e.g. it was purely a defer button), it is removed entirely. This forces the user to choose a non-deferral action (e.g. "Restart Now") or let the timeout expire.
Embed images (documentation slides, screenshots, diagrams) in the notification. The window auto-sizes taller when images are present. Multiple images render as a carousel with arrow navigation and keyboard support (left/right arrow keys).
{
"heading": "macOS 15.3 Update",
"message": "Review the changes below, then click Update.",
"images": [
"https://intranet.example.com/slides/macos-15.3-overview.png",
"https://intranet.example.com/slides/macos-15.3-timeline.png",
"data:image/png;base64,iVBORw0KGgo..."
],
"buttons": [
{"label": "Update Now", "value": "update", "style": "primary"},
{"label": "Defer 4h", "value": "defer_4h", "style": "secondary"}
]
}Images must be https:// URLs or data:image/ URIs (no SVG). Maximum 5 per notification. Recommended dimensions: 380x220px (matches the carousel track). Images are scaled with object-fit: contain, so larger images work but may have letterboxing.
Monitor filesystem paths for changes during the notification. When a watched path is created, modified, or deleted, the notification UI updates with the event. This is useful for validating installations (e.g. watch for a receipt file to appear after the user clicks "Install").
{
"heading": "Installing Security Agent",
"message": "Click Install, then wait for confirmation.",
"watch_paths": [
"/var/db/receipts/com.example.agent.plist",
"/Library/Application Support/SecurityAgent/version.txt"
],
"timeout": 600,
"buttons": [
{"label": "Install", "value": "url:https://intranet.example.com/install", "style": "primary"}
]
}The notification footer shows "Monitoring filesystem..." initially, then updates with each event (e.g. "create: version.txt"). The frontend receives events via the Wails fs:event event channel, so custom frontends can also subscribe.
If a watched path doesn't exist yet, hermes watches its parent directory to catch creation events.
hermes detects the OS Do Not Disturb / Focus mode on all platforms and adjusts notification behavior accordingly. The dnd field controls what happens when DND is active:
| Mode | Behavior |
|---|---|
"respect" (default) |
Wait and retry every 60 seconds until DND is off, then show the notification. Deadlines are still enforced while waiting. |
"ignore" |
Show the notification immediately regardless of DND. Use for critical security alerts. |
"skip" |
Silently complete with value "dnd_active" (exit code 0). The calling script can detect this and decide what to do. |
{
"heading": "Security Update Required",
"message": "Critical vulnerability patch. This alert overrides Do Not Disturb.",
"dnd": "ignore",
"buttons": [
{"label": "Update Now", "value": "update", "style": "primary"}
]
}Platform detection:
| Platform | Method |
|---|---|
| Windows | SHQueryUserNotificationState Win32 API (detects Focus Assist, fullscreen apps, presentation mode, quiet hours) |
| macOS | defaults read com.apple.controlcenter "NSStatusItem Visible FocusModes" (Monterey+), falls back to doNotDisturb pref for older versions |
| Linux | GNOME gsettings (show-banners), KDE D-Bus Inhibited property, Xfce xfconf (/do-not-disturb). Other DEs (Sway, Hyprland, i3) are not yet supported — DND detection returns false (fail-open). |
Detection is fail-open: if the API call fails or the platform is unsupported, hermes assumes DND is off and shows the notification.
Define progressive urgency that mutates the notification each time the user defers past a threshold:
{
"heading": "Restart Required",
"message": "Security updates need a restart.",
"max_defers": 5,
"defer_deadline": "24h",
"escalation": [
{
"after_defers": 2,
"timeout": 120,
"accent_color": "#FF6600",
"message_suffix": "\n\nThis action is required soon."
},
{
"after_defers": 4,
"timeout": 60,
"accent_color": "#FF0000",
"message_suffix": "\n\nFINAL NOTICE: Action required immediately."
}
]
}After 2 deferrals: timeout shortens to 120s, accent turns orange, warning appended. After 4: timeout 60s, accent red, final notice. The highest matching threshold wins.
Map user responses to automatic follow-up actions. The action runs server-side after the notification completes:
{
"buttons": [
{"label": "Restart Now", "value": "restart", "style": "primary"},
{"label": "Open Wiki", "value": "wiki", "style": "secondary"}
],
"result_actions": {
"restart": "cmd:shutdown /r /t 60",
"wiki": "url:https://wiki.example.com/vpn"
}
}Supported prefixes: cmd: (shell command) and url: (opens in browser). Actions also fire on timeout if timeout_value matches a key (e.g. "timeout:restart" matches "restart").
Suppress notifications during specified hours. The service delays delivery until the window ends:
{
"quiet_hours": {
"start": "22:00",
"end": "07:00",
"timezone": "America/Los_Angeles"
}
}Overnight ranges (start > end) are supported. Timezone defaults to local if omitted. Deadlines are still enforced — a notification past its deadline auto-actions even during quiet hours.
Provide translated heading and message text. The resolved locale selects the best match:
{
"heading": "Restart Required",
"heading_localized": {
"ja": "再起動が必要です",
"de": "Neustart erforderlich",
"es": "Reinicio requerido"
},
"message": "Please restart to apply updates.",
"message_localized": {
"ja": "アップデートを適用するため再起動してください。",
"de": "Ihr Computer muss neu gestartet werden."
}
}Locale resolution order: --locale flag > HERMES_LOCALE env > LANG env > "en" fallback.
Demo: hermes --locale ja --local --config testdata/localized-restart.json
Control delivery order with priority (0=low, 10=critical, default 5). Higher priority notifications are delivered first during offline queue drain:
{"heading": "Critical Patch", "priority": 10, "dnd": "ignore"}
{"heading": "Training Reminder", "priority": 3}Create multi-step workflows where notification B waits for notification A:
{"id": "accept-eula", "heading": "Accept EULA", ...}
{"id": "apply-update", "depends_on": "accept-eula", "heading": "Install Update", ...}The second notification is held in waiting_on_dependency state until the first completes. Submit both to the service — the manager handles sequencing automatically.
| Command | Description |
|---|---|
hermes serve |
Start the gRPC service daemon |
hermes notify [config] |
Send notification to service (blocks for result). Broadcasts when run as SYSTEM/root (details). |
hermes list |
List active notifications |
hermes cancel <id> |
Cancel an active notification |
hermes inbox |
View notification history (opens inbox UI) |
hermes inbox --json |
Print notification history as JSON to stdout |
hermes install |
Configure MOTD hook and launch daemon in active user sessions (when elevated). Called by package postinstall. |
hermes uninstall |
Remove MOTD hook. Called by package removal scripts. |
hermes stop |
Graceful daemon shutdown (gRPC then fallback kill) |
hermes motd |
Print pending notification summary for SSH login banners (called by profile.d scripts) |
hermes demo |
Show a demo notification |
hermes version |
Print version, build date, Go, and OS info |
| Flag | Scope | Description |
|---|---|---|
--config <path or json> |
root | config file or inline JSON/YAML — routes to service |
--local |
root | Render locally in current session (skip service) |
--locale <code> |
root | Override locale for localized notifications (e.g. ja, de) |
--port <int> |
serve, notify, list, cancel, stop, inbox | gRPC port (default: 4770) |
--db <path> |
serve, inbox, motd | Bolt database path (default: platform-specific, see Architecture) |
--json |
inbox | Print history as JSON instead of opening the UI |
--help |
all | Print help |
| Code | Meaning |
|---|---|
0 |
User chose an action (response on stdout) or dismissed (empty stdout) |
1 |
Error (bad config, validation, launch failure, or at capacity) |
200 |
User deferred (response on stdout, starts with defer) |
202 |
Timeout (countdown expired, auto-actioned per config) |
203 |
Queued (service unreachable, notification saved for later delivery; stdout: queued) |
Detecting dismissals: Exit 0 with empty stdout means the user dismissed the notification (ESC / window close) without choosing an action. Scripts should check both the exit code and stdout content.
hermes auto-detects how you're providing the config (JSON and YAML files are both supported):
hermes notify restart-notification.json
hermes --config restart-notification.jsonhermes notify restart-notification.yml
hermes --config restart-notification.yamlhermes notify '{"heading":"Update","message":"Please restart."}'echo '{"heading":"Update","message":"Please restart."}' | hermes notifyPowerShell 5.1 strips inner double quotes when passing strings to native executables. Piping via stdin avoids this entirely:
$config = @'
{
"heading": "System Restart Required",
"message": "Your computer needs to restart.",
"timeout": 300,
"timeout_value": "restart",
"defer_deadline": "24h",
"max_defers": 3,
"buttons": [
{"label": "Defer 1h", "value": "defer_1h", "style": "secondary"},
{"label": "Restart Now", "value": "restart", "style": "primary"}
]
}
'@
$config | & hermes.exe notifySee testdata/ for ready-to-use configs (JSON and YAML):
restart-notification.json— Restart with defer dropdownupdate-notification.json— Software update with defersimple-notification.json— Simple one-button acknowledgmentdefer-with-dropdown.json— VPN disconnect with defer dropdownshort-defer-restart.json— Short deferral (2m deadline, 3 max) for quick lifecycle testingshort-defer-deadline.json— Very short deadline (1m) for testing auto-actionimage-carousel.json— Multi-slide image carousel with placeholder imagesinstall-with-watch.json— Filesystem watch for install receipt validationescalation-restart.json— Escalation ladder: soft → firm → mandatory after repeated deferralsaction-chaining.json— Result actions: user response triggers automatic follow-upquiet-hours.json— Time-based delivery suppression (22:00–07:00)localized-restart.json— Multi-language restart (ja, de, es, fr, ko, zh)priority-critical.json— Priority 10 critical alert (ignores DND, no defer)workflow-step1-eula.json— Dependency chain step 1: accept EULAworkflow-step2-update.json— Dependency chain step 2: install update (waits for EULA)