1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
|
#+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".
|