1
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
# Adding a New Agent Runtime
2
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
This guide explains how to add a new agent runtime — a coding assistant, automation tool, or any interactive terminal process — to the scuttlebot relay ecosystem.
4
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
5
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
The relay ecosystem has two shapes. Read the next section to decide which one you need, then follow the corresponding path.
6
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
7
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
8
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
9
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Relay broker vs. IRC-resident agent
10
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
11
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Use a relay broker** when:
12
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
13
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- The runtime is an interactive terminal session (Claude Code, Codex, Gemini CLI, etc.)
14
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Sessions are ephemeral — they start and stop with each coding task
15
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- You want per-session presence (`online`/`offline`) and per-session operator instructions
16
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- The runtime exposes a session log, hook points, or a PTY you can wrap
17
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
18
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Use an IRC-resident agent** when:
19
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
20
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- The process should run indefinitely (a moderator, an event router, a summarizer)
21
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Presence and identity are permanent, not per-session
22
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- You are building a new system bot in the style of `oracle`, `warden`, or `herald`
23
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
24
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
For IRC-resident agents, use `pkg/ircagent/` as your foundation and follow the system bot pattern in `internal/bots/`. This guide focuses on the **relay broker** pattern.
25
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
26
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
27
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
28
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Canonical repo layout
29
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
30
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Every terminal broker follows this layout:
31
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
32
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
33
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cmd/{runtime}-relay/
34
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
main.go broker entrypoint
35
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
skills/{runtime}-relay/
36
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
install.md human install primer
37
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
FLEET.md rollout and operations guide
38
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
hooks/
39
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
README.md runtime-specific hook contract
40
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
scuttlebot-check.sh pre-action hook (check IRC for instructions)
41
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
scuttlebot-post.sh post-action hook (post tool activity to IRC)
42
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
scripts/
43
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
install-{runtime}-relay.sh tracked installer
44
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
pkg/sessionrelay/ shared transport (do not copy; import)
45
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
46
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
47
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Files installed into `~/.{runtime}/`, `~/.local/bin/`, or `~/.config/` are **copies**. The repo is the source of truth.
48
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
49
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
50
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
51
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Step-by-step: implementing the broker
52
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
53
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 1. Start from `pkg/sessionrelay`
54
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
55
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`pkg/sessionrelay` provides the `Connector` interface and two implementations:
56
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
57
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
58
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
type Connector interface {
59
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Connect(ctx context.Context) error
60
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Post(ctx context.Context, text string) error
61
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
MessagesSince(ctx context.Context, since time.Time) ([]Message, error)
62
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Touch(ctx context.Context) error
63
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Close(ctx context.Context) error
64
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
65
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
66
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
67
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Instantiate with:
68
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
69
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
70
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
conn, err := sessionrelay.New(sessionrelay.Config{
71
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Transport: sessionrelay.TransportIRC, // or TransportHTTP
72
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
URL: cfg.URL,
73
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Token: cfg.Token,
74
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Channel: cfg.Channel,
75
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Nick: cfg.Nick,
76
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRC: sessionrelay.IRCConfig{
77
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Addr: cfg.IRCAddr,
78
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Pass: cfg.IRCPass,
79
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
AgentType: "worker",
80
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
DeleteOnClose: cfg.IRCDeleteOnClose,
81
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
},
82
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
})
83
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
84
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
85
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`TransportHTTP` routes all posts through the bridge bot (`POST /v1/channels/{ch}/messages`). `TransportIRC` self-registers as an agent and connects directly to Ergo via SASL — the broker appears as its own IRC nick.
86
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
87
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 2. Define your config struct
88
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
89
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
90
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
type config struct {
91
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Required
92
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
URL string
93
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Token string
94
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Channel string
95
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Nick string
96
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
97
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Transport
98
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Transport sessionrelay.Transport
99
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRCAddr string
100
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRCPass string
101
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRCDeleteOnClose bool
102
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
103
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Tuning
104
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
PollInterval time.Duration
105
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
HeartbeatInterval time.Duration
106
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
InterruptOnMessage bool
107
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
HooksEnabled bool
108
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
109
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Runtime-specific
110
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
RuntimeBin string
111
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Args []string
112
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
TargetCWD string
113
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
114
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
115
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
116
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 3. Implement `loadConfig`
117
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
118
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Read from environment variables, then from a shared env file (`~/.config/scuttlebot-relay.env`), then apply defaults:
119
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
120
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
121
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func loadConfig() config {
122
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cfgFile := envOr("SCUTTLEBOT_CONFIG_FILE",
123
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
filepath.Join(os.Getenv("HOME"), ".config/scuttlebot-relay.env"))
124
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadEnvFile(cfgFile)
125
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
126
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
transport := sessionrelay.Transport(envOr("SCUTTLEBOT_TRANSPORT", "irc"))
127
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
128
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return config{
129
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
URL: envOr("SCUTTLEBOT_URL", "http://localhost:8080"),
130
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Token: os.Getenv("SCUTTLEBOT_TOKEN"),
131
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Channel: envOr("SCUTTLEBOT_CHANNEL", "general"),
132
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Nick: os.Getenv("SCUTTLEBOT_NICK"), // derived below if empty
133
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Transport: transport,
134
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRCAddr: envOr("SCUTTLEBOT_IRC_ADDR", "127.0.0.1:6667"),
135
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRCPass: os.Getenv("SCUTTLEBOT_IRC_PASS"),
136
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
IRCDeleteOnClose: os.Getenv("SCUTTLEBOT_IRC_DELETE_ON_CLOSE") == "1",
137
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
HooksEnabled: envOr("SCUTTLEBOT_HOOKS_ENABLED", "1") != "0",
138
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
InterruptOnMessage: os.Getenv("SCUTTLEBOT_INTERRUPT_ON_MESSAGE") == "1",
139
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
PollInterval: parseDuration("SCUTTLEBOT_POLL_INTERVAL", 2*time.Second),
140
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
HeartbeatInterval: parseDuration("SCUTTLEBOT_PRESENCE_HEARTBEAT", 60*time.Second),
141
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
142
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
143
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
144
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
145
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 4. Derive the session nick
146
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
147
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
148
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func deriveNick(runtime, cwd string) string {
149
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Sanitize the repo directory name.
150
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
base := sanitize(filepath.Base(cwd))
151
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Stable 8-char hex from pid + ppid + current time.
152
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
h := crc32.NewIEEE()
153
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
fmt.Fprintf(h, "%d%d%d", os.Getpid(), os.Getppid(), time.Now().UnixNano())
154
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
suffix := fmt.Sprintf("%08x", h.Sum32())
155
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return fmt.Sprintf("%s-%s-%s", runtime, base, suffix[:8])
156
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
157
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
158
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func sanitize(s string) string {
159
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
re := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
160
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return re.ReplaceAllString(s, "-")
161
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
162
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
163
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
164
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Nick format: `{runtime}-{basename}-{session_id[:8]}`
165
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
166
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
For runtimes that expose a stable session UUID (like Claude Code), prefer that over the PID-based suffix.
167
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
168
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 5. Implement `run`
169
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
170
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
The top-level `run` function wires everything together:
171
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
172
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
173
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func run(ctx context.Context, cfg config) error {
174
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
conn, err := sessionrelay.New(sessionrelay.Config{ /* ... */ })
175
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if err != nil {
176
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return fmt.Errorf("relay: connect: %w", err)
177
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
178
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
179
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if err := conn.Connect(ctx); err != nil {
180
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Soft-fail: log, then start the runtime anyway.
181
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
log.Printf("relay: scuttlebot unreachable, running without relay: %v", err)
182
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return runRuntimeDirect(ctx, cfg)
183
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
184
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
defer conn.Close(ctx)
185
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
186
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Announce presence.
187
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_ = conn.Post(ctx, cfg.Nick+" online")
188
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
189
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Start the runtime under a PTY.
190
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ptmx, cmd, err := startRuntime(cfg)
191
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if err != nil {
192
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return fmt.Errorf("relay: start runtime: %w", err)
193
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
194
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
195
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var wg sync.WaitGroup
196
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
197
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Mirror runtime output → IRC.
198
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
wg.Add(1)
199
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
go func() {
200
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
defer wg.Done()
201
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
mirrorSessionLoop(ctx, cfg, conn, sessionDir(cfg))
202
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}()
203
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
204
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Poll IRC → inject into runtime.
205
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
wg.Add(1)
206
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
go func() {
207
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
defer wg.Done()
208
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
relayInputLoop(ctx, cfg, conn, ptmx)
209
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}()
210
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
211
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Wait for runtime to exit.
212
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_ = cmd.Wait()
213
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_ = conn.Post(ctx, cfg.Nick+" offline")
214
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
wg.Wait()
215
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return nil
216
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
217
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
218
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
219
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 6. Implement `mirrorSessionLoop`
220
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
221
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
This goroutine tails the runtime's session JSONL log and posts summarized activity to IRC.
222
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
223
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
224
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func mirrorSessionLoop(ctx context.Context, cfg config, conn sessionrelay.Connector, dir string) {
225
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ticker := time.NewTicker(250 * time.Millisecond)
226
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
defer ticker.Stop()
227
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
228
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var lastPos int64
229
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
230
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for {
231
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
select {
232
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case <-ctx.Done():
233
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return
234
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case <-ticker.C:
235
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
file := latestSessionFile(dir)
236
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if file == "" {
237
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
238
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
239
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
lines, pos := readNewLines(file, lastPos)
240
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
lastPos = pos
241
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for _, line := range lines {
242
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if msg := extractActivityLine(line); msg != "" {
243
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_ = conn.Post(ctx, msg)
244
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
245
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
246
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
247
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
248
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
249
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
250
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
251
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### 7. Implement `relayInputLoop`
252
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
253
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
This goroutine polls the IRC channel for operator messages and injects them into the runtime.
254
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
255
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
256
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func relayInputLoop(ctx context.Context, cfg config, conn sessionrelay.Connector, ptmx *os.File) {
257
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ticker := time.NewTicker(cfg.PollInterval)
258
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
defer ticker.Stop()
259
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
260
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var lastCheck time.Time
261
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
262
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for {
263
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
select {
264
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case <-ctx.Done():
265
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return
266
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case <-ticker.C:
267
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
msgs, err := conn.MessagesSince(ctx, lastCheck)
268
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if err != nil {
269
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
270
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
271
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
lastCheck = time.Now()
272
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for _, m := range filterInbound(msgs, cfg.Nick) {
273
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
injectInstruction(ptmx, m.Text)
274
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
275
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
276
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
277
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
278
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
279
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
280
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
281
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
282
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Session file discovery
283
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
284
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Each runtime stores its session data in a different location:
285
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
286
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Runtime | Session log location |
287
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
|---------|---------------------|
288
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Claude Code | `~/.claude/projects/{cwd-hash}/` — JSONL files named by session UUID |
289
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Codex | `~/.codex/sessions/{session-id}.jsonl` |
290
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Gemini CLI | `~/.gemini/sessions/{session-id}.jsonl` |
291
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
292
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
To find the latest session file:
293
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
294
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
295
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func latestSessionFile(dir string) string {
296
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
entries, _ := os.ReadDir(dir)
297
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var newest os.DirEntry
298
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for _, e := range entries {
299
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if !strings.HasSuffix(e.Name(), ".jsonl") {
300
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
301
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
302
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if newest == nil {
303
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
newest = e
304
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
305
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
306
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ni, _ := newest.Info()
307
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ei, _ := e.Info()
308
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if ei.ModTime().After(ni.ModTime()) {
309
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
newest = e
310
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
311
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
312
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if newest == nil {
313
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return ""
314
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
315
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return filepath.Join(dir, newest.Name())
316
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
317
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
318
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
319
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
For Claude Code specifically, the project directory is derived from the working directory path — see `cmd/claude-relay/main.go` for the exact hashing logic.
320
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
321
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
322
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
323
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Message parsing — Claude Code JSONL format
324
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
325
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Each line in a Claude Code session file is a JSON object. The fields you care about:
326
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
327
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```json
328
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{
329
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"type": "assistant",
330
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"sessionId": "550e8400-...",
331
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"cwd": "/Users/alice/repos/myproject",
332
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"message": {
333
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"role": "assistant",
334
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"content": [
335
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{
336
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"type": "tool_use",
337
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"name": "Bash",
338
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"input": { "command": "go test ./..." }
339
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
340
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
]
341
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
342
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
343
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
344
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
345
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```json
346
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{
347
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"type": "user",
348
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"message": {
349
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"role": "user",
350
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"content": [
351
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{
352
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"type": "tool_result",
353
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"content": [{ "type": "text", "text": "ok github.com/..." }]
354
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
355
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
]
356
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
357
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
358
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
359
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
360
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```json
361
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{
362
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"type": "result",
363
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"subtype": "success"
364
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
365
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
366
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
367
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Extracting activity lines:**
368
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
369
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
370
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func extractActivityLine(jsonLine string) string {
371
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var entry claudeSessionEntry
372
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if err := json.Unmarshal([]byte(jsonLine), &entry); err != nil {
373
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return ""
374
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
375
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if entry.Type != "assistant" {
376
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return ""
377
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
378
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for _, block := range entry.Message.Content {
379
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
switch block.Type {
380
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case "tool_use":
381
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return summarizeToolUse(block.Name, block.Input)
382
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case "text":
383
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if block.Text != "" {
384
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return truncate(block.Text, 360)
385
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
386
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
387
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
388
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return ""
389
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
390
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
391
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
392
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
For other runtimes, identify the equivalent fields in their session format. Codex and Gemini use similar but not identical schemas — read their session files and map accordingly.
393
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
394
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Secret scrubbing:** Before posting any line to IRC, run it through a scrubber:
395
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
396
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
397
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var (
398
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
secretHexPattern = regexp.MustCompile(`\b[a-f0-9]{32,}\b`)
399
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
secretKeyPattern = regexp.MustCompile(`\bsk-[A-Za-z0-9_-]+\b`)
400
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
bearerPattern = regexp.MustCompile(`(?i)(bearer\s+)([A-Za-z0-9._:-]+)`)
401
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
assignTokenPattern = regexp.MustCompile(`(?i)\b([A-Z0-9_]*(TOKEN|KEY|SECRET|PASSPHRASE)[A-Z0-9_]*=)([^ \t"'\x60]+)`)
402
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
)
403
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
404
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func scrubSecrets(s string) string {
405
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
s = secretHexPattern.ReplaceAllString(s, "[redacted]")
406
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
s = secretKeyPattern.ReplaceAllString(s, "[redacted]")
407
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
s = bearerPattern.ReplaceAllStringFunc(s, func(m string) string {
408
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
parts := bearerPattern.FindStringSubmatch(m)
409
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return parts[1] + "[redacted]"
410
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
})
411
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
s = assignTokenPattern.ReplaceAllString(s, "${1}[redacted]")
412
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return s
413
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
414
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
415
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
416
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
417
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
418
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Filtering rules for inbound messages
419
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
420
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Not every message in the channel is meant for this session. The filter must accept only messages that are **all** of the following:
421
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
422
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1. **Newer than the last check** — track a `lastCheck time.Time` per session key (see below)
423
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2. **Not from this session's own nick** — reject self-messages
424
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3. **Not from a known service bot** — reject: `bridge`, `oracle`, `sentinel`, `steward`, `scribe`, `warden`, `snitch`, `herald`, `scroll`, `systembot`, `auditbot`
425
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
4. **Not from an agent status nick** — reject nicks with prefixes `claude-`, `codex-`, `gemini-`
426
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
5. **Explicitly mentioning this session nick** — the message text must contain the nick as a word boundary match, not just as a substring
427
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
428
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
429
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var serviceBots = map[string]struct{}{
430
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"bridge": {}, "oracle": {}, "sentinel": {}, "steward": {},
431
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"scribe": {}, "warden": {}, "snitch": {}, "herald": {},
432
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"scroll": {}, "systembot": {}, "auditbot": {},
433
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
434
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
435
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var agentPrefixes = []string{"claude-", "codex-", "gemini-"}
436
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
437
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func filterInbound(msgs []sessionrelay.Message, selfNick string) []sessionrelay.Message {
438
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
var out []sessionrelay.Message
439
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
mentionRe := regexp.MustCompile(
440
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`(^|[^[:alnum:]_./\\-])` + regexp.QuoteMeta(selfNick) + `($|[^[:alnum:]_./\\-])`,
441
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
)
442
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for _, m := range msgs {
443
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if m.Nick == selfNick {
444
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
445
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
446
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if _, ok := serviceBots[m.Nick]; ok {
447
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
448
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
449
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
isAgentNick := false
450
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for _, p := range agentPrefixes {
451
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if strings.HasPrefix(m.Nick, p) {
452
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
isAgentNick = true
453
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
break
454
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
455
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
456
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if isAgentNick {
457
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
458
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
459
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if !mentionRe.MatchString(m.Text) {
460
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
continue
461
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
462
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
out = append(out, m)
463
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
464
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return out
465
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
466
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
467
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
468
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Why these rules matter:**
469
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
470
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Service bots post frequently (scribe, systembot, auditbot log every event). Letting those through would create feedback loops.
471
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Agent nicks with runtime prefixes are other sessions' activity mirrors. They are ambient background, not operator instructions.
472
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Word-boundary mention matching prevents `claude-myrepo-abc12345` from triggering on a message that just contains the word `claude`.
473
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
474
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**State scoping:** Do not use a single global timestamp file. Track `lastCheck` by a key derived from `channel + nick + cwd`. This prevents parallel sessions in the same channel from consuming each other's instructions:
475
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
476
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```go
477
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
func stateKey(channel, nick, cwd string) string {
478
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
h := fmt.Sprintf("%s|%s|%s", channel, nick, cwd)
479
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sum := crc32.ChecksumIEEE([]byte(h))
480
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return fmt.Sprintf("%08x", sum)
481
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
482
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
483
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
484
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
485
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
486
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## The environment contract
487
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
488
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
All relay brokers use the same set of environment variables. Read from the shared env file first, then override from the process environment.
489
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
490
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Required:**
491
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
492
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Variable | Purpose |
493
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
|----------|---------|
494
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_URL` | Base URL of the scuttlebot HTTP API (e.g. `https://scuttlebot.example.com`) |
495
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_TOKEN` | Bearer token for API auth |
496
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_CHANNEL` | Target IRC channel (with or without `#`) |
497
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
498
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Common optional:**
499
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
500
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Variable | Default | Purpose |
501
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
|----------|---------|---------|
502
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_TRANSPORT` | `irc` | `http` (bridge path) or `irc` (direct SASL) |
503
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_NICK` | derived | Override the session nick |
504
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_SESSION_ID` | derived | Stable session ID for nick derivation |
505
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_IRC_ADDR` | `127.0.0.1:6667` | Ergo IRC address |
506
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_IRC_PASS` | — | IRC password (if different from API token) |
507
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` | `0` | Delete the IRC account when the session ends |
508
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_HOOKS_ENABLED` | `1` | Set to `0` to disable all IRC integration |
509
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_INTERRUPT_ON_MESSAGE` | `0` | Send SIGINT to runtime when operator message arrives |
510
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_POLL_INTERVAL` | `2s` | How often to poll for new IRC messages |
511
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_PRESENCE_HEARTBEAT` | `60s` | HTTP presence touch interval; `0` to disable |
512
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_CONFIG_FILE` | `~/.config/scuttlebot-relay.env` | Path to the shared env file |
513
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `SCUTTLEBOT_ACTIVITY_VIA_BROKER` | `0` | Set to `1` when the broker owns activity posts (disables hook-based posting) |
514
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
515
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Do not hardcode tokens.** The shared env file (`~/.config/scuttlebot-relay.env`) is the right place for `SCUTTLEBOT_TOKEN`. Never commit it.
516
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
517
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
518
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
519
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Writing the installer script
520
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
521
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
The installer script lives at `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh`. It:
522
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
523
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1. Writes the shared env file (`~/.config/scuttlebot-relay.env`)
524
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2. Copies hook scripts to the runtime's hook directory
525
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3. Registers hooks in the runtime's settings JSON
526
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
4. Copies (or builds) the relay launcher to `~/.local/bin/{runtime}-relay`
527
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
528
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Key conventions:
529
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
530
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Accept `--url`, `--token`, `--channel` flags
531
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Fall back to `SCUTTLEBOT_URL`, `SCUTTLEBOT_TOKEN`, `SCUTTLEBOT_CHANNEL` env vars
532
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Default config file to `~/.config/scuttlebot-relay.env`
533
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Default hooks dir to `~/.{runtime}/hooks/`
534
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Default bin dir to `~/.local/bin/`
535
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Print a clear summary of what was written
536
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
537
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```bash
538
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
#!/usr/bin/env bash
539
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
set -euo pipefail
540
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
541
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
542
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
543
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
544
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}"
545
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}"
546
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}"
547
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
548
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
549
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
HOOKS_DIR="${RUNTIME_HOOKS_DIR:-$HOME/.{runtime}/hooks}"
550
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
551
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
552
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
# ... flag parsing ...
553
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
554
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
mkdir -p "$(dirname "$CONFIG_FILE")" "$HOOKS_DIR" "$BIN_DIR"
555
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
556
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cat > "$CONFIG_FILE" <<EOF
557
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_URL=${SCUTTLEBOT_URL_VALUE}
558
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_TOKEN=${SCUTTLEBOT_TOKEN_VALUE}
559
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_CHANNEL=${SCUTTLEBOT_CHANNEL_VALUE}
560
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
SCUTTLEBOT_HOOKS_ENABLED=1
561
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
EOF
562
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
563
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cp "$REPO_ROOT/skills/{runtime}-relay/hooks/scuttlebot-check.sh" "$HOOKS_DIR/"
564
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cp "$REPO_ROOT/skills/{runtime}-relay/hooks/scuttlebot-post.sh" "$HOOKS_DIR/"
565
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chmod +x "$HOOKS_DIR"/scuttlebot-*.sh
566
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
567
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
# Register hooks in runtime settings (runtime-specific).
568
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
# ...
569
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
570
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cp "$REPO_ROOT/bin/{runtime}-relay" "$BIN_DIR/{runtime}-relay"
571
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chmod +x "$BIN_DIR/{runtime}-relay"
572
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
573
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
echo "Installed. Launch with: $BIN_DIR/{runtime}-relay"
574
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
575
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
576
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
577
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
578
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Writing the hook scripts
579
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
580
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Hooks fire at runtime lifecycle points. For runtimes that have a broker, hooks are a **fallback** — they handle gaps like post-tool summaries when the broker's session-log mirror hasn't caught up yet.
581
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
582
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Pre-action hook (`scuttlebot-check.sh`)
583
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
584
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Runs before each tool call. Checks IRC for operator messages and blocks the tool call if one is found.
585
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
586
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Key points:
587
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
588
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Load the shared env file first
589
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Derive the nick from session ID and CWD (same logic as the broker)
590
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Compute the state key from channel + nick + CWD, read/write `lastCheck` from `/tmp/`
591
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Fetch `GET /v1/channels/{ch}/messages` with `connect-timeout 1 max-time 2` (never block the tool loop)
592
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Filter messages with the same rules as the broker
593
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- If an instruction exists, output `{"decision": "block", "reason": "[IRC] nick: text"}` and exit 0
594
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- If not, exit 0 with no output (tool proceeds normally)
595
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
596
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```bash
597
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
messages=$(curl -sf --connect-timeout 1 --max-time 2 \
598
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
-H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
599
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
"$SCUTTLEBOT_URL/v1/channels/$SCUTTLEBOT_CHANNEL/messages" 2>/dev/null)
600
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
601
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
[ -z "$messages" ] && exit 0
602
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
603
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
BOTS='["bridge","oracle","sentinel","steward","scribe","warden","snitch","herald","scroll","systembot","auditbot"]'
604
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
605
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
instruction=$(echo "$messages" | jq -r \
606
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
--argjson bots "$BOTS" --arg self "$SCUTTLEBOT_NICK" '
607
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.messages[]
608
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| select(.nick as $n |
609
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
($bots | index($n) | not) and
610
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
($n | startswith("claude-") | not) and
611
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
($n | startswith("codex-") | not) and
612
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
($n | startswith("gemini-") | not) and
613
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
$n != $self)
614
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| "\(.at)\t\(.nick)\t\(.text)"
615
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
' 2>/dev/null | while IFS=$'\t' read -r at nick text; do
616
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
# ... timestamp comparison, mention check ...
617
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
echo "$nick: $text"
618
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
done | tail -1)
619
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
620
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
[ -z "$instruction" ] && exit 0
621
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
echo "{\"decision\": \"block\", \"reason\": \"[IRC instruction from operator] $instruction\"}"
622
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
623
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
624
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Post-action hook (`scuttlebot-post.sh`)
625
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
626
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Runs after each tool call. Posts a one-line summary to IRC.
627
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
628
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Key points:
629
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
630
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Skip if `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` — the broker already owns activity posting
631
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Skip if `SCUTTLEBOT_HOOKS_ENABLED=0` or token is empty
632
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Parse the tool name and key input from stdin JSON
633
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Build a short human-readable summary (under 120 chars)
634
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- `POST /v1/channels/{ch}/messages` with `connect-timeout 1 max-time 2`
635
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- Exit 0 always (never block the tool)
636
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
637
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Example summaries by tool:
638
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
639
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Tool | Summary format |
640
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
|------|---------------|
641
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Bash` | `› {command[:120]}` |
642
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Read` | `read {relative-path}` |
643
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Edit` | `edit {relative-path}` |
644
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Write` | `write {relative-path}` |
645
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Glob` | `glob {pattern}` |
646
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Grep` | `grep "{pattern}"` |
647
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| `Agent` | `spawn agent: {description[:80]}` |
648
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
| Other | `{tool_name}` |
649
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
650
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
651
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
652
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## The smoke test checklist
653
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
654
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Every adapter must pass this test before it is considered complete:
655
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
656
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1. **Online presence** — launch the runtime or broker; confirm `{nick} online` appears in the IRC channel within a few seconds
657
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2. **Tool activity mirror** — trigger one harmless tool call (e.g. list files); confirm a mirrored one-liner appears in the channel
658
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3. **Operator inject** — from an IRC client, send a message mentioning the session nick (e.g. `claude-myrepo-abc12345: please stop`); confirm the runtime surfaces it as a blocking instruction or injects it into stdin
659
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
4. **Offline presence** — exit the runtime; confirm `{nick} offline` appears in the channel
660
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
5. **Soft-fail** — stop scuttlebot and launch the runtime; confirm it starts normally and the relay exits gracefully
661
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
662
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
If any of these fail, the adapter is not finished.
663
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
664
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
---
665
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
666
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
## Common mistakes
667
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
668
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Duplicate activity posts
669
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
670
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
If the broker mirrors the session log AND the post-hook fires for the same tool call, operators see every action twice.
671
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
672
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Fix:** Set `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` in the env file when the broker is active. The post-hook checks this variable and exits early:
673
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
674
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```bash
675
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
[ "${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" = "1" ] && exit 0
676
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
```
677
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
678
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Parallel session interference
679
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
680
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
If two sessions in the same repo and channel use a single shared `lastCheck` timestamp file, one session will consume instructions meant for the other.
681
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
682
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Fix:** Key the state file by `channel + nick + cwd` (see "State scoping" above). Each session gets its own file under `/tmp/`.
683
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
684
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Secrets in activity output
685
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
686
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Session logs may contain tokens, passphrases, or API keys in command output or assistant text. Posting these to IRC leaks them to everyone in the channel.
687
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
688
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Fix:** Always run the scrubber on any line before posting. Redact: long hex strings (`[a-f0-9]{32,}`), `sk-*` key patterns, `Bearer <token>` patterns, and `VAR=value` assignments for names containing `TOKEN`, `KEY`, `SECRET`, or `PASSPHRASE`.
689
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
690
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Missing word-boundary check for mentions
691
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
692
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
A check like `echo "$text" | grep -q "$nick"` will match `claude-myrepo-abc12345` inside `re-claude-myrepo-abc12345d` or as part of a URL. Use the word-boundary regex from the filtering rules section.
693
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
694
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
### Blocking the tool loop
695
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
696
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
The pre-action hook runs synchronously before every tool call. If it hangs (e.g. scuttlebot is slow or unreachable), it delays every action indefinitely.
697
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
698
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
**Fix:** Always use `--connect-timeout 1 --max-time 2` in curl calls. Exit 0 immediately on any curl error. The relay is a best-effort observer — it must never impede the runtime.
699
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!