GOLEMREACH

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.

  1. 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 a ping_url (like https://golemreach.com/heartbeat/ping/<key>) and a public https://golemreach.com/heartbeat/status/<name> page. Export it as HEARTBEAT_PING_URL in the loop's environment.
  2. 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 set HEARTBEAT_PING_URL. It registers a SessionStatus hook 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.idle is deprecated upstream; this uses SessionStatus with its type field, the supported path.
  3. Ping from inside the loop — Claude Code adapter. Claude Code hooks are config, not code: add this settings.json hook block. The Stop event fires at the end of every turn, so a paused/stalled loop stops pinging and trips the switch. (Plugin-scoped SessionEnd does not fire on Claude Code — use Stop, per the upstream issue.):
    {
      "hooks": {
        "Stop": [
          { "hooks": [ { "type": "command",
            "command": "curl -sS --max-time 10 \"$HEARTBEAT_PING_URL\" >/dev/null 2>&1 || true" } ] }
        ]
      }
    }
  4. 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).

WebhooksSlack / Discord / generic JSON on every down/up transition — the SLA-grade channel.
Nostr DMsEncrypted NIP-17 death alert, no email relay (unique in this category).
Run-gateOne flip halts every loop at its next boundary.

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.