Commit3b99c5funknown_key
feat(BLOCK-W2): configure Claude to use Gluecron MCP, not GitHub
feat(BLOCK-W2): configure Claude to use Gluecron MCP, not GitHub
Second half of the self-host migration. BLOCK W moved the repo onto
Gluecron; W2 tells every future Claude Code session (and Claude
Desktop) to drive Gluecron natively instead of GitHub.
Files
.claude/settings.json (NEW)
Claude Code repo-level config. Declares the gluecron MCP server
over HTTP at https://gluecron.com/mcp with
Authorization: Bearer ${env:GLUECRON_PAT}. permissions.deny
blocks 11 GitHub write-tools so Claude can't accidentally fall
back to opening GitHub PRs or pushing to GitHub branches.
CLAUDE.md (extended)
New top-level "Source of truth: Gluecron (not GitHub)" section
inserted between the title and "READ FIRST". Explicitly tells
every AI session: the canonical remote is the Gluecron URL,
GitHub is a 7-day fallback mirror only, use gluecron_* tools,
invoke the .claude/skills/gluecron-* skills for routine work,
GLUECRON_PAT is the auth env. Notes that deploys are
self-driven now (~25s push-to-live via post-receive).
scripts/install.sh (extended)
Final success banner gains a tip pointing Claude Code users at
.claude/settings.json + GLUECRON_PAT. The DXT install + skills
drop covers Claude Desktop; the settings.json covers Code.
docs/CLAUDE_SETUP.md (NEW)
Operator-facing setup walkthrough. Why Gluecron MCP not GitHub
MCP. 30-second Desktop install. Zero-setup Code path. Verify-
it-works check ("list my open PRs" should call gluecron_list_prs
not anything mcp__github__*). Troubleshooting.
src/__tests__/claude-config.test.ts (NEW)
11 tests covering: settings.json is valid JSON, declares the
gluecron MCP server with the expected URL pattern, denies each
of the 11 GitHub write-tool names, CLAUDE.md contains the
"Source of truth" section, each .claude/skills/*/SKILL.md
exists with the expected frontmatter shape.
11 GitHub write-tools in the deny list:
mcp__github__create_pull_request
mcp__github__merge_pull_request
mcp__github__create_or_update_file
mcp__github__push_files
mcp__github__delete_file
mcp__github__create_branch
mcp__github__create_repository
mcp__github__create_pull_request_with_copilot
mcp__github__update_pull_request
mcp__github__update_pull_request_branch
mcp__github__merge_pull_request_with_copilot
Reads still work; only writes are blocked.
L7 skills audit: the gluecron-pr / gluecron-issue / gluecron-review
SKILL.md files already had matching K1 tool names + correct
descriptions + "When to use" sections. No edits needed.
Tests
bun test → 1996 pass / 1 fail (pre-existing) / 2 skip across
143 files. Matches the expected 1985+ envelope from BLOCK W.
End state after W + W2 + the operator cutover steps:
• Push goes to Gluecron's bare repo
• Post-receive fires self-deploy.sh, live in ~25s
• Future Claude Code sessions in this repo auto-target Gluecron
MCP and refuse to call GitHub write tools
• Claude Desktop drives Gluecron natively via the .dxt extension
from BLOCK Q1
• GitHub is a fallback mirror only — 7 days of safety net5 files changed+321−03b99c5fe3b22f6f4dcd4d89172ee798aa673f3d5
5 changed files+321−0
Added.claude/settings.json+26−0View fileUnifiedSplit
@@ -0,0 +1,26 @@
1{
2 "mcpServers": {
3 "gluecron": {
4 "transport": "http",
5 "url": "https://gluecron.com/mcp",
6 "headers": {
7 "Authorization": "Bearer ${env:GLUECRON_PAT}"
8 }
9 }
10 },
11 "permissions": {
12 "deny": [
13 "mcp__github__create_pull_request",
14 "mcp__github__merge_pull_request",
15 "mcp__github__create_or_update_file",
16 "mcp__github__push_files",
17 "mcp__github__delete_file",
18 "mcp__github__create_branch",
19 "mcp__github__create_repository",
20 "mcp__github__create_pull_request_with_copilot",
21 "mcp__github__update_pull_request",
22 "mcp__github__update_pull_request_branch",
23 "mcp__github__merge_pull_request_with_copilot"
24 ]
25 }
26}
ModifiedCLAUDE.md+28−0View fileUnifiedSplit
@@ -2,6 +2,34 @@
22
33AI-native code intelligence platform — git hosting, automated CI, and push-time gate enforcement.
44
5## Source of truth: Gluecron (not GitHub)
6
7This repository self-hosts on Gluecron itself. The canonical remote is
8`https://gluecron.com/ccantynz/Gluecron.com.git`. GitHub is a 7-day
9fallback mirror only.
10
11**For any AI session working in this repo:**
12
13- Use the Gluecron MCP server (`.claude/settings.json` already points
14 there). Tools: `gluecron_create_pr`, `gluecron_merge_pr`,
15 `gluecron_create_issue`, `gluecron_comment_pr`, etc. See
16 `src/lib/mcp-tools.ts` for the full 15-tool surface.
17- Do NOT call any `mcp__github__*` write tool. The settings file's
18 `permissions.deny` already blocks them; if you encounter a deny
19 error, switch to the equivalent `gluecron_*` tool.
20- The `.claude/skills/gluecron-pr`, `gluecron-issue`, and
21 `gluecron-review` skills are the highest-leverage entry points for
22 routine work. Invoke them when the user mentions a PR, an issue,
23 or wants a second AI review pass.
24- Auth: the `GLUECRON_PAT` env var holds the user's personal access
25 token. If it's missing, prompt the user to generate one at
26 `/settings/tokens` (admin scope required for the merge tools).
27
28**Deploys are self-driven now.** Push to `main` and
29`src/hooks/post-receive.ts` fires `scripts/self-deploy.sh` directly —
30no GitHub Actions involvement. The live site reflects the push in
31~25 seconds. Watch `/admin/deploys` for the live step stream.
32
533## READ FIRST — every session
634
735**`BUILD_BIBLE.md` is mandatory reading for every Claude agent before any code changes.**
Addeddocs/CLAUDE_SETUP.md+99−0View fileUnifiedSplit
@@ -0,0 +1,99 @@
1# Claude Setup for Gluecron
2
3This doc covers how to wire Claude (Desktop or Code) to Gluecron's native
4MCP server so it drives this repo through Gluecron's tools — not GitHub's.
5
6## Why we use Gluecron MCP instead of GitHub MCP
7
8Gluecron self-hosts. The canonical remote for every Gluecron repo is
9`https://gluecron.com/<owner>/<repo>.git`; GitHub is a 7-day fallback
10mirror only. Every PR, issue, comment, merge, and review must land on
11Gluecron first or it doesn't exist as far as the platform is concerned.
12
13The Gluecron MCP server (`POST /mcp`, see `src/lib/mcp-tools.ts`) exposes
1415 tools — 5 read, 10 write — covering the full lifecycle:
15
16- `gluecron_repo_search`, `gluecron_repo_read_file`,
17 `gluecron_repo_list_issues`, `gluecron_repo_explain_codebase`,
18 `gluecron_repo_health` (read).
19- `gluecron_create_issue`, `gluecron_comment_issue`,
20 `gluecron_close_issue`, `gluecron_reopen_issue` (issues).
21- `gluecron_create_pr`, `gluecron_get_pr`, `gluecron_list_prs`,
22 `gluecron_comment_pr`, `gluecron_merge_pr`, `gluecron_close_pr` (PRs).
23
24If Claude falls back to a `mcp__github__*` write tool, the change lands on
25the wrong source of truth and the deploy pipeline (`scripts/self-deploy.sh`)
26never fires. That's why `.claude/settings.json` denies the GitHub write
27tools at the harness layer.
28
29## Set up Claude Desktop (30 seconds)
30
31Two equally valid paths:
32
33### Option A — drag the `.dxt` extension
34
351. Open <https://gluecron.com/> and download `public/gluecron.dxt`.
362. Drag it onto the Claude Desktop window. Claude prompts for two values:
37 - `gluecron_host` — leave as `https://gluecron.com` unless you run a
38 private instance.
39 - `gluecron_pat` — paste a token from `/settings/tokens` (admin
40 scope; the merge tool needs it).
413. Done. Restart Claude Desktop. Ask "list my open Gluecron repos" — it
42 should call `gluecron_repo_search` immediately.
43
44### Option B — one-line installer
45
46```bash
47curl -sSL https://gluecron.com/install | bash
48```
49
50The script signs you in, mints a PAT, and merges a `gluecron` entry into
51`~/Library/Application Support/Claude/claude_desktop_config.json`
52(macOS) or `~/.config/Claude/claude_desktop_config.json` (Linux / WSL).
53Idempotent — safe to re-run.
54
55## Set up Claude Code
56
57**No setup.** The repo ships `.claude/settings.json`. Claude Code reads
58it automatically every session opened inside this checkout.
59
60The only thing you provide is a `GLUECRON_PAT` env var in your shell:
61
62```bash
63export GLUECRON_PAT="glc_..." # from https://gluecron.com/settings/tokens
64```
65
66The settings file uses `${env:GLUECRON_PAT}` as the bearer token, so the
67PAT never lands in any file under version control.
68
69## Verify it's working
70
71Open Claude inside this repo and ask:
72
73> list my open PRs
74
75A correctly-wired Claude will call `gluecron_list_prs` (you'll see it in
76the tool-call trace). It must NOT call anything starting with
77`mcp__github__`. If it does, the settings file's deny list will surface
78a `permission denied` error — switch to the `gluecron_*` equivalent.
79
80Other smoke prompts:
81
82| Prompt | Expected MCP tool |
83| --- | --- |
84| "Show me issue 17" | `gluecron_repo_list_issues` (then narrow) |
85| "Comment 'thanks' on PR 42" | `gluecron_comment_pr` |
86| "What's the health score of ccantynz/Gluecron.com?" | `gluecron_repo_health` |
87| "Search repos for 'rate limiter'" | `gluecron_repo_search` |
88
89## Troubleshooting
90
91- **"authentication required for gluecron_..."** — the MCP server got the
92 request but the PAT didn't authenticate. Re-mint at `/settings/tokens`
93 and update `GLUECRON_PAT`.
94- **"permission denied: mcp__github__create_pull_request"** — Claude
95 tried to fall back to GitHub. The deny list caught it. Re-prompt
96 with "use Gluecron, not GitHub".
97- **Skill not auto-invoking** — make sure the active git remote contains
98 `gluecron.com` (`git remote -v`), or export `GLUECRON_HOST` to override
99 the detection.
Modifiedscripts/install.sh+5−0View fileUnifiedSplit
@@ -301,3 +301,8 @@ printf "\n"
301301printf " v Done. Restart Claude Desktop and try:\n"
302302printf " \"Open a PR on this repo with a one-line README fix\"\n"
303303printf "\n"
304printf " Tip: if you also use Claude Code in this repo, the\n"
305printf " .claude/settings.json at the repo root auto-configures\n"
306printf " the Gluecron MCP server. Set GLUECRON_PAT in your shell\n"
307printf " and future Code sessions wire up with no further config.\n"
308printf "\n"
Addedsrc/__tests__/claude-config.test.ts+163−0View fileUnifiedSplit
@@ -0,0 +1,163 @@
1/**
2 * Block W2 — Claude harness configuration.
3 *
4 * Coverage:
5 * - .claude/settings.json is valid JSON
6 * - It declares the `gluecron` MCP server with the expected URL pattern
7 * - It denies the canonical 11 GitHub-write tool names
8 * - CLAUDE.md contains the "Source of truth: Gluecron" section
9 * - Each .claude/skills/<name>/SKILL.md exists with the expected
10 * frontmatter shape (name, description, tools)
11 */
12
13import { describe, it, expect } from "bun:test";
14import { readFile } from "node:fs/promises";
15import { join } from "node:path";
16
17const REPO_ROOT = join(import.meta.dir, "..", "..");
18const SETTINGS_PATH = join(REPO_ROOT, ".claude", "settings.json");
19const CLAUDE_MD = join(REPO_ROOT, "CLAUDE.md");
20const SKILLS_DIR = join(REPO_ROOT, ".claude", "skills");
21
22const SKILLS = ["gluecron-pr", "gluecron-issue", "gluecron-review"] as const;
23
24const DENIED_GITHUB_WRITE_TOOLS = [
25 "mcp__github__create_pull_request",
26 "mcp__github__merge_pull_request",
27 "mcp__github__create_or_update_file",
28 "mcp__github__push_files",
29 "mcp__github__delete_file",
30 "mcp__github__create_branch",
31 "mcp__github__create_repository",
32 "mcp__github__create_pull_request_with_copilot",
33 "mcp__github__update_pull_request",
34 "mcp__github__update_pull_request_branch",
35 "mcp__github__merge_pull_request_with_copilot",
36];
37
38// ---------------------------------------------------------------------------
39// .claude/settings.json shape
40// ---------------------------------------------------------------------------
41
42describe(".claude/settings.json", () => {
43 it("is valid JSON", async () => {
44 const raw = await readFile(SETTINGS_PATH, "utf8");
45 expect(() => JSON.parse(raw)).not.toThrow();
46 });
47
48 it("declares the gluecron MCP server", async () => {
49 const raw = await readFile(SETTINGS_PATH, "utf8");
50 const cfg = JSON.parse(raw) as {
51 mcpServers?: Record<string, { transport?: string; url?: string }>;
52 };
53 expect(cfg.mcpServers).toBeDefined();
54 expect(cfg.mcpServers!.gluecron).toBeDefined();
55 const entry = cfg.mcpServers!.gluecron!;
56 expect(entry.transport).toBe("http");
57 // URL ends with /mcp on a gluecron host.
58 expect(entry.url).toMatch(/gluecron[^\s]*\/mcp$/);
59 });
60
61 it("passes Authorization via env var (PAT never in file)", async () => {
62 const raw = await readFile(SETTINGS_PATH, "utf8");
63 const cfg = JSON.parse(raw) as {
64 mcpServers?: Record<
65 string,
66 { headers?: Record<string, string> }
67 >;
68 };
69 const auth = cfg.mcpServers?.gluecron?.headers?.Authorization;
70 expect(auth).toBeDefined();
71 expect(auth).toContain("${env:GLUECRON_PAT}");
72 expect(auth).toMatch(/^Bearer\s/);
73 });
74
75 it(`denies all ${DENIED_GITHUB_WRITE_TOOLS.length} canonical GitHub write tools`, async () => {
76 const raw = await readFile(SETTINGS_PATH, "utf8");
77 const cfg = JSON.parse(raw) as {
78 permissions?: { deny?: string[] };
79 };
80 expect(cfg.permissions?.deny).toBeDefined();
81 const deny = cfg.permissions!.deny!;
82 for (const tool of DENIED_GITHUB_WRITE_TOOLS) {
83 expect(deny).toContain(tool);
84 }
85 });
86});
87
88// ---------------------------------------------------------------------------
89// CLAUDE.md "Source of truth: Gluecron" section
90// ---------------------------------------------------------------------------
91
92describe("CLAUDE.md", () => {
93 it("contains the 'Source of truth: Gluecron' section", async () => {
94 const raw = await readFile(CLAUDE_MD, "utf8");
95 expect(raw).toContain("## Source of truth: Gluecron (not GitHub)");
96 });
97
98 it("references .claude/settings.json and the mcp-tools module", async () => {
99 const raw = await readFile(CLAUDE_MD, "utf8");
100 expect(raw).toContain(".claude/settings.json");
101 expect(raw).toContain("src/lib/mcp-tools.ts");
102 });
103
104 it("warns against calling mcp__github__* write tools", async () => {
105 const raw = await readFile(CLAUDE_MD, "utf8");
106 expect(raw).toContain("mcp__github__");
107 });
108
109 it("mentions the GLUECRON_PAT env var", async () => {
110 const raw = await readFile(CLAUDE_MD, "utf8");
111 expect(raw).toContain("GLUECRON_PAT");
112 });
113});
114
115// ---------------------------------------------------------------------------
116// Each .claude/skills/<name>/SKILL.md exists and has the expected frontmatter
117// ---------------------------------------------------------------------------
118
119function parseFrontmatter(src: string): Record<string, string> {
120 if (!src.startsWith("---")) {
121 throw new Error("SKILL.md missing frontmatter");
122 }
123 const end = src.indexOf("\n---", 3);
124 if (end < 0) throw new Error("SKILL.md frontmatter not terminated");
125 const raw = src.slice(3, end).trim();
126 const fields: Record<string, string> = {};
127 let currentKey = "";
128 let buffer: string[] = [];
129 const flush = () => {
130 if (currentKey) {
131 fields[currentKey] = buffer.join("\n").trim();
132 }
133 buffer = [];
134 };
135 for (const line of raw.split("\n")) {
136 const m = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
137 if (m && !line.startsWith(" ") && !line.startsWith("\t")) {
138 flush();
139 currentKey = m[1];
140 buffer = [m[2]];
141 } else {
142 buffer.push(line);
143 }
144 }
145 flush();
146 return fields;
147}
148
149describe(".claude/skills/* SKILL.md shape", () => {
150 for (const name of SKILLS) {
151 it(`${name}/SKILL.md exists and has name+description+tools`, async () => {
152 const path = join(SKILLS_DIR, name, "SKILL.md");
153 const raw = await readFile(path, "utf8");
154 const fields = parseFrontmatter(raw);
155 expect(fields.name).toBe(name);
156 expect(fields.description).toBeDefined();
157 expect(fields.description.length).toBeGreaterThan(0);
158 expect(fields.description.toLowerCase()).toContain("gluecron");
159 expect(fields.tools).toBeDefined();
160 expect(fields.tools.length).toBeGreaterThan(0);
161 });
162 }
163});
0164