@golproductions/check 3.2.9 → 3.2.11

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.
Files changed (4) hide show
  1. package/README.md +106 -106
  2. package/package.json +1 -1
  3. package/src/index.js +925 -897
  4. package/src/mcp.js +171 -171
package/src/index.js CHANGED
@@ -1,897 +1,925 @@
1
- #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream, chmodSync, unlinkSync, rmSync } from "node:fs";
3
- import { spawnSync } from "node:child_process";
4
- import { join } from "node:path";
5
- import { homedir, platform, arch } from "node:os";
6
- import { pipeline } from "node:stream/promises";
7
- import { Readable } from "node:stream";
8
-
9
- const VERSION = "3.2.7";
10
- const BINARY_VERSION = "3.0.0";
11
- const API = "https://triage.golproductions.com/preflight";
12
- const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
13
- const CLIENT_ID = process.env.GOL_CLIENT_ID || "";
14
-
15
- function getClientId() {
16
- if (CLIENT_ID) return CLIENT_ID;
17
- // Universal fallback written at install time. Cursor and Gemini spawn hooks
18
- // with no GOL_CLIENT_ID in the environment, so the env var alone is not enough.
19
- try {
20
- const k = readFileSync(join(homedir(), ".check", "key"), "utf8").trim();
21
- if (k) return k;
22
- } catch {}
23
- try {
24
- const cfg = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
25
- return cfg.env?.GOL_CLIENT_ID || "";
26
- } catch { return ""; }
27
- }
28
-
29
- async function validateKey(key) {
30
- try {
31
- const res = await fetch(API, {
32
- method: "POST",
33
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
34
- body: JSON.stringify({ command: "echo check-install-verify", cwd: process.cwd(), v: VERSION }),
35
- signal: AbortSignal.timeout(10000),
36
- });
37
- if (res.ok) return { valid: true, expired: false };
38
- let body = {};
39
- try { body = await res.json(); } catch {}
40
- const expired = /expir/i.test(body.reason || body.error || body.message || "");
41
- return { valid: false, expired };
42
- } catch {
43
- return { valid: false, expired: false };
44
- }
45
- }
46
-
47
- // Stable anonymous device fingerprint: one-way hash of coarse machine facts.
48
- // No personal data. Server uses it only to rate-limit free-key minting.
49
- async function deviceFingerprint() {
50
- const { createHash } = await import("node:crypto");
51
- const { hostname, userInfo } = await import("node:os");
52
- let user = "";
53
- try { user = userInfo().username || ""; } catch {}
54
- const seed = [hostname(), platform(), arch(), user].join("|");
55
- return createHash("sha256").update(seed).digest("hex");
56
- }
57
-
58
- // Mint a free key with no email and no browser. Returns the client_id or null.
59
- async function mintInstantKey() {
60
- try {
61
- const fp = await deviceFingerprint();
62
- const res = await fetch(API.replace("/preflight", "/instant-key"), {
63
- method: "POST",
64
- headers: { "Content-Type": "application/json", "User-Agent": "c/" + VERSION },
65
- body: JSON.stringify({ fingerprint: fp, channel: "npm" }),
66
- signal: AbortSignal.timeout(10000),
67
- });
68
- if (!res.ok) return null;
69
- const d = await res.json();
70
- return d && d.client_id ? d.client_id : null;
71
- } catch {
72
- return null;
73
- }
74
- }
75
-
76
- async function downloadBinary(dest) {
77
- const os = platform() === "win32" ? "win" : platform() === "darwin" ? "macos" : "linux";
78
- const cpu = arch() === "arm64" ? "arm64" : "x64";
79
- const ext = os === "win" ? ".exe" : "";
80
- const url = `${CDN}/truth-gate-v${BINARY_VERSION}-${os}-${cpu}${ext}`;
81
- const res = await fetch(url);
82
- if (!res.ok) throw new Error(`Download failed: ${res.status}`);
83
- await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
84
- if (os !== "win") try { chmodSync(dest, 0o755); } catch {}
85
- }
86
-
87
- async function install() {
88
- let key = process.argv[3] || process.env.GOL_CLIENT_ID || "";
89
-
90
- // No key provided: mint one instantly. No email, no browser, no copy-paste.
91
- if (!key || key === "your_key") {
92
- console.log("\n Activating Check (free, no signup)...\n");
93
- key = await mintInstantKey();
94
- if (!key) {
95
- console.log(" Could not activate automatically (offline, or this network hit its daily limit).\n");
96
- console.log(" Get a key the manual way: https://golproductions.com/check\n");
97
- console.log(" Then run: npx @golproductions/check --install YOUR_KEY\n");
98
- process.exit(1);
99
- }
100
- console.log(" Activated. 120 free checks per day.\n");
101
- }
102
-
103
- const result = await validateKey(key);
104
- if (!result.valid) {
105
- if (result.expired) {
106
- console.log("\n Your Client ID has expired: " + key);
107
- console.log("\n Renew your key at: https://golproductions.com/check\n");
108
- } else {
109
- console.log("\n Invalid Client ID: " + key);
110
- console.log("\n Get a valid key at: https://golproductions.com/check\n");
111
- }
112
- process.exit(1);
113
- }
114
-
115
- console.log("\n Key verified. Installing Check...\n");
116
-
117
- const home = homedir();
118
- let installed = 0;
119
-
120
- const scriptSrc = readFileSync(new URL(import.meta.url), "utf8");
121
- const hooksDir = join(home, ".check");
122
- mkdirSync(hooksDir, { recursive: true });
123
- const scriptPath = join(hooksDir, "check.mjs").replace(/\\/g, "/");
124
- writeFileSync(scriptPath, scriptSrc, "utf8");
125
- // Key file is the fallback for tools that don't pass env to hooks (Cursor, Gemini).
126
- writeFileSync(join(hooksDir, "key"), key, "utf8");
127
-
128
- const ext = platform() === "win32" ? ".exe" : "";
129
- const binaryPath = join(hooksDir, "truth-gate" + ext).replace(/\\/g, "/");
130
- await downloadBinary(binaryPath);
131
-
132
- const mcpEntry = { command: "npx", args: ["@golproductions/check", "--mcp"], env: { GOL_CLIENT_ID: key } };
133
-
134
- const targets = [
135
- {
136
- name: "Claude Code",
137
- dir: join(home, ".claude"),
138
- file: "settings.json",
139
- config: (existing) => {
140
- // Merge, never clobber: drop stale Check entries, keep the user's own
141
- // hooks, then append ours.
142
- existing.hooks = existing.hooks || {};
143
- existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
144
- existing.hooks.PreToolUse.push({
145
- matcher: "Bash|PowerShell",
146
- hooks: [{ type: "command", command: "node", args: [scriptPath] }]
147
- });
148
- existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
149
- existing.hooks.PostToolUse.push(
150
- { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] },
151
- { matcher: "Edit|Write|NotebookEdit", hooks: [{ type: "command", command: binaryPath }] }
152
- );
153
- existing.env = existing.env || {};
154
- existing.env.GOL_CLIENT_ID = key;
155
- return existing;
156
- }
157
- },
158
- {
159
- name: "Gemini CLI / Antigravity",
160
- dir: join(home, ".gemini"),
161
- file: "settings.json",
162
- config: (existing) => {
163
- existing.hooks = existing.hooks || {};
164
- existing.hooks.BeforeTool = existing.hooks.BeforeTool || [];
165
- const hooks = existing.hooks.BeforeTool;
166
- if (!hooks.find(h => h.hooks?.some(hh => hh.name === "check-gate"))) {
167
- hooks.push({
168
- matcher: ".*",
169
- hooks: [{ type: "command", command: "node " + scriptPath, name: "check-gate" }]
170
- });
171
- }
172
- return existing;
173
- }
174
- },
175
- {
176
- name: "Cursor",
177
- dir: join(home, ".cursor"),
178
- file: "hooks.json",
179
- config: (existing) => {
180
- existing.version = existing.version || 1;
181
- existing.hooks = existing.hooks || {};
182
- existing.hooks.beforeShellExecution = (existing.hooks.beforeShellExecution || []).filter(h => !isCheckRef(h.command));
183
- existing.hooks.beforeShellExecution.push({
184
- command: "node " + scriptPath
185
- });
186
- return existing;
187
- }
188
- },
189
- {
190
- name: "Homebase",
191
- dir: join(home, ".homebase"),
192
- file: "hooks.json",
193
- config: (existing) => {
194
- existing.hooks = existing.hooks || {};
195
- existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
196
- existing.hooks.PreToolUse.push({
197
- matcher: "Bash|PowerShell",
198
- hooks: [{ type: "command", command: "node", args: [scriptPath] }]
199
- });
200
- existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
201
- existing.hooks.PostToolUse.push(
202
- { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] }
203
- );
204
- existing.env = existing.env || {};
205
- existing.env.GOL_CLIENT_ID = key;
206
- return existing;
207
- }
208
- },
209
- {
210
- name: "Windsurf (MCP)",
211
- dir: join(home, ".codeium", "windsurf"),
212
- file: "mcp_config.json",
213
- config: (existing) => {
214
- existing.mcpServers = existing.mcpServers || {};
215
- existing.mcpServers.Check = mcpEntry;
216
- return existing;
217
- }
218
- },
219
- {
220
- name: "Continue (MCP)",
221
- dir: join(process.cwd(), ".continue", "mcpServers"),
222
- file: "check.json",
223
- config: () => {
224
- return { mcpServers: { Check: mcpEntry } };
225
- }
226
- },
227
- {
228
- name: "Amazon Q Developer (MCP)",
229
- dir: join(home, ".aws", "amazonq"),
230
- file: "mcp.json",
231
- config: (existing) => {
232
- existing.mcpServers = existing.mcpServers || {};
233
- existing.mcpServers.Check = { type: "stdio", ...mcpEntry };
234
- return existing;
235
- }
236
- },
237
- {
238
- name: "Roo Code (project MCP)",
239
- dir: join(process.cwd(), ".roo"),
240
- file: "mcp.json",
241
- config: (existing) => {
242
- existing.mcpServers = existing.mcpServers || {};
243
- existing.mcpServers.Check = mcpEntry;
244
- return existing;
245
- }
246
- },
247
- {
248
- name: "Project MCP (shared)",
249
- dir: process.cwd(),
250
- file: ".mcp.json",
251
- config: (existing) => {
252
- existing.mcpServers = existing.mcpServers || {};
253
- existing.mcpServers.Check = mcpEntry;
254
- return existing;
255
- }
256
- }
257
- ];
258
-
259
- const mcpTargets = ["Windsurf (MCP)", "Continue (MCP)", "Amazon Q Developer (MCP)", "Roo Code (project MCP)", "Project MCP (shared)"];
260
-
261
- let hasClaude = false;
262
- for (const t of targets) {
263
- const isMcp = mcpTargets.includes(t.name);
264
- if (!isMcp && !existsSync(t.dir)) continue;
265
- if (isMcp) mkdirSync(t.dir, { recursive: true });
266
- const filepath = join(t.dir, t.file);
267
- let existing = {};
268
- try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
269
- const updated = t.config(existing);
270
- writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
271
- installed++;
272
- if (t.name === "Claude Code") hasClaude = true;
273
- }
274
-
275
- if (installed === 0) {
276
- mkdirSync(join(home, ".claude"), { recursive: true });
277
- const filepath = join(home, ".claude", "settings.json");
278
- let existing = {};
279
- try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
280
- const updated = targets[0].config(existing);
281
- writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
282
- hasClaude = true;
283
- }
284
-
285
- const checkRule = "Never fabricate, hallucinate, or invent values. If you don't have it, say you don't have it. No fake keys, no fake IDs, no fake paths, no fake URLs. If unsure, ask. Never guess and present it as fact.";
286
- const ruleFile = hasClaude ? "CLAUDE.md" : "AGENTS.md";
287
- const rulePath = join(process.cwd(), ruleFile);
288
- try {
289
- const existing = existsSync(rulePath) ? readFileSync(rulePath, "utf8") : "";
290
- if (!existing.includes("Never fabricate")) {
291
- const prefix = existing.length > 0 ? existing.trimEnd() + "\n\n" : "";
292
- writeFileSync(rulePath, prefix + checkRule + "\n", "utf8");
293
- }
294
- } catch {}
295
-
296
- console.log(`\n Your free GOL Client ID (this machine): ${key}`);
297
- console.log(` Shared by every editor on this machine. 120 free checks/day.`);
298
- console.log(` Need more? Get a GOL API Key (paid, prepaid balance) at`);
299
- console.log(` https://www.golproductions.com/console`);
300
- console.log(`\n By using Check you agree to the Terms and Privacy Policy:`);
301
- console.log(` golproductions.com/terms | golproductions.com/privacy`);
302
- console.log(`\nSafe travels wanderer..\n`);
303
- process.exit(0);
304
- }
305
-
306
- function isCheckRef(str) {
307
- return str && (str.includes(".check") || str.includes("truth-gate"));
308
- }
309
-
310
- function isCheckHook(hook) {
311
- if (isCheckRef(hook.command)) return true;
312
- if (hook.args?.some(a => isCheckRef(a))) return true;
313
- return false;
314
- }
315
-
316
- async function uninstall() {
317
- const home = homedir();
318
- let removed = 0;
319
-
320
- console.log("\n Removing Check...\n");
321
-
322
- // Silent lifecycle ping. No output, failures swallowed — the uninstall
323
- // itself must never be affected. Awaited just before exit so the process
324
- // doesn't kill it mid-flight; the 3s abort caps worst-case delay.
325
- let uninstallPing = Promise.resolve();
326
- try {
327
- const key = getClientId();
328
- if (key) {
329
- uninstallPing = fetch(API.replace("/preflight", "/log"), {
330
- method: "POST",
331
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
332
- body: JSON.stringify({ verdict: "uninstall", channel: "npm", os: platform(), arch: arch(), version: VERSION }),
333
- signal: AbortSignal.timeout(3000),
334
- }).catch(() => {});
335
- }
336
- } catch {}
337
-
338
- const hookTargets = [
339
- {
340
- name: "Claude Code",
341
- path: join(home, ".claude", "settings.json"),
342
- clean: (cfg) => {
343
- if (cfg.hooks?.PreToolUse) {
344
- cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
345
- if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
346
- }
347
- if (cfg.hooks?.PostToolUse) {
348
- cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
349
- if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
350
- }
351
- if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
352
- if (cfg.env) {
353
- delete cfg.env.GOL_CLIENT_ID;
354
- if (Object.keys(cfg.env).length === 0) delete cfg.env;
355
- }
356
- return cfg;
357
- }
358
- },
359
- {
360
- name: "Gemini CLI",
361
- path: join(home, ".gemini", "settings.json"),
362
- clean: (cfg) => {
363
- if (cfg.hooks?.BeforeTool) {
364
- cfg.hooks.BeforeTool = cfg.hooks.BeforeTool.filter(h => !h.hooks?.some(hh => hh.name === "check-gate"));
365
- if (cfg.hooks.BeforeTool.length === 0) delete cfg.hooks.BeforeTool;
366
- if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
367
- }
368
- return cfg;
369
- }
370
- },
371
- {
372
- name: "Cursor",
373
- path: join(home, ".cursor", "hooks.json"),
374
- clean: (cfg) => {
375
- if (cfg.hooks?.beforeShellExecution) {
376
- cfg.hooks.beforeShellExecution = cfg.hooks.beforeShellExecution.filter(h => !isCheckRef(h.command));
377
- if (cfg.hooks.beforeShellExecution.length === 0) delete cfg.hooks.beforeShellExecution;
378
- if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
379
- }
380
- return cfg;
381
- }
382
- },
383
- {
384
- name: "Homebase",
385
- path: join(home, ".homebase", "hooks.json"),
386
- clean: (cfg) => {
387
- if (cfg.hooks?.PreToolUse) {
388
- cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
389
- if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
390
- }
391
- if (cfg.hooks?.PostToolUse) {
392
- cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
393
- if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
394
- }
395
- if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
396
- if (cfg.env) {
397
- delete cfg.env.GOL_CLIENT_ID;
398
- if (Object.keys(cfg.env).length === 0) delete cfg.env;
399
- }
400
- return cfg;
401
- }
402
- }
403
- ];
404
-
405
- const mcpUserTargets = [
406
- { name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") },
407
- { name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json") },
408
- ];
409
-
410
- const mcpProjectTargets = [
411
- { name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), deleteFile: true },
412
- { name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json") },
413
- { name: "Project MCP", path: join(process.cwd(), ".mcp.json") },
414
- ];
415
-
416
- for (const t of hookTargets) {
417
- if (!existsSync(t.path)) continue;
418
- try {
419
- const cfg = JSON.parse(readFileSync(t.path, "utf8"));
420
- const cleaned = t.clean(cfg);
421
- writeFileSync(t.path, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
422
- console.log(" removed " + t.name);
423
- removed++;
424
- } catch {}
425
- }
426
-
427
- for (const t of [...mcpUserTargets, ...mcpProjectTargets]) {
428
- if (!existsSync(t.path)) continue;
429
- try {
430
- if (t.deleteFile) {
431
- unlinkSync(t.path);
432
- console.log(" removed " + t.name);
433
- removed++;
434
- } else {
435
- const cfg = JSON.parse(readFileSync(t.path, "utf8"));
436
- if (cfg.mcpServers?.Check) {
437
- delete cfg.mcpServers.Check;
438
- if (Object.keys(cfg.mcpServers).length === 0) delete cfg.mcpServers;
439
- writeFileSync(t.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
440
- console.log(" removed " + t.name);
441
- removed++;
442
- }
443
- }
444
- } catch {}
445
- }
446
-
447
- const checkDir = join(home, ".check");
448
- if (existsSync(checkDir)) {
449
- try {
450
- rmSync(checkDir, { recursive: true, force: true });
451
- console.log(" removed ~/.check/");
452
- } catch {}
453
- }
454
-
455
- if (removed === 0) {
456
- console.log(" No Check installations found.\n");
457
- } else {
458
- console.log(`\n Removed from ${removed} tool${removed !== 1 ? "s" : ""}.`);
459
- console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n");
460
- }
461
-
462
- try { await uninstallPing; } catch {}
463
- process.exit(0);
464
- }
465
-
466
- async function status() {
467
- const home = homedir();
468
- const checkDir = join(home, ".check");
469
- const ext = platform() === "win32" ? ".exe" : "";
470
- const id = getClientId();
471
-
472
- console.log(`\n Check v${VERSION}\n`);
473
-
474
- if (id) {
475
- const masked = id.length > 12 ? id.slice(0, 8) + "..." + id.slice(-4) : id;
476
- console.log(" Client ID: " + masked);
477
- } else {
478
- console.log(" Client ID: not set");
479
- }
480
-
481
- const binaryPath = join(checkDir, "truth-gate" + ext);
482
- console.log(" Binary: " + (existsSync(binaryPath) ? "installed" : "not found"));
483
- const scriptPath = join(checkDir, "check.mjs");
484
- console.log(" Script: " + (existsSync(scriptPath) ? "installed" : "not found"));
485
-
486
- console.log("\n Integrations:\n");
487
-
488
- const checks = [
489
- {
490
- name: "Claude Code",
491
- path: join(home, ".claude", "settings.json"),
492
- test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
493
- detail: (cfg) => {
494
- const parts = [];
495
- if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
496
- if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
497
- return parts.join(", ");
498
- }
499
- },
500
- {
501
- name: "Gemini CLI",
502
- path: join(home, ".gemini", "settings.json"),
503
- test: (cfg) => cfg.hooks?.BeforeTool?.some(h => h.hooks?.some(hh => hh.name === "check-gate")),
504
- detail: () => "BeforeTool"
505
- },
506
- {
507
- name: "Cursor",
508
- path: join(home, ".cursor", "hooks.json"),
509
- test: (cfg) => cfg.hooks?.beforeShellExecution?.some(h => isCheckRef(h.command)),
510
- detail: () => "beforeShellExecution"
511
- },
512
- {
513
- name: "Homebase",
514
- path: join(home, ".homebase", "hooks.json"),
515
- test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
516
- detail: (cfg) => {
517
- const parts = [];
518
- if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
519
- if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
520
- return parts.join(", ");
521
- }
522
- },
523
- { name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
524
- { name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
525
- { name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
526
- { name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
527
- { name: "Project MCP", path: join(process.cwd(), ".mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
528
- ];
529
-
530
- for (const c of checks) {
531
- if (!existsSync(c.path)) {
532
- console.log(" - " + c.name.padEnd(14) + " not detected");
533
- continue;
534
- }
535
- try {
536
- const cfg = JSON.parse(readFileSync(c.path, "utf8"));
537
- if (c.test(cfg)) {
538
- console.log(" + " + c.name.padEnd(14) + " active (" + c.detail(cfg) + ")");
539
- } else {
540
- console.log(" - " + c.name.padEnd(14) + " tool installed, Check not configured");
541
- }
542
- } catch {
543
- console.log(" ? " + c.name.padEnd(14) + " config unreadable");
544
- }
545
- }
546
-
547
- console.log("");
548
- process.exit(0);
549
- }
550
-
551
- async function credits() {
552
- const id = getClientId();
553
- if (!id) {
554
- console.log("\n No Client ID found. Set GOL_CLIENT_ID or run --install first.\n");
555
- process.exit(1);
556
- }
557
-
558
- try {
559
- const res = await fetch(API.replace("/preflight", "/credits"), {
560
- method: "GET",
561
- headers: { "X-GOL-CLIENT-ID": id, "User-Agent": "c/" + VERSION },
562
- signal: AbortSignal.timeout(10000),
563
- });
564
- if (!res.ok) {
565
- console.log("\n Could not fetch credits (HTTP " + res.status + ").\n");
566
- process.exit(1);
567
- }
568
- const d = await res.json();
569
- console.log(`\n Check v${VERSION} — Credits\n`);
570
- if (d.free_remaining !== undefined) console.log(" Free today: " + d.free_remaining + " / " + (d.free_daily || 120));
571
- if (d.calls_today !== undefined) console.log(" Calls today: " + d.calls_today);
572
- if (d.balance !== undefined) console.log(" Balance: $" + d.balance + " AUD");
573
- if (d.total_calls !== undefined) console.log(" Total calls: " + d.total_calls);
574
- if (d.plan) console.log(" Plan: " + d.plan);
575
- if (Object.keys(d).length === 0) console.log(" " + JSON.stringify(d));
576
- console.log("");
577
- } catch (err) {
578
- console.log("\n Could not reach API: " + err.message + "\n");
579
- }
580
- process.exit(0);
581
- }
582
-
583
- async function docs() {
584
- console.log(`
585
- Check v${VERSION} — Documentation
586
-
587
- Install: npx @golproductions/check --install <key>
588
- Uninstall: npx @golproductions/check --uninstall
589
- Status: npx @golproductions/check --status
590
- Credits: npx @golproductions/check --credits
591
-
592
- How it works:
593
-
594
- Check installs hooks into your AI coding tools. Every time
595
- the AI runs a command or edits a file, Check verifies it
596
- before or after execution.
597
-
598
- PreToolUse Validates commands before they run.
599
- PostToolUse Verifies results after execution.
600
- MCP Server Provides Check and CheckAndExecute tools.
601
- CLAUDE.md Adds anti-fabrication instructions.
602
-
603
- Supported tools:
604
-
605
- Claude Code, Gemini CLI, Cursor, Windsurf, Continue,
606
- Amazon Q Developer, Roo Code, Homebase
607
-
608
- Pricing:
609
-
610
- 120 free checks per day. Then $0.0068 AUD per check.
611
- Credits never expire. Pay as you go.
612
-
613
- Links:
614
-
615
- Website: https://golproductions.com/check
616
- Docs: https://github.com/golproductions/check
617
- Support: support@golproductions.com
618
- `);
619
-
620
- try {
621
- const { execSync } = await import("node:child_process");
622
- const url = "https://github.com/golproductions/check";
623
- if (platform() === "win32") execSync(`start "" "${url}"`, { stdio: "ignore", shell: true });
624
- else if (platform() === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
625
- else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
626
- } catch {}
627
-
628
- process.exit(0);
629
- }
630
-
631
- function help() {
632
- console.log(`
633
- Check v${VERSION} — Anti-hallucination layer for AI coding agents
634
-
635
- Usage:
636
-
637
- npx @golproductions/check --install <key> Install Check with your Client ID
638
- npx @golproductions/check --uninstall Remove Check from all tools
639
- npx @golproductions/check --status Show what is installed and active
640
- npx @golproductions/check --credits Check your usage and balance
641
- npx @golproductions/check --docs View documentation
642
- npx @golproductions/check --help Show this help
643
-
644
- Get your key: https://golproductions.com/check
645
- Pricing: 120 free/day, then $0.0068 AUD per check
646
- `);
647
- process.exit(0);
648
- }
649
-
650
- // CLI routing
651
- const args = process.argv.slice(2);
652
- if (args.includes("--install")) { await install(); }
653
- if (args.includes("--uninstall")) { await uninstall(); }
654
- if (args.includes("--status")) { await status(); }
655
- if (args.includes("--credits")) { await credits(); }
656
- if (args.includes("--docs")) { await docs(); }
657
- if (args.includes("--help") || args.includes("-h")) { help(); }
658
- if (args.includes("--mcp")) { await import("./mcp.js"); process.exit(0); }
659
-
660
- // No flags + interactive terminal = show help
661
- if (!args.some(a => a.startsWith("-")) && process.stdin.isTTY) { help(); }
662
-
663
- // Hook handler (invoked by tools via stdin)
664
- function detect(p) {
665
- if (typeof p.command === "string" && !p.tool_input) return "cursor";
666
- if (p.hook_event_name === "BeforeTool" || p.toolCall?.argumentsJson) return "gemini";
667
- return "claude";
668
- }
669
-
670
- function cmd(p, f) {
671
- if (f === "cursor") return p.command;
672
- if (f === "gemini") {
673
- if (p.tool_input?.command) return p.tool_input.command;
674
- if (p.toolCall?.argumentsJson) {
675
- try {
676
- const a = typeof p.toolCall.argumentsJson === "string" ? JSON.parse(p.toolCall.argumentsJson) : p.toolCall.argumentsJson;
677
- return a.command || a.CommandLine || a.command_line;
678
- } catch { return null; }
679
- }
680
- return null;
681
- }
682
- return p.tool_input?.command;
683
- }
684
-
685
- function out(f, ok, reason) {
686
- if (ok) {
687
- const r = { cursor: { permission: "allow" }, gemini: { decision: "allow" }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } } };
688
- process.stdout.write(JSON.stringify(r[f] || r.claude));
689
- process.exit(0);
690
- }
691
- if (f === "gemini") {
692
- process.stdout.write(JSON.stringify({ decision: "deny", reason }));
693
- process.exit(2);
694
- }
695
- const r = { cursor: { permission: "deny", user_message: reason, agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
696
- process.stdout.write(JSON.stringify(r[f] || r.claude));
697
- process.exit(0);
698
- }
699
-
700
- // Out of checks is a billing state, not a bad command. Warn, then let the
701
- // command run without the gate. The warning fires once per day per machine.
702
- function outCreditsExhausted(f, reason) {
703
- let warned = true;
704
- try {
705
- const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
706
- warned = existsSync(marker);
707
- if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
708
- } catch { warned = false; }
709
- const msg = warned ? undefined : reason;
710
- if (f === "gemini") {
711
- process.stdout.write(JSON.stringify({ decision: "allow" }));
712
- if (msg) process.stderr.write(msg);
713
- process.exit(0);
714
- }
715
- if (f === "cursor") {
716
- const r = { permission: "allow" };
717
- if (msg) r.user_message = msg;
718
- process.stdout.write(JSON.stringify(r));
719
- process.exit(0);
720
- }
721
- const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
722
- if (msg) r.systemMessage = msg;
723
- process.stdout.write(JSON.stringify(r));
724
- process.exit(0);
725
- }
726
-
727
- // The machine's own PATH resolution is the ground truth for whether a
728
- // command exists. This is the same lookup the shell performs at launch:
729
- // walk PATH (plus PATHEXT on Windows), pure filesystem reads, deterministic.
730
- // The server honors binary_exists as proof, so real-but-unlisted tools
731
- // (claude, codex, aider, internal CLIs) are never falsely blocked.
732
- const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
733
-
734
- // POSIX shell builtins and keywords. Frozen by spec since 1992: these are
735
- // provided by the shell itself, not the filesystem, and are treated as
736
- // present. This set is evergreen — it does not rot the way a tool list does.
737
- const SHELL_BUILTINS = new Set([
738
- "cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
739
- "true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
740
- "shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
741
- "read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
742
- "type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
743
- "popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
744
- "done", "case", "esac", "in", "function", "select", "{", "}", "!",
745
- ]);
746
-
747
- // Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
748
- // quotes are literal text, not separators.
749
- function segmentBases(command) {
750
- const segs = [[]];
751
- let cur = "", q = null;
752
- for (let i = 0; i < command.length; i++) {
753
- const ch = command[i];
754
- if (q) { if (ch === q) q = null; else cur += ch; continue; }
755
- if (ch === '"' || ch === "'") { q = ch; continue; }
756
- if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
757
- if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
758
- if (segs[segs.length - 1].length) segs.push([]);
759
- if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
760
- continue;
761
- }
762
- if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
763
- cur += ch;
764
- }
765
- if (cur) segs[segs.length - 1].push(cur);
766
-
767
- const bases = [];
768
- for (const words of segs) {
769
- let base = null;
770
- for (const w of words) {
771
- if (!w) continue;
772
- if (PREFIX_WORDS.has(w)) continue;
773
- if (/^[\d.]+[smhd]?$/.test(w)) continue;
774
- if (w.includes("=") && !w.startsWith("-")) continue;
775
- if (w.startsWith("-")) continue;
776
- base = w;
777
- break;
778
- }
779
- if (base) bases.push(base);
780
- }
781
- return bases;
782
- }
783
-
784
- // THE ROD: do not model the shell — ask it. `type` is the shell reporting
785
- // on itself: builtins, keywords, functions, and PATH binaries all answer
786
- // through the exact resolution the shell will use at launch. A model of the
787
- // shell can drift from the shell; the shell cannot drift from itself.
788
- // On Windows, bare "bash" is a trap: PATH can resolve to WSL's
789
- // System32\bash.exe — a different operating system with a different
790
- // filesystem. The command will actually run in Git Bash, so the probe
791
- // must ask Git Bash, located explicitly. If it cannot be located,
792
- // abstain — never let the wrong interpreter answer for the right one.
793
- function bashPath() {
794
- if (platform() !== "win32") return "bash";
795
- const candidates = [
796
- process.env.CLAUDE_CODE_GIT_BASH_PATH,
797
- "C:/Program Files/Git/bin/bash.exe",
798
- "C:/Program Files/Git/usr/bin/bash.exe",
799
- "C:/Program Files (x86)/Git/bin/bash.exe",
800
- ].filter(Boolean);
801
- for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
802
- return null;
803
- }
804
-
805
- function shellKnows(name, cwd, shell) {
806
- try {
807
- if (shell === "powershell") {
808
- const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
809
- "if (Get-Command $env:CHECK_PROBE -ErrorAction SilentlyContinue) { exit 0 }; exit 1"],
810
- { cwd, timeout: 4000, stdio: "ignore", env: { ...process.env, CHECK_PROBE: name } });
811
- return r.error ? null : r.status === 0;
812
- }
813
- const bash = bashPath();
814
- if (!bash) return null;
815
- const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
816
- { cwd, timeout: 4000, stdio: "ignore" });
817
- return r.error ? null : r.status === 0;
818
- } catch { return null; }
819
- }
820
-
821
- // Tri-state ground truth from the interpreter itself: true = every command
822
- // word proven resolvable, false = at least one proven unresolvable, null =
823
- // cannot be determined without executing (subshells, expansions) — abstain,
824
- // never guess.
825
- function allBinariesExist(command, cwd, shell) {
826
- try {
827
- let bases = segmentBases(command);
828
- if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
829
- if (bases.length === 0) return true;
830
- if (bases.some(b => /[(){}$`]/.test(b))) return null;
831
- let verdict = true;
832
- for (const b of bases) {
833
- const known = shellKnows(b, cwd, shell);
834
- if (known === null) return null;
835
- if (known === false) verdict = false;
836
- }
837
- return verdict;
838
- } catch { return null; }
839
- }
840
-
841
- async function main() {
842
- let f = "claude";
843
- try {
844
- let input = "";
845
- for await (const chunk of process.stdin) input += chunk;
846
- input = input.replace(/^/, "").trim();
847
- let p;
848
- try { p = JSON.parse(input); } catch { out("claude", true); return; }
849
- if (p.hook_event_name === "PostToolUse") { process.exit(0); return; }
850
- f = detect(p);
851
- const c = cmd(p, f);
852
- if (!c) { out(f, true); return; }
853
- const hookKey = getClientId();
854
- if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
855
- const controller = new AbortController();
856
- const timer = setTimeout(() => controller.abort(), 5000);
857
- // Ask the same interpreter that will run the command.
858
- const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash");
859
- // Send only what the verdict needs. No transcript paths, no usernames
860
- // beyond what cwd itself carries — the check is about the command.
861
- const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
862
- if (exists !== null) body.binary_exists = exists;
863
- const res = await fetch(API, {
864
- method: "POST",
865
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
866
- body: JSON.stringify(body),
867
- signal: controller.signal
868
- });
869
- clearTimeout(timer);
870
- const d = await res.json();
871
-
872
- // Billing and account states arrive as non-2xx and carry no `verdict`.
873
- // Surface what the server actually said so the user knows this is about
874
- // their account, not their command. Without this, the free-tier wall
875
- // (HTTP 402) falls through to the generic "denied" below and reads like
876
- // the command itself was rejected.
877
- if (!res.ok) {
878
- if (res.status === 402) {
879
- const free = d.daily_free || 120;
880
- const upsell = d.upgrade || "Top up at https://www.golproductions.com/console.html";
881
- outCreditsExhausted(f, `check: you've used all ${free} free checks for today and your balance is empty. Check is off until tomorrow — commands run unverified. Credits you buy never expire. ${upsell}`);
882
- return;
883
- }
884
- out(f, false, d.reason || d.error || "check: request not authorized, command blocked");
885
- return;
886
- }
887
-
888
- if (d.verdict === "runnable") { out(f, true); }
889
- else { out(f, false, d.reason || "denied — address the issue before continuing"); }
890
- } catch {
891
- // Network failure or timeout: fail OPEN. An unreachable validation
892
- // service must never block the user's work (Terms, section 5).
893
- outCreditsExhausted(f, "check: validation service unreachable, command ran unverified. Check resumes automatically.");
894
- }
895
- }
896
-
897
- main();
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream, chmodSync, unlinkSync, rmSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { join } from "node:path";
5
+ import { homedir, platform, arch } from "node:os";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { Readable } from "node:stream";
8
+
9
+ const VERSION = "3.2.11";
10
+ const BINARY_VERSION = "3.0.0";
11
+ const API = "https://triage.golproductions.com/preflight";
12
+ const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
13
+ const CLIENT_ID = process.env.GOL_CLIENT_ID || "";
14
+
15
+ function getClientId() {
16
+ if (CLIENT_ID) return CLIENT_ID;
17
+ // Universal fallback written at install time. Cursor and Gemini spawn hooks
18
+ // with no GOL_CLIENT_ID in the environment, so the env var alone is not enough.
19
+ try {
20
+ const k = readFileSync(join(homedir(), ".check", "key"), "utf8").trim();
21
+ if (k) return k;
22
+ } catch {}
23
+ try {
24
+ const cfg = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
25
+ return cfg.env?.GOL_CLIENT_ID || "";
26
+ } catch { return ""; }
27
+ }
28
+
29
+ async function validateKey(key) {
30
+ try {
31
+ const res = await fetch(API, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
34
+ body: JSON.stringify({ command: "echo check-install-verify", cwd: process.cwd(), v: VERSION }),
35
+ signal: AbortSignal.timeout(10000),
36
+ });
37
+ if (res.ok) return { valid: true, expired: false };
38
+ let body = {};
39
+ try { body = await res.json(); } catch {}
40
+ const expired = /expir/i.test(body.reason || body.error || body.message || "");
41
+ return { valid: false, expired };
42
+ } catch {
43
+ return { valid: false, expired: false };
44
+ }
45
+ }
46
+
47
+ // Stable anonymous device fingerprint: one-way hash of coarse machine facts.
48
+ // No personal data. Server uses it only to rate-limit free-key minting.
49
+ async function deviceFingerprint() {
50
+ const { createHash } = await import("node:crypto");
51
+ const { hostname, userInfo } = await import("node:os");
52
+ let user = "";
53
+ try { user = userInfo().username || ""; } catch {}
54
+ const seed = [hostname(), platform(), arch(), user].join("|");
55
+ return createHash("sha256").update(seed).digest("hex");
56
+ }
57
+
58
+ // Mint a free key with no email and no browser. Returns the client_id or null.
59
+ async function mintInstantKey() {
60
+ try {
61
+ const fp = await deviceFingerprint();
62
+ const res = await fetch(API.replace("/preflight", "/instant-key"), {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json", "User-Agent": "c/" + VERSION },
65
+ body: JSON.stringify({ fingerprint: fp, channel: "npm" }),
66
+ signal: AbortSignal.timeout(10000),
67
+ });
68
+ if (!res.ok) return null;
69
+ const d = await res.json();
70
+ return d && d.client_id ? d.client_id : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ async function downloadBinary(dest) {
77
+ const os = platform() === "win32" ? "win" : platform() === "darwin" ? "macos" : "linux";
78
+ const cpu = arch() === "arm64" ? "arm64" : "x64";
79
+ const ext = os === "win" ? ".exe" : "";
80
+ const url = `${CDN}/truth-gate-v${BINARY_VERSION}-${os}-${cpu}${ext}`;
81
+ const res = await fetch(url);
82
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
83
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
84
+ if (os !== "win") try { chmodSync(dest, 0o755); } catch {}
85
+ }
86
+
87
+ async function install() {
88
+ let key = process.argv[3] || process.env.GOL_CLIENT_ID || "";
89
+
90
+ // No key provided: mint one instantly. No email, no browser, no copy-paste.
91
+ if (!key || key === "your_key") {
92
+ console.log("\n Activating Check (free, no signup)...\n");
93
+ key = await mintInstantKey();
94
+ if (!key) {
95
+ console.log(" Could not activate automatically (offline, or this network hit its daily limit).\n");
96
+ console.log(" Get a key the manual way: https://golproductions.com/check\n");
97
+ console.log(" Then run: npx @golproductions/check --install YOUR_KEY\n");
98
+ process.exit(1);
99
+ }
100
+ console.log(" Activated. 120 free checks per day.\n");
101
+ }
102
+
103
+ const result = await validateKey(key);
104
+ if (!result.valid) {
105
+ if (result.expired) {
106
+ console.log("\n Your Client ID has expired: " + key);
107
+ console.log("\n Renew your key at: https://golproductions.com/check\n");
108
+ } else {
109
+ console.log("\n Invalid Client ID: " + key);
110
+ console.log("\n Get a valid key at: https://golproductions.com/check\n");
111
+ }
112
+ process.exit(1);
113
+ }
114
+
115
+ console.log("\n Key verified. Installing Check...\n");
116
+
117
+ const home = homedir();
118
+ let installed = 0;
119
+
120
+ const scriptSrc = readFileSync(new URL(import.meta.url), "utf8");
121
+ const hooksDir = join(home, ".check");
122
+ mkdirSync(hooksDir, { recursive: true });
123
+ const scriptPath = join(hooksDir, "check.mjs").replace(/\\/g, "/");
124
+ writeFileSync(scriptPath, scriptSrc, "utf8");
125
+ // Key file is the fallback for tools that don't pass env to hooks (Cursor, Gemini).
126
+ writeFileSync(join(hooksDir, "key"), key, "utf8");
127
+
128
+ const ext = platform() === "win32" ? ".exe" : "";
129
+ const binaryPath = join(hooksDir, "truth-gate" + ext).replace(/\\/g, "/");
130
+ await downloadBinary(binaryPath);
131
+
132
+ const mcpEntry = { command: "npx", args: ["@golproductions/check", "--mcp"], env: { GOL_CLIENT_ID: key } };
133
+
134
+ const targets = [
135
+ {
136
+ name: "Claude Code",
137
+ dir: join(home, ".claude"),
138
+ file: "settings.json",
139
+ config: (existing) => {
140
+ // Merge, never clobber: drop stale Check entries, keep the user's own
141
+ // hooks, then append ours.
142
+ existing.hooks = existing.hooks || {};
143
+ existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
144
+ existing.hooks.PreToolUse.push({
145
+ matcher: "Bash|PowerShell",
146
+ hooks: [{ type: "command", command: "node", args: [scriptPath] }]
147
+ });
148
+ existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
149
+ existing.hooks.PostToolUse.push(
150
+ { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] },
151
+ { matcher: "Edit|Write|NotebookEdit", hooks: [{ type: "command", command: binaryPath }] }
152
+ );
153
+ existing.env = existing.env || {};
154
+ existing.env.GOL_CLIENT_ID = key;
155
+ return existing;
156
+ }
157
+ },
158
+ {
159
+ name: "Gemini CLI / Antigravity",
160
+ dir: join(home, ".gemini"),
161
+ file: "settings.json",
162
+ config: (existing) => {
163
+ existing.hooks = existing.hooks || {};
164
+ existing.hooks.BeforeTool = existing.hooks.BeforeTool || [];
165
+ const hooks = existing.hooks.BeforeTool;
166
+ if (!hooks.find(h => h.hooks?.some(hh => hh.name === "check-gate"))) {
167
+ hooks.push({
168
+ matcher: ".*",
169
+ hooks: [{ type: "command", command: "node " + scriptPath, name: "check-gate" }]
170
+ });
171
+ }
172
+ return existing;
173
+ }
174
+ },
175
+ {
176
+ name: "Cursor",
177
+ dir: join(home, ".cursor"),
178
+ file: "hooks.json",
179
+ config: (existing) => {
180
+ existing.version = existing.version || 1;
181
+ existing.hooks = existing.hooks || {};
182
+ existing.hooks.beforeShellExecution = (existing.hooks.beforeShellExecution || []).filter(h => !isCheckRef(h.command));
183
+ existing.hooks.beforeShellExecution.push({
184
+ command: "node " + scriptPath
185
+ });
186
+ return existing;
187
+ }
188
+ },
189
+ {
190
+ name: "Homebase",
191
+ dir: join(home, ".homebase"),
192
+ file: "hooks.json",
193
+ config: (existing) => {
194
+ existing.hooks = existing.hooks || {};
195
+ existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
196
+ existing.hooks.PreToolUse.push({
197
+ matcher: "Bash|PowerShell",
198
+ hooks: [{ type: "command", command: "node", args: [scriptPath] }]
199
+ });
200
+ existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
201
+ existing.hooks.PostToolUse.push(
202
+ { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] }
203
+ );
204
+ existing.env = existing.env || {};
205
+ existing.env.GOL_CLIENT_ID = key;
206
+ return existing;
207
+ }
208
+ },
209
+ {
210
+ name: "Windsurf (MCP)",
211
+ dir: join(home, ".codeium", "windsurf"),
212
+ file: "mcp_config.json",
213
+ config: (existing) => {
214
+ existing.mcpServers = existing.mcpServers || {};
215
+ existing.mcpServers.Check = mcpEntry;
216
+ return existing;
217
+ }
218
+ },
219
+ {
220
+ name: "Continue (MCP)",
221
+ dir: join(process.cwd(), ".continue", "mcpServers"),
222
+ file: "check.json",
223
+ config: () => {
224
+ return { mcpServers: { Check: mcpEntry } };
225
+ }
226
+ },
227
+ {
228
+ name: "Amazon Q Developer (MCP)",
229
+ dir: join(home, ".aws", "amazonq"),
230
+ file: "mcp.json",
231
+ config: (existing) => {
232
+ existing.mcpServers = existing.mcpServers || {};
233
+ existing.mcpServers.Check = { type: "stdio", ...mcpEntry };
234
+ return existing;
235
+ }
236
+ },
237
+ {
238
+ name: "Roo Code (project MCP)",
239
+ dir: join(process.cwd(), ".roo"),
240
+ file: "mcp.json",
241
+ config: (existing) => {
242
+ existing.mcpServers = existing.mcpServers || {};
243
+ existing.mcpServers.Check = mcpEntry;
244
+ return existing;
245
+ }
246
+ },
247
+ {
248
+ name: "Project MCP (shared)",
249
+ dir: process.cwd(),
250
+ file: ".mcp.json",
251
+ config: (existing) => {
252
+ existing.mcpServers = existing.mcpServers || {};
253
+ existing.mcpServers.Check = mcpEntry;
254
+ return existing;
255
+ }
256
+ }
257
+ ];
258
+
259
+ const mcpTargets = ["Windsurf (MCP)", "Continue (MCP)", "Amazon Q Developer (MCP)", "Roo Code (project MCP)", "Project MCP (shared)"];
260
+
261
+ let hasClaude = false;
262
+ for (const t of targets) {
263
+ const isMcp = mcpTargets.includes(t.name);
264
+ if (!isMcp && !existsSync(t.dir)) continue;
265
+ if (isMcp) mkdirSync(t.dir, { recursive: true });
266
+ const filepath = join(t.dir, t.file);
267
+ let existing = {};
268
+ try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
269
+ const updated = t.config(existing);
270
+ writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
271
+ installed++;
272
+ if (t.name === "Claude Code") hasClaude = true;
273
+ }
274
+
275
+ if (installed === 0) {
276
+ mkdirSync(join(home, ".claude"), { recursive: true });
277
+ const filepath = join(home, ".claude", "settings.json");
278
+ let existing = {};
279
+ try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
280
+ const updated = targets[0].config(existing);
281
+ writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
282
+ hasClaude = true;
283
+ }
284
+
285
+ const checkRule = "Never fabricate, hallucinate, or invent values. If you don't have it, say you don't have it. No fake keys, no fake IDs, no fake paths, no fake URLs. If unsure, ask. Never guess and present it as fact.";
286
+ const ruleFile = hasClaude ? "CLAUDE.md" : "AGENTS.md";
287
+ const rulePath = join(process.cwd(), ruleFile);
288
+ try {
289
+ const existing = existsSync(rulePath) ? readFileSync(rulePath, "utf8") : "";
290
+ if (!existing.includes("Never fabricate")) {
291
+ const prefix = existing.length > 0 ? existing.trimEnd() + "\n\n" : "";
292
+ writeFileSync(rulePath, prefix + checkRule + "\n", "utf8");
293
+ }
294
+ } catch {}
295
+
296
+ console.log(`\n Your free GOL Client ID (this machine): ${key}`);
297
+ console.log(` Shared by every editor on this machine. 120 free checks/day.`);
298
+ console.log(` Need more? Get a GOL API Key (paid, prepaid balance) at`);
299
+ console.log(` https://www.golproductions.com/console`);
300
+ console.log(`\n By using Check you agree to the Terms and Privacy Policy:`);
301
+ console.log(` golproductions.com/terms | golproductions.com/privacy`);
302
+ console.log(`\nSafe travels wanderer..\n`);
303
+ process.exit(0);
304
+ }
305
+
306
+ function isCheckRef(str) {
307
+ return str && (str.includes(".check") || str.includes("truth-gate"));
308
+ }
309
+
310
+ function isCheckHook(hook) {
311
+ if (isCheckRef(hook.command)) return true;
312
+ if (hook.args?.some(a => isCheckRef(a))) return true;
313
+ return false;
314
+ }
315
+
316
+ async function uninstall() {
317
+ const home = homedir();
318
+ let removed = 0;
319
+
320
+ console.log("\n Removing Check...\n");
321
+
322
+ // Silent lifecycle ping. No output, failures swallowed — the uninstall
323
+ // itself must never be affected. Awaited just before exit so the process
324
+ // doesn't kill it mid-flight; the 3s abort caps worst-case delay.
325
+ let uninstallPing = Promise.resolve();
326
+ try {
327
+ const key = getClientId();
328
+ if (key) {
329
+ uninstallPing = fetch(API.replace("/preflight", "/log"), {
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
332
+ body: JSON.stringify({ verdict: "uninstall", channel: "npm", os: platform(), arch: arch(), version: VERSION }),
333
+ signal: AbortSignal.timeout(3000),
334
+ }).catch(() => {});
335
+ }
336
+ } catch {}
337
+
338
+ const hookTargets = [
339
+ {
340
+ name: "Claude Code",
341
+ path: join(home, ".claude", "settings.json"),
342
+ clean: (cfg) => {
343
+ if (cfg.hooks?.PreToolUse) {
344
+ cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
345
+ if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
346
+ }
347
+ if (cfg.hooks?.PostToolUse) {
348
+ cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
349
+ if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
350
+ }
351
+ if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
352
+ if (cfg.env) {
353
+ delete cfg.env.GOL_CLIENT_ID;
354
+ if (Object.keys(cfg.env).length === 0) delete cfg.env;
355
+ }
356
+ return cfg;
357
+ }
358
+ },
359
+ {
360
+ name: "Gemini CLI",
361
+ path: join(home, ".gemini", "settings.json"),
362
+ clean: (cfg) => {
363
+ if (cfg.hooks?.BeforeTool) {
364
+ cfg.hooks.BeforeTool = cfg.hooks.BeforeTool.filter(h => !h.hooks?.some(hh => hh.name === "check-gate"));
365
+ if (cfg.hooks.BeforeTool.length === 0) delete cfg.hooks.BeforeTool;
366
+ if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
367
+ }
368
+ return cfg;
369
+ }
370
+ },
371
+ {
372
+ name: "Cursor",
373
+ path: join(home, ".cursor", "hooks.json"),
374
+ clean: (cfg) => {
375
+ if (cfg.hooks?.beforeShellExecution) {
376
+ cfg.hooks.beforeShellExecution = cfg.hooks.beforeShellExecution.filter(h => !isCheckRef(h.command));
377
+ if (cfg.hooks.beforeShellExecution.length === 0) delete cfg.hooks.beforeShellExecution;
378
+ if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
379
+ }
380
+ return cfg;
381
+ }
382
+ },
383
+ {
384
+ name: "Homebase",
385
+ path: join(home, ".homebase", "hooks.json"),
386
+ clean: (cfg) => {
387
+ if (cfg.hooks?.PreToolUse) {
388
+ cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
389
+ if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
390
+ }
391
+ if (cfg.hooks?.PostToolUse) {
392
+ cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
393
+ if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
394
+ }
395
+ if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
396
+ if (cfg.env) {
397
+ delete cfg.env.GOL_CLIENT_ID;
398
+ if (Object.keys(cfg.env).length === 0) delete cfg.env;
399
+ }
400
+ return cfg;
401
+ }
402
+ }
403
+ ];
404
+
405
+ const mcpUserTargets = [
406
+ { name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") },
407
+ { name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json") },
408
+ ];
409
+
410
+ const mcpProjectTargets = [
411
+ { name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), deleteFile: true },
412
+ { name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json") },
413
+ { name: "Project MCP", path: join(process.cwd(), ".mcp.json") },
414
+ ];
415
+
416
+ for (const t of hookTargets) {
417
+ if (!existsSync(t.path)) continue;
418
+ try {
419
+ const cfg = JSON.parse(readFileSync(t.path, "utf8"));
420
+ const cleaned = t.clean(cfg);
421
+ writeFileSync(t.path, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
422
+ console.log(" removed " + t.name);
423
+ removed++;
424
+ } catch {}
425
+ }
426
+
427
+ for (const t of [...mcpUserTargets, ...mcpProjectTargets]) {
428
+ if (!existsSync(t.path)) continue;
429
+ try {
430
+ if (t.deleteFile) {
431
+ unlinkSync(t.path);
432
+ console.log(" removed " + t.name);
433
+ removed++;
434
+ } else {
435
+ const cfg = JSON.parse(readFileSync(t.path, "utf8"));
436
+ if (cfg.mcpServers?.Check) {
437
+ delete cfg.mcpServers.Check;
438
+ if (Object.keys(cfg.mcpServers).length === 0) delete cfg.mcpServers;
439
+ writeFileSync(t.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
440
+ console.log(" removed " + t.name);
441
+ removed++;
442
+ }
443
+ }
444
+ } catch {}
445
+ }
446
+
447
+ const checkDir = join(home, ".check");
448
+ if (existsSync(checkDir)) {
449
+ try {
450
+ rmSync(checkDir, { recursive: true, force: true });
451
+ console.log(" removed ~/.check/");
452
+ } catch {}
453
+ }
454
+
455
+ if (removed === 0) {
456
+ console.log(" No Check installations found.\n");
457
+ } else {
458
+ console.log(`\n Removed from ${removed} tool${removed !== 1 ? "s" : ""}.`);
459
+ console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n");
460
+ }
461
+
462
+ try { await uninstallPing; } catch {}
463
+ process.exit(0);
464
+ }
465
+
466
+ async function status() {
467
+ const home = homedir();
468
+ const checkDir = join(home, ".check");
469
+ const ext = platform() === "win32" ? ".exe" : "";
470
+ const id = getClientId();
471
+
472
+ console.log(`\n Check v${VERSION}\n`);
473
+
474
+ if (id) {
475
+ const masked = id.length > 12 ? id.slice(0, 8) + "..." + id.slice(-4) : id;
476
+ console.log(" Client ID: " + masked);
477
+ } else {
478
+ console.log(" Client ID: not set");
479
+ }
480
+
481
+ const binaryPath = join(checkDir, "truth-gate" + ext);
482
+ console.log(" Binary: " + (existsSync(binaryPath) ? "installed" : "not found"));
483
+ const scriptPath = join(checkDir, "check.mjs");
484
+ console.log(" Script: " + (existsSync(scriptPath) ? "installed" : "not found"));
485
+
486
+ console.log("\n Integrations:\n");
487
+
488
+ const checks = [
489
+ {
490
+ name: "Claude Code",
491
+ path: join(home, ".claude", "settings.json"),
492
+ test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
493
+ detail: (cfg) => {
494
+ const parts = [];
495
+ if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
496
+ if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
497
+ return parts.join(", ");
498
+ }
499
+ },
500
+ {
501
+ name: "Gemini CLI",
502
+ path: join(home, ".gemini", "settings.json"),
503
+ test: (cfg) => cfg.hooks?.BeforeTool?.some(h => h.hooks?.some(hh => hh.name === "check-gate")),
504
+ detail: () => "BeforeTool"
505
+ },
506
+ {
507
+ name: "Cursor",
508
+ path: join(home, ".cursor", "hooks.json"),
509
+ test: (cfg) => cfg.hooks?.beforeShellExecution?.some(h => isCheckRef(h.command)),
510
+ detail: () => "beforeShellExecution"
511
+ },
512
+ {
513
+ name: "Homebase",
514
+ path: join(home, ".homebase", "hooks.json"),
515
+ test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
516
+ detail: (cfg) => {
517
+ const parts = [];
518
+ if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
519
+ if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
520
+ return parts.join(", ");
521
+ }
522
+ },
523
+ { name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
524
+ { name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
525
+ { name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
526
+ { name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
527
+ { name: "Project MCP", path: join(process.cwd(), ".mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
528
+ ];
529
+
530
+ for (const c of checks) {
531
+ if (!existsSync(c.path)) {
532
+ console.log(" - " + c.name.padEnd(14) + " not detected");
533
+ continue;
534
+ }
535
+ try {
536
+ const cfg = JSON.parse(readFileSync(c.path, "utf8"));
537
+ if (c.test(cfg)) {
538
+ console.log(" + " + c.name.padEnd(14) + " active (" + c.detail(cfg) + ")");
539
+ } else {
540
+ console.log(" - " + c.name.padEnd(14) + " tool installed, Check not configured");
541
+ }
542
+ } catch {
543
+ console.log(" ? " + c.name.padEnd(14) + " config unreadable");
544
+ }
545
+ }
546
+
547
+ console.log("");
548
+ process.exit(0);
549
+ }
550
+
551
+ async function credits() {
552
+ const id = getClientId();
553
+ if (!id) {
554
+ console.log("\n No Client ID found. Set GOL_CLIENT_ID or run --install first.\n");
555
+ process.exit(1);
556
+ }
557
+
558
+ try {
559
+ const res = await fetch(API.replace("/preflight", "/credits"), {
560
+ method: "GET",
561
+ headers: { "X-GOL-CLIENT-ID": id, "User-Agent": "c/" + VERSION },
562
+ signal: AbortSignal.timeout(10000),
563
+ });
564
+ if (!res.ok) {
565
+ console.log("\n Could not fetch credits (HTTP " + res.status + ").\n");
566
+ process.exit(1);
567
+ }
568
+ const d = await res.json();
569
+ console.log(`\n Check v${VERSION} — Credits\n`);
570
+ if (d.free_remaining !== undefined) console.log(" Free today: " + d.free_remaining + " / " + (d.free_daily || 120));
571
+ if (d.calls_today !== undefined) console.log(" Calls today: " + d.calls_today);
572
+ if (d.balance !== undefined) console.log(" Balance: $" + d.balance + " AUD");
573
+ if (d.total_calls !== undefined) console.log(" Total calls: " + d.total_calls);
574
+ if (d.plan) console.log(" Plan: " + d.plan);
575
+ if (Object.keys(d).length === 0) console.log(" " + JSON.stringify(d));
576
+ console.log("");
577
+ } catch (err) {
578
+ console.log("\n Could not reach API: " + err.message + "\n");
579
+ }
580
+ process.exit(0);
581
+ }
582
+
583
+ async function docs() {
584
+ console.log(`
585
+ Check v${VERSION} — Documentation
586
+
587
+ Install: npx @golproductions/check --install <key>
588
+ Uninstall: npx @golproductions/check --uninstall
589
+ Status: npx @golproductions/check --status
590
+ Credits: npx @golproductions/check --credits
591
+
592
+ How it works:
593
+
594
+ Check installs hooks into your AI coding tools. Every time
595
+ the AI runs a command or edits a file, Check verifies it
596
+ before or after execution.
597
+
598
+ PreToolUse Validates commands before they run.
599
+ PostToolUse Verifies results after execution.
600
+ MCP Server Provides Check and CheckAndExecute tools.
601
+ CLAUDE.md Adds anti-fabrication instructions.
602
+
603
+ Supported tools:
604
+
605
+ Claude Code, Gemini CLI, Cursor, Windsurf, Continue,
606
+ Amazon Q Developer, Roo Code, Homebase
607
+
608
+ Pricing:
609
+
610
+ 120 free checks per day. Then $0.0068 AUD per check.
611
+ Credits never expire. Pay as you go.
612
+
613
+ Links:
614
+
615
+ Website: https://golproductions.com/check
616
+ Docs: https://github.com/golproductions/check
617
+ Support: support@golproductions.com
618
+ `);
619
+
620
+ try {
621
+ const { execSync } = await import("node:child_process");
622
+ const url = "https://github.com/golproductions/check";
623
+ if (platform() === "win32") execSync(`start "" "${url}"`, { stdio: "ignore", shell: true });
624
+ else if (platform() === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
625
+ else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
626
+ } catch {}
627
+
628
+ process.exit(0);
629
+ }
630
+
631
+ function help() {
632
+ console.log(`
633
+ Check v${VERSION} — Anti-hallucination layer for AI coding agents
634
+
635
+ Usage:
636
+
637
+ npx @golproductions/check --install <key> Install Check with your Client ID
638
+ npx @golproductions/check --uninstall Remove Check from all tools
639
+ npx @golproductions/check --status Show what is installed and active
640
+ npx @golproductions/check --credits Check your usage and balance
641
+ npx @golproductions/check --docs View documentation
642
+ npx @golproductions/check --help Show this help
643
+
644
+ Get your key: https://golproductions.com/check
645
+ Pricing: 120 free/day, then $0.0068 AUD per check
646
+ `);
647
+ process.exit(0);
648
+ }
649
+
650
+ // CLI routing
651
+ const args = process.argv.slice(2);
652
+ if (args.includes("--install")) { await install(); }
653
+ if (args.includes("--uninstall")) { await uninstall(); }
654
+ if (args.includes("--status")) { await status(); }
655
+ if (args.includes("--credits")) { await credits(); }
656
+ if (args.includes("--docs")) { await docs(); }
657
+ if (args.includes("--help") || args.includes("-h")) { help(); }
658
+ if (args.includes("--mcp")) { await import("./mcp.js"); process.exit(0); }
659
+
660
+ // No flags + interactive terminal = show help
661
+ if (!args.some(a => a.startsWith("-")) && process.stdin.isTTY) { help(); }
662
+
663
+ // Hook handler (invoked by tools via stdin)
664
+ function detect(p) {
665
+ if (typeof p.command === "string" && !p.tool_input) return "cursor";
666
+ if (p.hook_event_name === "BeforeTool" || p.toolCall?.argumentsJson) return "gemini";
667
+ return "claude";
668
+ }
669
+
670
+ function cmd(p, f) {
671
+ if (f === "cursor") return p.command;
672
+ if (f === "gemini") {
673
+ if (p.tool_input?.command) return p.tool_input.command;
674
+ if (p.toolCall?.argumentsJson) {
675
+ try {
676
+ const a = typeof p.toolCall.argumentsJson === "string" ? JSON.parse(p.toolCall.argumentsJson) : p.toolCall.argumentsJson;
677
+ return a.command || a.CommandLine || a.command_line;
678
+ } catch { return null; }
679
+ }
680
+ return null;
681
+ }
682
+ return p.tool_input?.command;
683
+ }
684
+
685
+ function out(f, ok, reason) {
686
+ if (ok) {
687
+ const r = { cursor: { permission: "allow" }, gemini: { decision: "allow" }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } } };
688
+ process.stdout.write(JSON.stringify(r[f] || r.claude));
689
+ process.exit(0);
690
+ }
691
+ if (f === "gemini") {
692
+ process.stdout.write(JSON.stringify({ decision: "deny", reason }));
693
+ process.exit(2);
694
+ }
695
+ const r = { cursor: { permission: "deny", user_message: reason, agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
696
+ process.stdout.write(JSON.stringify(r[f] || r.claude));
697
+ process.exit(0);
698
+ }
699
+
700
+ // Out of checks is a billing state, not a bad command. Warn, then let the
701
+ // command run without the gate. The warning fires once per day per machine.
702
+ function outCreditsExhausted(f, reason) {
703
+ let warned = true;
704
+ try {
705
+ const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
706
+ warned = existsSync(marker);
707
+ if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
708
+ } catch { warned = false; }
709
+ const msg = warned ? undefined : reason;
710
+ if (f === "gemini") {
711
+ process.stdout.write(JSON.stringify({ decision: "allow" }));
712
+ if (msg) process.stderr.write(msg);
713
+ process.exit(0);
714
+ }
715
+ if (f === "cursor") {
716
+ const r = { permission: "allow" };
717
+ if (msg) r.user_message = msg;
718
+ process.stdout.write(JSON.stringify(r));
719
+ process.exit(0);
720
+ }
721
+ const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
722
+ if (msg) r.systemMessage = msg;
723
+ process.stdout.write(JSON.stringify(r));
724
+ process.exit(0);
725
+ }
726
+
727
+ // The machine's own PATH resolution is the ground truth for whether a
728
+ // command exists. This is the same lookup the shell performs at launch:
729
+ // walk PATH (plus PATHEXT on Windows), pure filesystem reads, deterministic.
730
+ // The server honors binary_exists as proof, so real-but-unlisted tools
731
+ // (claude, codex, aider, internal CLIs) are never falsely blocked.
732
+ const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
733
+
734
+ // POSIX shell builtins and keywords. Frozen by spec since 1992: these are
735
+ // provided by the shell itself, not the filesystem, and are treated as
736
+ // present. This set is evergreen — it does not rot the way a tool list does.
737
+ const SHELL_BUILTINS = new Set([
738
+ "cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
739
+ "true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
740
+ "shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
741
+ "read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
742
+ "type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
743
+ "popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
744
+ "done", "case", "esac", "in", "function", "select", "{", "}", "!",
745
+ ]);
746
+
747
+ // Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
748
+ // quotes are literal text, not separators.
749
+ function segmentBases(command) {
750
+ const segs = [[]];
751
+ let cur = "", q = null;
752
+ for (let i = 0; i < command.length; i++) {
753
+ const ch = command[i];
754
+ if (q) { if (ch === q) q = null; else cur += ch; continue; }
755
+ if (ch === '"' || ch === "'") { q = ch; continue; }
756
+ if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
757
+ if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
758
+ if (segs[segs.length - 1].length) segs.push([]);
759
+ if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
760
+ continue;
761
+ }
762
+ if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
763
+ cur += ch;
764
+ }
765
+ if (cur) segs[segs.length - 1].push(cur);
766
+
767
+ const bases = [];
768
+ for (const words of segs) {
769
+ let base = null;
770
+ for (const w of words) {
771
+ if (!w) continue;
772
+ if (PREFIX_WORDS.has(w)) continue;
773
+ if (/^[\d.]+[smhd]?$/.test(w)) continue;
774
+ if (w.includes("=") && !w.startsWith("-")) continue;
775
+ if (w.startsWith("-")) continue;
776
+ base = w;
777
+ break;
778
+ }
779
+ if (base) bases.push(base);
780
+ }
781
+ return bases;
782
+ }
783
+
784
+ // THE ROD: do not model the shell — ask it. `type` is the shell reporting
785
+ // on itself: builtins, keywords, functions, and PATH binaries all answer
786
+ // through the exact resolution the shell will use at launch. A model of the
787
+ // shell can drift from the shell; the shell cannot drift from itself.
788
+ // On Windows, bare "bash" is a trap: PATH can resolve to WSL's
789
+ // System32\bash.exe — a different operating system with a different
790
+ // filesystem. The command will actually run in Git Bash, so the probe
791
+ // must ask Git Bash, located explicitly. If it cannot be located,
792
+ // abstain — never let the wrong interpreter answer for the right one.
793
+ function bashPath() {
794
+ if (platform() !== "win32") return "bash";
795
+ const candidates = [
796
+ process.env.CLAUDE_CODE_GIT_BASH_PATH,
797
+ "C:/Program Files/Git/bin/bash.exe",
798
+ "C:/Program Files/Git/usr/bin/bash.exe",
799
+ "C:/Program Files (x86)/Git/bin/bash.exe",
800
+ ].filter(Boolean);
801
+ for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
802
+ return null;
803
+ }
804
+
805
+ function shellKnows(name, cwd, shell) {
806
+ try {
807
+ if (shell === "powershell") {
808
+ const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
809
+ "if (Get-Command $env:CHECK_PROBE -ErrorAction SilentlyContinue) { exit 0 }; exit 1"],
810
+ { cwd, timeout: 4000, stdio: "ignore", env: { ...process.env, CHECK_PROBE: name } });
811
+ return r.error ? null : r.status === 0;
812
+ }
813
+ const bash = bashPath();
814
+ if (!bash) return null;
815
+ const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
816
+ { cwd, timeout: 4000, stdio: "ignore" });
817
+ return r.error ? null : r.status === 0;
818
+ } catch { return null; }
819
+ }
820
+
821
+ // Tri-state ground truth from the interpreter itself: true = every command
822
+ // word proven resolvable, false = at least one proven unresolvable, null =
823
+ // cannot be determined without executing (subshells, expansions) — abstain,
824
+ // never guess.
825
+ function allBinariesExist(command, cwd, shell) {
826
+ try {
827
+ let bases = segmentBases(command);
828
+ if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
829
+ if (bases.length === 0) return true;
830
+ if (bases.some(b => /[(){}$`]/.test(b))) return null;
831
+ let verdict = true;
832
+ for (const b of bases) {
833
+ const known = shellKnows(b, cwd, shell);
834
+ if (known === null) return null;
835
+ if (known === false) verdict = false;
836
+ }
837
+ return verdict;
838
+ } catch { return null; }
839
+ }
840
+
841
+ async function main() {
842
+ let f = "claude";
843
+ try {
844
+ let input = "";
845
+ for await (const chunk of process.stdin) input += chunk;
846
+ input = input.replace(/^/, "").trim();
847
+ // A firewall fails closed on a broken contract. Empty stdin means no tool
848
+ // is asking for a verdict (someone ran the CLI wrong): refuse loudly, and
849
+ // never print an "allow" nobody asked for. Garbage stdin means a tool IS
850
+ // asking but the payload is corrupt: deny, because we cannot see the
851
+ // command we are supposed to gate. Only an unreachable network fails open
852
+ // (Terms, section 5) — that path is below, and it is deliberate.
853
+ if (!input) {
854
+ process.stderr.write("check: no hook input on stdin. This entrypoint is for tool hooks; run --help for CLI usage.\n");
855
+ process.exit(1);
856
+ }
857
+ let p;
858
+ try { p = JSON.parse(input); } catch {
859
+ out("claude", false, "check: unreadable hook input (not JSON), command blocked");
860
+ return;
861
+ }
862
+ if (p.hook_event_name === "PostToolUse") { process.exit(0); return; }
863
+ f = detect(p);
864
+ const c = cmd(p, f);
865
+ if (!c) { out(f, true); return; }
866
+ const hookKey = getClientId();
867
+ if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
868
+ const controller = new AbortController();
869
+ const timer = setTimeout(() => controller.abort(), 5000);
870
+ // Ask the same interpreter that will run the command.
871
+ const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash");
872
+ // Send only what the verdict needs. No transcript paths, no usernames
873
+ // beyond what cwd itself carries — the check is about the command.
874
+ const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
875
+ if (exists !== null) body.binary_exists = exists;
876
+ const res = await fetch(API, {
877
+ method: "POST",
878
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
879
+ body: JSON.stringify(body),
880
+ signal: controller.signal
881
+ });
882
+ clearTimeout(timer);
883
+ const d = await res.json();
884
+
885
+ // Billing and account states arrive as non-2xx and carry no `verdict`.
886
+ // Surface what the server actually said so the user knows this is about
887
+ // their account, not their command. Without this, the free-tier wall
888
+ // (HTTP 402) falls through to the generic "denied" below and reads like
889
+ // the command itself was rejected.
890
+ if (!res.ok) {
891
+ // Rate limit is a traffic state, not a verdict. A deny must mean "your
892
+ // command is wrong", never "you typed too fast" — heavy agent sessions
893
+ // hit 60/min in normal use. Same trade as the credits wall: warn, let
894
+ // the command run ungated, resume when the window clears.
895
+ if (res.status === 429 || /rate limit/i.test(d.reason || d.error || "")) {
896
+ outCreditsExhausted(f, "check: rate limited (60 checks/min), command ran unverified. Check resumes automatically.");
897
+ return;
898
+ }
899
+ // Server-side failure is the service breaking, not the command being
900
+ // wrong. Same law as an unreachable network (Terms, section 5): the
901
+ // gate's own outage must never block the user's work.
902
+ if (res.status >= 500) {
903
+ outCreditsExhausted(f, "check: validation service error (HTTP " + res.status + "), command ran unverified. Check resumes automatically.");
904
+ return;
905
+ }
906
+ if (res.status === 402) {
907
+ const free = d.daily_free || 120;
908
+ const upsell = d.upgrade || "Top up at https://www.golproductions.com/console.html";
909
+ outCreditsExhausted(f, `check: you've used all ${free} free checks for today and your balance is empty. Check is off until tomorrow — commands run unverified. Credits you buy never expire. ${upsell}`);
910
+ return;
911
+ }
912
+ out(f, false, d.reason || d.error || "check: request not authorized, command blocked");
913
+ return;
914
+ }
915
+
916
+ if (d.verdict === "runnable") { out(f, true); }
917
+ else { out(f, false, d.reason || "denied — address the issue before continuing"); }
918
+ } catch {
919
+ // Network failure or timeout: fail OPEN. An unreachable validation
920
+ // service must never block the user's work (Terms, section 5).
921
+ outCreditsExhausted(f, "check: validation service unreachable, command ran unverified. Check resumes automatically.");
922
+ }
923
+ }
924
+
925
+ main();