thumbgate 1.27.20 → 1.28.1

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 (63) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +124 -129
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/mcp/server-stdio.js +18 -33
  6. package/adapters/opencode/opencode.json +1 -1
  7. package/bin/cli.js +95 -78
  8. package/bin/postinstall.js +1 -1
  9. package/config/entitlement-public-keys.json +6 -0
  10. package/config/gates/default.json +3 -3
  11. package/config/github-about.json +2 -5
  12. package/config/merge-quality-checks.json +3 -0
  13. package/config/post-deploy-marketing-pages.json +1 -1
  14. package/hooks/hooks.json +38 -0
  15. package/package.json +28 -10
  16. package/public/about.html +1 -4
  17. package/public/agent-manager.html +6 -6
  18. package/public/agents-cost-savings.html +3 -3
  19. package/public/ai-malpractice-prevention.html +1 -1
  20. package/public/blog.html +12 -9
  21. package/public/chatgpt-app.html +3 -3
  22. package/public/codex-enterprise.html +3 -3
  23. package/public/codex-plugin.html +5 -5
  24. package/public/compare.html +10 -10
  25. package/public/dashboard.html +1 -1
  26. package/public/diagnostic.html +23 -1
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +14 -14
  29. package/public/index.html +202 -220
  30. package/public/install.html +14 -14
  31. package/public/learn.html +4 -17
  32. package/public/numbers.html +2 -2
  33. package/public/partner-intake.html +198 -0
  34. package/public/pricing.html +61 -31
  35. package/public/pro.html +2 -2
  36. package/scripts/agent-reward-model.js +13 -0
  37. package/scripts/bayes-optimal-gate.js +6 -1
  38. package/scripts/billing.js +815 -137
  39. package/scripts/checkout-attribution-reference.js +85 -0
  40. package/scripts/claude-feedback-sync.js +23 -6
  41. package/scripts/cli-feedback.js +33 -8
  42. package/scripts/commercial-offer.js +3 -3
  43. package/scripts/entitlement.js +250 -0
  44. package/scripts/export-databricks-bundle.js +5 -0
  45. package/scripts/export-dpo-pairs.js +6 -0
  46. package/scripts/export-hf-dataset.js +5 -0
  47. package/scripts/feedback-loop.js +290 -1
  48. package/scripts/feedback-quality.js +25 -26
  49. package/scripts/feedback-sanitizer.js +151 -3
  50. package/scripts/gates-engine.js +134 -43
  51. package/scripts/hosted-config.js +9 -2
  52. package/scripts/imperative-detector.js +85 -0
  53. package/scripts/intervention-policy.js +13 -0
  54. package/scripts/mailer/resend-mailer.js +11 -5
  55. package/scripts/pr-manager.js +9 -22
  56. package/scripts/pro-local-dashboard.js +198 -0
  57. package/scripts/risk-scorer.js +6 -0
  58. package/scripts/secret-scanner.js +363 -68
  59. package/scripts/self-protection.js +21 -36
  60. package/scripts/seo-gsd.js +2 -2
  61. package/scripts/thompson-sampling.js +11 -2
  62. package/src/api/server.js +202 -22
  63. package/public/assets/brand/github-social-preview.png +0 -0
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.27.20", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.28.1", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.27.20", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.28.1", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -231,7 +231,7 @@ const {
231
231
  finalizeSession: finalizeFeedbackSession,
232
232
  } = require('../../scripts/feedback-session');
233
233
 
234
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.27.20' };
234
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.28.1' };
235
235
  const COMMERCE_CATEGORIES = [
236
236
  'product_recommendation',
237
237
  'brand_compliance',
@@ -1430,22 +1430,14 @@ function writeNdjsonResponse(id, payload, error = null) {
1430
1430
  process.stdout.write(`${body}\n`);
1431
1431
  }
1432
1432
 
1433
- /**
1434
- * Default staleness threshold: if a lock is older than this (ms), the holder
1435
- * is considered orphaned even if its PID is still alive — it likely belongs
1436
- * to a defunct Claude Code session whose process was never reaped.
1437
- */
1438
- const LOCK_STALE_MS = Number(process.env.THUMBGATE_LOCK_STALE_MS) || 2 * 60 * 60 * 1000; // 2 hours
1439
-
1440
1433
  /**
1441
1434
  * Acquire a file-system lock to prevent duplicate MCP server instances.
1442
- * Returns { lockFile, cleanupLock } on success, or calls process.exit(1)
1443
- * if another live server holds the lock.
1435
+ * Returns { lockFile, cleanupLock } on success.
1444
1436
  *
1445
- * Staleness reaping: if the lock-holding process is alive but the lock is
1446
- * older than LOCK_STALE_MS, the holder is killed (SIGTERM) and the lock is
1447
- * reclaimed. This prevents orphaned `thumbgate serve` processes from permanently
1448
- * blocking new sessions.
1437
+ * Every live stdio client needs its own MCP process, and those sessions can
1438
+ * legitimately run for many hours. A lock's age is therefore not evidence that
1439
+ * its owner is orphaned. Live owners coexist through per-session locks; only a
1440
+ * lock whose PID is no longer running is reclaimed.
1449
1441
  */
1450
1442
  function acquireLock() {
1451
1443
  const feedbackDir = getFeedbackPaths().FEEDBACK_DIR;
@@ -1458,26 +1450,19 @@ function acquireLock() {
1458
1450
  try { process.kill(lockData.pid, 0); isRunning = true; } catch { /* process is dead */ }
1459
1451
 
1460
1452
  if (isRunning) {
1461
- const lockAge = Date.now() - new Date(lockData.startedAt).getTime();
1462
- if (lockAge > LOCK_STALE_MS) {
1463
- // Orphaned process kill it and take over
1464
- process.stderr.write(`[thumbgate] Lock held by PID ${lockData.pid} is ${Math.round(lockAge / 60000)}m old (threshold: ${Math.round(LOCK_STALE_MS / 60000)}m). Reaping orphaned process.\n`);
1465
- try { process.kill(lockData.pid, 'SIGTERM'); } catch { /* already gone */ }
1466
- } else {
1467
- // Another session's MCP server is running coexist via per-session lock.
1468
- // Each Claude session communicates via its own stdio pipe and needs its own server.
1469
- // SQLite WAL mode handles concurrent access safely.
1470
- process.stderr.write(`[thumbgate] Another MCP server (PID ${lockData.pid}) is running for ${feedbackDir}. Starting concurrent session.\n`);
1471
- const sessionLockFile = path.join(feedbackDir, `.mcp-server-${process.pid}.lock`);
1472
- fs.writeFileSync(sessionLockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
1473
- const cleanupSessionLock = () => { try { fs.unlinkSync(sessionLockFile); } catch { /* already removed */ } };
1474
- process.on('exit', cleanupSessionLock);
1475
- process.on('SIGTERM', () => { cleanupSessionLock(); process.exit(0); });
1476
- process.on('SIGINT', () => { cleanupSessionLock(); process.exit(0); });
1477
- return { lockFile: sessionLockFile, cleanupLock: cleanupSessionLock };
1478
- }
1453
+ // Another session's MCP server is running — coexist via per-session lock.
1454
+ // Each client communicates via its own stdio pipe and needs its own server.
1455
+ // SQLite WAL mode handles concurrent access safely.
1456
+ process.stderr.write(`[thumbgate] Another MCP server (PID ${lockData.pid}) is running for ${feedbackDir}. Starting concurrent session.\n`);
1457
+ const sessionLockFile = path.join(feedbackDir, `.mcp-server-${process.pid}.lock`);
1458
+ fs.writeFileSync(sessionLockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
1459
+ const cleanupSessionLock = () => { try { fs.unlinkSync(sessionLockFile); } catch { /* already removed */ } };
1460
+ process.on('exit', cleanupSessionLock);
1461
+ process.on('SIGTERM', () => { cleanupSessionLock(); process.exit(0); });
1462
+ process.on('SIGINT', () => { cleanupSessionLock(); process.exit(0); });
1463
+ return { lockFile: sessionLockFile, cleanupLock: cleanupSessionLock };
1479
1464
  }
1480
- // Stale lock from a dead or reaped process — remove it
1465
+ // Stale lock from a dead process — remove it.
1481
1466
  try { fs.unlinkSync(lockFile); } catch { /* already gone */ }
1482
1467
  process.stderr.write(`[thumbgate] Removed stale lock (PID ${lockData.pid} is no longer running).\n`);
1483
1468
  }
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.27.20",
10
+ "thumbgate@1.28.1",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/bin/cli.js CHANGED
@@ -134,7 +134,7 @@ function upgradeNudge() {
134
134
  '\n Team rollout: start with the $499 Workflow Hardening Diagnostic\n' +
135
135
  ` ${diagnosticUrl}\n` +
136
136
  `\n Solo side lane: Pro — ${PRO_PRICE_LABEL}\n` +
137
- ' Keeps lessons, rules, and the dashboard synced across machines and agent runtimes.\n' +
137
+ ' Removes solo caps; adds personal recall, dashboard proof, exports, and managed adapters.\n' +
138
138
  ` ${pricingUrl}\n\n`
139
139
  );
140
140
  }
@@ -234,9 +234,9 @@ function proNudge(context) {
234
234
  const checkoutUrl = checkoutUrlFor('cli_nudge', context || COMMAND || 'general');
235
235
  const pricingUrl = pricingUrlFor('cli_nudge', context || COMMAND || 'general');
236
236
  const messages = [
237
- `\n 💡 Pro (${PRO_PRICE_LABEL}): keep lessons, rules, and dashboard state synced across machines and agent runtimes.\n See pricing: ${pricingUrl}\n`,
238
- `\n 💡 You just taught ThumbGate something locally. Pro keeps that lesson alive on every laptop, CI box, and agent runtime.\n See pricing: ${pricingUrl}\n`,
239
- `\n 💡 ThumbGate Pro syncs lessons/rules across Claude, Codex, Cursor, containers, and CI. ${PRO_PRICE_LABEL}.\n Start Pro: ${checkoutUrl}\n`,
237
+ `\n 💡 Pro (${PRO_PRICE_LABEL}): personal recall, dashboard proof, exports, and managed adapters.\n See pricing: ${pricingUrl}\n`,
238
+ `\n 💡 You just taught ThumbGate something locally. Pro keeps personal lessons searchable and exportable without free-tier caps.\n See pricing: ${pricingUrl}\n`,
239
+ `\n 💡 ThumbGate Pro maintains adapter coverage across Claude, Codex, Cursor, containers, and CI. ${PRO_PRICE_LABEL}.\n Start Pro: ${checkoutUrl}\n`,
240
240
  ];
241
241
  // Rotate message daily — no Math.random (security policy)
242
242
  const msg = messages[Math.floor(Date.now() / 86400000) % messages.length];
@@ -921,7 +921,7 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
921
921
  console.log('Scaffold ThumbGate in the current project and wire detected agent integrations.');
922
922
  console.log('');
923
923
  console.log('Options:');
924
- console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline');
924
+ console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline, opencode');
925
925
  console.log(' --wire-hooks Wire hooks only; do not scaffold project files');
926
926
  console.log(' --email <email> Subscribe installer to the setup guide and trial reminders');
927
927
  console.log(' --dry-run Show hook changes without writing them');
@@ -1593,6 +1593,7 @@ function pro() {
1593
1593
  trackEvent('cli_pro_view', { command: 'pro' });
1594
1594
  const args = parseArgs(process.argv.slice(3));
1595
1595
  const {
1596
+ notifyHostedProActivation,
1596
1597
  resolveProKey,
1597
1598
  saveLicense,
1598
1599
  startLocalProDashboard,
@@ -1606,9 +1607,10 @@ function pro() {
1606
1607
  console.log('Self-serve side lane today: Pro ($19/mo or $149/yr).');
1607
1608
  console.log('Every licensed Pro user gets a personal local dashboard on localhost.');
1608
1609
  console.log('\nWhat is available:');
1609
- console.log(' - Hosted sync: keep lessons, rules, and dashboard state aligned across laptops, CI, containers, and agent runtimes');
1610
+ console.log(' - Personal recall: search lessons, rules, and proof');
1610
1611
  console.log(' - Local Pro dashboard: your own browser dashboard for search, gates, and DPO export');
1611
- console.log(' - Team rollout path: shared hosted lessons, org visibility, and workflow proof');
1612
+ console.log(' - Managed adapters: Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode');
1613
+ console.log(' - Team rollout path: Enterprise adds shared hosted lessons, org visibility, workflow proof');
1612
1614
  console.log(' - Commercial truth doc: source of truth for traction, pricing, and proof claims');
1613
1615
  console.log('\nLinks:');
1614
1616
  console.log(` Buy Pro : ${PRO_CHECKOUT_URL}`);
@@ -1661,7 +1663,22 @@ function pro() {
1661
1663
  console.log('\n✅ Pro license activated!');
1662
1664
  console.log(` Key saved to: ${licensePath}`);
1663
1665
  console.log(' Launching your personal local dashboard...\n');
1664
- return launchDashboard(license.key, 'pro_activate');
1666
+ return notifyHostedProActivation({
1667
+ key: license.key,
1668
+ source: 'cli_pro_activate',
1669
+ version: license.version,
1670
+ })
1671
+ .then((notificationResult) => {
1672
+ appendLocalTelemetry({
1673
+ eventType: 'pro_activation_alert',
1674
+ version: license.version,
1675
+ timestamp: new Date().toISOString(),
1676
+ notified: Boolean(notificationResult && notificationResult.notified),
1677
+ sent: Boolean(notificationResult && notificationResult.alert && notificationResult.alert.sent),
1678
+ reason: notificationResult && notificationResult.reason ? notificationResult.reason : null,
1679
+ });
1680
+ return launchDashboard(license.key, 'pro_activate');
1681
+ });
1665
1682
  }
1666
1683
 
1667
1684
  if (args.upgrade) {
@@ -2678,23 +2695,27 @@ function install() {
2678
2695
  }
2679
2696
 
2680
2697
  async function gateCheck() {
2681
- // Explicit emergency escape hatch ONLY. The 2026-06-03 hotfix made this
2682
- // bypass-by-default, which silently disabled ThumbGate's enforcement entirely
2683
- // (the firewall approved everything). Restored 2026-06-04: enforcement runs by
2684
- // default in warn-by-default posture (see gates-engine applyEnforcementPosture);
2685
- // set THUMBGATE_HOTFIX_BYPASS=1 to disable all checks if a gate ever misfires.
2686
- if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
2687
- process.stdout.write(JSON.stringify({
2688
- decision: 'approve',
2689
- reason: 'hotfix-bypass-opt-in',
2690
- hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
2691
- }) + '\n');
2692
- return;
2693
- }
2694
2698
  try {
2695
2699
  const payload = readStdinText();
2696
2700
  const input = payload ? JSON.parse(payload) : {};
2697
2701
  const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2702
+
2703
+ // The operator bypass only skips advisory and strict-mode gates. Security and
2704
+ // self-protection floors are evaluated before the early approval path.
2705
+ if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
2706
+ const hardFloorOutput = gatesEngine.runHardFloor(input);
2707
+ if (hardFloorOutput) {
2708
+ process.stdout.write(hardFloorOutput + '\n');
2709
+ return;
2710
+ }
2711
+ process.stdout.write(JSON.stringify({
2712
+ decision: 'approve',
2713
+ reason: 'operator-bypass-opt-in',
2714
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
2715
+ }) + '\n');
2716
+ return;
2717
+ }
2718
+
2698
2719
  const output = await gatesEngine.runAsync(input);
2699
2720
  process.stdout.write(output + '\n');
2700
2721
  } catch (err) {
@@ -2732,11 +2753,18 @@ function statuslineRender() {
2732
2753
 
2733
2754
  function hookAutoCapture() {
2734
2755
  syncActiveProjectContext();
2756
+ const { extractPromptEnvelope } = require(path.join(PKG_ROOT, 'scripts', 'feedback-sanitizer'));
2757
+ const rawStdin = readStdinText();
2758
+ const promptEnvelope = extractPromptEnvelope(rawStdin);
2759
+ // stdin on a UserPromptSubmit hook is the JSON payload
2760
+ // {session_id, transcript_path, cwd, prompt, ...}. Persist ONLY the human
2761
+ // `.prompt` field — never the whole stdin object — so session metadata blobs
2762
+ // can't be promoted as lessons.
2735
2763
  const prompt = process.env.CLAUDE_USER_PROMPT
2736
2764
  || process.env.THUMBGATE_USER_PROMPT
2737
2765
  || process.env.CODEX_USER_PROMPT
2738
2766
  || process.env.USER_PROMPT
2739
- || readStdinText().trim();
2767
+ || promptEnvelope.prompt;
2740
2768
  const { evaluatePromptGuard } = require(path.join(PKG_ROOT, 'scripts', 'prompt-guard'));
2741
2769
  const { processInlineFeedback, formatCliOutput } = require(path.join(PKG_ROOT, 'scripts', 'cli-feedback'));
2742
2770
  const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
@@ -2749,25 +2777,39 @@ function hookAutoCapture() {
2749
2777
  })
2750
2778
  );
2751
2779
 
2752
- recordConversationEntry({
2753
- author: 'user',
2754
- text: prompt,
2755
- source: 'claude_user_prompt',
2756
- });
2757
-
2758
2780
  const guardResult = evaluatePromptGuard(prompt);
2759
2781
  if (guardResult) {
2782
+ recordConversationEntry({
2783
+ author: 'user',
2784
+ text: prompt,
2785
+ source: 'claude_user_prompt',
2786
+ });
2760
2787
  process.stdout.write(`${JSON.stringify(guardResult)}\n`);
2761
2788
  return;
2762
2789
  }
2763
2790
 
2764
2791
  const detected = detectFeedbackSignal(prompt);
2765
2792
  if (!detected) {
2793
+ recordConversationEntry({
2794
+ author: 'user',
2795
+ text: prompt,
2796
+ source: 'claude_user_prompt',
2797
+ });
2766
2798
  return;
2767
2799
  }
2768
2800
 
2769
2801
  const signal = detected.signal;
2770
2802
  const conversationWindow = readRecentConversationWindow({ limit: 8 });
2803
+ const { buildFeedbackSourceIdentity } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
2804
+ const sourceEvent = buildFeedbackSourceIdentity({
2805
+ signal,
2806
+ promptText: prompt,
2807
+ sessionId: promptEnvelope.sessionId || process.env.CLAUDE_SESSION_ID,
2808
+ promptId: promptEnvelope.promptId,
2809
+ projectDir: promptEnvelope.projectDir || CWD,
2810
+ timestamp: promptEnvelope.timestamp,
2811
+ source: 'claude-user-prompt',
2812
+ });
2771
2813
  const result = processInlineFeedback({
2772
2814
  signal,
2773
2815
  context: prompt,
@@ -2776,7 +2818,15 @@ function hookAutoCapture() {
2776
2818
  : undefined,
2777
2819
  whatWentWrong: signal === 'down' ? prompt : undefined,
2778
2820
  whatWorked: signal === 'up' ? prompt : undefined,
2821
+ sourceEvent,
2779
2822
  });
2823
+ if (!result.feedbackResult || !result.feedbackResult.duplicate) {
2824
+ recordConversationEntry({
2825
+ author: 'user',
2826
+ text: prompt,
2827
+ source: 'claude_user_prompt',
2828
+ });
2829
+ }
2780
2830
  process.stdout.write(formatCliOutput(result) + '\n');
2781
2831
  }
2782
2832
 
@@ -3930,53 +3980,6 @@ switch (COMMAND) {
3930
3980
  case 'dispatch-brief':
3931
3981
  dispatchBrief();
3932
3982
  break;
3933
- case 'gate-check': {
3934
- // PreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict
3935
- // Used by: generate-pretool-hook.sh → npx thumbgate gate-check
3936
- const { run: gateRun, runAsync: gateRunAsync } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
3937
- const { evaluateSelfProtection } = require(path.join(PKG_ROOT, 'scripts', 'self-protection'));
3938
- let stdinData = '';
3939
- process.stdin.setEncoding('utf8');
3940
- process.stdin.on('data', (chunk) => { stdinData += chunk; });
3941
- process.stdin.on('end', async () => {
3942
- try {
3943
- const input = JSON.parse(stdinData);
3944
- const output = await gateRunAsync(input);
3945
- // Self-protection overlay (2026-07-08): the gate engine only denies edits
3946
- // to ThumbGate's own governance files under strict enforcement; by default
3947
- // such edits pass as a clean ALLOW. Surface them (or block in strict) so an
3948
- // agent can't silently disable the firewall. Only overlays when the engine
3949
- // did not already produce a hard deny.
3950
- let verdict = output;
3951
- try {
3952
- const parsed = JSON.parse(output || '{}');
3953
- const alreadyDeny = (parsed.hookSpecificOutput && parsed.hookSpecificOutput.permissionDecision === 'deny')
3954
- || parsed.decision === 'block';
3955
- if (!alreadyDeny) {
3956
- const sp = evaluateSelfProtection(input.tool_name, input.tool_input);
3957
- if (sp && sp.action === 'block') {
3958
- verdict = JSON.stringify({
3959
- decision: 'block',
3960
- reason: sp.message,
3961
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: sp.message },
3962
- });
3963
- } else if (sp && sp.action === 'warn') {
3964
- verdict = JSON.stringify({
3965
- hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: sp.message },
3966
- });
3967
- }
3968
- }
3969
- } catch (_e) { /* non-JSON engine output: pass through unchanged */ }
3970
- process.stdout.write(verdict + '\n');
3971
- process.exit(0);
3972
- } catch (err) {
3973
- process.stderr.write(`gate-check error: ${err.message}\n`);
3974
- process.stdout.write(JSON.stringify({}) + '\n');
3975
- process.exit(0);
3976
- }
3977
- });
3978
- break;
3979
- }
3980
3983
  case 'hermes-gate': {
3981
3984
  // Nous Research Hermes Agent `pre_tool_call` shell hook.
3982
3985
  // Hermes pipes each pending tool call as JSON to stdin and reads a decision from stdout;
@@ -3987,7 +3990,7 @@ switch (COMMAND) {
3987
3990
  // Hermes `pre_tool_call` is binary (block or allow) with no warn channel, and the whole point
3988
3991
  // of wiring it is to gate, so we run STRICT enforcement by default — otherwise ThumbGate's
3989
3992
  // warn-by-default posture would pass every deny through and the hook would block nothing.
3990
- // Opt out with THUMBGATE_HERMES_WARN_ONLY=1; THUMBGATE_HOTFIX_BYPASS=1 still disables checks.
3993
+ // Opt out with THUMBGATE_HERMES_WARN_ONLY=1; the operator bypass also leaves ordinary gates advisory.
3991
3994
  // Wire it in ~/.hermes/config.yaml — see adapters/hermes/config.yaml.
3992
3995
  if (process.env.THUMBGATE_HERMES_WARN_ONLY !== '1' && process.env.THUMBGATE_HOTFIX_BYPASS !== '1') {
3993
3996
  process.env.THUMBGATE_STRICT_ENFORCEMENT = '1';
@@ -3999,8 +4002,22 @@ switch (COMMAND) {
3999
4002
  process.stdin.on('end', async () => {
4000
4003
  try {
4001
4004
  const payload = JSON.parse(hermesStdin);
4002
- // Hermes sends snake_case tool_name/tool_input gates-engine reads these directly.
4003
- const verdict = await hermesGateRun({ tool_name: payload.tool_name, tool_input: payload.tool_input });
4005
+ const hermesToolName = String(payload.tool_name || '');
4006
+ const canonicalToolNames = {
4007
+ terminal: 'Bash',
4008
+ process: 'Bash',
4009
+ execute_code: 'Bash',
4010
+ patch: 'Edit',
4011
+ write_file: 'Write',
4012
+ read_file: 'Read',
4013
+ };
4014
+ const verdict = await hermesGateRun({
4015
+ tool_name: canonicalToolNames[hermesToolName] || hermesToolName,
4016
+ tool_input: {
4017
+ ...(payload.tool_input || {}),
4018
+ hermes_tool_name: hermesToolName,
4019
+ },
4020
+ });
4004
4021
  let parsed = {};
4005
4022
  try { parsed = JSON.parse(verdict); } catch (_e) { parsed = {}; }
4006
4023
  const hso = parsed.hookSpecificOutput || {};
@@ -36,7 +36,7 @@ process.stderr.write(`
36
36
  ╰─────────────────────────────────────────────────────╯
37
37
 
38
38
  Trial unlocks: unlimited rules, lesson search, DPO export,
39
- hosted dashboard. After 7 days, free tier limits apply.
39
+ and personal dashboard proof. After 7 days, free tier limits apply.
40
40
  Subscribe for the 5-min setup guide + weekly tips:
41
41
  npx thumbgate subscribe you@company.com
42
42
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "activeKid": "tgk_fd49d8ab2055",
3
+ "keys": {
4
+ "tgk_fd49d8ab2055": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAEBVF8rLha9zsIhuiZ0CCslJK0SuCG6HL/bWXbkYVRiA=\n-----END PUBLIC KEY-----\n"
5
+ }
6
+ }
@@ -210,7 +210,7 @@
210
210
  {
211
211
  "id": "deny-network-egress",
212
212
  "layer": "Cloud",
213
- "pattern": "curl\\s|wget\\s|fetch\\(|https?://(?!github\\.com|registry\\.npmjs\\.org|api\\.anthropic\\.com)",
213
+ "pattern": "fetch\\(|https?://(?!(?:github\\.com|registry\\.npmjs\\.org|api\\.anthropic\\.com|localhost|127\\.0\\.0\\.1|\\[::1\\]|0\\.0\\.0\\.0)(?:[:/?#]|$))|(?:curl|wget)\\s+(?:-\\S+\\s+)*(?!https?://|(?:github\\.com|registry\\.npmjs\\.org|api\\.anthropic\\.com|localhost|127\\.0\\.0\\.1|\\[::1\\]|0\\.0\\.0\\.0)(?:[:/?#]|$))[\\w.-]+\\.[a-z]{2,}",
214
214
  "action": "warn",
215
215
  "unless": "egress_approved",
216
216
  "message": "Potential unauthorized network egress detected.",
@@ -342,7 +342,7 @@
342
342
  "id": "self-protect-config",
343
343
  "layer": "Execution",
344
344
  "toolNames": ["Edit", "Write", "MultiEdit"],
345
- "pattern": "(?:config/gates/|config/budget\\.json|\\.thumbgate/|thumbgate\\.json)",
345
+ "pattern": "(?:config/gates/|config/(?:budget|enforcement|mcp-allowlists)\\.json|\\.thumbgate/config\\.json|thumbgate\\.json)",
346
346
  "action": "block",
347
347
  "message": "Self-protection: agent cannot modify ThumbGate configuration, gate rules, or budget settings.",
348
348
  "severity": "critical",
@@ -372,7 +372,7 @@
372
372
  "id": "self-protect-hooks-disable",
373
373
  "layer": "Execution",
374
374
  "toolNames": ["Edit", "Write", "Bash"],
375
- "pattern": "(?:settings\\.json|settings\\.local\\.json).*(?:hooks|PreToolUse|PostToolUse)",
375
+ "pattern": "(?:\\.claude/settings(?:\\.local)?\\.json|\\.codex/config\\.toml|scripts/hook-[^/]+\\.(?:js|sh))",
376
376
  "action": "block",
377
377
  "message": "Self-protection: agent cannot modify hook registrations.",
378
378
  "severity": "critical",
@@ -2,14 +2,11 @@
2
2
  "repo": "IgorGanapolsky/ThumbGate",
3
3
  "repositoryUrl": "https://github.com/IgorGanapolsky/ThumbGate",
4
4
  "homepageUrl": "https://thumbgate.ai",
5
- "githubDescription": "Agent governance for ThumbGate: 👍/👎 become Pre-Action Checks that block repeat mistakes before code, money, or customer systems change.",
6
- "metaDescription": "Stripe Connect integration engineering for ticketing and marketplace platforms — Pretix plugin shipped to Hilltown Media. ThumbGate is also the pre-action governance runtime for AI coding agents (Claude Code, Cursor, Codex, Gemini CLI): 👍 thumbs up and 👎 thumbs down feedback become history-aware lessons, shared lessons and org visibility, and PreToolUse checks that block repeated mistakes before code, money, or customer systems change. Perfect for vibe coding and human-in-the-loop workflows.",
5
+ "githubDescription": "ThumbGate Pre-Action Checks derive rules from repeated failures, flag risky tool calls, hard-block detected secret leaks, and block matches in strict mode.",
6
+ "metaDescription": "ThumbGate is the local pre-action governance runtime for AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode): accepted 👍 thumbs-up and 👎 thumbs-down feedback becomes history-aware local lessons; repeated failures can become prevention rules. PreToolUse checks flag and log risky tool calls, hard-block detected secret leaks, and block matching actions in strict mode. Pro is for individual operators; Enterprise rollout is scoped after intake.",
7
7
  "topics": [
8
8
  "thumbgate",
9
9
  "pre-action-checks",
10
- "save-llm-tokens",
11
- "reduce-llm-cost",
12
- "ai-cost-optimization",
13
10
  "mcp",
14
11
  "mcp-server",
15
12
  "ai-agents",
@@ -18,5 +18,8 @@
18
18
  "failingBuckets": [
19
19
  "fail",
20
20
  "cancel"
21
+ ],
22
+ "selfReferentialChecks": [
23
+ "Trunk Merge Queue (main)"
21
24
  ]
22
25
  }
@@ -4,7 +4,7 @@
4
4
  "pages": [
5
5
  {
6
6
  "route": "/",
7
- "sentinel": "Stop paying for the same AI mistake twice",
7
+ "sentinel": "Catch AI agents before they spend money",
8
8
  "description": "Home page hero (lifeblood — never regress)"
9
9
  },
10
10
  {
@@ -0,0 +1,38 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash|Edit|Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node",
10
+ "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/hook-pre-tool-use.js"]
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node",
21
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/cli.js", "hook-auto-capture"]
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "SessionStart": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node",
32
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/cli.js", "session-start"]
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ }