Your unattended agent loops die silently. Nobody tells you.
An opencode or Claude Code loop fired by cron/systemd runs while you sleep. When it crashes, stalls, or gets stuck in a retry loop, the only witness is a log file nobody reads. Every local watcher in this space keeps its signals on the box — none pings an external dead-man service. Heartbeat is that external service: one event hook pings on every loop turn, so a stalled agent simply stops pinging and pages you over a webhook or an encrypted Nostr DM.
- Create a monitor. One self-serve call from the agent's own shell:
curl -sS https://golemreach.com/heartbeat/api/monitors/self-serve \ -H 'Content-Type: application/json' \ -d '{"name":"my-agent-loop","period":900,"kind":"push"}'The response returns aping_url(likehttps://golemreach.com/heartbeat/ping/<key>) and a publichttps://golemreach.com/heartbeat/status/<name>page. Export it asHEARTBEAT_PING_URLin the loop's environment. - Ping from inside the loop — opencode plugin. Drop this
plugin.json into your agent's
config dir (e.g.
~/.config/opencode/plugins/heartbeat/plugin.json) and setHEARTBEAT_PING_URL. It registers aSessionStatushook that fires on the busy→idle transition after every turn — so a turn that never completes stops the pings and pages you:
{ "name": "heartbeat-deadmanswitch", "version": "1.0.0", "hooks": { "SessionStatus": [ { "hooks": [ { "type": "command", "command": "curl -sS --max-time 10 \"${HEARTBEAT_PING_URL}\" >/dev/null 2>&1 || true" } ] } ] } }session.idleis deprecated upstream; this usesSessionStatuswith itstypefield, the supported path. - Ping from inside the loop — Claude Code adapter. Claude Code hooks are
config, not code: add this
settings.json hook block. The
Stopevent fires at the end of every turn, so a paused/stalled loop stops pinging and trips the switch. (Plugin-scopedSessionEnddoes not fire on Claude Code — useStop, per the upstream issue.):
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "curl -sS --max-time 10 \"$HEARTBEAT_PING_URL\" >/dev/null 2>&1 || true" } ] } ] } } - Zero-dependency supervisor wrapper. For boxes without plugin support, wrap
the run in a systemd timer → agent → curl. The agent pings on success; if
it never returns (crash/stall), the ping never arrives and you get paged:
[Unit] Description=Unattended agent loop with dead-man switch [Service] Type=oneshot ExecStartPre=-curl -sS --max-time 10 https://golemreach.com/heartbeat/ping/KEY >/dev/null 2>&1 ExecStart=/usr/bin/opencode run "ship the next ticket" ExecStartPost=-/bin/sh -c 'curl -sS --max-time 10 https://golemreach.com/heartbeat/fail/KEY >/dev/null 2>&1 || true' [Timer] OnCalendar=*:0/15 Persistent=true
Why an external ping, not a local watcher
If the box itself dies — OOM kill, disk full, network partition — an
on-box watcher dies with it and you learn nothing. A heartbeat lives outside your
stack: the signal is "did a ping arrive," and the absence of a ping is the alert. Pair
it with our run-gate and your loop checks
https://golemreach.com/heartbeat/gate/KEY before each turn — one flip halts every agent at its next
boundary from the outside, covering the mirror direction (control pull, not just
liveness push).
Honest posture
Heartbeat is a dead-man switch, not an agent dashboard. It tells you one thing reliably: the pings stopped. It does not read your agent's context or manage its lifecycle. The plugin/hook snippets are config-only and ping a URL you control; we never receive your code or logs. Free while in beta; a $5 USDC pass lifts the rate limits — see pricing. Back to all integrations.