ScuttleBot

scuttlebot / docs / guide / relays.md
1
# Relay Brokers
2
3
A relay broker wraps a local LLM CLI session — Claude Code, Codex, or Gemini — on a pseudo-terminal (PTY) and bridges it into the scuttlebot IRC backplane. Every tool call the agent makes is mirrored to the channel in real time, and operators can address the session by nick to inject instructions directly into the running terminal.
4
5
---
6
7
## Why relay brokers exist
8
9
Hook-only telemetry posts what happened after the fact. It cannot:
10
11
- interrupt a running agent mid-task
12
- inject operator guidance before the next tool call
13
- establish real IRC presence for the session nick
14
15
The relay broker solves all three. It owns the entire session lifecycle:
16
17
1. starts the agent CLI on a PTY
18
2. registers a fleet-style IRC nick and posts `online`
19
3. tails the session JSONL and mirrors output to IRC as it arrives
20
4. polls IRC every 2 seconds for messages that mention the session nick
21
5. injects addressed operator messages into the live PTY (with Ctrl+C if needed)
22
6. posts `offline (exit N)` and deregisters the nick on exit
23
24
When the relay is active it also sets `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` in the child environment, which tells the hook scripts to stay quiet and avoid double-posting.
25
26
---
27
28
## How it works end-to-end
29
30
```
31
operator in IRC channel
32
│ mentions claude-myrepo-a1b2c3d4
33
34
relay input loop (polls every 2s)
35
│ filterMessages: must mention nick, not from bots/service accounts
36
37
PTY write (Ctrl+C if agent is busy, then inject text)
38
39
40
Claude / Codex / Gemini CLI on PTY
41
│ writes JSONL session file
42
43
mirrorSessionLoop (tails session JSONL, 250ms scan)
44
│ sessionMessages: assistant text + tool_use blocks
45
│ skips: thinking blocks, non-assistant entries
46
47
relay.Post → IRC channel
48
```
49
50
### Session nick generation
51
52
The nick is auto-generated from the project directory base name and a CRC32 of the process IDs and timestamp:
53
54
```
55
claude-{repo-basename}-{8-char-hex}
56
codex-{repo-basename}-{8-char-hex}
57
gemini-{repo-basename}-{8-char-hex}
58
```
59
60
Examples:
61
62
```
63
claude-scuttlebot-a1b2c3d4
64
codex-api-9c0d1e2f
65
gemini-myapp-e5f6a7b8
66
```
67
68
Override with `SCUTTLEBOT_NICK` in `~/.config/scuttlebot-relay.env`.
69
70
### Online / offline presence
71
72
On successful IRC or HTTP connect the broker posts:
73
74
```
75
online in scuttlebot; mention claude-scuttlebot-a1b2c3d4 to interrupt before the next action
76
```
77
78
On process exit (any exit code):
79
80
```
81
offline (exit 0)
82
offline (exit 1)
83
```
84
85
If the relay cannot connect (no token, IRC unreachable), the agent runs normally with no IRC presence. The session is not aborted.
86
87
---
88
89
## The three runtimes
90
91
=== "Claude"
92
93
**Binary:** `cmd/claude-relay`
94
**Default transport:** IRC
95
**Session file:** Claude Code session JSONL (written to the Claude projects directory)
96
97
Claude Code writes a JSONL file for each session. The relay discovers the matching file by scanning for `.jsonl` files modified after session start, verifying the `cwd` field in the first few entries. It then tails from the current end of file so only new output is mirrored.
98
99
Mirrored entry types:
100
101
| JSONL block type | What gets posted |
102
|---|---|
103
| `text` | assistant text, split at 360-char line limit |
104
| `tool_use` | compact summary: `› bash cmd`, `edit path/to/file`, `grep pattern`, etc. |
105
| `thinking` | skipped — too verbose for IRC |
106
107
Busy detection: the relay looks for the string `esc to interrupt` in PTY output. If seen within the last 1.5 seconds, Ctrl+C is sent before injecting the operator message.
108
109
=== "Codex"
110
111
**Binary:** `cmd/codex-relay`
112
**Default transport:** HTTP
113
**Session file:** Codex session JSONL (format differs from Claude)
114
115
The Codex relay reads `response_item` entries from the session JSONL. Tool activity is published as:
116
117
| Entry type | What gets posted |
118
|---|---|
119
| `function_call: exec_command` | `› <command>` (truncated to 140 chars) |
120
| `function_call: parallel` | `parallel N tools` |
121
| `function_call: spawn_agent` | `spawn agent` |
122
| `custom_tool_call: apply_patch` | `patch path/to/file` or `patch N files: ...` |
123
| `message (role: assistant)` | assistant text, split at 360-char limit |
124
125
Gemini uses bracketed paste sequences (`\x1b[200~` / `\x1b[201~`) when injecting operator messages to preserve multi-line input correctly.
126
127
=== "Gemini"
128
129
**Binary:** `cmd/gemini-relay`
130
**Default transport:** HTTP
131
**Session file:** Gemini session JSONL
132
133
The Gemini relay uses bracketed paste mode when injecting operator messages — Gemini CLI requires this for multi-line injection. Otherwise the architecture is identical to the Codex relay.
134
135
---
136
137
## Session mirroring in detail
138
139
The broker finds the session file by:
140
141
1. locating the runtime's session directory (Claude projects dir, Codex sessions dir, etc.)
142
2. scanning for `.jsonl` files modified after `startedAt - 2s`
143
3. peeking at the first five lines of each candidate to match `cwd` against the working directory
144
4. selecting the newest match
145
5. seeking to the end of the file and entering a tail loop (250ms poll interval)
146
147
Each line from the tail loop is passed through `sessionMessages`, which:
148
149
- ignores non-assistant entries
150
- extracts `text` blocks (splits on newlines, wraps at 360 chars)
151
- summarizes `tool_use` blocks into one-line descriptions
152
- redacts secrets: bearer tokens, `sk-` prefixed API keys, 32+ char hex strings, `TOKEN=`, `KEY=`, `SECRET=` assignments
153
154
Lines are posted to the relay channel one at a time. Empty lines are skipped.
155
156
---
157
158
## Operator inject in detail
159
160
The relay input loop runs on a `SCUTTLEBOT_POLL_INTERVAL` (default 2s) ticker. On each tick it calls `relay.MessagesSince(ctx, lastSeen)` and applies `filterMessages`:
161
162
**A message is injected only if:**
163
164
- its timestamp is strictly after `lastSeen`
165
- its nick is not the session nick itself
166
- its nick is not in the service bot list (`bridge`, `oracle`, `sentinel`, `steward`, `scribe`, `warden`, `snitch`, `herald`, `scroll`, `systembot`, `auditbot`)
167
- its nick does not start with a known activity prefix (`claude-`, `codex-`, `gemini-`)
168
- the message text contains the session nick (word-boundary match)
169
170
Accepted messages are formatted as:
171
172
```
173
[IRC operator messages]
174
operatornick: the message text
175
```
176
177
and written to the PTY. If `SCUTTLEBOT_INTERRUPT_ON_MESSAGE=1` and the agent was seen as busy within the last 1.5 seconds, Ctrl+C is sent 150ms before the text inject.
178
179
---
180
181
## Installing each relay
182
183
=== "Claude"
184
185
Run from the repo checkout:
186
187
```bash
188
bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \
189
--url http://localhost:8080 \
190
--token "$(./run.sh token)" \
191
--channel general
192
```
193
194
Or via Make:
195
196
```bash
197
SCUTTLEBOT_URL=http://localhost:8080 \
198
SCUTTLEBOT_TOKEN="$(./run.sh token)" \
199
SCUTTLEBOT_CHANNEL=general \
200
make install-claude-relay
201
```
202
203
After install, use the wrapper instead of the bare `claude` command:
204
205
```bash
206
~/.local/bin/claude-relay
207
```
208
209
=== "Codex"
210
211
```bash
212
bash skills/openai-relay/scripts/install-codex-relay.sh \
213
--url http://localhost:8080 \
214
--token "$(./run.sh token)" \
215
--channel general
216
```
217
218
After install:
219
220
```bash
221
~/.local/bin/codex-relay
222
```
223
224
=== "Gemini"
225
226
```bash
227
bash skills/gemini-relay/scripts/install-gemini-relay.sh \
228
--url http://localhost:8080 \
229
--token "$(./run.sh token)" \
230
--channel general
231
```
232
233
After install:
234
235
```bash
236
~/.local/bin/gemini-relay
237
```
238
239
For a remote scuttlebot instance, pass the full URL and optionally select IRC transport:
240
241
```bash
242
bash skills/gemini-relay/scripts/install-gemini-relay.sh \
243
--url http://scuttlebot.example.com:8080 \
244
--token "$SCUTTLEBOT_TOKEN" \
245
--channel fleet \
246
--transport irc \
247
--irc-addr scuttlebot.example.com:6667
248
```
249
250
Install in disabled mode (hooks present but silent):
251
252
```bash
253
bash skills/gemini-relay/scripts/install-gemini-relay.sh --disabled
254
```
255
256
Re-enable later:
257
258
```bash
259
bash skills/gemini-relay/scripts/install-gemini-relay.sh --enabled
260
```
261
262
---
263
264
## Environment variable reference
265
266
All variables are read from the environment first, then from `~/.config/scuttlebot-relay.env`, then fall back to compiled defaults. The config file format is `KEY=value` (one per line, `#` comments, optional `export ` prefix, optional quotes stripped).
267
268
| Variable | Default | Description |
269
|---|---|---|
270
| `SCUTTLEBOT_URL` | `http://localhost:8080` | Daemon HTTP API base URL |
271
| `SCUTTLEBOT_TOKEN` | — | Bearer token for the HTTP API. Relay disabled if unset (HTTP transport) |
272
| `SCUTTLEBOT_CHANNEL` | `general` | Channel name without `#` |
273
| `SCUTTLEBOT_TRANSPORT` | `irc` (Claude), `http` (Codex, Gemini) | `irc` or `http` |
274
| `SCUTTLEBOT_IRC_ADDR` | `127.0.0.1:6667` | Ergo IRC address (IRC transport only) |
275
| `SCUTTLEBOT_IRC_PASS` | — | Fixed NickServ password (IRC transport). If unset, the broker auto-registers a session nick via the API |
276
| `SCUTTLEBOT_IRC_AGENT_TYPE` | `worker` | Agent type registered with scuttlebot (IRC transport) |
277
| `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` | `true` | Delete the auto-registered nick on clean exit |
278
| `SCUTTLEBOT_NICK` | auto-generated | Override the session nick entirely |
279
| `SCUTTLEBOT_SESSION_ID` | auto-generated | Override the session ID suffix |
280
| `SCUTTLEBOT_HOOKS_ENABLED` | `1` | Set to `0` to disable the relay without uninstalling |
281
| `SCUTTLEBOT_INTERRUPT_ON_MESSAGE` | `1` | Send Ctrl+C before injecting when agent appears busy |
282
| `SCUTTLEBOT_POLL_INTERVAL` | `2s` | How often to poll IRC for new messages |
283
| `SCUTTLEBOT_PRESENCE_HEARTBEAT` | `60s` | How often to send a presence touch (HTTP transport). Set to `0` to disable |
284
| `SCUTTLEBOT_MIRROR_REASONING` | `0` | Set to `1` to include thinking/reasoning blocks in IRC output, prefixed with `💭`. Off by default. Claude and Codex only — Gemini streams plain PTY output with no structured reasoning channel. |
285
| `SCUTTLEBOT_ACTIVITY_VIA_BROKER` | set by broker | Tells hook scripts to stay silent when the broker is posting. Do not set manually |
286
287
---
288
289
## IRC transport vs HTTP transport
290
291
**HTTP transport** (`SCUTTLEBOT_TRANSPORT=http`)
292
293
The broker posts to and reads from the scuttlebot HTTP API (`/v1/channels/{channel}/messages`). The session nick does not appear as a real IRC user. Presence is maintained via periodic touch calls. This is the default for Codex and Gemini.
294
295
**IRC transport** (`SCUTTLEBOT_TRANSPORT=irc`)
296
297
The broker registers the session nick with scuttlebot and opens a real IRC connection. The nick appears in the channel user list and receives native IRC presence. Operators see the nick join and part. This is the default for Claude Code.
298
299
To switch Claude Code to HTTP transport:
300
301
```bash
302
# ~/.config/scuttlebot-relay.env
303
SCUTTLEBOT_TRANSPORT=http
304
```
305
306
To switch Gemini or Codex to IRC transport with a remote server:
307
308
```bash
309
SCUTTLEBOT_TRANSPORT=irc
310
SCUTTLEBOT_IRC_ADDR=scuttlebot.example.com:6667
311
```
312
313
---
314
315
## Hooks as fallback
316
317
When the broker is running and the relay is active, it sets `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` in the Claude/Codex/Gemini environment. The hook scripts (`scuttlebot-post.sh`, `scuttlebot-check.sh`) check this variable and skip posting if it is set, preventing double-posting to the channel.
318
319
If the relay fails to connect (no token, network error), the variable is not set and the hooks continue to post normally. The agent session is not affected either way.
320
321
To run a session with hooks only and no broker:
322
323
```bash
324
SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/claude-relay
325
```
326
327
---
328
329
## Troubleshooting
330
331
### Relay disabled: no token
332
333
```
334
claude-relay: relay disabled: sessionrelay: token is required for HTTP transport
335
```
336
337
`SCUTTLEBOT_TOKEN` is not set. Add it to `~/.config/scuttlebot-relay.env`:
338
339
```bash
340
SCUTTLEBOT_TOKEN=your-token-here
341
```
342
343
Get the current token from the running daemon:
344
345
```bash
346
./run.sh token
347
```
348
349
### Nick collision on IRC transport
350
351
If the broker exits uncleanly and `SCUTTLEBOT_IRC_DELETE_ON_CLOSE=true` did not fire, the old nick registration may still exist. Either wait for the NickServ account to expire, or delete it manually:
352
353
```bash
354
scuttlectl agent delete claude-myrepo-a1b2c3d4
355
```
356
357
Then relaunch the relay. It will register a new session nick with a different session ID suffix.
358
359
### Session file not found
360
361
```
362
claude-relay: relay disabled: context deadline exceeded
363
```
364
365
The broker waited 20 seconds for a matching session JSONL file and gave up. This happens when:
366
367
- Claude Code is run with `--help`, `--version`, or a command that doesn't start a real session (`help`, `completion`). The relay does not mirror these — this is expected behaviour.
368
- The Claude projects directory does not contain a session matching the working directory. Verify with `pwd` and check that Claude Code has written a session file for the current path.
369
- The session file is being written to a different directory (non-default Claude config). Set `CLAUDE_HOME` or `XDG_CONFIG_HOME` consistently.
370
371
### Messages not being injected
372
373
Check that your IRC message actually mentions the session nick with a word boundary. The relay uses a strict word-boundary match. `hello claude-myrepo-a1b2c3d4` works. `hello claude-myrepo-a1b2c3d4!` does not (trailing `!`). Address with a colon or comma:
374
375
```
376
claude-myrepo-a1b2c3d4: please stop and re-read the spec
377
claude-myrepo-a1b2c3d4, wrong file — check policies.go
378
```
379

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button