thumbgate 1.26.8 → 1.27.3

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 (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/agentic-verify.txt +1 -0
  3. package/.well-known/llms.txt +2 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +44 -31
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  8. package/adapters/mcp/server-stdio.js +41 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/thumbgate-bench.json +2 -2
  11. package/bin/cli.js +184 -8
  12. package/bin/dashboard-cli.js +7 -0
  13. package/config/gate-classifier-routing.json +98 -0
  14. package/config/gate-templates.json +60 -0
  15. package/config/mcp-allowlists.json +8 -7
  16. package/config/model-candidates.json +71 -6
  17. package/package.json +28 -12
  18. package/public/about.html +162 -0
  19. package/public/chatgpt-app.html +330 -0
  20. package/public/codex-plugin.html +66 -14
  21. package/public/compare.html +2 -2
  22. package/public/dashboard.html +224 -36
  23. package/public/guide.html +2 -2
  24. package/public/index.html +122 -40
  25. package/public/learn.html +70 -0
  26. package/public/lessons.html +129 -6
  27. package/public/numbers.html +2 -2
  28. package/public/pricing.html +28 -23
  29. package/public/pro.html +3 -3
  30. package/scripts/agent-operations-planner.js +621 -0
  31. package/scripts/agent-reward-model.js +53 -1
  32. package/scripts/ai-component-inventory.js +367 -0
  33. package/scripts/classifier-routing.js +130 -0
  34. package/scripts/cli-schema.js +26 -0
  35. package/scripts/commercial-offer.js +10 -2
  36. package/scripts/dashboard-chat.js +199 -51
  37. package/scripts/feedback-sanitizer.js +105 -0
  38. package/scripts/gates-engine.js +301 -67
  39. package/scripts/hybrid-feedback-context.js +141 -7
  40. package/scripts/memory-scope-readiness.js +159 -0
  41. package/scripts/oss-pr-opportunity-scout.js +35 -5
  42. package/scripts/parallel-workflow-orchestrator.js +293 -0
  43. package/scripts/plausible-domain-config.js +86 -0
  44. package/scripts/plausible-server-events.js +4 -2
  45. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  46. package/scripts/qa-scenario-planner.js +136 -0
  47. package/scripts/rate-limiter.js +2 -2
  48. package/scripts/repeat-metric.js +28 -12
  49. package/scripts/secret-fixture-tokens.js +61 -0
  50. package/scripts/secret-scanner.js +44 -5
  51. package/scripts/security-scanner.js +80 -0
  52. package/scripts/seo-gsd.js +113 -0
  53. package/scripts/thumbgate-bench.js +16 -1
  54. package/scripts/tool-registry.js +37 -0
  55. package/scripts/workflow-sentinel.js +282 -54
  56. package/src/api/server.js +466 -60
  57. package/.claude-plugin/marketplace.json +0 -85
@@ -20,6 +20,10 @@ const {
20
20
  recordDecisionEvaluation,
21
21
  recordDecisionOutcome,
22
22
  } = require('./decision-journal');
23
+ const {
24
+ actionFingerprint,
25
+ sanitizeFeedbackText,
26
+ } = require('./feedback-sanitizer');
23
27
 
24
28
  /**
25
29
  * Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
@@ -54,6 +58,8 @@ const {
54
58
  scanHookInput,
55
59
  buildSafeSummary,
56
60
  redactText,
61
+ isSafeSecretStorageWrite,
62
+ SAFE_SECRET_STORAGE_DIRS,
57
63
  } = require('./secret-scanner');
58
64
  const {
59
65
  evaluateSecurityScan,
@@ -109,12 +115,49 @@ const DEFAULT_PROTECTED_FILE_GLOBS = [
109
115
  const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
110
116
  const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish|rm\s+-rf)\b/i;
111
117
  const REMOTE_SIDE_EFFECT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+(?:create|merge|close|reopen|ready|edit)\b|gh\s+release\s+(?:create|delete|edit|upload)\b|npm\s+publish\b|yarn\s+publish\b|pnpm\s+publish\b)\b/i;
112
- const GH_API_PR_CREATE_PATTERN = /\bgh\s+api\b(?=.*(?:\/pulls\b|repos\/[^\s]+\/[^\s]+\/pulls\b))(?=.*(?:-f\b|--field\b|-F\b|--raw-field\b|--method\s+POST\b|-X\s+POST\b))/i;
118
+ const MAX_COMMAND_SCAN_CHARS = 20000;
113
119
  const BOOSTED_RISK_BLOCK_SCORE = 0.8;
114
120
  const BOOSTED_RISK_MIN_EXAMPLES = 3;
115
121
  const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
116
122
  const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
117
123
  const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f|railway\s+(?:deploy|up)\b|gcloud\s+(?:run\s+deploy|app\s+deploy)\b|firebase\s+deploy\b|vercel\s+--prod\b|kubectl\s+(?:apply|delete)\b|terraform\s+(?:apply|destroy)\b)\b/i;
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Enforcement posture (CEO decision 2026-06-04): warn-by-default.
127
+ // The firewall ALWAYS fires and logs every decision, but most gates WARN rather
128
+ // than hard-block — only TRULY CATASTROPHIC, irreversible actions hard-block:
129
+ // - secret exfiltration (handled on its own deny path; never downgraded)
130
+ // - security-vulnerability / supply-chain denies (own deny path; not downgraded)
131
+ // - irreversibly destructive filesystem commands (rm -rf class, mkfs, dd to disk,
132
+ // fork bomb) — kept as hard deny via DESTRUCTIVE_FS_PATTERN below.
133
+ // Everything else (memory-high-risk, workflow-sequence, off-scope, git push, deploy,
134
+ // approval gates) downgrades deny/approve -> warn so legitimate work is never blocked.
135
+ // Opt back into full hard enforcement with THUMBGATE_STRICT_ENFORCEMENT=1.
136
+ // Enforcement posture (CEO decision 2026-06-04): WARN + AUDIT by default.
137
+ // The firewall fires and LOGS every decision, but downgrades deny/approve -> warn so
138
+ // legitimate work is never hard-blocked. We deliberately do NOT try to hard-block
139
+ // arbitrary destructive commands here: a regex "catastrophic floor" is unwinnable
140
+ // (sudo / bash -c / find -exec / eval / base64|sh all evade it) and gives false confidence.
141
+ // HARD enforcement is an explicit opt-in via THUMBGATE_STRICT_ENFORCEMENT=1, which keeps
142
+ // the engine's FULL gate set (its high-risk-command gates catch prefixed/obfuscated forms
143
+ // far better than any single regex). Secret exfiltration and the security-vulnerability
144
+ // scan hard-deny on their OWN paths before this runs, so irreversible data-leak / supply
145
+ // chain risks stay blocked regardless of posture.
146
+ function applyEnforcementPosture(result) {
147
+ if (!result || (result.decision !== 'deny' && result.decision !== 'approve')) return result;
148
+ // Full hard enforcement opt-in: keep every deny.
149
+ if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') return result;
150
+ // Honor the explicit strict-knowledge-conflict opt-in for that gate.
151
+ if (process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1' && result.gate === 'knowledge-conflict-gate') return result;
152
+ // Warn-by-default: the gate still fired and is recorded; the action is allowed through
153
+ // with the warning surfaced instead of hard-blocked, so legitimate work is never blocked.
154
+ return {
155
+ ...result,
156
+ decision: 'warn',
157
+ warnByDefault: true,
158
+ message: `${result.message}\n\n⚠️ ThumbGate is in warn-by-default mode — this was flagged and logged, not blocked. Set THUMBGATE_STRICT_ENFORCEMENT=1 to hard-block, or THUMBGATE_HOTFIX_BYPASS=1 to disable checks entirely.`,
159
+ };
160
+ }
118
161
  const BREAK_GLASS_CONDITION = 'thumbgate_break_glass';
119
162
  const BREAK_GLASS_SETTINGS_GLOBS = [
120
163
  '.claude/settings.local.json',
@@ -131,6 +174,60 @@ function isRuntimePlanGateEnabled() {
131
174
  const PR_THREAD_RESOLUTION_CLAIM_PATTERN = '(?:thread|review|comment).*?(?:resolved|verified|checked|addressed|fixed)|(?:resolved|verified|checked|addressed|fixed).*?(?:thread|review|comment)';
132
175
  const PR_THREAD_RESOLUTION_REQUIRED_ACTIONS = ['pr_threads_checked', 'thread_resolution_verified'];
133
176
 
177
+ function commandScanText(command) {
178
+ return String(command || '').slice(0, MAX_COMMAND_SCAN_CHARS);
179
+ }
180
+
181
+ function commandWords(command) {
182
+ return commandScanText(command).toLowerCase().split(/\s+/).filter(Boolean);
183
+ }
184
+
185
+ function commandContainsSequence(words, sequence) {
186
+ if (!Array.isArray(words) || !Array.isArray(sequence) || sequence.length === 0) return false;
187
+ for (let i = 0; i <= words.length - sequence.length; i += 1) {
188
+ let matched = true;
189
+ for (let j = 0; j < sequence.length; j += 1) {
190
+ if (words[i + j] !== sequence[j]) {
191
+ matched = false;
192
+ break;
193
+ }
194
+ }
195
+ if (matched) return true;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ function commandHasPostMethod(words) {
201
+ for (let i = 0; i < words.length; i += 1) {
202
+ const word = words[i];
203
+ if ((word === '-x' || word === '--method') && words[i + 1] === 'post') return true;
204
+ if (word === '--method=post' || word === '-xpost') return true;
205
+ }
206
+ return false;
207
+ }
208
+
209
+ function isGhApiPrCreateCommand(command) {
210
+ const words = commandWords(command);
211
+ if (!commandContainsSequence(words, ['gh', 'api'])) return false;
212
+ const hasPullsEndpoint = words.some((word) => word === '/pulls' || word.endsWith('/pulls'));
213
+ if (!hasPullsEndpoint) return false;
214
+ const fieldFlags = new Set(['-f', '--field', '--raw-field']);
215
+ const hasFieldWrite = words.some((word) => (
216
+ fieldFlags.has(word) ||
217
+ word.startsWith('-f=') ||
218
+ word.startsWith('--field=') ||
219
+ word.startsWith('--raw-field=')
220
+ ));
221
+ return hasFieldWrite || commandHasPostMethod(words);
222
+ }
223
+
224
+ function isRecursiveChmodCommand(command) {
225
+ const words = commandWords(command);
226
+ const chmodIndex = words.indexOf('chmod');
227
+ if (chmodIndex === -1) return false;
228
+ return words.slice(chmodIndex + 1).includes('-r') || words.slice(chmodIndex + 1).some((word) => word.includes('r') && word.startsWith('-'));
229
+ }
230
+
134
231
  // ---------------------------------------------------------------------------
135
232
  // Config loading
136
233
  // ---------------------------------------------------------------------------
@@ -227,6 +324,27 @@ function sanitizeGlobList(globs) {
227
324
  return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
228
325
  }
229
326
 
327
+ // Affected files are compared as repo-relative paths (git --name-only output and
328
+ // inline paths are relative to the repo root). A caller who passes an ABSOLUTE
329
+ // allowedPath (e.g. "/Users/me/proj/src/**") therefore declares a glob that can never
330
+ // match — a silent no-op scope. When repoPath is known, rebase absolute globs that
331
+ // live under it to the repo-relative form so the scope actually applies. Globs already
332
+ // relative, or absolute but outside repoPath, are returned unchanged.
333
+ function rebaseGlobsToRepoRoot(globs, repoPath) {
334
+ // normalizeGlob strips both leading AND trailing slashes, so a repoPath with a
335
+ // trailing slash ("/Users/me/proj/") still matches the repo-relative globs.
336
+ const repoRel = normalizeGlob(repoPath);
337
+ if (!repoRel) return globs;
338
+ return globs.map((glob) => {
339
+ if (glob === repoRel) return '**';
340
+ if (glob.startsWith(`${repoRel}/`)) {
341
+ const rebased = glob.slice(repoRel.length + 1);
342
+ return rebased || '**';
343
+ }
344
+ return glob;
345
+ });
346
+ }
347
+
230
348
  function globToRegExp(glob) {
231
349
  const normalized = normalizeGlob(glob);
232
350
  let pattern = '^';
@@ -280,6 +398,9 @@ function loadGovernanceState() {
280
398
  branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
281
399
  ? raw.branchGovernance
282
400
  : null,
401
+ workflowContract: raw && raw.workflowContract && typeof raw.workflowContract === 'object'
402
+ ? raw.workflowContract
403
+ : null,
283
404
  };
284
405
  const now = Date.now();
285
406
  const activeApprovals = state.protectedApprovals.filter((entry) => {
@@ -299,6 +420,7 @@ function saveGovernanceState(state) {
299
420
  taskScope: state && state.taskScope ? state.taskScope : null,
300
421
  protectedApprovals: Array.isArray(state && state.protectedApprovals) ? state.protectedApprovals : [],
301
422
  branchGovernance: state && state.branchGovernance ? state.branchGovernance : null,
423
+ workflowContract: state && state.workflowContract ? state.workflowContract : null,
302
424
  };
303
425
  saveJSON(module.exports.GOVERNANCE_STATE_PATH, next);
304
426
  }
@@ -310,33 +432,39 @@ function setTaskScope(scopeInput = {}) {
310
432
  taskScope: null,
311
433
  protectedApprovals: currentState.protectedApprovals,
312
434
  branchGovernance: currentState.branchGovernance,
435
+ workflowContract: null,
313
436
  };
314
437
  saveGovernanceState(cleared);
438
+ refreshLocalOnlyConstraint(cleared);
315
439
  return null;
316
440
  }
317
441
 
318
- const allowedPaths = sanitizeGlobList(scopeInput.allowedPaths);
442
+ const repoPath = String(scopeInput.repoPath || '').trim() || null;
443
+ const allowedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(scopeInput.allowedPaths), repoPath);
319
444
  if (allowedPaths.length === 0) {
320
445
  throw new Error('allowedPaths must be a non-empty array');
321
446
  }
322
447
 
323
- const protectedPaths = sanitizeGlobList(
448
+ const protectedPaths = rebaseGlobsToRepoRoot(sanitizeGlobList(
324
449
  Array.isArray(scopeInput.protectedPaths) && scopeInput.protectedPaths.length > 0
325
450
  ? scopeInput.protectedPaths
326
451
  : DEFAULT_PROTECTED_FILE_GLOBS
327
- );
452
+ ), repoPath);
328
453
  const taskScope = {
329
454
  taskId: String(scopeInput.taskId || '').trim() || null,
330
455
  summary: String(scopeInput.summary || '').trim() || null,
331
456
  allowedPaths,
332
457
  protectedPaths,
333
458
  localOnly: scopeInput.localOnly === true,
334
- repoPath: String(scopeInput.repoPath || '').trim() || null,
459
+ repoPath,
335
460
  createdAt: new Date().toISOString(),
336
461
  timestamp: Date.now(),
337
462
  };
338
463
  const state = loadGovernanceState();
339
464
  state.taskScope = taskScope;
465
+ state.workflowContract = scopeInput.workflowContract && typeof scopeInput.workflowContract === 'object'
466
+ ? scopeInput.workflowContract
467
+ : null;
340
468
  saveGovernanceState(state);
341
469
  if (taskScope.localOnly) {
342
470
  setConstraint('local_only', true);
@@ -409,6 +537,7 @@ function setBranchGovernance(input = {}) {
409
537
  const state = loadGovernanceState();
410
538
  state.branchGovernance = null;
411
539
  saveGovernanceState(state);
540
+ refreshLocalOnlyConstraint(state);
412
541
  return null;
413
542
  }
414
543
 
@@ -459,6 +588,24 @@ function setConstraint(key, value) {
459
588
  return constraints[key];
460
589
  }
461
590
 
591
+ function clearConstraint(key) {
592
+ const constraints = loadConstraints();
593
+ delete constraints[key];
594
+ saveConstraints(constraints);
595
+ }
596
+
597
+ function refreshLocalOnlyConstraint(governanceState = loadGovernanceState()) {
598
+ const localOnlyActive = Boolean(
599
+ (governanceState.taskScope && governanceState.taskScope.localOnly) ||
600
+ (governanceState.branchGovernance && governanceState.branchGovernance.localOnly)
601
+ );
602
+ if (localOnlyActive) {
603
+ setConstraint('local_only', true);
604
+ } else {
605
+ clearConstraint('local_only');
606
+ }
607
+ }
608
+
462
609
  function isConditionSatisfied(conditionId) {
463
610
  const state = loadState();
464
611
  const entry = state[conditionId];
@@ -498,7 +645,29 @@ function loadStats() {
498
645
 
499
646
  function saveStats(stats) { saveJSON(module.exports.STATS_PATH, stats); }
500
647
 
501
- function recordStat(gateId, action, gate) {
648
+ function buildGateActionFingerprint(gateId, options = {}) {
649
+ if (options.actionFingerprint) return String(options.actionFingerprint);
650
+ const toolName = options.toolName || options.tool_name || '';
651
+ const toolInput = options.toolInput || options.tool_input || {};
652
+ const parts = [toolName];
653
+ if (typeof toolInput === 'string') {
654
+ parts.push(toolInput);
655
+ } else if (toolInput && typeof toolInput === 'object') {
656
+ parts.push(
657
+ toolInput.command || '',
658
+ toolInput.cmd || '',
659
+ toolInput.file_path || '',
660
+ toolInput.path || '',
661
+ toolInput.description || '',
662
+ toolInput.prompt || '',
663
+ toolInput.pattern || '',
664
+ );
665
+ if (Array.isArray(toolInput.affectedFiles)) parts.push(...toolInput.affectedFiles);
666
+ }
667
+ return actionFingerprint(parts);
668
+ }
669
+
670
+ function recordStat(gateId, action, gate, options = {}) {
502
671
  const stats = loadStats();
503
672
  if (action === 'block') stats.blocked = (stats.blocked || 0) + 1;
504
673
  else if (action === 'warn') stats.warned = (stats.warned || 0) + 1;
@@ -512,16 +681,25 @@ function recordStat(gateId, action, gate) {
512
681
  else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
513
682
  else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
514
683
 
515
- // Track per-gate recurrence within a session for first-time fix rate
684
+ // Track same-action recurrence within a session for first-time fix rate.
685
+ // Gate-only recurrence over-counts noisy gates; repeats require a stable,
686
+ // sanitized action fingerprint.
516
687
  if (action === 'block' || action === 'warn') {
517
688
  if (!stats.sessionFiredGates) stats.sessionFiredGates = {};
689
+ if (!stats.sessionFiredActions) stats.sessionFiredActions = {};
518
690
  const sessionKey = `session_${Math.floor(Date.now() / SESSION_ACTION_TTL_MS)}`;
519
691
  if (!stats.sessionFiredGates[sessionKey]) stats.sessionFiredGates[sessionKey] = {};
520
- if (stats.sessionFiredGates[sessionKey][gateId]) {
521
- // Same gate fired again in this session — it's a recurring block
522
- stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
523
- } else {
524
- stats.sessionFiredGates[sessionKey][gateId] = true;
692
+ stats.sessionFiredGates[sessionKey][gateId] = true;
693
+
694
+ const fingerprint = buildGateActionFingerprint(gateId, options);
695
+ if (fingerprint) {
696
+ if (!stats.sessionFiredActions[sessionKey]) stats.sessionFiredActions[sessionKey] = {};
697
+ if (!stats.sessionFiredActions[sessionKey][gateId]) stats.sessionFiredActions[sessionKey][gateId] = {};
698
+ if (stats.sessionFiredActions[sessionKey][gateId][fingerprint]) {
699
+ stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
700
+ } else {
701
+ stats.sessionFiredActions[sessionKey][gateId][fingerprint] = true;
702
+ }
525
703
  }
526
704
  }
527
705
 
@@ -737,7 +915,7 @@ function extractAffectedFiles(toolName, toolInput = {}) {
737
915
  }
738
916
  }
739
917
 
740
- if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command) || GH_API_PR_CREATE_PATTERN.test(command)) {
918
+ if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command) || isGhApiPrCreateCommand(command)) {
741
919
  for (const filePath of getBranchDiffFiles(repoRoot)) {
742
920
  files.add(normalizePosix(filePath));
743
921
  }
@@ -756,7 +934,7 @@ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
756
934
  const command = String(toolInput.command || '');
757
935
  // Original high-risk pattern (git writes, publishes, destructive ops)
758
936
  if (HIGH_RISK_BASH_PATTERN.test(command)) return true;
759
- if (GH_API_PR_CREATE_PATTERN.test(command)) return true;
937
+ if (isGhApiPrCreateCommand(command)) return true;
760
938
  // Broadened: any Bash command that modifies files or has side effects.
761
939
  // Excludes pure read/analysis commands (node --test, cat, ls, echo, etc.)
762
940
  // to avoid false positives on benign operations.
@@ -995,7 +1173,7 @@ function getLocalOnlyScopeSources(governanceState = {}, constraints = {}) {
995
1173
  function isRemoteSideEffectCommand(toolName, toolInput = {}) {
996
1174
  if (toolName !== 'Bash') return false;
997
1175
  const command = String(toolInput.command || '');
998
- return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) || GH_API_PR_CREATE_PATTERN.test(command);
1176
+ return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) || isGhApiPrCreateCommand(command);
999
1177
  }
1000
1178
 
1001
1179
  function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governanceState = {}, constraints = {}) {
@@ -1018,7 +1196,7 @@ function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governa
1018
1196
  }
1019
1197
 
1020
1198
  function recordStructuralGateBlock(toolName, toolInput, result) {
1021
- recordStat(result.gate, 'block');
1199
+ recordStat(result.gate, 'block', null, { toolName, toolInput });
1022
1200
  const auditRecord = recordAuditEvent({
1023
1201
  toolName,
1024
1202
  toolInput,
@@ -1379,7 +1557,7 @@ function matchesGate(gate, toolName, toolInput) {
1379
1557
  function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
1380
1558
  if (toolName !== 'Bash') return false;
1381
1559
  const command = String(toolInput.command || '').trim();
1382
- if (!command || /(?:^|\s)chmod\s+[^&;|]*\s+-R\b/i.test(command)) return false;
1560
+ if (!command || isRecursiveChmodCommand(command)) return false;
1383
1561
  if (/[;&|`$()<>*?[\]{}]/.test(command)) return false;
1384
1562
 
1385
1563
  const match = command.match(/(?:^|\s)chmod\s+(?:-[fv]\s+)?0?([46]00)\s+(['"]?)(\S+)\2\s*$/i);
@@ -1387,7 +1565,7 @@ function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
1387
1565
 
1388
1566
  const target = match[3];
1389
1567
  if (!target || target === '/' || target === '~') return false;
1390
- if (/^\.\.?(?:\/|$)/.test(target)) return false;
1568
+ if (target.includes('..')) return false;
1391
1569
 
1392
1570
  const normalized = target.replace(/^['"]|['"]$/g, '').toLowerCase();
1393
1571
  const looksLikeCredentialPath = /(?:^|\/)(?:\.config|\.ssh|\.gnupg|\.aws|\.gcloud|\.gemini|\.resume_secrets|\.thumbgate|secrets?|credentials?)(?:\/|$)/.test(normalized)
@@ -1400,6 +1578,16 @@ function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
1400
1578
  function evaluateMemoryGuard(toolName, toolInput = {}) {
1401
1579
  const affected = extractAffectedFiles(toolName, toolInput);
1402
1580
  const affectedFiles = affected.files;
1581
+ if (isSafeSecretStorageWrite(toolName, toolInput, process.cwd())) {
1582
+ return null;
1583
+ }
1584
+ // Hardening a credential file's permissions (chmod 600 on a key/secret path) is
1585
+ // a safety action, not a risk. The same exemption already guards the
1586
+ // permission-change-approval gate; without it here, `chmod 600 ~/.resume_secrets/key`
1587
+ // gets hard-denied by recurring-negative-memory matching — the opposite of intent.
1588
+ if (isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
1589
+ return null;
1590
+ }
1403
1591
  if (!isHighRiskAction(toolName, toolInput, affectedFiles)) {
1404
1592
  return null;
1405
1593
  }
@@ -1414,7 +1602,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1414
1602
 
1415
1603
  const command = String(toolInput.command || '');
1416
1604
  const isPrCreateCommand = toolName === 'Bash' && (
1417
- /\bgh\s+pr\s+create\b/i.test(command) || GH_API_PR_CREATE_PATTERN.test(command)
1605
+ /\bgh\s+pr\s+create\b/i.test(command) || isGhApiPrCreateCommand(command)
1418
1606
  );
1419
1607
  if (isPrCreateCommand && isConditionSatisfied('pr_create_allowed')) {
1420
1608
  const branchGovernanceViolation = buildBranchGovernanceViolation(
@@ -1431,7 +1619,7 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1431
1619
 
1432
1620
  if (toolName === 'Bash' && (
1433
1621
  /\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command) ||
1434
- GH_API_PR_CREATE_PATTERN.test(command)
1622
+ isGhApiPrCreateCommand(command)
1435
1623
  )) {
1436
1624
  const branchGovernanceViolation = buildBranchGovernanceViolation(
1437
1625
  governanceState,
@@ -1470,7 +1658,14 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1470
1658
  filePath: toolInput.file_path || toolInput.path || null,
1471
1659
  affectedFiles,
1472
1660
  });
1473
- const guard = hybrid.evaluatePretool(toolName, serializedInput);
1661
+ // Claw/hybrid support: pass context if agent provides claw metadata (for EnterpriseClaw/OpenShell/Perplexity hybrid agents)
1662
+ let guard;
1663
+ if (toolInput && (toolInput.clawContext || toolInput._claw || toolInput.hybridRoute || toolInput.agentId)) {
1664
+ const clawCtx = toolInput.clawContext || toolInput._claw || { actionType: toolInput.actionType || 'unknown', agentId: toolInput.agentId || 'unknown', hybridRoute: toolInput.hybridRoute || 'unknown' };
1665
+ guard = hybrid.evaluateClawPretool ? hybrid.evaluateClawPretool(toolName, serializedInput, clawCtx) : hybrid.evaluatePretool(toolName, serializedInput);
1666
+ } else {
1667
+ guard = hybrid.evaluatePretool(toolName, serializedInput);
1668
+ }
1474
1669
  if (!guard || guard.mode === 'allow') {
1475
1670
  return null;
1476
1671
  }
@@ -1623,7 +1818,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1623
1818
 
1624
1819
  const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
1625
1820
  if (pendingThreadResolutionGate) {
1626
- recordStat(pendingThreadResolutionGate.gate, 'block');
1821
+ recordStat(pendingThreadResolutionGate.gate, 'block', null, { toolName, toolInput });
1627
1822
  const auditRecord = recordAuditEvent({
1628
1823
  toolName,
1629
1824
  toolInput,
@@ -1639,7 +1834,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1639
1834
 
1640
1835
  const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
1641
1836
  if (boostedRiskGuard) {
1642
- recordStat(boostedRiskGuard.gate, 'block');
1837
+ recordStat(boostedRiskGuard.gate, 'block', null, { toolName, toolInput });
1643
1838
  const auditRecord = recordAuditEvent({
1644
1839
  toolName,
1645
1840
  toolInput,
@@ -1659,13 +1854,13 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1659
1854
  if (isRuntimePlanGateEnabled()) {
1660
1855
  const planGate = evaluatePlanGate(toolName, toolInput);
1661
1856
  if (planGate) {
1662
- recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn');
1857
+ recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
1663
1858
  return planGate;
1664
1859
  }
1665
1860
 
1666
1861
  const trajectory = getTrajectoryScore();
1667
1862
  if (trajectory.isDrifting) {
1668
- recordStat('strategic-drift', 'block');
1863
+ recordStat('strategic-drift', 'block', null, { toolName, toolInput });
1669
1864
  return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
1670
1865
  }
1671
1866
  }
@@ -1719,12 +1914,12 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1719
1914
  // Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
1720
1915
  const cappedResult = applyDailyBlockCap(denyResult);
1721
1916
  if (cappedResult) {
1722
- recordStat(gate.id, 'warn', gate);
1917
+ recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1723
1918
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
1724
1919
  auditToFeedback(auditRecord);
1725
1920
  return cappedResult;
1726
1921
  }
1727
- recordStat(gate.id, 'block', gate);
1922
+ recordStat(gate.id, 'block', gate, { toolName, toolInput });
1728
1923
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1729
1924
  auditToFeedback(auditRecord);
1730
1925
  return denyResult;
@@ -1733,13 +1928,13 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1733
1928
  if (gate.action === 'approve') {
1734
1929
  const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
1735
1930
  if (approvalEnabled) {
1736
- recordStat(gate.id, 'approve', gate);
1931
+ recordStat(gate.id, 'approve', gate, { toolName, toolInput });
1737
1932
  const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
1738
1933
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1739
1934
  auditToFeedback(auditRecord);
1740
1935
  return result;
1741
1936
  }
1742
- recordStat(gate.id, 'warn', gate);
1937
+ recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1743
1938
  const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
1744
1939
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1745
1940
  auditToFeedback(auditRecord);
@@ -1747,7 +1942,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1747
1942
  }
1748
1943
 
1749
1944
  if (gate.action === 'log') {
1750
- recordStat(gate.id, 'log', gate);
1945
+ recordStat(gate.id, 'log', gate, { toolName, toolInput });
1751
1946
  const result = { decision: 'log', gate: gate.id, message, severity: gate.severity, reasoning, logged: true };
1752
1947
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1753
1948
  auditToFeedback(auditRecord);
@@ -1756,7 +1951,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1756
1951
  }
1757
1952
 
1758
1953
  if (gate.action === 'warn') {
1759
- recordStat(gate.id, 'warn', gate);
1954
+ recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1760
1955
  const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
1761
1956
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1762
1957
  auditToFeedback(auditRecord);
@@ -1764,14 +1959,15 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1764
1959
  }
1765
1960
  }
1766
1961
 
1767
- const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
1962
+ const skipAdvisoryGuards = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
1963
+ const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
1768
1964
  governanceState,
1769
1965
  });
1770
1966
  const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
1771
1967
  const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
1772
1968
  if (memoryGuard) {
1773
1969
  const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
1774
- recordStat(enrichedMemoryGuard.gate, 'block');
1970
+ recordStat(enrichedMemoryGuard.gate, 'block', null, { toolName, toolInput });
1775
1971
  recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
1776
1972
  const auditRecord = recordAuditEvent({
1777
1973
  toolName,
@@ -1788,7 +1984,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1788
1984
 
1789
1985
  if (sentinelReport && sentinelReport.decision !== 'allow') {
1790
1986
  const sentinelResult = buildSentinelGateResult(sentinelReport);
1791
- recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
1987
+ recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
1792
1988
  recordSentinelBlockDecision(sentinelDecision, sentinelResult);
1793
1989
  const auditRecord = recordAuditEvent({
1794
1990
  toolName,
@@ -1849,7 +2045,7 @@ function evaluateGates(toolName, toolInput, configPath) {
1849
2045
 
1850
2046
  const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
1851
2047
  if (pendingThreadResolutionGate) {
1852
- recordStat(pendingThreadResolutionGate.gate, 'block');
2048
+ recordStat(pendingThreadResolutionGate.gate, 'block', null, { toolName, toolInput });
1853
2049
  const auditRecord = recordAuditEvent({
1854
2050
  toolName,
1855
2051
  toolInput,
@@ -1865,7 +2061,7 @@ function evaluateGates(toolName, toolInput, configPath) {
1865
2061
 
1866
2062
  const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
1867
2063
  if (boostedRiskGuard) {
1868
- recordStat(boostedRiskGuard.gate, 'block');
2064
+ recordStat(boostedRiskGuard.gate, 'block', null, { toolName, toolInput });
1869
2065
  const auditRecord = recordAuditEvent({
1870
2066
  toolName,
1871
2067
  toolInput,
@@ -1885,13 +2081,13 @@ function evaluateGates(toolName, toolInput, configPath) {
1885
2081
  if (isRuntimePlanGateEnabled()) {
1886
2082
  const planGate = evaluatePlanGate(toolName, toolInput);
1887
2083
  if (planGate) {
1888
- recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn');
2084
+ recordStat(planGate.gate, planGate.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
1889
2085
  return planGate;
1890
2086
  }
1891
2087
 
1892
2088
  const trajectory = getTrajectoryScore();
1893
2089
  if (trajectory.isDrifting) {
1894
- recordStat('strategic-drift', 'block');
2090
+ recordStat('strategic-drift', 'block', null, { toolName, toolInput });
1895
2091
  return { decision: 'deny', gate: 'strategic-drift', message: trajectory.message, severity: 'high' };
1896
2092
  }
1897
2093
  }
@@ -1918,12 +2114,12 @@ function evaluateGates(toolName, toolInput, configPath) {
1918
2114
  // Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
1919
2115
  const cappedResult = applyDailyBlockCap(denyResult);
1920
2116
  if (cappedResult) {
1921
- recordStat(gate.id, 'warn', gate);
2117
+ recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1922
2118
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
1923
2119
  auditToFeedback(auditRecord);
1924
2120
  return cappedResult;
1925
2121
  }
1926
- recordStat(gate.id, 'block', gate);
2122
+ recordStat(gate.id, 'block', gate, { toolName, toolInput });
1927
2123
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1928
2124
  auditToFeedback(auditRecord);
1929
2125
  return denyResult;
@@ -1932,13 +2128,13 @@ function evaluateGates(toolName, toolInput, configPath) {
1932
2128
  if (gate.action === 'approve') {
1933
2129
  const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
1934
2130
  if (approvalEnabled) {
1935
- recordStat(gate.id, 'approve', gate);
2131
+ recordStat(gate.id, 'approve', gate, { toolName, toolInput });
1936
2132
  const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
1937
2133
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1938
2134
  auditToFeedback(auditRecord);
1939
2135
  return result;
1940
2136
  }
1941
- recordStat(gate.id, 'warn', gate);
2137
+ recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1942
2138
  const result = { decision: 'warn', gate: gate.id, message: `[approval gate disabled] ${message}`, severity: gate.severity, reasoning };
1943
2139
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1944
2140
  auditToFeedback(auditRecord);
@@ -1946,7 +2142,7 @@ function evaluateGates(toolName, toolInput, configPath) {
1946
2142
  }
1947
2143
 
1948
2144
  if (gate.action === 'log') {
1949
- recordStat(gate.id, 'log', gate);
2145
+ recordStat(gate.id, 'log', gate, { toolName, toolInput });
1950
2146
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1951
2147
  auditToFeedback(auditRecord);
1952
2148
  // 'log' action allows the tool call to proceed — continue to next gate
@@ -1954,7 +2150,7 @@ function evaluateGates(toolName, toolInput, configPath) {
1954
2150
  }
1955
2151
 
1956
2152
  if (gate.action === 'warn') {
1957
- recordStat(gate.id, 'warn', gate);
2153
+ recordStat(gate.id, 'warn', gate, { toolName, toolInput });
1958
2154
  const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
1959
2155
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1960
2156
  auditToFeedback(auditRecord);
@@ -1962,14 +2158,15 @@ function evaluateGates(toolName, toolInput, configPath) {
1962
2158
  }
1963
2159
  }
1964
2160
 
1965
- const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
2161
+ const skipAdvisoryGuards = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2162
+ const sentinelReport = skipAdvisoryGuards ? null : evaluateWorkflowSentinel(toolName, toolInput, {
1966
2163
  governanceState,
1967
2164
  });
1968
2165
  const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
1969
2166
  const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
1970
2167
  if (memoryGuard) {
1971
2168
  const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
1972
- recordStat(enrichedMemoryGuard.gate, 'block');
2169
+ recordStat(enrichedMemoryGuard.gate, 'block', null, { toolName, toolInput });
1973
2170
  recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
1974
2171
  const auditRecord = recordAuditEvent({
1975
2172
  toolName,
@@ -1986,7 +2183,7 @@ function evaluateGates(toolName, toolInput, configPath) {
1986
2183
 
1987
2184
  if (sentinelReport && sentinelReport.decision !== 'allow') {
1988
2185
  const sentinelResult = buildSentinelGateResult(sentinelReport);
1989
- recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
2186
+ recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn', null, { toolName, toolInput });
1990
2187
  recordSentinelBlockDecision(sentinelDecision, sentinelResult);
1991
2188
  const auditRecord = recordAuditEvent({
1992
2189
  toolName,
@@ -2006,14 +2203,43 @@ function evaluateGates(toolName, toolInput, configPath) {
2006
2203
  return null;
2007
2204
  }
2008
2205
 
2009
- function buildSecretGuardResult(scanResult) {
2206
+ // Turn a secret-exfiltration block into actionable guidance that names the
2207
+ // safe path, instead of a dead-end that drives agents toward brittle
2208
+ // workarounds (e.g. writing secrets to /tmp). The vault dirs referenced here
2209
+ // are the SAME constant the scanner whitelists, so the hint can never drift
2210
+ // from enforcement.
2211
+ function buildSecretRemediation(toolName = '', toolInput = {}) {
2212
+ const vaultDirs = (SAFE_SECRET_STORAGE_DIRS || []).map((dir) => `~/${dir}`);
2213
+ const primaryVault = vaultDirs[0] || '~/.resume_secrets';
2214
+ const vaultList = vaultDirs.join(', ') || primaryVault;
2215
+
2216
+ if (EDIT_LIKE_TOOLS && EDIT_LIKE_TOOLS.has(toolName)) {
2217
+ const target = toolInput.file_path || toolInput.path || toolInput.filePath || toolInput.target_path;
2218
+ const where = target ? ` (you targeted ${redactText(String(target))})` : '';
2219
+ return `To store this secret safely, write it with the Write/Edit tool to a file under ${vaultList}${where}. `
2220
+ + `Those locations are whitelisted for secret storage and will NOT be blocked. `
2221
+ + `Do not route around this by writing to /tmp or another path — that leaves the secret in a world-readable location and does not make it safe.`;
2222
+ }
2223
+
2224
+ if (toolName === 'Bash') {
2225
+ return `Do not inline a live secret literal into a shell command — it leaks into shell history and process args. `
2226
+ + `Instead, store the value with the Write tool to a file under ${vaultList}, then reference it via an environment variable or by reading that file at runtime.`;
2227
+ }
2228
+
2229
+ return `Store secrets in the whitelisted vault (${vaultList}) using the Write tool rather than passing the literal through this action.`;
2230
+ }
2231
+
2232
+ function buildSecretGuardResult(scanResult, context = {}) {
2233
+ const remediation = buildSecretRemediation(context.toolName, context.toolInput || {});
2234
+ const summary = buildSafeSummary(
2235
+ scanResult.findings,
2236
+ 'Blocked because the action appears to expose secret material'
2237
+ );
2010
2238
  return {
2011
2239
  decision: 'deny',
2012
2240
  gate: 'secret-exfiltration',
2013
- message: buildSafeSummary(
2014
- scanResult.findings,
2015
- 'Blocked because the action appears to expose secret material'
2016
- ),
2241
+ message: `${summary}. ${remediation}`,
2242
+ remediation,
2017
2243
  severity: 'critical',
2018
2244
  secretScan: {
2019
2245
  provider: scanResult.provider,
@@ -2092,9 +2318,15 @@ function evaluateSecretGuard(input = {}) {
2092
2318
  if (!scanResult.detected) {
2093
2319
  return null;
2094
2320
  }
2095
- recordStat('secret-exfiltration', 'block');
2321
+ recordStat('secret-exfiltration', 'block', null, {
2322
+ toolName: input.tool_name || input.toolName || 'unknown',
2323
+ toolInput: input.tool_input || {},
2324
+ });
2096
2325
  recordSecretViolation(input, scanResult);
2097
- const result = buildSecretGuardResult(scanResult);
2326
+ const result = buildSecretGuardResult(scanResult, {
2327
+ toolName: input.tool_name || input.toolName,
2328
+ toolInput: input.tool_input || {},
2329
+ });
2098
2330
  // Audit trail: record secret guard denial
2099
2331
  const auditRecord = recordAuditEvent({
2100
2332
  toolName: input.tool_name || input.toolName || 'unknown',
@@ -2422,7 +2654,7 @@ function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
2422
2654
  const message = `Knowledge conflict warning: retrieved lessons disagree for this action (entropy ${entropy}). Treat the reminders below as cautionary context, but do not stop unrelated work solely because memory is noisy.`;
2423
2655
 
2424
2656
  if (isKnowledgeConflictHardBlockAction(toolName, toolInput)) {
2425
- recordStat('retrieval_entropy_high', 'block');
2657
+ recordStat('retrieval_entropy_high', 'block', null, { toolName, toolInput });
2426
2658
  return {
2427
2659
  decision: 'deny',
2428
2660
  gate: 'knowledge-conflict-gate',
@@ -2431,7 +2663,7 @@ function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
2431
2663
  };
2432
2664
  }
2433
2665
 
2434
- recordStat('retrieval_entropy_high', 'warn');
2666
+ recordStat('retrieval_entropy_high', 'warn', null, { toolName, toolInput });
2435
2667
  return mergeContextStrings(`[ThumbGate] ${message}`, lessonContext);
2436
2668
  }
2437
2669
 
@@ -2443,7 +2675,7 @@ function extractActionContext(toolName, toolInput) {
2443
2675
  if (toolInput.description) parts.push(String(toolInput.description).slice(0, 200));
2444
2676
  if (toolInput.prompt) parts.push(String(toolInput.prompt).slice(0, 400));
2445
2677
  if (toolInput.pattern) parts.push(String(toolInput.pattern).slice(0, 200));
2446
- return parts.filter(Boolean).join(' ');
2678
+ return sanitizeFeedbackText(parts.filter(Boolean).join(' ')) || toolName;
2447
2679
  }
2448
2680
 
2449
2681
  function extractAvoidanceAdvice(content) {
@@ -2472,10 +2704,11 @@ async function runAsync(input) {
2472
2704
 
2473
2705
  const toolName = input.tool_name || '';
2474
2706
  const toolInput = input.tool_input || {};
2707
+ const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2475
2708
 
2476
2709
  const sequenceGuard = evaluateSequenceState(toolName, toolInput);
2477
2710
  if (sequenceGuard && sequenceGuard.decision === 'deny') {
2478
- return formatOutput(sequenceGuard);
2711
+ return formatOutput(applyEnforcementPosture(sequenceGuard));
2479
2712
  }
2480
2713
 
2481
2714
  const result = await evaluateGatesAsync(toolName, toolInput);
@@ -2491,16 +2724,16 @@ async function runAsync(input) {
2491
2724
  }
2492
2725
 
2493
2726
 
2494
- const behavioralContext = buildBehavioralContext();
2495
- const lessonContext = await buildRelevantLessonContextAsync(toolName, toolInput);
2727
+ const behavioralContext = safeSecretStorageWrite ? null : buildBehavioralContext();
2728
+ const lessonContext = safeSecretStorageWrite ? null : await buildRelevantLessonContextAsync(toolName, toolInput);
2496
2729
 
2497
2730
  if (lessonContext && lessonContext.decision === "deny") {
2498
- return formatOutput(lessonContext);
2731
+ return formatOutput(applyEnforcementPosture(lessonContext));
2499
2732
  }
2500
2733
 
2501
2734
  const recentContext = buildRecentCorrectiveActionsContext();
2502
2735
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
2503
- return formatOutput(result, combinedContext);
2736
+ return formatOutput(applyEnforcementPosture(result), combinedContext);
2504
2737
 
2505
2738
  }
2506
2739
 
@@ -2518,10 +2751,11 @@ function run(input) {
2518
2751
 
2519
2752
  const toolName = input.tool_name || '';
2520
2753
  const toolInput = input.tool_input || {};
2754
+ const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, process.cwd());
2521
2755
 
2522
2756
  const sequenceGuard = evaluateSequenceState(toolName, toolInput);
2523
2757
  if (sequenceGuard && sequenceGuard.decision === 'deny') {
2524
- return formatOutput(sequenceGuard);
2758
+ return formatOutput(applyEnforcementPosture(sequenceGuard));
2525
2759
  }
2526
2760
 
2527
2761
  const result = evaluateGates(toolName, toolInput);
@@ -2537,16 +2771,16 @@ function run(input) {
2537
2771
  }
2538
2772
 
2539
2773
 
2540
- const behavioralContext = buildBehavioralContext();
2541
- const lessonContext = buildRelevantLessonContext(toolName, toolInput);
2774
+ const behavioralContext = safeSecretStorageWrite ? null : buildBehavioralContext();
2775
+ const lessonContext = safeSecretStorageWrite ? null : buildRelevantLessonContext(toolName, toolInput);
2542
2776
 
2543
2777
  if (lessonContext && lessonContext.decision === "deny") {
2544
- return formatOutput(lessonContext);
2778
+ return formatOutput(applyEnforcementPosture(lessonContext));
2545
2779
  }
2546
2780
 
2547
2781
  const recentContext = buildRecentCorrectiveActionsContext();
2548
2782
  const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
2549
- return formatOutput(result, combinedContext);
2783
+ return formatOutput(applyEnforcementPosture(result), combinedContext);
2550
2784
 
2551
2785
  }
2552
2786