#+TITLE: Signal Pager Runbook #+AUTHOR: Craig Jennings #+DATE: 2026-07-20 The operational reference for the agent pager — how a page reaches Craig's phone, how his replies come back, how the account stays healthy, and the signal-cli setup behind it. This is the Signal successor to the retired ntfy runbook. Canonical home is rulesets because the pager is cross-machine tooling. * What the pager is One Signal identity, =+15045173983=, registered in *velox's* signal-cli (account file 465310, velox is the primary device). It is a dedicated pager number, not Craig's personal Signal. Pages go *from* that identity *to* Craig's own Signal account, which fires a normal mobile push on his phone. As of 2026-07-20 the identity spans two devices: velox (primary) and ratio (linked device "ratio-pager"). Any machine holding the account sends directly; a machine that doesn't relays to velox over the tailnet. Two constants the tooling depends on: - Pager account: =+15045173983= (primary on velox, linked on ratio). - Recipient: Craig's Signal account UUID =b1b5601e-6126-47f8-afaa-0a59f5188fde=. His phone *number* reads as unregistered in Signal's directory — always target the UUID, never the number. velox is the laptop that travels with Craig, so the pager account rides with him; ratio holds it too, so a page still lands when velox is down. * Choosing a channel Two trigger words, two channels, and both work from any agent runtime (nothing here is Claude-specific). protocols.org "Reaching Craig" is the short version pointed at every project; this runbook is the full one for the Signal side. - *"page me"* — desktop notification, stays up until dismissed: #+begin_src bash notify info "Title" "Message" --persist #+end_src - *"text me"* — the phone, over Signal: #+begin_src bash agent-text "Message for Craig's phone" #+end_src - *"text and page me"* — both. The default when a run can't tell whether he's away: the desktop one is free and the phone one reaches him if he is. * Sending a text =agent-text= (shipped at =claude-templates/bin/agent-text=, installed to =~/.local/bin= by =make -C ~/code/rulesets install=) is the interface. It hides the machine topology by checking whether the account is registered in the local signal-cli: - If the account is local (velox's primary or a linked device like ratio), it sends directly — no velox dependency. - Otherwise it ssh-relays the send to velox over the tailnet. - On failure (velox down or unreachable from a non-linked machine) it prints the desktop fallback line and exits non-zero, so a caller can tell the page did not land. The raw command it runs, for reference or a manual send from velox: #+begin_src bash signal-cli -a +15045173983 send -m "your message" b1b5601e-6126-47f8-afaa-0a59f5188fde #+end_src From another machine, the same send relayed over the tailnet: #+begin_src bash ssh velox.tailf3bb8c.ts.net \ "signal-cli -a +15045173983 send -m 'your message' b1b5601e-6126-47f8-afaa-0a59f5188fde" #+end_src Prefer =agent-text= over the raw command — it hardens the message for the remote shell and handles the fallback. Reach for the raw form only when debugging. * Reading replies Craig replies to a page straight from Signal on his phone. The reply is a normal data message *to* the pager account, so it is waiting in the pager's inbound queue until something receives it. Drain the queue and read what is there: #+begin_src bash # On velox: signal-cli -a +15045173983 receive --timeout 10 # From another machine: ssh velox.tailf3bb8c.ts.net "signal-cli -a +15045173983 receive --timeout 10" #+end_src =receive= prints every queued envelope and exits 0 once the queue drains or the timeout elapses. A text reply from Craig arrives as an envelope from his UUID carrying a =Body:= line — that line is the reply text. Most envelopes are delivery/read receipts and typing indicators (no =Body:=); the reply you want is the data message with body text. For a script that waits on a reply, add =--send-read-receipts= so his phone shows the page was read, and parse stdout for the =Body:= line on an envelope from =b1b5601e-…=. Note: =receive= is destructive — it consumes the queue. Whatever drains the queue (an on-demand read, or the warm-keeping timer below) is what sees the reply, and it is seen once. An agent that pages and then waits for an answer should do its own =receive= rather than race the timer. * Keeping the account warm (receive timer) Signal expects a registered account to receive regularly. Left alone, the pager account drifts stale — signal-cli warns "Messages have been last received N days ago" (observed at 47 days on 2026-07-20 before a manual drain reset it). A stale account is a reliability risk on the one channel that reaches Craig when he is away. The fix mirrors roam-sync: a systemd user timer that drains the queue on a cadence, keeping the account warm and, as a bonus, picking up async replies. With the account linked on both machines, each device wants its own regular receive, so the timer runs on *both* velox and ratio (the shared =common= dotfiles package, same home as roam-sync). - Script: =scripts/signal-receive.sh= (rulesets, so both machines get it on =git pull=). It no-ops cleanly on a machine that lacks the account. - Units: =scripts/signal-receive.service= + =.timer= (reference copies under =scripts/systemd/=; the stowed copies live in =common/.config/systemd/user/= of the dotfiles repo, so both machines get them). - Cadence: every 15 minutes (=OnUnitActiveSec=15min=), matching roam-sync. Enable on each machine (one-time, per daily-drivers.md's one-time-setup class): #+begin_src bash # After the dotfiles + rulesets pull, on each daily driver: systemctl --user daemon-reload systemctl --user enable --now signal-receive.timer systemctl --user status signal-receive.service # confirm a clean receive #+end_src * signal-cli setup notes - *Version:* signal-cli 0.14.5 on velox (2026-07-20). - *Accounts:* velox's signal-cli holds the pager identity =+15045173983= as the registered primary (account file 465310). ratio's signal-cli holds two accounts: Craig's personal number =+15103169357= (its own primary, note-to-self only — no phone push) and the pager identity as a *linked device* (Device 2, "ratio-pager", linked 2026-07-20). Both accounts coexist; target the pager with =-a +15045173983=. A future daily driver joins the same way. - *signal-mcp:* on velox, Claude sessions may also expose a =signal-mcp= tool (=send_message_to_user=, same pager identity) configured in velox's global =~/.claude.json=. It works there but is invisible from any other machine and from non-Claude runtimes, so =agent-text= is the portable habit. The old =page-signal= shell script was removed 2026-06-12 — do not resurrect it. - *Linking a device:* to add a second signal-cli as a linked device of the pager account (see the open decision below), provision it from the new machine and approve the link from the account holder: #+begin_src bash # On the new machine — prints a tsdevice:/ URI (render as QR to approve): signal-cli link -n "ratio-pager" # Approve from velox (the primary device): signal-cli -a +15045173983 addDevice --uri "tsdevice:/?uuid=…" #+end_src * Decision — linked device (2026-07-20) The topology question — ssh-relay only vs. registering daily drivers as linked devices — was decided in favor of linked devices. ssh-relay only was simpler (one identity, one receive point) but had a single point of failure: a page failed when velox was down or off the tailnet. Registering ratio as a linked device removes that: ratio sends directly, so a page lands even when velox is down. The costs, both paid: linked-device provisioning per machine (the =link= / =addDevice= handshake above), and each device wanting its own regular =receive= — so the warm-keeping timer moved from a velox-only home to the shared =common= package, running on both. Adding another daily driver later is the same handshake plus a dotfiles stow; the timer and =agent-text= already generalize to "any machine holding the account." * History - 2026-07-04 — home retired ntfy (self-hosted on ratio) and tore it down, switching agent paging to Signal. Handoff to rulesets to document and own. - 2026-07-13 — reconciled to one pager identity on velox; =agent-page= shipped (direct on velox, ssh-relay elsewhere, desktop fallback); protocols.org "Paging Craig" rewritten around the two channels. - 2026-07-20 — this runbook; receive-timer script + units added; a manual drain cleared the 47-day staleness live; ratio linked as a device of the pager account and its direct send verified; the tool (still named =agent-page= that morning) generalized to send directly from any machine holding the account; receive timer moved to the shared =common= package and enabled on both machines. - 2026-07-20 (later) — notification vocabulary split: "page me" is the desktop channel, "text me" is Signal, "text and page me" is both. The tool was renamed =agent-page= → =agent-text= to match, with a deprecated =agent-page= shim delegating to it. protocols.org section renamed "Paging Craig" → "Reaching Craig".