Stateless autonomous agent for Claude Code — no -p, no SDK credits.
The heartbeat hook turns an interactive Claude Code session into a stateless task processor. Each message from the inbox gets its own fresh session, just like -p — but using your regular subscription.
Anthropic is separating -p and SDK usage into a dedicated credit bucket. If you've been using claude -p for automation, your costs just went up.
The heartbeat hook gives you -p behavior in interactive mode, which uses your regular subscription — not SDK credits.
supervisor.js → claude (interactive) → hooks/heartbeat.js (stop hook)
↓
io/inbox.jsonl ← external events
io/outbox.jsonl → relay → discord/slack/webhook
- The supervisor launches Claude Code in interactive mode
- After each response, the stop hook fires
- The hook reads the next line from
io/inbox.jsonl - If a message exists, it's injected — one message per session
- The agent processes it, writes a response to
io/outbox.jsonl - The hook signals the supervisor to kill and restart with fresh context
- Next message gets a clean session — true stateless operation
One message = one session = fresh context every time.
When the inbox is empty, the hook polls internally and blocks with minimal idle ticks (~20 tokens each) to keep the session alive until work arrives.
git clone https://github.com/Siigari/claude-heartbeat.git
cd claude-heartbeatnode supervisor.jsThat's it. The hook is already configured in .claude/settings.json. The supervisor launches Claude, and the heartbeat hook handles the rest.
Options:
node supervisor.js sonnet "Read CLAUDE.md" # default
node supervisor.js opus "Read CLAUDE.md" # use opusFrom another terminal:
# Plain text works
echo "what is 2+2" >> io/inbox.jsonl
# JSON format also works
echo '{"ts":"2025-01-01","channel":"test","author":"you","content":"hello agent"}' >> io/inbox.jsonlMessages are processed one at a time. If 5 messages pile up, each gets its own fresh session.
cd relay
npm install discord.js
BOT_TOKEN=your_token node relay.js# From a cron job
node examples/cron-trigger.js
# From an HTTP webhook
node examples/webhook-receiver.js- No SDK credits — interactive mode uses your subscription
- Stateless — each message gets fresh context, like
-p - Autonomous — watches inbox, processes messages, writes responses
- Cheap idle — minimal ticks (~20 tokens) while waiting for work
- One message per session — no context inflation from batching
- Auto-restart — supervisor handles crashes and session recycling
- Watchdog — detects stuck sessions and force-restarts them
- Orphan reaper — cleans up stale child processes on startup
- fsync'd state — offset and flag writes are flushed to disk
- Startup cost — each message pays the CLAUDE.md read (~500 tokens)
- One session — no parallelism per terminal (but run multiple terminals)
- Needs a terminal — even if backgrounded (use
screenortmux)
Each line in io/inbox.jsonl is either plain text or a JSON object:
fix the bug in auth.js
{"ts":"2025-05-13T10:00:00Z","channel":"discord","author":"username","content":"message text"}Lines are consumed one at a time. The hook tracks its position via a byte offset in io/.inbox-offset.
The agent writes responses as JSON lines to io/outbox.jsonl:
{"action":"send","channelId":"123456789","content":"response text"} ┌─────────────┐
│ supervisor │ (restarts on exit)
└──────┬──────┘
│
┌──────▼──────┐
│ claude code │ (interactive session)
└──────┬──────┘
│ stop hook fires after each response
┌──────▼──────┐
│ heartbeat.js │
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
inbox.jsonl .responded .restart
(read 1 line) (flag) (signal supervisor)
Idle flow: hook polls inbox → nothing → blocks with minimal tick → agent responds . → repeat
Message flow: hook reads one line → blocks with message → agent processes → hook sets .restart → supervisor kills session → restarts fresh
Run multiple terminals, each with their own working directory and inbox/outbox:
# Terminal 1: coding agent
cd ~/agents/coder && node supervisor.js
# Terminal 2: monitoring agent
cd ~/agents/monitor && node supervisor.js
# Terminal 3: research agent
cd ~/agents/research && node supervisor.js| Variable | Default | Description |
|---|---|---|
HEARTBEAT_INTERVAL |
60 |
Seconds between idle ticks |
WATCHDOG_TIMEOUT |
300 |
Seconds before supervisor kills a stuck session |
examples/cron-trigger.js— inject tick messages on a scheduleexamples/webhook-receiver.js— HTTP endpoint that writes to inboxrelay/relay.js— Discord relay that sends outbox messages
Convergence — companion AI with memory, personality, and physical connection.