claude-code-github-ci-channel 0.1.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,543 @@
1
+ # claude-code-github-ci-channel
2
+
3
+ [![CI](https://github.com/drmf-cz/claude-code-github-ci-channel/actions/workflows/ci.yml/badge.svg)](https://github.com/drmf-cz/claude-code-github-ci-channel/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/claude-code-github-ci-channel.svg)](https://www.npmjs.com/package/claude-code-github-ci-channel)
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 | Silent — no notification |
19
+ | `push` to main/master | 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
+ ## Architecture
30
+
31
+ ```
32
+ GitHub Actions / PR / push event
33
+ │ HMAC-SHA256 signed webhook
34
+
35
+ [cloudflared tunnel] ← free, no account needed for temp URLs
36
+
37
+
38
+ HTTP :9443 (webhook receiver — subprocess of Claude Code)
39
+
40
+ ├─ workflow_run/job/check_suite → parseWorkflowEvent()
41
+ ├─ pull_request (dirty/behind) → parsePullRequestEvent()
42
+ └─ push to main → checkPRsAfterPush() [async, API call]
43
+
44
+
45
+ notifications/claude/channel
46
+
47
+
48
+ Claude Code session
49
+ ├─ fetch_workflow_logs tool
50
+ └─ spawns subagents to fix/rebase
51
+ ```
52
+
53
+ The MCP server is started automatically by Claude Code as a subprocess — you never run it manually.
54
+
55
+ ## Requirements
56
+
57
+ - [Bun](https://bun.sh) ≥ 1.1.0 — the package runs on Bun; `npx` will not work
58
+ - Claude Code ≥ 2.1.80
59
+ - [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) (or ngrok)
60
+ - GitHub PAT — fine-grained: **Actions: Read** + **Pull requests: Read** | classic: `public_repo`
61
+
62
+ > `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.
63
+
64
+ ## Installation
65
+
66
+ No cloning required. Install once with Bun:
67
+
68
+ ```bash
69
+ bun add -g claude-code-github-ci-channel
70
+ ```
71
+
72
+ This puts two binaries in `~/.bun/bin/`:
73
+
74
+ | Binary | Purpose |
75
+ |---|---|
76
+ | `github-ci` | Standalone MCP server — spawned as subprocess by Claude Code (single session) |
77
+ | `github-ci-mux` | Mux server — run once, connects all Claude Code sessions via HTTP |
78
+
79
+ To update to a newer version: `bun add -g claude-code-github-ci-channel@latest`
80
+
81
+ > **No global install?** You can also run directly with `bunx`:
82
+ > ```bash
83
+ > bunx claude-code-github-ci-channel # standalone (github-ci)
84
+ > bunx -p claude-code-github-ci-channel github-ci-mux # mux
85
+ > ```
86
+
87
+ ## Setup (Option A — Webhook + Tunnel)
88
+
89
+ Real-time. Supports all event types including `workflow_job`, `check_suite`, and PR status.
90
+
91
+ ### 1. Install
92
+
93
+ ```bash
94
+ bun add -g claude-code-github-ci-channel
95
+ ```
96
+
97
+ Or clone if you want to hack on the source:
98
+
99
+ ```bash
100
+ git clone https://github.com/drmf-cz/claude-code-github-ci-channel
101
+ cd claude-code-github-ci-channel
102
+ bun install
103
+ ```
104
+
105
+ ### 2. Generate a webhook secret
106
+
107
+ ```bash
108
+ openssl rand -hex 32
109
+ # → copy the output — you'll paste it into GitHub and .mcp.json
110
+ ```
111
+
112
+ ### 3. Start the tunnel
113
+
114
+ ```bash
115
+ cloudflared tunnel --url http://localhost:9443
116
+ # → prints: https://random-name.trycloudflare.com ← copy this URL
117
+ ```
118
+
119
+ Leave the tunnel running. Each restart produces a new URL — update GitHub when that happens.
120
+ 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).
121
+
122
+ ### 4. Register the webhook on GitHub
123
+
124
+ 1. Repo → **Settings → Webhooks → Add webhook**
125
+ 2. **Payload URL** — paste your tunnel URL
126
+ 3. **Content type** — `application/json`
127
+ 4. **Secret** — paste the value from step 2
128
+ 5. **Which events** — choose *Let me select individual events*, then tick:
129
+ - ✅ Workflow runs
130
+ - ✅ Workflow jobs
131
+ - ✅ Check suites
132
+ - ✅ Pull requests
133
+ - ✅ Pull request reviews
134
+ - ✅ Pull request review comments
135
+ - ✅ Pull request review threads
136
+ - ✅ Issue comments
137
+ - ✅ Pushes
138
+ 6. Click **Add webhook** — GitHub sends a ping; you should see a green ✓
139
+
140
+ ### 5. Configure `.mcp.json`
141
+
142
+ Create or edit `~/.mcp.json` (all projects) or `.mcp.json` in your project root.
143
+
144
+ **After global install** (`bun add -g`):
145
+
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "github-ci": {
150
+ "command": "/home/you/.bun/bin/github-ci",
151
+ "env": {
152
+ "GITHUB_WEBHOOK_SECRET": "your-secret-from-step-2",
153
+ "GITHUB_TOKEN": "your-pat"
154
+ }
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ Replace `/home/you` with your home directory (`echo $HOME`). Bun installs globals to `~/.bun/bin/`.
161
+
162
+ **If you cloned the repo** (or prefer `bunx` for always-latest):
163
+
164
+ ```json
165
+ {
166
+ "mcpServers": {
167
+ "github-ci": {
168
+ "command": "/home/you/.bun/bin/bunx",
169
+ "args": ["claude-code-github-ci-channel"],
170
+ "env": {
171
+ "GITHUB_WEBHOOK_SECRET": "your-secret-from-step-2",
172
+ "GITHUB_TOKEN": "your-pat"
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ```
178
+
179
+ > Claude Code spawns MCP subprocesses without your shell PATH, so always use absolute paths to binaries. Find them with `which github-ci` or `which bunx`.
180
+
181
+ ### 6. Start Claude Code
182
+
183
+ ```bash
184
+ claude --dangerously-load-development-channels server:github-ci
185
+ ```
186
+
187
+ You should see:
188
+ ```
189
+ Listening for channel messages from: server:github-ci
190
+ ```
191
+
192
+ 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.
193
+
194
+ ---
195
+
196
+ ## Option B — GitHub CLI Events watcher (no tunnel)
197
+
198
+ No tunnel, no webhook config. Polls the [GitHub Events API](https://docs.github.com/en/rest/activity/events) using your existing `gh` CLI session.
199
+
200
+ **Trade-offs vs Option A:**
201
+ - ~30–60 s latency (server-dictated poll interval)
202
+ - `WorkflowRunEvent` only — no `workflow_job`, `check_suite`, or PR events
203
+ - No "behind PR" detection (Events API doesn't include pull request merge status)
204
+
205
+ ```json
206
+ {
207
+ "mcpServers": {
208
+ "github-ci": {
209
+ "command": "/home/you/.bun/bin/bun",
210
+ "args": ["run", "/path/to/claude-code-github-ci-channel/src/ghwatch.ts"],
211
+ "env": {
212
+ "WATCH_REPOS": "owner/repo1,owner/repo2"
213
+ }
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ Auth: uses `gh auth token` automatically. Override with `GITHUB_TOKEN` if needed.
220
+
221
+ Start the same way:
222
+ ```bash
223
+ claude --dangerously-load-development-channels server:github-ci
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Environment variables
229
+
230
+ | Variable | Option A | Option B | Description |
231
+ |---|---|---|---|
232
+ | `WEBHOOK_PORT` | optional | — | HTTP port for webhook receiver (default: `9443`) |
233
+ | `GITHUB_WEBHOOK_SECRET` | required | — | HMAC-SHA256 secret — must match GitHub webhook settings exactly |
234
+ | `GITHUB_TOKEN` | required* | optional | PAT for log fetching and PR status checks |
235
+ | `WATCH_REPOS` | — | required | Comma-separated `owner/repo` list |
236
+ | `REVIEW_DEBOUNCE_MS` | optional | — | Debounce window for review events (default: `30000` ms) |
237
+
238
+ \* Without `GITHUB_TOKEN`, `fetch_workflow_logs` and behind-PR detection silently no-op.
239
+
240
+ ---
241
+
242
+ ## Running multiple Claude Code sessions (mux server)
243
+
244
+ 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.
245
+
246
+ 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.
247
+
248
+ ```
249
+ GitHub ──► :9443 (webhook receiver)
250
+
251
+ [mux — you start this once]
252
+
253
+ :9444/mcp (MCP over HTTP — localhost only)
254
+ ┌────┴────┐
255
+ Session A Session B Session C …
256
+ ```
257
+
258
+ ### Quick setup
259
+
260
+ **1. Create your `.env` file with your secrets:**
261
+
262
+ ```bash
263
+ cp .env.example .env
264
+ # Edit .env — fill in GITHUB_WEBHOOK_SECRET and GITHUB_TOKEN
265
+ ```
266
+
267
+ **2. Start the mux once** (tmux pane, background terminal, or [systemd unit](docs/multi-session.md#running-as-a-systemd-unit)):
268
+
269
+ ```bash
270
+ # After global install (recommended)
271
+ github-ci-mux # reads .env from current directory
272
+ github-ci-mux --config my-config.yaml # optional YAML config
273
+
274
+ # Or via bunx (no install)
275
+ bunx -p claude-code-github-ci-channel github-ci-mux
276
+
277
+ # Or from cloned repo
278
+ bun run start:mux
279
+ ```
280
+
281
+ **3. Register the mux in Claude Code** (run once — applies to all projects):
282
+
283
+ ```bash
284
+ claude mcp add --transport http github-ci http://127.0.0.1:9444/mcp
285
+ ```
286
+
287
+ Or add to `.mcp.json` in your project:
288
+
289
+ ```json
290
+ {
291
+ "mcpServers": {
292
+ "github-ci": {
293
+ "url": "http://127.0.0.1:9444/mcp",
294
+ "type": "http"
295
+ }
296
+ }
297
+ }
298
+ ```
299
+
300
+ **4. Start Claude Code normally:**
301
+
302
+ ```bash
303
+ claude --dangerously-load-development-channels server:github-ci
304
+ ```
305
+
306
+ **5. Register your session filter** — tell the mux which repo and branch this session is watching:
307
+
308
+ ```
309
+ Call set_filter with the output of:
310
+ git remote get-url origin → parse to owner/repo
311
+ git branch --show-current → current branch
312
+ ```
313
+
314
+ To make this automatic, add to `~/.claude/CLAUDE.md`:
315
+
316
+ ```markdown
317
+ ## GitHub CI Channel — session filter
318
+ When github-ci MCP connects, call `set_filter` immediately:
319
+ run `git remote get-url origin` (parse to owner/repo) and
320
+ `git branch --show-current`, then call set_filter with those values.
321
+ ```
322
+
323
+ For full details — routing rules, systemd setup, comparison table — see **[docs/multi-session.md](docs/multi-session.md)**.
324
+
325
+ ---
326
+
327
+ ## YAML configuration file
328
+
329
+ 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.
330
+
331
+ ### Quick start
332
+
333
+ ```bash
334
+ cp config.example.yaml my-config.yaml
335
+ # edit my-config.yaml to taste
336
+ ```
337
+
338
+ Pass it via `.mcp.json`:
339
+
340
+ ```json
341
+ {
342
+ "mcpServers": {
343
+ "github-ci": {
344
+ "command": "/home/you/.bun/bin/bun",
345
+ "args": [
346
+ "run", "/path/to/claude-code-github-ci-channel/src/index.ts",
347
+ "--config", "/path/to/my-config.yaml"
348
+ ],
349
+ "env": {
350
+ "GITHUB_WEBHOOK_SECRET": "...",
351
+ "GITHUB_TOKEN": "..."
352
+ }
353
+ }
354
+ }
355
+ }
356
+ ```
357
+
358
+ 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.
359
+
360
+ ### Server options
361
+
362
+ | Key | Type | Default | Description |
363
+ |---|---|---|---|
364
+ | `server.port` | number | `9443` | HTTP port for the webhook receiver |
365
+ | `server.debounce_ms` | number | `30000` | How long (ms) to accumulate review events before firing |
366
+ | `server.cooldown_ms` | number | `300000` | Suppress duplicate notifications for the same PR within this window |
367
+ | `server.max_events_per_window` | number | `50` | Maximum review events buffered per debounce window |
368
+ | `server.main_branches` | string[] | `["main","master"]` | Branch names treated as production |
369
+
370
+ ### Webhook filters
371
+
372
+ | Key | Type | Default | Description |
373
+ |---|---|---|---|
374
+ | `webhooks.allowed_events` | string[] | `[]` (all) | Allowlist of GitHub event types to process. Empty = accept all supported types |
375
+ | `webhooks.allowed_repos` | string[] | `[]` (all) | Allowlist of repos as `"owner/repo"`. Empty = accept all |
376
+
377
+ 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`.
378
+
379
+ ### Behavior — agent instructions
380
+
381
+ 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)).
382
+
383
+ | Key | Triggered by | Notable sub-fields |
384
+ |---|---|---|
385
+ | `behavior.on_ci_failure_main.instruction` | `workflow_run` failure on a main branch | — |
386
+ | `behavior.on_ci_failure_branch.instruction` | `workflow_run` failure on any other branch | — |
387
+ | `behavior.on_pr_review.instruction` | PR review / comment events (after debounce) | `require_plan`, `skill` |
388
+ | `behavior.on_merge_conflict.instruction` | PR with `mergeable_state: dirty` | — |
389
+ | `behavior.on_branch_behind.instruction` | PR with `mergeable_state: behind` | — |
390
+
391
+ **`on_pr_review` sub-fields:**
392
+
393
+ | Key | Type | Default | Description |
394
+ |---|---|---|---|
395
+ | `behavior.on_pr_review.require_plan` | boolean | `true` | Whether Claude must enter plan mode before touching code |
396
+ | `behavior.on_pr_review.skill` | string | `"pr-comment-response"` | Skill name invoked to handle the review |
397
+
398
+ **Code style context:**
399
+
400
+ ```yaml
401
+ behavior:
402
+ code_style: |
403
+ - Use TypeScript strict mode; never cast to `any`
404
+ - Prefer `const` over `let`; avoid mutation
405
+ ```
406
+
407
+ The `code_style` string is prepended to every PR review notification so Claude applies consistent standards when addressing comments.
408
+
409
+ ### Template placeholders
410
+
411
+ The following placeholders are replaced at runtime inside any `instruction` string:
412
+
413
+ | Placeholder | Value |
414
+ |---|---|
415
+ | `{repo}` | `owner/repo` |
416
+ | `{branch}` | Branch name |
417
+ | `{pr_number}` | Pull request number |
418
+ | `{workflow_name}` | Workflow / check name |
419
+ | `{run_id}` | GitHub Actions run ID |
420
+
421
+ ### Minimal example
422
+
423
+ ```yaml
424
+ server:
425
+ debounce_ms: 10000 # faster review batching
426
+ main_branches: [main, release]
427
+
428
+ webhooks:
429
+ allowed_repos:
430
+ - my-org/my-repo # only watch this repo
431
+
432
+ behavior:
433
+ code_style: |
434
+ - Go 1.23+; run gofmt before committing
435
+ - All exported symbols must have godoc comments
436
+ on_pr_review:
437
+ require_plan: true
438
+ skill: pr-comment-response
439
+ ```
440
+
441
+ ### Migrating from environment variables
442
+
443
+ | Old env var | YAML equivalent |
444
+ |---|---|
445
+ | `WEBHOOK_PORT` | `server.port` |
446
+ | `REVIEW_DEBOUNCE_MS` | `server.debounce_ms` |
447
+
448
+ `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).
449
+
450
+ ---
451
+
452
+ ## Granting Claude automatic action permissions
453
+
454
+ 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.
455
+
456
+ To authorize automatic actions, add this block to `~/.claude/CLAUDE.md`:
457
+
458
+ ```markdown
459
+ ## GitHub CI Channel — Automatic Actions
460
+
461
+ When the `github-ci` MCP channel sends a notification, you are pre-authorized to act without additional confirmation:
462
+
463
+ - **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.
464
+ - **Merge conflicts**: Same worktree workflow; resolve conflicts during the interactive rebase.
465
+ - **CI failure on main**: Spawn a subagent — call `fetch_workflow_logs`, diagnose the root cause, apply a fix, push to restore main.
466
+ - **CI failure on feature branch**: Spawn a subagent — fetch logs, investigate, push a fix.
467
+ - **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`.
468
+
469
+ The channel notification IS the permission. No additional confirmation needed for any of the above.
470
+ ```
471
+
472
+ **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.
473
+
474
+ ---
475
+
476
+ ## Troubleshooting
477
+
478
+ ### MCP shows red / "Failed to reconnect"
479
+
480
+ 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.
481
+
482
+ ```bash
483
+ lsof -i :9443 # find the process
484
+ kill <PID> # free the port
485
+ # restart Claude Code
486
+ ```
487
+
488
+ The server logs: `ERROR: Port 9443 is already in use. Kill the existing process (lsof -i :9443) and restart Claude Code.`
489
+
490
+ ### Webhooks return 401 Unauthorized
491
+
492
+ HMAC signature mismatch — the server and GitHub are using different secrets.
493
+
494
+ - **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`.
495
+ - Also check that the secret in `.mcp.json` exactly matches what was pasted into GitHub webhook settings (no extra whitespace).
496
+
497
+ ### No notification when a PR falls behind
498
+
499
+ 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.
500
+
501
+ ### "bun: command not found" in MCP logs
502
+
503
+ Use the absolute path in `.mcp.json`: `"command": "/home/you/.bun/bin/bun"`. Find the path with `which bun`.
504
+
505
+ ### `claude_desktop_config.json` vs `.mcp.json`
506
+
507
+ These are for different apps:
508
+ - `~/.config/Claude/claude_desktop_config.json` → Claude **Desktop** (GUI)
509
+ - `~/.mcp.json` or `.mcp.json` → Claude Code **CLI** (`claude` command)
510
+
511
+ `--dangerously-load-development-channels` reads from `.mcp.json`, not the Desktop config.
512
+
513
+ ### Tunnel URL changed
514
+
515
+ 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.
516
+
517
+ ---
518
+
519
+ ## Development
520
+
521
+ ```bash
522
+ bun test # 32 tests
523
+ bun run typecheck # tsc --noEmit (strict + noUncheckedIndexedAccess)
524
+ bun run lint # Biome v2
525
+ bun run lint:fix # Auto-fix lint issues
526
+ bun run build # Bundle to dist/
527
+ ```
528
+
529
+ ### Linting
530
+
531
+ Biome v2 is configured in `biome.json` with strict rules. One deliberate deviation from the defaults:
532
+
533
+ - **`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.
534
+
535
+ ## Security
536
+
537
+ - HMAC-SHA256 verification uses `timingSafeEqual` — constant-time, no timing oracle
538
+ - Fallback handler emits only `event + action + repo` — raw payload is never forwarded to Claude (prompt injection guard)
539
+ - `GITHUB_TOKEN` is read-only (`actions:read` + `pull_requests:read`) — no write access needed
540
+ - `.env` is gitignored — secrets stay local
541
+ - `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
542
+
543
+ See [AGENTS.md](AGENTS.md) for the full security analysis and architecture reference.