claude-beacon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,45 @@
1
+ # ── github-ci-channel environment variables ───────────────────────────────────
2
+ #
3
+ # Copy this file to .env in the repo root:
4
+ # cp .env.example .env
5
+ #
6
+ # Bun loads .env automatically from the working directory when you run
7
+ # any entry point directly (index.ts, mux.ts, ghwatch.ts).
8
+ #
9
+ # DO NOT commit .env — it is already in .gitignore.
10
+ #
11
+ # Mux mode: bun run src/mux.ts
12
+ # Standalone: bun run src/index.ts
13
+ # Polling: bun run src/ghwatch.ts
14
+
15
+ # ── Required ──────────────────────────────────────────────────────────────────
16
+
17
+ # HMAC-SHA256 secret shared with GitHub webhook settings.
18
+ # Generate: openssl rand -hex 32
19
+ # Must match the Secret field in GitHub → Settings → Webhooks exactly.
20
+ GITHUB_WEBHOOK_SECRET=
21
+
22
+ # Fine-grained PAT: Actions (Read) + Pull requests (Read)
23
+ # Classic PAT: public_repo
24
+ # Used by fetch_workflow_logs and behind-PR detection after a push to main.
25
+ GITHUB_TOKEN=
26
+
27
+ # ── Optional — webhook receiver ───────────────────────────────────────────────
28
+
29
+ # HTTP port for incoming GitHub webhooks (default: 9443).
30
+ # WEBHOOK_PORT=9443
31
+
32
+ # Skip HMAC verification. ONLY for local dev without a real webhook secret.
33
+ # NEVER enable in production.
34
+ # WEBHOOK_DEV_MODE=false
35
+
36
+ # ── Optional — mux server (src/mux.ts only) ───────────────────────────────────
37
+
38
+ # Localhost-only port for MCP Streamable HTTP (default: 9444).
39
+ # Claude Code sessions connect to http://127.0.0.1:${MCP_PORT}/mcp
40
+ # MCP_PORT=9444
41
+
42
+ # ── Optional — GitHub polling watcher (src/ghwatch.ts only) ──────────────────
43
+
44
+ # Comma-separated list of repositories to watch (owner/repo).
45
+ # WATCH_REPOS=myorg/frontend,myorg/backend
package/README.md ADDED
@@ -0,0 +1,558 @@
1
+ # claude-beacon
2
+
3
+ [![CI](https://github.com/drmf-cz/claude-beacon/actions/workflows/ci.yml/badge.svg)](https://github.com/drmf-cz/claude-beacon/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/claude-beacon.svg)](https://www.npmjs.com/package/claude-beacon)
5
+
6
+ > MCP channel plugin that pushes GitHub Actions CI/CD results and PR merge status directly into running Claude Code sessions — triggering automatic investigation and remediation.
7
+
8
+ Built on the [Claude Code Channels API](https://docs.anthropic.com/en/docs/claude-code/channels) (research preview, ≥ v2.1.80).
9
+
10
+ ## What it does
11
+
12
+ The plugin runs inside your Claude Code session and listens for GitHub events. When something requires attention, it pushes an actionable instruction directly into the session — Claude reads it and acts on it immediately.
13
+
14
+ | GitHub event | Condition | What Claude does |
15
+ |---|---|---|
16
+ | `workflow_run` completed | failure on **main/master** | Fetches logs, diagnoses root cause, spawns subagent to fix and push |
17
+ | `workflow_run` completed | failure on feature branch | Fetches logs, spawns subagent to investigate |
18
+ | `workflow_run` completed | success / cancelled / skipped | Silent — no notification |
19
+ | `push` to **main/master only** | open PRs exist | Checks each PR's merge status via API, notifies on `dirty` or `behind` |
20
+ | `pull_request` opened/synced | `mergeable_state: dirty` | Spawns subagent to rebase and resolve conflicts |
21
+ | `pull_request` opened/synced | `mergeable_state: behind` | Spawns subagent to rebase cleanly |
22
+ | `pull_request_review` submitted | any non-draft state | Debounced 30 s, then enters plan mode + `pr-comment-response` skill |
23
+ | `pull_request_review_comment` created | — | Accumulated in same debounce window |
24
+ | `pull_request_review_thread` unresolved | thread re-opened | Accumulated in same debounce window, shown as 🔄 |
25
+ | `issue_comment` created | PR comment (not issue) | Accumulated in same debounce window |
26
+
27
+ > **Why `push` events for PRs?** GitHub does not fire a `pull_request` event when the base branch advances and makes a PR go `behind`. The only way to detect this is to listen to `push` on main and then query the API for open PRs.
28
+ >
29
+ > **Push to a feature branch does not trigger PR checks.** Only a push to a main/master branch can make other PRs go `behind`. Pushing your own feature branch just updates that branch — it doesn't affect other PRs' merge status.
30
+
31
+ ## Architecture
32
+
33
+ ```
34
+ GitHub Actions / PR / push event
35
+ │ HMAC-SHA256 signed webhook
36
+
37
+ [cloudflared tunnel] ← free, no account needed for temp URLs
38
+
39
+
40
+ HTTP :9443 (webhook receiver — subprocess of Claude Code)
41
+
42
+ ├─ workflow_run/job/check_suite → parseWorkflowEvent()
43
+ ├─ pull_request (dirty/behind) → parsePullRequestEvent()
44
+ └─ push to main → checkPRsAfterPush() [async, API call]
45
+
46
+
47
+ notifications/claude/channel
48
+
49
+
50
+ Claude Code session
51
+ ├─ fetch_workflow_logs tool
52
+ └─ spawns subagents to fix/rebase
53
+ ```
54
+
55
+ The MCP server is started automatically by Claude Code as a subprocess — you never run it manually.
56
+
57
+ ## Requirements
58
+
59
+ - [Bun](https://bun.sh) ≥ 1.1.0 — the package runs on Bun; `npx` will not work
60
+ - Claude Code ≥ 2.1.80
61
+ - [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) (or ngrok)
62
+ - GitHub PAT — fine-grained: **Actions: Read** + **Pull requests: Read** | classic: `public_repo`
63
+
64
+ > `GITHUB_TOKEN` is required for two features: `fetch_workflow_logs` (log fetching) and `checkPRsAfterPush` (listing open PRs after a push). Without it, those features silently no-op.
65
+
66
+ ## Installation
67
+
68
+ No cloning required. Install once with Bun:
69
+
70
+ ```bash
71
+ bun add -g claude-beacon
72
+ ```
73
+
74
+ This puts two binaries in `~/.bun/bin/`:
75
+
76
+ | Binary | Purpose |
77
+ |---|---|
78
+ | `claude-beacon` | Standalone MCP server — spawned as subprocess by Claude Code (single session) |
79
+ | `claude-beacon-mux` | Mux server — run once, connects all Claude Code sessions via HTTP |
80
+
81
+ To update to a newer version: `bun add -g claude-beacon@latest`
82
+
83
+ > **No global install?** You can also run directly with `bunx`:
84
+ > ```bash
85
+ > bunx claude-beacon # standalone
86
+ > bunx -p claude-beacon claude-beacon-mux # mux
87
+ > ```
88
+
89
+ ## Setup (Option A — Webhook + Tunnel)
90
+
91
+ Real-time. Supports all event types including `workflow_job`, `check_suite`, and PR status.
92
+
93
+ ### 1. Install
94
+
95
+ ```bash
96
+ bun add -g claude-beacon
97
+ ```
98
+
99
+ Or clone if you want to hack on the source:
100
+
101
+ ```bash
102
+ git clone https://github.com/drmf-cz/claude-beacon
103
+ cd claude-beacon
104
+ bun install
105
+ ```
106
+
107
+ ### 2. Generate a webhook secret
108
+
109
+ ```bash
110
+ openssl rand -hex 32
111
+ # → copy the output — you'll paste it into GitHub and .mcp.json
112
+ ```
113
+
114
+ ### 3. Start the tunnel
115
+
116
+ ```bash
117
+ cloudflared tunnel --url http://localhost:9443
118
+ # → prints: https://random-name.trycloudflare.com ← copy this URL
119
+ ```
120
+
121
+ Leave the tunnel running. Each restart produces a new URL — update GitHub when that happens.
122
+ For a stable URL: [Cloudflare named tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/) (free account) or [ngrok static domain](https://ngrok.com/blog-post/free-static-domains-ngrok-users) (free tier).
123
+
124
+ ### 4. Register the webhook on GitHub
125
+
126
+ 1. Repo → **Settings → Webhooks → Add webhook**
127
+ 2. **Payload URL** — paste your tunnel URL
128
+ 3. **Content type** — `application/json`
129
+ 4. **Secret** — paste the value from step 2
130
+ 5. **Which events** — choose *Let me select individual events*, then tick:
131
+ - ✅ Workflow runs
132
+ - ✅ Workflow jobs
133
+ - ✅ Check suites
134
+ - ✅ Pull requests
135
+ - ✅ Pull request reviews
136
+ - ✅ Pull request review comments
137
+ - ✅ Pull request review threads
138
+ - ✅ Issue comments
139
+ - ✅ Pushes
140
+ 6. Click **Add webhook** — GitHub sends a ping; you should see a green ✓
141
+
142
+ ### 5. Configure `.mcp.json`
143
+
144
+ Create or edit `~/.mcp.json` (all projects) or `.mcp.json` in your project root.
145
+
146
+ **After global install** (`bun add -g`):
147
+
148
+ ```json
149
+ {
150
+ "mcpServers": {
151
+ "claude-beacon": {
152
+ "command": "/home/you/.bun/bin/claude-beacon",
153
+ "env": {
154
+ "GITHUB_WEBHOOK_SECRET": "your-secret-from-step-2",
155
+ "GITHUB_TOKEN": "your-pat"
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ Replace `/home/you` with your home directory (`echo $HOME`). Bun installs globals to `~/.bun/bin/`.
163
+
164
+ **If you cloned the repo** (or prefer `bunx` for always-latest):
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "claude-beacon": {
170
+ "command": "/home/you/.bun/bin/bunx",
171
+ "args": ["claude-beacon"],
172
+ "env": {
173
+ "GITHUB_WEBHOOK_SECRET": "your-secret-from-step-2",
174
+ "GITHUB_TOKEN": "your-pat"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ > Claude Code spawns MCP subprocesses without your shell PATH, so always use absolute paths to binaries. Find them with `which claude-beacon` or `which bunx`.
182
+
183
+ ### 6. Start Claude Code
184
+
185
+ ```bash
186
+ claude --dangerously-load-development-channels server:claude-beacon
187
+ ```
188
+
189
+ You should see:
190
+ ```
191
+ Listening for channel messages from: server:claude-beacon
192
+ ```
193
+
194
+ The server is now running. Push a commit, trigger a CI run, or let a PR fall behind — notifications will appear in your session automatically.
195
+
196
+ ---
197
+
198
+ ## Option B — GitHub CLI Events watcher (no tunnel)
199
+
200
+ No tunnel, no webhook config. Polls the [GitHub Events API](https://docs.github.com/en/rest/activity/events) using your existing `gh` CLI session.
201
+
202
+ **Trade-offs vs Option A:**
203
+ - ~30–60 s latency (server-dictated poll interval)
204
+ - `WorkflowRunEvent` only — no `workflow_job`, `check_suite`, or PR events
205
+ - No "behind PR" detection (Events API doesn't include pull request merge status)
206
+
207
+ ```json
208
+ {
209
+ "mcpServers": {
210
+ "claude-beacon": {
211
+ "command": "/home/you/.bun/bin/bun",
212
+ "args": ["run", "/path/to/claude-beacon/src/ghwatch.ts"],
213
+ "env": {
214
+ "WATCH_REPOS": "owner/repo1,owner/repo2"
215
+ }
216
+ }
217
+ }
218
+ }
219
+ ```
220
+
221
+ Auth: uses `gh auth token` automatically. Override with `GITHUB_TOKEN` if needed.
222
+
223
+ Start the same way:
224
+ ```bash
225
+ claude --dangerously-load-development-channels server:claude-beacon
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Environment variables
231
+
232
+ | Variable | Option A | Option B | Description |
233
+ |---|---|---|---|
234
+ | `WEBHOOK_PORT` | optional | — | HTTP port for webhook receiver (default: `9443`) |
235
+ | `GITHUB_WEBHOOK_SECRET` | required | — | HMAC-SHA256 secret — must match GitHub webhook settings exactly |
236
+ | `GITHUB_TOKEN` | required* | optional | PAT for log fetching and PR status checks |
237
+ | `WATCH_REPOS` | — | required | Comma-separated `owner/repo` list |
238
+ | `REVIEW_DEBOUNCE_MS` | optional | — | Debounce window for review events (default: `30000` ms) |
239
+
240
+ \* Without `GITHUB_TOKEN`, `fetch_workflow_logs` and behind-PR detection silently no-op.
241
+
242
+ ---
243
+
244
+ ## Running multiple Claude Code sessions (mux server)
245
+
246
+ By default the server is spawned as a subprocess by Claude Code and binds port 9443. A second session fails to bind that port and misses all events.
247
+
248
+ The **mux server** (`src/mux.ts`) solves this: you run it once as a persistent process and every Claude Code session connects to it via a single local URL — no subprocess is spawned per session.
249
+
250
+ ```
251
+ GitHub ──► :9443 (webhook receiver)
252
+
253
+ [mux — you start this once]
254
+
255
+ :9444/mcp (MCP over HTTP — localhost only)
256
+ ┌────┴────┐
257
+ Session A Session B Session C …
258
+ ```
259
+
260
+ ### Quick setup
261
+
262
+ **1. Create your `.env` file with your secrets:**
263
+
264
+ ```bash
265
+ cp .env.example .env
266
+ # Edit .env — fill in GITHUB_WEBHOOK_SECRET and GITHUB_TOKEN
267
+ ```
268
+
269
+ **2. Start the mux once** (tmux pane, background terminal, or [systemd unit](docs/multi-session.md#running-as-a-systemd-unit)):
270
+
271
+ ```bash
272
+ # After global install (recommended)
273
+ claude-beacon-mux # reads .env from current directory
274
+ claude-beacon-mux --config my-config.yaml # optional YAML config
275
+
276
+ # Or via bunx (no install)
277
+ bunx -p claude-beacon claude-beacon-mux
278
+
279
+ # Or from cloned repo
280
+ bun run start:mux
281
+ ```
282
+
283
+ **3. Register the mux in Claude Code** (run once — applies to all projects):
284
+
285
+ ```bash
286
+ claude mcp add --transport http claude-beacon http://127.0.0.1:9444/mcp
287
+ ```
288
+
289
+ Or add to `.mcp.json` in your project:
290
+
291
+ ```json
292
+ {
293
+ "mcpServers": {
294
+ "claude-beacon": {
295
+ "url": "http://127.0.0.1:9444/mcp",
296
+ "type": "http"
297
+ }
298
+ }
299
+ }
300
+ ```
301
+
302
+ **4. Start Claude Code normally:**
303
+
304
+ ```bash
305
+ claude --dangerously-load-development-channels server:claude-beacon
306
+ ```
307
+
308
+ **5. Register your session filter** — tell the mux which repo and branch this session is watching:
309
+
310
+ ```
311
+ Call set_filter with the output of:
312
+ git remote get-url origin → parse to owner/repo
313
+ git branch --show-current → current branch
314
+ ```
315
+
316
+ To make this automatic, add to `~/.claude/CLAUDE.md`:
317
+
318
+ ```markdown
319
+ ## GitHub CI Channel — session filter
320
+ When claude-beacon MCP connects, call `set_filter` immediately:
321
+ run `git remote get-url origin` (parse to owner/repo) and
322
+ `git branch --show-current`, then call set_filter with those values.
323
+ ```
324
+
325
+ For full details — routing rules, systemd setup, comparison table — see **[docs/multi-session.md](docs/multi-session.md)**.
326
+
327
+ ---
328
+
329
+ ## YAML configuration file
330
+
331
+ For anything beyond the five environment variables, use a YAML config file. It lets you tune debounce windows, filter which repos or event types trigger Claude, and — most importantly — replace the built-in agent instructions with your own.
332
+
333
+ ### Quick start
334
+
335
+ ```bash
336
+ cp config.example.yaml my-config.yaml
337
+ # edit my-config.yaml to taste
338
+ ```
339
+
340
+ Pass it via `.mcp.json`:
341
+
342
+ ```json
343
+ {
344
+ "mcpServers": {
345
+ "claude-beacon": {
346
+ "command": "/home/you/.bun/bin/bun",
347
+ "args": [
348
+ "run", "/path/to/claude-beacon/src/index.ts",
349
+ "--config", "/path/to/my-config.yaml"
350
+ ],
351
+ "env": {
352
+ "GITHUB_WEBHOOK_SECRET": "...",
353
+ "GITHUB_TOKEN": "..."
354
+ }
355
+ }
356
+ }
357
+ }
358
+ ```
359
+
360
+ Environment variables (`WEBHOOK_PORT`, `GITHUB_WEBHOOK_SECRET`, `GITHUB_TOKEN`) always take precedence over their YAML equivalents. All fields are optional — omitted keys inherit from the defaults below.
361
+
362
+ ### Server options
363
+
364
+ | Key | Type | Default | Description |
365
+ |---|---|---|---|
366
+ | `server.port` | number | `9443` | HTTP port for the webhook receiver |
367
+ | `server.debounce_ms` | number | `30000` | How long (ms) to accumulate review events before firing |
368
+ | `server.cooldown_ms` | number | `300000` | Suppress duplicate notifications for the same PR within this window |
369
+ | `server.max_events_per_window` | number | `50` | Maximum review events buffered per debounce window |
370
+ | `server.main_branches` | string[] | `["main","master"]` | Branch names treated as production |
371
+
372
+ ### Webhook filters
373
+
374
+ | Key | Type | Default | Description |
375
+ |---|---|---|---|
376
+ | `webhooks.allowed_events` | string[] | `[]` (all) | Allowlist of GitHub event types to process. Empty = accept all supported types |
377
+ | `webhooks.allowed_repos` | string[] | `[]` (all) | Allowlist of repos as `"owner/repo"`. Empty = accept all |
378
+
379
+ Supported event type strings: `push`, `workflow_run`, `workflow_job`, `check_suite`, `pull_request`, `pull_request_review`, `pull_request_review_comment`, `pull_request_review_thread`, `issue_comment`.
380
+
381
+ ### Behavior — agent instructions
382
+
383
+ These control what Claude is asked to do when an event fires. Each field accepts a multi-line string. Placeholders are interpolated at runtime (see [Template placeholders](#template-placeholders)).
384
+
385
+ | Key | Triggered by | Notable sub-fields |
386
+ |---|---|---|
387
+ | `behavior.on_ci_failure_main.instruction` | `workflow_run` failure on a main branch | — |
388
+ | `behavior.on_ci_failure_branch.instruction` | `workflow_run` failure on any other branch | — |
389
+ | `behavior.on_pr_review.instruction` | PR review / comment events (after debounce) | `require_plan`, `skill` |
390
+ | `behavior.on_merge_conflict.instruction` | PR with `mergeable_state: dirty` | — |
391
+ | `behavior.on_branch_behind.instruction` | PR with `mergeable_state: behind` | — |
392
+
393
+ **`on_pr_review` sub-fields:**
394
+
395
+ | Key | Type | Default | Description |
396
+ |---|---|---|---|
397
+ | `behavior.on_pr_review.require_plan` | boolean | `true` | Whether Claude must enter plan mode before touching code |
398
+ | `behavior.on_pr_review.skill` | string | `"pr-comment-response"` | Skill name invoked to handle the review |
399
+
400
+ **Code style context:**
401
+
402
+ ```yaml
403
+ behavior:
404
+ code_style: |
405
+ - Use TypeScript strict mode; never cast to `any`
406
+ - Prefer `const` over `let`; avoid mutation
407
+ ```
408
+
409
+ The `code_style` string is prepended to every PR review notification so Claude applies consistent standards when addressing comments.
410
+
411
+ ### Template placeholders
412
+
413
+ The following placeholders are replaced at runtime inside any `instruction` string:
414
+
415
+ | Placeholder | Value |
416
+ |---|---|
417
+ | `{repo}` | `owner/repo` |
418
+ | `{branch}` | Branch name |
419
+ | `{pr_number}` | Pull request number |
420
+ | `{workflow_name}` | Workflow / check name |
421
+ | `{run_id}` | GitHub Actions run ID |
422
+
423
+ ### Minimal example
424
+
425
+ ```yaml
426
+ server:
427
+ debounce_ms: 10000 # faster review batching
428
+ main_branches: [main, release]
429
+
430
+ webhooks:
431
+ allowed_repos:
432
+ - my-org/my-repo # only watch this repo
433
+
434
+ behavior:
435
+ code_style: |
436
+ - Go 1.23+; run gofmt before committing
437
+ - All exported symbols must have godoc comments
438
+ on_pr_review:
439
+ require_plan: true
440
+ skill: pr-comment-response
441
+ ```
442
+
443
+ ### Migrating from environment variables
444
+
445
+ | Old env var | YAML equivalent |
446
+ |---|---|
447
+ | `WEBHOOK_PORT` | `server.port` |
448
+ | `REVIEW_DEBOUNCE_MS` | `server.debounce_ms` |
449
+
450
+ `GITHUB_WEBHOOK_SECRET` and `GITHUB_TOKEN` have no YAML equivalent — keep them in the `env` block inside `.mcp.json` (secrets should not live in a config file that could be committed to source control).
451
+
452
+ ---
453
+
454
+ ## Granting Claude automatic action permissions
455
+
456
+ By default Claude Code will pause and ask for confirmation before rebasing or force-pushing, even when the channel notification says "Act immediately". This is because Claude's `CLAUDE.md` rules rank above channel message instructions — which is correct security behaviour.
457
+
458
+ To authorize automatic actions, add this block to `~/.claude/CLAUDE.md`:
459
+
460
+ ```markdown
461
+ ## GitHub CI Channel — Automatic Actions
462
+
463
+ When the `claude-beacon` MCP channel sends a notification, you are pre-authorized to act without additional confirmation:
464
+
465
+ - **Branch behind base**: Create a git worktree at `/tmp/pr-<N>-rebase`, rebase onto the base branch, `git push --force-with-lease`, remove the worktree.
466
+ - **Merge conflicts**: Same worktree workflow; resolve conflicts during the interactive rebase.
467
+ - **CI failure on main**: Spawn a subagent — call `fetch_workflow_logs`, diagnose the root cause, apply a fix, push to restore main.
468
+ - **CI failure on feature branch**: Spawn a subagent — fetch logs, investigate, push a fix.
469
+ - **PR review / comments**: Enter plan mode, then use the `pr-comment-response` skill to address each thread. Apply code fixes in a worktree, reply to comments with `gh-pr-reply.sh --batch`.
470
+
471
+ The channel notification IS the permission. No additional confirmation needed for any of the above.
472
+ ```
473
+
474
+ **Why this is required:** Channel messages cannot grant permissions — only `CLAUDE.md` (or direct user instruction) can. The notification text "Act immediately" is a hint to Claude, but any conflicting `CLAUDE.md` rule wins. Adding the block above creates a standing permission that makes the channel notification a valid trigger rather than an override attempt.
475
+
476
+ ---
477
+
478
+ ## Troubleshooting
479
+
480
+ ### MCP shows red / "Failed to reconnect"
481
+
482
+ Port 9443 is likely still held by a previous session. Claude Code spawns the server as a subprocess — if an old one didn't exit cleanly, the new one fails to bind.
483
+
484
+ ```bash
485
+ lsof -i :9443 # find the process
486
+ kill <PID> # free the port
487
+ # restart Claude Code
488
+ ```
489
+
490
+ The server logs: `ERROR: Port 9443 is already in use. Kill the existing process (lsof -i :9443) and restart Claude Code.`
491
+
492
+ ### Webhooks return 401 Unauthorized
493
+
494
+ HMAC signature mismatch — the server and GitHub are using different secrets.
495
+
496
+ - **Most common cause:** a `.env` file in the repo directory has a different `GITHUB_WEBHOOK_SECRET` than `.mcp.json`. Bun loads `.env` automatically, but `.mcp.json` values take precedence. If you have both, make sure they match, or delete `.env`.
497
+ - Also check that the secret in `.mcp.json` exactly matches what was pasted into GitHub webhook settings (no extra whitespace).
498
+
499
+ ### No notification when a PR falls behind
500
+
501
+ Check that **Pushes** is ticked in your GitHub webhook event settings. Without push events, the server never knows main has advanced. Also confirm `GITHUB_TOKEN` is set — it's required to query the PR list after a push.
502
+
503
+ Also note: **push events only trigger behind-checks when the push is to a main/master branch.** Pushing to a feature branch does not trigger any PR checks.
504
+
505
+ ### Mux sends to too many sessions / Claude doesn't receive notifications
506
+
507
+ Streamable HTTP transport has no persistent connection, so the mux can't detect when a Claude Code session exits. Each restart of Claude Code creates a new session; stale sessions accumulate.
508
+
509
+ The mux includes a 30-minute idle TTL — sessions with no incoming requests for 30 minutes are removed automatically. You will see log lines like:
510
+ ```
511
+ [claude-beacon:mux] Session a1b2c3d4 idle >30 min — removed (total: 2)
512
+ ```
513
+
514
+ If you need to clear stale sessions immediately, restart the mux process. The session count should match the number of active Claude Code windows connected to it.
515
+
516
+ ### "bun: command not found" in MCP logs
517
+
518
+ Use the absolute path in `.mcp.json`: `"command": "/home/you/.bun/bin/bun"`. Find the path with `which bun`.
519
+
520
+ ### `claude_desktop_config.json` vs `.mcp.json`
521
+
522
+ These are for different apps:
523
+ - `~/.config/Claude/claude_desktop_config.json` → Claude **Desktop** (GUI)
524
+ - `~/.mcp.json` or `.mcp.json` → Claude Code **CLI** (`claude` command)
525
+
526
+ `--dangerously-load-development-channels` reads from `.mcp.json`, not the Desktop config.
527
+
528
+ ### Tunnel URL changed
529
+
530
+ Cloudflared free tier gives a new random URL on each restart. Update it: Repo → Settings → Webhooks → Edit → change Payload URL. For a permanent URL use a named tunnel or ngrok static domain.
531
+
532
+ ---
533
+
534
+ ## Development
535
+
536
+ ```bash
537
+ bun test # 32 tests
538
+ bun run typecheck # tsc --noEmit (strict + noUncheckedIndexedAccess)
539
+ bun run lint # Biome v2
540
+ bun run lint:fix # Auto-fix lint issues
541
+ bun run build # Bundle to dist/
542
+ ```
543
+
544
+ ### Linting
545
+
546
+ Biome v2 is configured in `biome.json` with strict rules. One deliberate deviation from the defaults:
547
+
548
+ - **`maxAllowedComplexity: 45`** — Biome's recommended default is 15. The main webhook `fetch()` handler in `server.ts` is a single large dispatcher that handles all event types; its cyclomatic complexity scores ~40 after the dispatcher refactors in this codebase. Splitting it further would obscure the control flow. The threshold is set just above the real worst-offender score — if it ever exceeds 45, that's a signal to refactor.
549
+
550
+ ## Security
551
+
552
+ - HMAC-SHA256 verification uses `timingSafeEqual` — constant-time, no timing oracle
553
+ - Fallback handler emits only `event + action + repo` — raw payload is never forwarded to Claude (prompt injection guard)
554
+ - `GITHUB_TOKEN` is read-only (`actions:read` + `pull_requests:read`) — no write access needed
555
+ - `.env` is gitignored — secrets stay local
556
+ - `GITHUB_WEBHOOK_SECRET` is **required** — omitting it causes all requests to be rejected; set `WEBHOOK_DEV_MODE=true` to bypass verification in local dev only
557
+
558
+ See [AGENTS.md](AGENTS.md) for the full security analysis and architecture reference.