@synkro-sh/cli 1.4.67 → 1.4.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bootstrap.js CHANGED
@@ -286,6 +286,15 @@ var init_ccHookConfig = __esm({
286
286
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
287
287
  import { dirname as dirname2, resolve, normalize } from "path";
288
288
  import { homedir as homedir2 } from "os";
289
+ function shellQuote(s) {
290
+ return "'" + s.replace(/'/g, "'\\''") + "'";
291
+ }
292
+ function cursorCcCmd(scriptPath) {
293
+ return "env SYNKRO_HOOK_FORMAT=cursor bun run " + shellQuote(scriptPath);
294
+ }
295
+ function bunRunCmd(scriptPath) {
296
+ return "bun run " + shellQuote(scriptPath);
297
+ }
289
298
  function validateHooksPath(path) {
290
299
  const resolved = resolve(normalize(path));
291
300
  if (!ALLOWED_PARENT_DIRS.some((dir) => resolved.startsWith(dir + "/") || resolved === dir)) {
@@ -320,6 +329,16 @@ function removeSynkroEntries2(hooks, event) {
320
329
  if (!Array.isArray(arr)) return;
321
330
  hooks[event] = arr.filter((entry) => !isSynkroEntry2(entry));
322
331
  }
332
+ function pushCcHook(hooks, event, scriptPath, opts) {
333
+ hooks[event] = hooks[event] ?? [];
334
+ hooks[event].push({
335
+ command: cursorCcCmd(scriptPath),
336
+ timeout: opts.timeout,
337
+ failClosed: opts.failClosed ?? false,
338
+ ...opts.matcher ? { matcher: opts.matcher } : {},
339
+ [SYNKRO_MARKER2]: true
340
+ });
341
+ }
323
342
  function installCursorHooks(hooksJsonPath, config) {
324
343
  const file = readHooksFile(hooksJsonPath);
325
344
  file.version = file.version ?? 1;
@@ -327,37 +346,58 @@ function installCursorHooks(hooksJsonPath, config) {
327
346
  for (const evt of ALL_EVENTS) {
328
347
  removeSynkroEntries2(file.hooks, evt);
329
348
  }
330
- file.hooks.sessionStart = file.hooks.sessionStart ?? [];
331
- file.hooks.sessionStart.push({
332
- command: config.sessionStartScriptPath,
333
- timeout: 5,
349
+ const h = file.hooks;
350
+ pushCcHook(h, "sessionStart", config.sessionStartScriptPath, { timeout: 5 });
351
+ pushCcHook(h, "sessionEnd", config.stopSummaryScriptPath, { timeout: 10 });
352
+ pushCcHook(h, "beforeSubmitPrompt", config.userPromptSubmitScriptPath, { timeout: 5 });
353
+ pushCcHook(h, "stop", config.transcriptSyncScriptPath, { timeout: 3 });
354
+ h.beforeShellExecution = h.beforeShellExecution ?? [];
355
+ h.beforeShellExecution.push({
356
+ command: bunRunCmd(config.bashJudgeScriptPath),
357
+ timeout: 15,
358
+ failClosed: false,
334
359
  [SYNKRO_MARKER2]: true
335
360
  });
336
- file.hooks.beforeShellExecution = file.hooks.beforeShellExecution ?? [];
337
- file.hooks.beforeShellExecution.push({
338
- command: config.bashJudgeScriptPath,
339
- timeout: 10,
340
- failClosed: true,
361
+ pushCcHook(h, "afterShellExecution", config.bashFollowupScriptPath, { timeout: 10 });
362
+ h.preToolUse = h.preToolUse ?? [];
363
+ h.preToolUse.push({
364
+ command: bunRunCmd(config.bashJudgeScriptPath),
365
+ timeout: 15,
366
+ failClosed: false,
367
+ matcher: "Shell|Bash|Read|ReadFile|Grep|Glob|terminal|run_terminal_cmd|execute_command|read_file|grep_search|file_search|list_dir|codebase_search|delete_file",
341
368
  [SYNKRO_MARKER2]: true
342
369
  });
343
- file.hooks.preToolUse = file.hooks.preToolUse ?? [];
344
- file.hooks.preToolUse.push({
345
- command: config.editPrecheckScriptPath,
370
+ pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
346
371
  timeout: 15,
347
- [SYNKRO_MARKER2]: true
372
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
348
373
  });
349
- file.hooks.afterFileEdit = file.hooks.afterFileEdit ?? [];
350
- file.hooks.afterFileEdit.push({
351
- command: config.editCaptureScriptPath,
374
+ pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
352
375
  timeout: 15,
353
- [SYNKRO_MARKER2]: true
376
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
354
377
  });
355
- file.hooks.postToolUse = file.hooks.postToolUse ?? [];
356
- file.hooks.postToolUse.push({
357
- command: config.bashFollowupScriptPath,
378
+ pushCcHook(h, "preToolUse", config.cvePrecheckScriptPath, {
358
379
  timeout: 10,
380
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
381
+ });
382
+ pushCcHook(h, "preToolUse", config.agentJudgeScriptPath, {
383
+ timeout: 15,
384
+ matcher: "Agent|Task"
385
+ });
386
+ pushCcHook(h, "preToolUse", config.planJudgeScriptPath, {
387
+ timeout: 20,
388
+ matcher: "ExitPlanMode|SwitchMode|CreatePlan"
389
+ });
390
+ h.afterFileEdit = h.afterFileEdit ?? [];
391
+ h.afterFileEdit.push({
392
+ command: bunRunCmd(config.editCaptureScriptPath),
393
+ timeout: 15,
394
+ failClosed: false,
359
395
  [SYNKRO_MARKER2]: true
360
396
  });
397
+ pushCcHook(h, "postToolUse", config.bashFollowupScriptPath, {
398
+ timeout: 10,
399
+ matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command|delete_file"
400
+ });
361
401
  writeHooksFileAtomic(hooksJsonPath, file);
362
402
  }
363
403
  function uninstallCursorHooks(hooksJsonPath) {
@@ -382,24 +422,67 @@ function uninstallCursorHooks(hooksJsonPath) {
382
422
  writeHooksFileAtomic(hooksJsonPath, file);
383
423
  return true;
384
424
  }
425
+ function preToolUseUsesScript(hooks, scriptBasename) {
426
+ return (hooks ?? []).some(
427
+ (e) => isSynkroEntry2(e) && typeof e.command === "string" && e.command.includes(scriptBasename)
428
+ );
429
+ }
385
430
  function inspectCursorHooks(hooksJsonPath) {
386
431
  let file;
387
432
  try {
388
433
  file = readHooksFile(hooksJsonPath);
389
434
  } catch {
390
- return { installed: false, sessionStart: false, beforeShellExecution: false, preToolUse: false, afterFileEdit: false, postToolUse: false };
435
+ return {
436
+ installed: false,
437
+ sessionStart: false,
438
+ sessionEnd: false,
439
+ beforeSubmitPrompt: false,
440
+ stop: false,
441
+ beforeShellExecution: false,
442
+ afterShellExecution: false,
443
+ preToolUse: false,
444
+ preToolUseBash: false,
445
+ preToolUseEdit: false,
446
+ preToolUseCwe: false,
447
+ preToolUseCve: false,
448
+ preToolUseAgent: false,
449
+ preToolUsePlan: false,
450
+ afterFileEdit: false,
451
+ postToolUse: false
452
+ };
391
453
  }
392
454
  const h = file.hooks ?? {};
393
455
  const sessionStart = (h.sessionStart ?? []).some((e) => isSynkroEntry2(e));
456
+ const sessionEnd = (h.sessionEnd ?? []).some((e) => isSynkroEntry2(e));
457
+ const beforeSubmitPrompt = (h.beforeSubmitPrompt ?? []).some((e) => isSynkroEntry2(e));
458
+ const stop = (h.stop ?? []).some((e) => isSynkroEntry2(e));
394
459
  const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
395
- const preToolUse = (h.preToolUse ?? []).some((e) => isSynkroEntry2(e));
460
+ const afterShellExecution = (h.afterShellExecution ?? []).some((e) => isSynkroEntry2(e));
461
+ const pre = h.preToolUse ?? [];
462
+ const preToolUseBash = preToolUseUsesScript(pre, "cc-bash-judge") || preToolUseUsesScript(pre, "cursor-bash-judge");
463
+ const preToolUseEdit = preToolUseUsesScript(pre, "cc-edit-precheck") || preToolUseUsesScript(pre, "cursor-edit-precheck");
464
+ const preToolUseCwe = preToolUseUsesScript(pre, "cc-cwe-precheck");
465
+ const preToolUseCve = preToolUseUsesScript(pre, "cc-cve-precheck");
466
+ const preToolUseAgent = preToolUseUsesScript(pre, "cc-agent-judge");
467
+ const preToolUsePlan = preToolUseUsesScript(pre, "cc-plan-judge");
468
+ const preToolUse = preToolUseBash || preToolUseEdit || preToolUseCwe || preToolUseCve || preToolUseAgent || preToolUsePlan;
396
469
  const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
397
470
  const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
398
471
  return {
399
- installed: sessionStart || beforeShellExecution || preToolUse || afterFileEdit || postToolUse,
472
+ installed: sessionStart || sessionEnd || beforeSubmitPrompt || stop || beforeShellExecution || afterShellExecution || preToolUse || afterFileEdit || postToolUse,
400
473
  sessionStart,
474
+ sessionEnd,
475
+ beforeSubmitPrompt,
476
+ stop,
401
477
  beforeShellExecution,
478
+ afterShellExecution,
402
479
  preToolUse,
480
+ preToolUseBash,
481
+ preToolUseEdit,
482
+ preToolUseCwe,
483
+ preToolUseCve,
484
+ preToolUseAgent,
485
+ preToolUsePlan,
403
486
  afterFileEdit,
404
487
  postToolUse
405
488
  };
@@ -413,7 +496,17 @@ var init_cursorHookConfig = __esm({
413
496
  resolve(homedir2(), ".cursor"),
414
497
  resolve(homedir2(), ".config", "cursor")
415
498
  ];
416
- ALL_EVENTS = ["sessionStart", "beforeShellExecution", "preToolUse", "afterFileEdit", "postToolUse"];
499
+ ALL_EVENTS = [
500
+ "sessionStart",
501
+ "sessionEnd",
502
+ "beforeSubmitPrompt",
503
+ "stop",
504
+ "beforeShellExecution",
505
+ "afterShellExecution",
506
+ "preToolUse",
507
+ "afterFileEdit",
508
+ "postToolUse"
509
+ ];
417
510
  }
418
511
  });
419
512
 
@@ -496,13 +589,76 @@ function inspectMcpConfig() {
496
589
  }
497
590
  return { installed: true, configPath: CC_CONFIG_PATH, url: entry.url };
498
591
  }
499
- var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH;
592
+ function readCursorMcpJson() {
593
+ if (!existsSync3(CURSOR_MCP_PATH)) return {};
594
+ try {
595
+ const raw = readFileSync3(CURSOR_MCP_PATH, "utf-8");
596
+ return JSON.parse(raw);
597
+ } catch (err) {
598
+ throw new Error(`Failed to parse ${CURSOR_MCP_PATH}: ${err.message}`);
599
+ }
600
+ }
601
+ function writeCursorMcpJsonAtomic(config) {
602
+ mkdirSync3(dirname3(CURSOR_MCP_PATH), { recursive: true });
603
+ const tmpPath = `${CURSOR_MCP_PATH}.synkro.tmp`;
604
+ writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
605
+ renameSync3(tmpPath, CURSOR_MCP_PATH);
606
+ }
607
+ function installCursorMcpConfig(opts) {
608
+ const config = readCursorMcpJson();
609
+ config.mcpServers = config.mcpServers ?? {};
610
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
611
+ if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
612
+ }
613
+ if (opts.local) {
614
+ const url2 = "http://127.0.0.1:8931/";
615
+ const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
616
+ let localToken = "";
617
+ try {
618
+ localToken = readFileSync3(tokenPath, "utf-8").trim();
619
+ } catch {
620
+ }
621
+ config.mcpServers[SYNKRO_SERVER_NAME] = {
622
+ url: url2,
623
+ ...localToken ? { headers: { Authorization: `Bearer ${localToken}` } } : {},
624
+ [SYNKRO_MARKER3]: true
625
+ };
626
+ writeCursorMcpJsonAtomic(config);
627
+ return { path: CURSOR_MCP_PATH, url: url2 };
628
+ }
629
+ const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
630
+ config.mcpServers[SYNKRO_SERVER_NAME] = {
631
+ url,
632
+ headers: { Authorization: `Bearer ${opts.bearerToken}` },
633
+ [SYNKRO_MARKER3]: true
634
+ };
635
+ writeCursorMcpJsonAtomic(config);
636
+ return { path: CURSOR_MCP_PATH, url };
637
+ }
638
+ function uninstallCursorMcpConfig() {
639
+ if (!existsSync3(CURSOR_MCP_PATH)) return false;
640
+ const config = readCursorMcpJson();
641
+ if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
642
+ let removed = false;
643
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
644
+ if (entry?.[SYNKRO_MARKER3] === true) {
645
+ delete config.mcpServers[name];
646
+ removed = true;
647
+ }
648
+ }
649
+ if (!removed) return false;
650
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
651
+ writeCursorMcpJsonAtomic(config);
652
+ return true;
653
+ }
654
+ var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH;
500
655
  var init_mcpConfig = __esm({
501
656
  "cli/installer/mcpConfig.ts"() {
502
657
  "use strict";
503
658
  SYNKRO_MARKER3 = "__synkro_managed__";
504
659
  SYNKRO_SERVER_NAME = "synkro-guardrails";
505
660
  CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
661
+ CURSOR_MCP_PATH = join2(homedir3(), ".cursor", "mcp.json");
506
662
  }
507
663
  });
508
664
 
@@ -702,7 +858,7 @@ synkro_post_with_retry() {
702
858
  });
703
859
 
704
860
  // cli/installer/hookScriptsTs.ts
705
- var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_PRECHECK_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_BASH_FOLLOWUP_TS, CURSOR_SESSION_START_TS;
861
+ var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS;
706
862
  var init_hookScriptsTs = __esm({
707
863
  "cli/installer/hookScriptsTs.ts"() {
708
864
  "use strict";
@@ -739,7 +895,17 @@ if (existsSync(CONFIG_PATH)) {
739
895
  } catch {}
740
896
  }
741
897
 
742
- export const GATEWAY_URL = process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh';
898
+ const ALLOWED_GATEWAY_HOSTS = new Set(['api.synkro.sh', 'localhost', '127.0.0.1']);
899
+ function validateGatewayUrl(raw: string): string {
900
+ try {
901
+ const u = new URL(raw);
902
+ if (!ALLOWED_GATEWAY_HOSTS.has(u.hostname)) return 'https://api.synkro.sh';
903
+ return raw.replace(/\\/+$/, '');
904
+ } catch {
905
+ return 'https://api.synkro.sh';
906
+ }
907
+ }
908
+ export const GATEWAY_URL = validateGatewayUrl(process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh');
743
909
  export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
744
910
  const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
745
911
 
@@ -1083,11 +1249,11 @@ async function channelGrade(role: GradeRole, prompt: string, jwt: string, port:
1083
1249
  return String(data.result || '');
1084
1250
  }
1085
1251
 
1086
- export async function localGrade(surface: string, prompt: string): Promise<string> {
1252
+ export async function localGrade(surface: string, prompt: string, timeoutMs = 20000): Promise<string> {
1087
1253
  if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1088
1254
  const jwt = loadJwt();
1089
1255
  if (!jwt) throw new Error('NO_JWT');
1090
- return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929);
1256
+ return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929, timeoutMs);
1091
1257
  }
1092
1258
 
1093
1259
  export async function localGradeCwe(prompt: string): Promise<string> {
@@ -1101,6 +1267,7 @@ export async function localGradeCwe(prompt: string): Promise<string> {
1101
1267
  export interface Verdict {
1102
1268
  ok: boolean;
1103
1269
  reason: string;
1270
+ suggestedFix: string;
1104
1271
  ruleId: string;
1105
1272
  ruleMode: string;
1106
1273
  severity: string;
@@ -1111,6 +1278,7 @@ export function parseVerdict(resp: string): Verdict {
1111
1278
  const verdict: Verdict = {
1112
1279
  ok: true,
1113
1280
  reason: '',
1281
+ suggestedFix: '',
1114
1282
  ruleId: '',
1115
1283
  ruleMode: '',
1116
1284
  severity: 'low',
@@ -1129,6 +1297,9 @@ export function parseVerdict(resp: string): Verdict {
1129
1297
  const reasonMatch = inner.match(/<reason>(.*?)<\\/reason>/) || inner.match(/<reasoning>(.*?)<\\/reasoning>/);
1130
1298
  if (reasonMatch) verdict.reason = reasonMatch[1].trim();
1131
1299
 
1300
+ const fixMatch = inner.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
1301
+ if (fixMatch) verdict.suggestedFix = fixMatch[1].trim();
1302
+
1132
1303
  if (!verdict.ok) {
1133
1304
  const ruleIdMatch = inner.match(/<rule_id>(.*?)<\\/rule_id>/);
1134
1305
  const ruleModeMatch = inner.match(/<rule_mode>(.*?)<\\/rule_mode>/);
@@ -1147,6 +1318,10 @@ export function parseVerdict(resp: string): Verdict {
1147
1318
  const vReason = vBlock.match(/<reason>(.*?)<\\/reason>/);
1148
1319
  if (vReason) verdict.reason = vReason[1].trim();
1149
1320
  }
1321
+ if (!verdict.suggestedFix) {
1322
+ const vFix = vBlock.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
1323
+ if (vFix) verdict.suggestedFix = vFix[1].trim();
1324
+ }
1150
1325
  if (!sevMatch) {
1151
1326
  const vSev = vBlock.match(/<severity>(.*?)<\\/severity>/);
1152
1327
  if (vSev) verdict.severity = vSev[1].trim();
@@ -1301,8 +1476,10 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
1301
1476
  }
1302
1477
  case 'NotebookEdit':
1303
1478
  return toolInput.new_source || '';
1479
+ case 'StrReplace':
1480
+ return toolInput.new_string || toolInput.content || toolInput.code_edit || '';
1304
1481
  default:
1305
- return '';
1482
+ return toolInput.content || toolInput.new_string || toolInput.code_edit || '';
1306
1483
  }
1307
1484
  }
1308
1485
 
@@ -1616,13 +1793,102 @@ export function dispatchFinding(
1616
1793
  }).catch(() => {});
1617
1794
  }
1618
1795
 
1796
+ // \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
1797
+
1798
+ export const EDIT_TOOL_NAMES = new Set([
1799
+ 'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'StrReplace',
1800
+ ]);
1801
+ export const SHELL_TOOL_NAMES = new Set([
1802
+ 'Bash', 'Shell', 'Read', 'Grep', 'Glob', 'terminal', 'run_terminal_cmd', 'execute_command',
1803
+ ]);
1804
+ export const AGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
1805
+ export const PLAN_TOOL_NAMES = new Set(['ExitPlanMode', 'SwitchMode', 'CreatePlan']);
1806
+
1807
+ export function isEditTool(toolName: string): boolean {
1808
+ return EDIT_TOOL_NAMES.has(toolName);
1809
+ }
1810
+ export function isShellTool(toolName: string): boolean {
1811
+ return SHELL_TOOL_NAMES.has(toolName);
1812
+ }
1813
+ export function isAgentTool(toolName: string): boolean {
1814
+ return AGENT_TOOL_NAMES.has(toolName);
1815
+ }
1816
+ export function isPlanTool(toolName: string): boolean {
1817
+ return PLAN_TOOL_NAMES.has(toolName);
1818
+ }
1819
+
1820
+ export function hookSessionId(payload: Record<string, unknown>): string {
1821
+ return String(payload.session_id ?? payload.conversation_id ?? '');
1822
+ }
1823
+
1824
+ export function isCursorHookFormat(): boolean {
1825
+ return process.env.SYNKRO_HOOK_FORMAT === 'cursor';
1826
+ }
1827
+
1828
+ let cursorHookExited = false;
1829
+
1830
+ export function setupCursorHookSignals(): void {
1831
+ if (!isCursorHookFormat()) return;
1832
+ process.on('SIGTERM', () => outputEmpty());
1833
+ }
1834
+
1835
+ function cursorHookExit(): never {
1836
+ cursorHookExited = true;
1837
+ process.exit(0);
1838
+ }
1839
+
1619
1840
  // \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
1620
1841
 
1621
1842
  export function outputJson(obj: any): void {
1843
+ if (isCursorHookFormat()) {
1844
+ if (obj?.permission === 'allow') {
1845
+ const u = typeof obj.user_message === 'string' ? obj.user_message : '';
1846
+ const a = typeof obj.agent_message === 'string' ? obj.agent_message : u;
1847
+ if (u || a) {
1848
+ if (!cursorHookExited) {
1849
+ cursorHookExited = true;
1850
+ process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\\n');
1851
+ }
1852
+ cursorHookExit();
1853
+ }
1854
+ }
1855
+ const hso = obj?.hookSpecificOutput;
1856
+ const sys = typeof obj?.systemMessage === 'string' ? obj.systemMessage : '';
1857
+ if (hso?.permissionDecision === 'deny') {
1858
+ const reason = hso.permissionDecisionReason || hso.additionalContext || sys;
1859
+ if (!cursorHookExited) {
1860
+ cursorHookExited = true;
1861
+ process.stdout.write(JSON.stringify({
1862
+ permission: 'deny',
1863
+ user_message: sys || reason,
1864
+ agent_message: hso.additionalContext || reason,
1865
+ }) + '\\n');
1866
+ }
1867
+ cursorHookExit();
1868
+ }
1869
+ const addCtx = typeof hso?.additionalContext === 'string' ? hso.additionalContext : '';
1870
+ const ctx = sys || addCtx;
1871
+ if (ctx) {
1872
+ if (!cursorHookExited) {
1873
+ cursorHookExited = true;
1874
+ process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\\n');
1875
+ }
1876
+ cursorHookExit();
1877
+ }
1878
+ outputEmpty();
1879
+ return;
1880
+ }
1622
1881
  console.log(JSON.stringify(obj));
1623
1882
  }
1624
1883
 
1625
1884
  export function outputEmpty(): void {
1885
+ if (isCursorHookFormat()) {
1886
+ if (!cursorHookExited) {
1887
+ cursorHookExited = true;
1888
+ try { process.stdout.write('{}\\n'); } catch {}
1889
+ }
1890
+ cursorHookExit();
1891
+ }
1626
1892
  console.log('{}');
1627
1893
  }
1628
1894
  `;
@@ -1631,26 +1897,27 @@ import {
1631
1897
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
1632
1898
  parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
1633
1899
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
1634
- outputJson, outputEmpty, GATEWAY_URL,
1900
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
1635
1901
  type HookConfig, type Rule,
1636
1902
  } from './_synkro-common.ts';
1637
1903
  import { existsSync, readFileSync } from 'node:fs';
1638
1904
  import { basename, dirname, join } from 'node:path';
1639
1905
 
1640
1906
  async function main() {
1907
+ setupCursorHookSignals();
1641
1908
  try {
1642
1909
  const input = await readStdin();
1643
1910
  if (!input.trim()) { outputEmpty(); return; }
1644
1911
 
1645
1912
  const payload = JSON.parse(input);
1646
1913
  const toolName = payload.tool_name || '';
1647
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
1914
+ if (!isEditTool(toolName)) {
1648
1915
  outputEmpty();
1649
1916
  return;
1650
1917
  }
1651
1918
 
1652
1919
  const toolInput = payload.tool_input || {};
1653
- const sessionId = payload.session_id || '';
1920
+ const sessionId = hookSessionId(payload);
1654
1921
  const toolUseId = payload.tool_use_id || '';
1655
1922
  const cwd = payload.cwd || '';
1656
1923
  const permissionMode = payload.permission_mode || '';
@@ -1721,7 +1988,7 @@ async function main() {
1721
1988
  try {
1722
1989
  gradeResp = await localGrade('edit', graderPrompt);
1723
1990
  } catch {
1724
- outputEmpty();
1991
+ outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 local grader unavailable, skipped' });
1725
1992
  return;
1726
1993
  }
1727
1994
 
@@ -1755,7 +2022,7 @@ async function main() {
1755
2022
  rulesChecked: config.rules, violatedRules,
1756
2023
  ccModel: transcript.ccModel,
1757
2024
  });
1758
- outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 warning: ' + guardReason });
2025
+ outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 warning: ' + guardReason, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge (audit). ' + guardReason } });
1759
2026
  return;
1760
2027
  }
1761
2028
 
@@ -1766,7 +2033,8 @@ async function main() {
1766
2033
  rulesChecked: config.rules, violatedRules: [],
1767
2034
  ccModel: transcript.ccModel,
1768
2035
  });
1769
- outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 pass: ' + (verdict.reason || 'no policy violations detected') });
2036
+ const passLine = tagStr + ' editGuard ' + fileShort + ' \\u2192 pass: ' + (verdict.reason || 'no policy violations detected');
2037
+ outputJson({ systemMessage: passLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') } });
1770
2038
  return;
1771
2039
  }
1772
2040
 
@@ -1841,24 +2109,25 @@ main();
1841
2109
  import {
1842
2110
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
1843
2111
  localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
1844
- outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
2112
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
1845
2113
  } from './_synkro-common.ts';
1846
2114
  import { basename, extname } from 'node:path';
1847
2115
 
1848
2116
  async function main() {
2117
+ setupCursorHookSignals();
1849
2118
  try {
1850
2119
  const input = await readStdin();
1851
2120
  if (!input.trim()) { outputEmpty(); return; }
1852
2121
 
1853
2122
  const payload = JSON.parse(input);
1854
2123
  const toolName = payload.tool_name || '';
1855
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
2124
+ if (!isEditTool(toolName)) {
1856
2125
  outputEmpty();
1857
2126
  return;
1858
2127
  }
1859
2128
 
1860
2129
  const toolInput = payload.tool_input || {};
1861
- const sessionId = payload.session_id || '';
2130
+ const sessionId = hookSessionId(payload);
1862
2131
  const cwd = payload.cwd || '';
1863
2132
  const gitRepo = detectRepo(cwd || '.');
1864
2133
 
@@ -1959,6 +2228,12 @@ async function main() {
1959
2228
  if (id && !cweIds.includes(id)) cweIds.push(id);
1960
2229
  }
1961
2230
 
2231
+ const fixMatches = gradeResp.match(/<suggested_fix>([^<]+)<\\/suggested_fix>/g) || [];
2232
+ const fixes: Record<string, string> = {};
2233
+ for (let i = 0; i < Math.min(cweIds.length, fixMatches.length); i++) {
2234
+ fixes[cweIds[i]] = fixMatches[i].replace(/<\\/?suggested_fix>/g, '').trim();
2235
+ }
2236
+
1962
2237
  // Filter out exempted CWEs for this file
1963
2238
  const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
1964
2239
 
@@ -1977,7 +2252,11 @@ async function main() {
1977
2252
  const label = count === 1 ? 'match' : 'matches';
1978
2253
  const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
1979
2254
  const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
1980
- const ctx = 'CWE: ' + denyDetail + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
2255
+ const fixLines = activeCweIds
2256
+ .filter(id => fixes[id])
2257
+ .map(id => '[' + id + '] Fix: ' + fixes[id]);
2258
+ const fixHint = fixLines.length > 0 ? '\\n' + fixLines.join('\\n') : '';
2259
+ const ctx = 'CWE: ' + denyDetail + fixHint + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
1981
2260
 
1982
2261
  for (const cweId of activeCweIds) {
1983
2262
  dispatchFinding(jwt, {
@@ -1992,6 +2271,13 @@ async function main() {
1992
2271
  }, config.captureDepth);
1993
2272
  }
1994
2273
 
2274
+ dispatchCapture(jwt, 'cwe', 'block', verdict.severity || 'high', verdict.category || 'security',
2275
+ toolName, gitRepo, sessionId, config.captureDepth, {
2276
+ command: 'edit ' + filePath,
2277
+ reasoning: denyDetail,
2278
+ violatedRules: activeCweIds,
2279
+ });
2280
+
1995
2281
  outputJson({
1996
2282
  systemMessage: cweMsg,
1997
2283
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
@@ -2007,7 +2293,14 @@ async function main() {
2007
2293
  status: 'resolved',
2008
2294
  }, config.captureDepth);
2009
2295
 
2010
- outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \\u2192 clean' });
2296
+ dispatchCapture(jwt, 'cwe', 'pass', 'audit', 'clean',
2297
+ toolName, gitRepo, sessionId, config.captureDepth, {
2298
+ command: 'edit ' + filePath,
2299
+ reasoning: verdict.reason || 'no CWE weaknesses detected',
2300
+ });
2301
+
2302
+ const cleanMsg = cweTag + ' ' + fileShort + ' \\u2192 clean' + (verdict.reason ? ' (' + verdict.reason + ')' : '');
2303
+ outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
2011
2304
  return;
2012
2305
  }
2013
2306
 
@@ -2026,7 +2319,7 @@ main();
2026
2319
  import {
2027
2320
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
2028
2321
  reconstructContent, readStdin, findNearestDeps, log,
2029
- outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
2322
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, GATEWAY_URL,
2030
2323
  } from './_synkro-common.ts';
2031
2324
  import { basename } from 'node:path';
2032
2325
 
@@ -2044,20 +2337,22 @@ function isManifest(filename: string): boolean {
2044
2337
  }
2045
2338
 
2046
2339
  async function main() {
2340
+ setupCursorHookSignals();
2047
2341
  try {
2048
2342
  const input = await readStdin();
2049
2343
  if (!input.trim()) { outputEmpty(); return; }
2050
2344
 
2051
2345
  const payload = JSON.parse(input);
2052
2346
  const toolName = payload.tool_name || '';
2053
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
2347
+ if (!isEditTool(toolName)) {
2054
2348
  outputEmpty();
2055
2349
  return;
2056
2350
  }
2057
2351
 
2058
2352
  const toolInput = payload.tool_input || {};
2059
- const sessionId = payload.session_id || '';
2353
+ const sessionId = hookSessionId(payload);
2060
2354
  const cwd = payload.cwd || '';
2355
+ const gitRepo = detectRepo(cwd || '.');
2061
2356
 
2062
2357
  const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
2063
2358
  if (!filePath) { outputEmpty(); return; }
@@ -2152,6 +2447,16 @@ async function main() {
2152
2447
  const cveMsg = cveTag + ' ' + fileShort + ' \\u2192 ' + count + ' ' + label;
2153
2448
  const ctx = 'CVE: ' + top3 + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 upgrade the vulnerable dependencies yourself.';
2154
2449
 
2450
+ const cveIds = findings.slice(0, 10).map((f: any) =>
2451
+ (f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || 'unknown'
2452
+ );
2453
+ dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
2454
+ toolName, gitRepo, sessionId, config.captureDepth, {
2455
+ command: 'edit ' + filePath,
2456
+ reasoning: top3,
2457
+ violatedRules: cveIds,
2458
+ });
2459
+
2155
2460
  outputJson({
2156
2461
  systemMessage: cveMsg,
2157
2462
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
@@ -2173,12 +2478,12 @@ import {
2173
2478
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2174
2479
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
2175
2480
  extractTranscript, readLastPrompt, log,
2176
- outputJson, outputEmpty, GATEWAY_URL,
2481
+ outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
2177
2482
  type HookConfig, type Rule,
2178
2483
  } from './_synkro-common.ts';
2179
2484
 
2180
2485
  const TOP_NPM_PKGS = new Set([
2181
- 'express','react','lodash','axios','chalk','commander','debug','dotenv','webpack',
2486
+ 'express','react','lodash','chalk','commander','debug','dotenv','webpack',
2182
2487
  'typescript','moment','uuid','cors','body-parser','mongoose','jsonwebtoken','bcrypt',
2183
2488
  'nodemon','eslint','prettier','jest','mocha','chai','sinon','supertest','request',
2184
2489
  'async','bluebird','underscore','ramda','rxjs','socket.io','redis','pg','mysql',
@@ -2249,28 +2554,35 @@ interface PkgMeta {
2249
2554
  }
2250
2555
 
2251
2556
  async function main() {
2557
+ setupCursorHookSignals();
2252
2558
  try {
2253
2559
  const input = await readStdin();
2254
2560
  if (!input.trim()) { outputEmpty(); return; }
2255
2561
 
2256
2562
  const payload = JSON.parse(input);
2257
2563
  const toolName = payload.tool_name || '';
2258
- if (!['Bash', 'Read', 'Grep', 'Glob'].includes(toolName)) {
2564
+ if (!isShellTool(toolName)) {
2259
2565
  outputEmpty();
2260
2566
  return;
2261
2567
  }
2262
2568
 
2263
2569
  const toolInput = payload.tool_input || {};
2264
- const sessionId = payload.session_id || '';
2570
+ const sessionId = hookSessionId(payload);
2265
2571
  const toolUseId = payload.tool_use_id || '';
2266
2572
  const cwd = payload.cwd || '';
2267
2573
  const permissionMode = payload.permission_mode || '';
2268
2574
  const transcriptPath = payload.transcript_path || '';
2269
2575
  const gitRepo = detectRepo(cwd || '.');
2576
+ const transcript = extractTranscript(transcriptPath);
2270
2577
 
2271
2578
  let command = '';
2272
2579
  switch (toolName) {
2273
- case 'Bash': command = toolInput.command || ''; break;
2580
+ case 'Bash':
2581
+ case 'Shell':
2582
+ case 'terminal':
2583
+ case 'run_terminal_cmd':
2584
+ case 'execute_command':
2585
+ command = toolInput.command || ''; break;
2274
2586
  case 'Read': command = 'cat ' + (toolInput.file_path || ''); break;
2275
2587
  case 'Grep': command = "grep -r '" + (toolInput.pattern || '') + "' " + (toolInput.path || '.'); break;
2276
2588
  case 'Glob': command = "find . -name '" + (toolInput.pattern || '') + "'"; break;
@@ -2441,6 +2753,15 @@ async function main() {
2441
2753
  }, config.captureDepth);
2442
2754
  }
2443
2755
 
2756
+ const cveIds = findings.map((f: any) => f.cve || f.id || f.package);
2757
+ dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
2758
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
2759
+ command,
2760
+ reasoning: top3,
2761
+ violatedRules: cveIds,
2762
+ ccModel: transcript.ccModel,
2763
+ });
2764
+
2444
2765
  outputJson({
2445
2766
  systemMessage: cveMsg,
2446
2767
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
@@ -2461,7 +2782,6 @@ async function main() {
2461
2782
  }
2462
2783
  }
2463
2784
 
2464
- const transcript = extractTranscript(transcriptPath);
2465
2785
  const lastPrompt = readLastPrompt();
2466
2786
 
2467
2787
  const config = await loadConfig(jwt);
@@ -2488,7 +2808,7 @@ async function main() {
2488
2808
  try {
2489
2809
  gradeResp = await localGrade('bash', graderPrompt);
2490
2810
  } catch {
2491
- outputEmpty();
2811
+ outputJson({ systemMessage: tagStr + ' bashGuard \\u2192 local grader unavailable, skipped' });
2492
2812
  return;
2493
2813
  }
2494
2814
 
@@ -2601,24 +2921,25 @@ import {
2601
2921
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2602
2922
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
2603
2923
  extractTranscript, readLastPrompt, log,
2604
- outputJson, outputEmpty, GATEWAY_URL,
2924
+ outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
2605
2925
  type HookConfig, type Rule,
2606
2926
  } from './_synkro-common.ts';
2607
2927
 
2608
2928
  async function main() {
2929
+ setupCursorHookSignals();
2609
2930
  try {
2610
2931
  const input = await readStdin();
2611
2932
  if (!input.trim()) { outputEmpty(); return; }
2612
2933
 
2613
2934
  const payload = JSON.parse(input);
2614
2935
  const toolName = payload.tool_name || '';
2615
- if (toolName !== 'Agent') {
2936
+ if (!isAgentTool(toolName)) {
2616
2937
  outputEmpty();
2617
2938
  return;
2618
2939
  }
2619
2940
 
2620
2941
  const toolInput = payload.tool_input || {};
2621
- const sessionId = payload.session_id || '';
2942
+ const sessionId = hookSessionId(payload);
2622
2943
  const toolUseId = payload.tool_use_id || '';
2623
2944
  const cwd = payload.cwd || '';
2624
2945
  const permissionMode = payload.permission_mode || '';
@@ -2668,7 +2989,7 @@ async function main() {
2668
2989
  try {
2669
2990
  gradeResp = await localGrade('bash', graderPrompt);
2670
2991
  } catch {
2671
- outputEmpty();
2992
+ outputJson({ systemMessage: tagStr + ' agentGuard \\u2192 local grader unavailable, skipped' });
2672
2993
  return;
2673
2994
  }
2674
2995
 
@@ -2764,14 +3085,13 @@ main();
2764
3085
  import {
2765
3086
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2766
3087
  parseVerdict, dispatchCapture, postWithRetry, readStdin, log,
2767
- outputJson, outputEmpty, GATEWAY_URL,
3088
+ outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
2768
3089
  } from './_synkro-common.ts';
2769
3090
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
2770
3091
  import { join } from 'node:path';
2771
3092
  import { homedir } from 'node:os';
2772
3093
 
2773
- function findLatestPlan(): string | null {
2774
- const plansDir = join(homedir(), '.claude', 'plans');
3094
+ function findLatestPlanInDir(plansDir: string): string | null {
2775
3095
  if (!existsSync(plansDir)) return null;
2776
3096
  try {
2777
3097
  const files = readdirSync(plansDir)
@@ -2784,6 +3104,23 @@ function findLatestPlan(): string | null {
2784
3104
  }
2785
3105
  }
2786
3106
 
3107
+ function findLatestPlan(): string | null {
3108
+ const dirs = [
3109
+ join(homedir(), '.claude', 'plans'),
3110
+ join(homedir(), '.cursor', 'plans'),
3111
+ ];
3112
+ let best: { path: string; mtime: number } | null = null;
3113
+ for (const dir of dirs) {
3114
+ const p = findLatestPlanInDir(dir);
3115
+ if (!p) continue;
3116
+ try {
3117
+ const mtime = statSync(p).mtimeMs;
3118
+ if (!best || mtime > best.mtime) best = { path: p, mtime };
3119
+ } catch {}
3120
+ }
3121
+ return best?.path ?? null;
3122
+ }
3123
+
2787
3124
  function appendReviewToPlan(planFile: string, verdict: string): void {
2788
3125
  try {
2789
3126
  let content = readFileSync(planFile, 'utf-8');
@@ -2795,20 +3132,21 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
2795
3132
  }
2796
3133
 
2797
3134
  async function main() {
3135
+ setupCursorHookSignals();
2798
3136
  try {
2799
3137
  const input = await readStdin();
2800
3138
  if (!input.trim()) { outputEmpty(); return; }
2801
3139
 
2802
3140
  const payload = JSON.parse(input);
2803
3141
  const toolName = payload.tool_name || '';
2804
- if (toolName !== 'ExitPlanMode') { outputEmpty(); return; }
3142
+ if (!isPlanTool(toolName)) { outputEmpty(); return; }
2805
3143
 
2806
3144
  const planFile = findLatestPlan();
2807
3145
  if (!planFile) { outputEmpty(); return; }
2808
3146
  const plan = readFileSync(planFile, 'utf-8');
2809
3147
  if (plan.length < 20) { outputEmpty(); return; }
2810
3148
 
2811
- const sessionId = payload.session_id || '';
3149
+ const sessionId = hookSessionId(payload);
2812
3150
  const cwd = payload.cwd || '';
2813
3151
  const gitRepo = detectRepo(cwd || '.');
2814
3152
 
@@ -2841,7 +3179,7 @@ async function main() {
2841
3179
  try {
2842
3180
  gradeResp = await localGrade('plan', graderPrompt);
2843
3181
  } catch {
2844
- outputEmpty();
3182
+ outputJson({ systemMessage: tagStr + ' planReview \\u2192 local grader unavailable, skipped' });
2845
3183
  return;
2846
3184
  }
2847
3185
 
@@ -2852,7 +3190,8 @@ async function main() {
2852
3190
  if (!verdict.ok) {
2853
3191
  const reviewMsg = (verdict.ruleId ? '(first: ' + verdict.ruleId + ') ' : '') + (verdict.reason || 'check org rules during implementation');
2854
3192
  appendReviewToPlan(planFile, '\\u26a0\\ufe0f Advisory \\u2014 ' + reviewMsg);
2855
- outputJson({ systemMessage: tagStr + ' planReview \\u2192 ' + reviewMsg });
3193
+ const advLine = tagStr + ' planReview \\u2192 ' + reviewMsg;
3194
+ outputJson({ systemMessage: advLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge (advisory). ' + reviewMsg } });
2856
3195
  dispatchCapture(jwt, 'plan_review', 'advisory', verdict.severity || 'medium', verdict.category || 'general',
2857
3196
  'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
2858
3197
  command: planContent, reasoning: verdict.reason || 'check org rules',
@@ -2861,7 +3200,8 @@ async function main() {
2861
3200
  } else {
2862
3201
  const reviewMsg = verdict.reason || 'no relevant org rules for this plan';
2863
3202
  appendReviewToPlan(planFile, '\\u2705 Clean \\u2014 ' + reviewMsg);
2864
- outputJson({ systemMessage: tagStr + ' planReview \\u2192 clean: ' + reviewMsg });
3203
+ const cleanLine = tagStr + ' planReview \\u2192 clean: ' + reviewMsg;
3204
+ outputJson({ systemMessage: cleanLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge. ' + reviewMsg } });
2865
3205
  dispatchCapture(jwt, 'plan_review', 'clean', 'audit', verdict.category || 'general',
2866
3206
  'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
2867
3207
  command: planContent, reasoning: reviewMsg,
@@ -2913,16 +3253,17 @@ main();
2913
3253
  STOP_SUMMARY_TS = `#!/usr/bin/env bun
2914
3254
  import {
2915
3255
  loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
2916
- outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
3256
+ outputJson, outputEmpty, appendLocalTelemetry, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
2917
3257
  } from './_synkro-common.ts';
2918
3258
 
2919
3259
  async function main() {
3260
+ setupCursorHookSignals();
2920
3261
  try {
2921
3262
  const input = await readStdin();
2922
3263
  if (!input.trim()) { outputEmpty(); return; }
2923
3264
 
2924
3265
  const payload = JSON.parse(input);
2925
- const sessionId = payload.session_id || '';
3266
+ const sessionId = hookSessionId(payload);
2926
3267
  if (!sessionId) { outputEmpty(); return; }
2927
3268
 
2928
3269
  const cwd = payload.cwd || '';
@@ -3000,18 +3341,19 @@ main();
3000
3341
  SESSION_START_TS = `#!/usr/bin/env bun
3001
3342
  import {
3002
3343
  loadJwt, detectRepo, channelUp, tag, readStdin,
3003
- outputJson, outputEmpty, GATEWAY_URL,
3344
+ outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3004
3345
  type HookConfig,
3005
3346
  } from './_synkro-common.ts';
3006
3347
 
3007
3348
  async function main() {
3349
+ setupCursorHookSignals();
3008
3350
  try {
3009
3351
  const input = await readStdin();
3010
3352
  if (!input.trim()) { outputEmpty(); return; }
3011
3353
 
3012
3354
  const payload = JSON.parse(input);
3013
3355
  const cwd = payload.cwd || '';
3014
- const sessionId = payload.session_id || '';
3356
+ const sessionId = hookSessionId(payload);
3015
3357
  const gitRepo = detectRepo(cwd || '.');
3016
3358
 
3017
3359
  let jwt = loadJwt();
@@ -3064,27 +3406,33 @@ main();
3064
3406
  BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
3065
3407
  import {
3066
3408
  loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
3067
- outputEmpty, appendLocalTelemetry, GATEWAY_URL,
3409
+ outputEmpty, appendLocalTelemetry, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
3068
3410
  } from './_synkro-common.ts';
3069
3411
 
3070
3412
  async function main() {
3413
+ setupCursorHookSignals();
3071
3414
  try {
3072
3415
  const input = await readStdin();
3073
3416
  if (!input.trim()) { outputEmpty(); return; }
3074
3417
 
3075
3418
  const payload = JSON.parse(input);
3076
3419
  const toolName = payload.tool_name || '';
3077
- if (toolName !== 'Bash') { outputEmpty(); return; }
3420
+ const shellCmd = typeof payload.command === 'string' ? payload.command : (payload.tool_input?.command || '');
3421
+ if (!isShellTool(toolName) && !shellCmd) { outputEmpty(); return; }
3078
3422
 
3079
3423
  const jwt = loadJwt();
3080
3424
  if (!jwt) { outputEmpty(); return; }
3081
3425
 
3082
- const sessionId = payload.session_id || '';
3083
- const toolUseId = payload.tool_use_id || '';
3084
- if (!sessionId || !toolUseId) { outputEmpty(); return; }
3426
+ const sessionId = hookSessionId(payload);
3427
+ const toolUseId = payload.tool_use_id || payload.tool_call_id || 'cursor-shell';
3428
+ if (!sessionId) { outputEmpty(); return; }
3085
3429
 
3086
- const isError = payload.tool_result?.is_error === true;
3087
- const cmd = payload.tool_input?.command || '';
3430
+ let isError = payload.tool_result?.is_error === true;
3431
+ try {
3432
+ const out = JSON.parse(payload.tool_output || '{}');
3433
+ if (out.exitCode !== 0 || out.is_error === true) isError = true;
3434
+ } catch {}
3435
+ const cmd = shellCmd;
3088
3436
  const cmdHash = cmd ? hashCommand(cmd) : '';
3089
3437
 
3090
3438
  if (cmdHash && sessionId) {
@@ -3128,19 +3476,20 @@ main();
3128
3476
  TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
3129
3477
  import {
3130
3478
  loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
3131
- outputEmpty, GATEWAY_URL,
3479
+ outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3132
3480
  } from './_synkro-common.ts';
3133
3481
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3134
3482
  import { join, dirname } from 'node:path';
3135
3483
  import { homedir } from 'node:os';
3136
3484
 
3137
3485
  async function main() {
3486
+ setupCursorHookSignals();
3138
3487
  try {
3139
3488
  const input = await readStdin();
3140
3489
  if (!input.trim()) { outputEmpty(); return; }
3141
3490
 
3142
3491
  const payload = JSON.parse(input);
3143
- const sessionId = payload.session_id || '';
3492
+ const sessionId = hookSessionId(payload);
3144
3493
  const transcriptPath = payload.transcript_path || '';
3145
3494
  const cwd = payload.cwd || '';
3146
3495
 
@@ -3268,15 +3617,16 @@ async function main() {
3268
3617
  main();
3269
3618
  `;
3270
3619
  USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
3271
- import { readStdin, appendLocalTelemetry, aggregateUsage } from './_synkro-common.ts';
3620
+ import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId } from './_synkro-common.ts';
3272
3621
  import { writeFileSync, mkdirSync } from 'node:fs';
3273
3622
  import { join, dirname } from 'node:path';
3274
3623
  import { homedir } from 'node:os';
3275
3624
 
3276
3625
  async function main() {
3626
+ setupCursorHookSignals();
3277
3627
  try {
3278
3628
  const input = await readStdin();
3279
- if (!input.trim()) return;
3629
+ if (!input.trim()) { outputEmpty(); return; }
3280
3630
  const payload = JSON.parse(input);
3281
3631
  const msg = payload.message || payload.prompt || payload.content || '';
3282
3632
  if (msg) {
@@ -3285,7 +3635,7 @@ async function main() {
3285
3635
  writeFileSync(promptFile, msg, 'utf-8');
3286
3636
  }
3287
3637
 
3288
- const sessionId = payload.session_id || '';
3638
+ const sessionId = hookSessionId(payload);
3289
3639
  const transcriptPath = payload.transcript_path || '';
3290
3640
  if (sessionId && transcriptPath) {
3291
3641
  const usage = aggregateUsage(transcriptPath);
@@ -3306,7 +3656,10 @@ async function main() {
3306
3656
  });
3307
3657
  }
3308
3658
  }
3309
- } catch {}
3659
+ outputEmpty();
3660
+ } catch {
3661
+ outputEmpty();
3662
+ }
3310
3663
  }
3311
3664
 
3312
3665
  main();
@@ -3315,169 +3668,126 @@ main();
3315
3668
  import {
3316
3669
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
3317
3670
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
3318
- appendLocalTelemetry, log, GATEWAY_URL,
3319
- type HookConfig, type Rule,
3671
+ extractTranscript, readLastPrompt, log, GATEWAY_URL,
3672
+ type Rule,
3320
3673
  } from './_synkro-common.ts';
3674
+ import { createHash } from 'node:crypto';
3675
+ import { existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
3321
3676
 
3322
- async function main() {
3323
- try {
3324
- const input = await readStdin();
3325
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3326
-
3327
- const payload = JSON.parse(input);
3328
- const command = payload.command || '';
3329
- if (!command) { process.stdout.write('{}\\n'); return; }
3330
-
3331
- const cwd = payload.cwd || '';
3332
- const sessionId = payload.conversation_id || '';
3333
- const repo = detectRepo(cwd || '.');
3334
-
3335
- const cmdShort = command.slice(0, 80);
3336
- log('bashGuard checking: ' + cmdShort);
3337
-
3338
- let jwt = loadJwt();
3339
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3340
- jwt = await ensureFreshJwt(jwt);
3341
-
3342
- const config = await loadConfig(jwt);
3343
- if (config.silent) { process.stdout.write('{}\\n'); return; }
3344
-
3345
- const rt = await route(config);
3346
- const tagStr = tag(rt, config);
3347
-
3348
- if (rt === 'local') {
3349
- // Build grading prompt with rules
3350
- const rulesBlock = config.rules.map((r: Rule, i: number) =>
3351
- (i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
3352
- ).join('\\n');
3677
+ const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
3678
+ const DEDUP_TTL_MS = 3000;
3353
3679
 
3354
- const graderPrompt = [
3355
- 'RULES:',
3356
- rulesBlock || '(none)',
3357
- '',
3358
- 'COMMAND TO EVALUATE:',
3359
- command,
3360
- ].join('\\n');
3680
+ function isDuplicate(command: string, sessionId: string): boolean {
3681
+ const hash = createHash('md5').update(sessionId + ':' + command).digest('hex').slice(0, 12);
3682
+ const marker = DEDUP_DIR + '/' + hash;
3683
+ try {
3684
+ if (existsSync(marker)) {
3685
+ const age = Date.now() - statSync(marker).mtimeMs;
3686
+ if (age < DEDUP_TTL_MS) return true;
3687
+ }
3688
+ } catch {}
3689
+ try {
3690
+ mkdirSync(DEDUP_DIR, { recursive: true });
3691
+ writeFileSync(marker, '', { flag: 'w' });
3692
+ } catch {}
3693
+ return false;
3694
+ }
3361
3695
 
3362
- let gradeResp: string;
3363
- try {
3364
- gradeResp = await localGrade('bash', graderPrompt);
3365
- } catch {
3366
- process.stdout.write('{}\\n');
3367
- return;
3368
- }
3696
+ // Cursor beforeShellExecution timeout is 15s; stay under it (JWT refresh + grade).
3697
+ const CURSOR_GRADE_TIMEOUT_MS = 7500;
3698
+ const CURSOR_CLOUD_TIMEOUT_MS = 6000;
3369
3699
 
3370
- const verdict = parseVerdict(gradeResp);
3700
+ let hookDone = false;
3371
3701
 
3372
- if (!verdict.ok) {
3373
- const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
3374
- const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
3702
+ function finishAllow(): never {
3703
+ if (!hookDone) {
3704
+ hookDone = true;
3705
+ try { process.stdout.write('{}\\n'); } catch {}
3706
+ }
3707
+ process.exit(0);
3708
+ }
3375
3709
 
3376
- if (mode !== 'audit') {
3377
- dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
3378
- 'Bash', repo, sessionId, config.captureDepth, {
3379
- command, reasoning: guardReason,
3380
- rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3381
- });
3382
- const result = {
3383
- permission: 'deny',
3384
- user_message: tagStr + ' bashGuard \\u2192 block: ' + guardReason,
3385
- agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
3386
- };
3387
- process.stdout.write(JSON.stringify(result) + '\\n');
3388
- return;
3389
- }
3710
+ function finishWith(payload: Record<string, unknown>): never {
3711
+ hookDone = true;
3712
+ process.stdout.write(JSON.stringify(payload) + '\\n');
3713
+ process.exit(0);
3714
+ }
3390
3715
 
3391
- // Audit mode \u2014 warn but allow
3392
- dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3393
- 'Bash', repo, sessionId, config.captureDepth, {
3394
- command, reasoning: guardReason,
3395
- rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3396
- });
3397
- } else {
3398
- dispatchCapture(jwt, 'bash', 'pass', 'audit', verdict.category || 'clean',
3399
- 'Bash', repo, sessionId, config.captureDepth, {
3400
- command, reasoning: verdict.reason || 'no policy violations detected',
3401
- rulesChecked: config.rules, violatedRules: [],
3402
- });
3403
- }
3716
+ process.on('SIGTERM', () => finishAllow());
3404
3717
 
3405
- process.stdout.write('{}\\n');
3406
- return;
3407
- }
3718
+ const SHELL_TOOL_NAMES = new Set(['Bash', 'Shell', 'terminal', 'run_terminal_cmd', 'execute_command']);
3719
+ const READ_TOOL_NAMES = new Set(['Read', 'ReadFile', 'read_file']);
3720
+ const SEARCH_TOOL_NAMES = new Set(['Grep', 'grep_search', 'codebase_search', 'file_search']);
3721
+ const DIR_TOOL_NAMES = new Set(['Glob', 'list_dir']);
3722
+ const DELETE_TOOL_NAMES = new Set(['delete_file']);
3723
+ const BASH_PRE_TOOL_NAMES = new Set([...SHELL_TOOL_NAMES, ...READ_TOOL_NAMES, ...SEARCH_TOOL_NAMES, ...DIR_TOOL_NAMES, ...DELETE_TOOL_NAMES]);
3408
3724
 
3409
- // \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
3410
- const body = {
3411
- hook_event: 'PreToolUse',
3412
- tool_name: 'Bash',
3413
- tool_input: { command },
3414
- response_format: 'cursor',
3415
- session_id: sessionId || null,
3416
- cwd: cwd || null,
3417
- repo: repo || null,
3418
- };
3725
+ function extractCommand(payload: Record<string, unknown>): { command: string; toolName: string } {
3726
+ const direct = typeof payload.command === 'string' ? payload.command : '';
3727
+ if (direct) return { command: direct, toolName: 'Bash' };
3419
3728
 
3420
- const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 6000);
3729
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
3730
+ if (!BASH_PRE_TOOL_NAMES.has(toolName)) return { command: '', toolName };
3421
3731
 
3422
- if (!resp) {
3423
- log('bashGuard ' + cmdShort + ' \\u2192 error (timeout)');
3424
- process.stdout.write('{}\\n');
3425
- return;
3426
- }
3732
+ const toolInput = (payload.tool_input && typeof payload.tool_input === 'object')
3733
+ ? payload.tool_input as Record<string, unknown>
3734
+ : {};
3427
3735
 
3428
- if (resp.hook_response) {
3429
- process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
3430
- } else {
3431
- process.stdout.write('{}\\n');
3432
- }
3433
- } catch {
3434
- process.stdout.write('{}\\n');
3736
+ let command = '';
3737
+ if (SHELL_TOOL_NAMES.has(toolName)) {
3738
+ command = String(toolInput.command ?? '');
3739
+ } else if (READ_TOOL_NAMES.has(toolName)) {
3740
+ command = 'cat ' + String(toolInput.file_path ?? toolInput.path ?? '');
3741
+ } else if (SEARCH_TOOL_NAMES.has(toolName)) {
3742
+ command = "grep -r '" + String(toolInput.pattern ?? toolInput.query ?? '') + "' " + String(toolInput.path ?? '.');
3743
+ } else if (DIR_TOOL_NAMES.has(toolName)) {
3744
+ command = "find . -name '" + String(toolInput.pattern ?? toolInput.relative_workspace_path ?? '') + "'";
3745
+ } else if (DELETE_TOOL_NAMES.has(toolName)) {
3746
+ command = 'rm ' + String(toolInput.target_file ?? toolInput.file_path ?? toolInput.path ?? '');
3435
3747
  }
3748
+ return { command, toolName: toolName || 'Bash' };
3436
3749
  }
3437
3750
 
3438
- main();
3439
- `;
3440
- CURSOR_EDIT_PRECHECK_TS = `#!/usr/bin/env bun
3441
- import {
3442
- loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
3443
- parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
3444
- appendLocalTelemetry, log, GATEWAY_URL,
3445
- type HookConfig, type Rule,
3446
- } from './_synkro-common.ts';
3447
- import { basename } from 'node:path';
3448
-
3449
3751
  async function main() {
3450
3752
  try {
3451
3753
  const input = await readStdin();
3452
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3754
+ if (!input.trim()) finishAllow();
3453
3755
 
3454
- const payload = JSON.parse(input);
3455
- const toolName = payload.tool_name || '';
3456
- const toolInput = payload.tool_input || {};
3457
- const cwd = payload.cwd || '';
3458
- const sessionId = payload.conversation_id || '';
3756
+ const payload = JSON.parse(input) as Record<string, unknown>;
3757
+ const { command, toolName } = extractCommand(payload);
3758
+ if (!command) finishAllow();
3459
3759
 
3460
- const filePath = toolInput.file_path || toolInput.path || toolInput.target_file || '';
3461
- const content = toolInput.content || toolInput.new_string || toolInput.code_edit || '';
3462
- if (!filePath) { process.stdout.write('{}\\n'); return; }
3760
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
3761
+ const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
3463
3762
 
3464
- const fileShort = basename(filePath);
3465
- log('editGuard checking: ' + fileShort);
3763
+ if (isDuplicate(command, sessionId)) {
3764
+ log('bashGuard skip (dedup): ' + command.slice(0, 80));
3765
+ finishAllow();
3766
+ }
3466
3767
 
3768
+ const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
3769
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
3770
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
3771
+ const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
3467
3772
  const repo = detectRepo(cwd || '.');
3468
3773
 
3774
+ const cmdShort = command.slice(0, 80);
3775
+ log('bashGuard checking: ' + cmdShort);
3776
+
3469
3777
  let jwt = loadJwt();
3470
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3778
+ if (!jwt) finishAllow();
3471
3779
  jwt = await ensureFreshJwt(jwt);
3472
3780
 
3781
+ const transcript = extractTranscript(transcriptPath);
3782
+ const lastPrompt = readLastPrompt();
3783
+
3473
3784
  const config = await loadConfig(jwt);
3474
- if (config.silent) { process.stdout.write('{}\\n'); return; }
3785
+ if (config.silent) finishAllow();
3475
3786
 
3476
3787
  const rt = await route(config);
3477
3788
  const tagStr = tag(rt, config);
3478
3789
 
3479
3790
  if (rt === 'local') {
3480
- const contentShort = content.slice(0, 4000);
3481
3791
  const rulesBlock = config.rules.map((r: Rule, i: number) =>
3482
3792
  (i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
3483
3793
  ).join('\\n');
@@ -3486,93 +3796,107 @@ async function main() {
3486
3796
  'RULES:',
3487
3797
  rulesBlock || '(none)',
3488
3798
  '',
3489
- 'FILE: ' + filePath,
3799
+ 'COMMAND TO EVALUATE:',
3800
+ command,
3490
3801
  '',
3491
- 'CONTENT TO EVALUATE (first 4000 chars):',
3492
- contentShort,
3802
+ 'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
3803
+ 'Last user prompt: ' + (lastPrompt || 'none'),
3493
3804
  ].join('\\n');
3494
3805
 
3495
3806
  let gradeResp: string;
3496
3807
  try {
3497
- gradeResp = await localGrade('edit', graderPrompt);
3498
- } catch {
3499
- process.stdout.write('{}\\n');
3500
- return;
3808
+ gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS);
3809
+ } catch (e) {
3810
+ log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
3811
+ finishWith({ permission: 'allow' });
3501
3812
  }
3502
3813
 
3503
3814
  const verdict = parseVerdict(gradeResp);
3504
- const editContent = 'file=' + filePath + ' content=' + content.slice(0, 2000);
3505
3815
 
3506
3816
  if (!verdict.ok) {
3507
3817
  const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
3508
3818
  const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
3509
3819
 
3510
3820
  if (mode !== 'audit') {
3511
- dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
3512
- toolName || 'Edit', repo, sessionId, config.captureDepth, {
3513
- command: editContent, reasoning: guardReason,
3821
+ dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
3822
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
3823
+ command, reasoning: guardReason,
3514
3824
  rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3825
+ ccModel: model,
3515
3826
  });
3516
- const result = {
3827
+ finishWith({
3517
3828
  permission: 'deny',
3518
- user_message: tagStr + ' editGuard ' + fileShort + ' \\u2192 block: ' + guardReason,
3829
+ user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
3519
3830
  agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
3520
- };
3521
- process.stdout.write(JSON.stringify(result) + '\\n');
3522
- return;
3831
+ });
3523
3832
  }
3524
3833
 
3525
- // Audit mode
3526
- dispatchCapture(jwt, 'edit', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3527
- toolName || 'Edit', repo, sessionId, config.captureDepth, {
3528
- command: editContent, reasoning: guardReason,
3834
+ dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3835
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
3836
+ command, reasoning: guardReason,
3529
3837
  rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3838
+ ccModel: model,
3530
3839
  });
3840
+ log('bashGuard ' + cmdShort + ' \u2192 audit warning');
3841
+ finishWith({ permission: 'allow' });
3531
3842
  } else {
3532
- dispatchCapture(jwt, 'edit', 'pass', 'audit', verdict.category || 'trivial_edit',
3533
- toolName || 'Edit', repo, sessionId, config.captureDepth, {
3534
- command: editContent, reasoning: verdict.reason || 'no policy violations detected',
3843
+ dispatchCapture(jwt, 'bash', 'pass', 'audit', verdict.category || 'clean',
3844
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
3845
+ command, reasoning: verdict.reason || 'no policy violations detected',
3535
3846
  rulesChecked: config.rules, violatedRules: [],
3847
+ ccModel: model,
3536
3848
  });
3537
3849
  }
3538
3850
 
3539
- process.stdout.write('{}\\n');
3540
- return;
3851
+ const passReason = verdict.reason || 'no policy violations detected';
3852
+ log('bashGuard ' + cmdShort + ' \u2192 pass: ' + passReason);
3853
+ finishWith({ permission: 'allow' });
3541
3854
  }
3542
3855
 
3543
- // \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
3544
- const body = {
3856
+ const body: Record<string, any> = {
3545
3857
  hook_event: 'PreToolUse',
3546
- tool_name: toolName || 'Edit',
3547
- tool_input: { file_path: filePath, content },
3548
- file_path: filePath,
3549
- content,
3858
+ tool_name: toolName || 'Bash',
3859
+ tool_input: { command },
3550
3860
  response_format: 'cursor',
3861
+ user_intent: transcript.userIntent || null,
3862
+ last_user_message: lastPrompt || null,
3863
+ recent_user_messages: transcript.recentUserMessages,
3864
+ recent_messages: transcript.recentMessages,
3551
3865
  session_id: sessionId || null,
3552
3866
  cwd: cwd || null,
3553
3867
  repo: repo || null,
3554
3868
  };
3555
3869
 
3556
- const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 8000);
3870
+ const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, CURSOR_CLOUD_TIMEOUT_MS);
3557
3871
 
3558
3872
  if (!resp) {
3559
- log('editGuard ' + fileShort + ' \\u2192 error (timeout)');
3560
- process.stdout.write('{}\\n');
3561
- return;
3873
+ log('bashGuard ' + cmdShort + ' \u2192 pass (cloud timeout)');
3874
+ finishAllow();
3562
3875
  }
3563
3876
 
3564
3877
  if (resp.hook_response) {
3565
- process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
3566
- } else {
3567
- process.stdout.write('{}\\n');
3878
+ const hr = resp.hook_response as Record<string, unknown>;
3879
+ if (hr.permission === 'allow') {
3880
+ const um = String(hr.user_message || '');
3881
+ const am = String(hr.agent_message || um);
3882
+ if (um || am) {
3883
+ finishWith({ permission: 'allow' });
3884
+ }
3885
+ }
3886
+ finishWith(hr);
3568
3887
  }
3569
- } catch {
3570
- process.stdout.write('{}\\n');
3888
+ log('bashGuard ' + cmdShort + ' \u2192 pass (no hook_response)');
3889
+ finishAllow();
3890
+ } catch (e) {
3891
+ log('bashGuard error: ' + String(e));
3892
+ finishAllow();
3571
3893
  }
3572
3894
  }
3573
3895
 
3574
- main();
3575
- `;
3896
+ main().catch((e) => {
3897
+ log('bashGuard fatal: ' + String(e));
3898
+ finishAllow();
3899
+ });`;
3576
3900
  CURSOR_EDIT_CAPTURE_TS = `#!/usr/bin/env bun
3577
3901
  import {
3578
3902
  loadJwt, ensureFreshJwt, detectRepo, readStdin,
@@ -3582,26 +3906,37 @@ import { existsSync, readFileSync } from 'node:fs';
3582
3906
  import { basename, dirname, join } from 'node:path';
3583
3907
  import { homedir } from 'node:os';
3584
3908
 
3909
+ let hookDone = false;
3910
+
3911
+ function finish(): never {
3912
+ if (!hookDone) {
3913
+ hookDone = true;
3914
+ try { process.stdout.write('{}\\n'); } catch {}
3915
+ }
3916
+ process.exit(0);
3917
+ }
3918
+
3919
+ process.on('SIGTERM', () => finish());
3920
+
3585
3921
  async function main() {
3586
3922
  try {
3587
3923
  const input = await readStdin();
3588
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3924
+ if (!input.trim()) finish();
3589
3925
 
3590
3926
  const payload = JSON.parse(input);
3591
3927
  const filePath = payload.file_path || '';
3592
- if (!filePath) { process.stdout.write('{}\\n'); return; }
3928
+ if (!filePath) finish();
3593
3929
 
3594
- const cwd = payload.cwd || '';
3930
+ const cwd = payload.cwd || payload.workspace_roots?.[0] || '';
3595
3931
  const sessionId = payload.conversation_id || '';
3596
3932
  const repo = detectRepo(cwd || '.');
3597
3933
 
3598
3934
  log('editScan ' + basename(filePath));
3599
3935
 
3600
3936
  let jwt = loadJwt();
3601
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3937
+ if (!jwt) finish();
3602
3938
  jwt = await ensureFreshJwt(jwt);
3603
3939
 
3604
- // Read actual file content (up to 50KB)
3605
3940
  let fileContent = '';
3606
3941
  const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
3607
3942
  try {
@@ -3611,7 +3946,6 @@ async function main() {
3611
3946
  }
3612
3947
  } catch {}
3613
3948
 
3614
- // Walk up to find package.json dependencies
3615
3949
  let dependencies: Record<string, string> = {};
3616
3950
  let pkgDir = cwd || dirname(fullPath);
3617
3951
  while (pkgDir !== '/' && pkgDir !== '.') {
@@ -3638,12 +3972,10 @@ async function main() {
3638
3972
  if (cwd) captureBody.cwd = cwd;
3639
3973
  if (repo) captureBody.repo = repo;
3640
3974
 
3641
- // Check if local_only
3642
3975
  const rulesPath = join(homedir(), '.synkro', 'rules.json');
3643
3976
  if (existsSync(rulesPath)) {
3644
3977
  appendLocalTelemetry(captureBody);
3645
3978
  } else {
3646
- // Fire-and-forget to cloud
3647
3979
  fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3648
3980
  method: 'POST',
3649
3981
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
@@ -3653,157 +3985,17 @@ async function main() {
3653
3985
  appendLocalTelemetry(captureBody);
3654
3986
  }
3655
3987
 
3656
- process.stdout.write('{}\\n');
3657
- } catch {
3658
- process.stdout.write('{}\\n');
3659
- }
3660
- }
3661
-
3662
- main();
3663
- `;
3664
- CURSOR_BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
3665
- import {
3666
- loadJwt, readStdin, appendLocalTelemetry, log, GATEWAY_URL,
3667
- } from './_synkro-common.ts';
3668
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
3669
- import { join, dirname } from 'node:path';
3670
- import { createHash } from 'node:crypto';
3671
- import { homedir } from 'node:os';
3672
-
3673
- const CONSENT_FILE = join(homedir(), '.synkro', '.local-consent');
3674
-
3675
- function hashCmd(cmd: string): string {
3676
- return createHash('sha256').update(cmd).digest('hex').slice(0, 16);
3677
- }
3678
-
3679
- function consentGrant(sid: string, hash: string): void {
3680
- try {
3681
- const dir = dirname(CONSENT_FILE);
3682
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
3683
- appendFileSync(CONSENT_FILE, sid + '\\t' + hash + '\\tactive\\n', 'utf-8');
3684
- } catch {}
3685
- }
3686
-
3687
- function consentHasActive(sid: string, hash: string): boolean {
3688
- try {
3689
- if (!existsSync(CONSENT_FILE)) return false;
3690
- const content = readFileSync(CONSENT_FILE, 'utf-8');
3691
- return content.includes(sid + '\\t' + hash + '\\tactive');
3692
- } catch {
3693
- return false;
3694
- }
3695
- }
3696
-
3697
- function consentConsume(sid: string, hash: string): void {
3698
- try {
3699
- if (!existsSync(CONSENT_FILE)) return;
3700
- const content = readFileSync(CONSENT_FILE, 'utf-8');
3701
- const target = sid + '\\t' + hash + '\\tactive';
3702
- const replacement = sid + '\\t' + hash + '\\tconsumed';
3703
- const updated = content.split('\\n').map((l: string) => l === target ? replacement : l).join('\\n');
3704
- writeFileSync(CONSENT_FILE, updated, 'utf-8');
3705
- } catch {}
3706
- }
3707
-
3708
- async function main() {
3709
- try {
3710
- const input = await readStdin();
3711
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3712
-
3713
- const payload = JSON.parse(input);
3714
- const toolName = payload.tool_name || '';
3715
-
3716
- // Only process shell/bash tool types
3717
- const shellTools = ['Shell', 'Bash', 'terminal', 'run_terminal_cmd', 'execute_command'];
3718
- if (!shellTools.includes(toolName)) { process.stdout.write('{}\\n'); return; }
3719
-
3720
- const sessionId = payload.conversation_id || '';
3721
- const toolUseId = payload.tool_use_id || '';
3722
- const isError = payload.tool_result?.is_error === true;
3723
- const command = payload.tool_input?.command || '';
3724
- const cmdHash = command ? hashCmd(command) : '';
3725
-
3726
- // Consent tracking
3727
- if (cmdHash && sessionId) {
3728
- if (!isError) {
3729
- consentConsume(sessionId, cmdHash);
3730
- } else {
3731
- if (!consentHasActive(sessionId, cmdHash)) {
3732
- consentGrant(sessionId, cmdHash);
3733
- }
3734
- }
3735
- }
3736
-
3737
- // Build capture body
3738
- const captureBody: Record<string, any> = {
3739
- capture_type: 'bash_followup',
3740
- session_id: sessionId || null,
3741
- tool_use_id: toolUseId || null,
3742
- is_error: isError,
3743
- command_hash: cmdHash,
3744
- };
3745
-
3746
- // Check if local_only
3747
- const rulesPath = join(homedir(), '.synkro', 'rules.json');
3748
- if (existsSync(rulesPath)) {
3749
- appendLocalTelemetry(captureBody);
3750
- } else {
3751
- const jwt = loadJwt();
3752
- if (jwt && sessionId && toolUseId) {
3753
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3754
- method: 'POST',
3755
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3756
- body: JSON.stringify(captureBody),
3757
- signal: AbortSignal.timeout(3000),
3758
- }).catch(() => {});
3759
- }
3760
- appendLocalTelemetry(captureBody);
3761
- }
3762
-
3763
- process.stdout.write('{}\\n');
3764
- } catch {
3765
- process.stdout.write('{}\\n');
3766
- }
3767
- }
3768
-
3769
- main();
3770
- `;
3771
- CURSOR_SESSION_START_TS = `#!/usr/bin/env bun
3772
- import {
3773
- loadJwt, loadConfig, readStdin,
3774
- type HookConfig,
3775
- } from './_synkro-common.ts';
3776
-
3777
- async function main() {
3778
- try {
3779
- const input = await readStdin();
3780
-
3781
- let jwt = loadJwt();
3782
- const config: HookConfig = jwt ? await loadConfig(jwt) : {
3783
- captureDepth: 'local_only', tier: 'standard', silent: false,
3784
- policyName: '', rules: [], scanExemptions: [],
3785
- };
3786
-
3787
- const policyName = config.policyName || 'default';
3788
- const ruleCount = config.rules.length;
3789
- const mode = config.silent ? 'silent' : 'active';
3790
-
3791
- const context = [
3792
- 'This session is monitored by Synkro (' + mode + ' mode, policy: "' + policyName + '", ' + ruleCount + ' rules).',
3793
- 'Synkro enforces security and compliance rules on tool calls (shell commands, file edits).',
3794
- 'If a tool call is blocked, Synkro will explain which rule was violated and why.',
3795
- 'Do not suggest workarounds to bypass Synkro hooks \u2014 fix the underlying issue instead.',
3796
- ].join(' ');
3797
-
3798
- const result = { additional_context: context };
3799
- process.stdout.write(JSON.stringify(result) + '\\n');
3800
- } catch {
3801
- process.stdout.write('{}\\n');
3988
+ finish();
3989
+ } catch (e) {
3990
+ log('editScan error: ' + String(e));
3991
+ finish();
3802
3992
  }
3803
3993
  }
3804
3994
 
3805
- main();
3806
- `;
3995
+ main().catch((e) => {
3996
+ log('editScan fatal: ' + String(e));
3997
+ finish();
3998
+ });`;
3807
3999
  }
3808
4000
  });
3809
4001
 
@@ -6048,10 +6240,7 @@ function writeHookScripts() {
6048
6240
  const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
6049
6241
  const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
6050
6242
  const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
6051
- const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.ts");
6052
6243
  const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
6053
- const cursorBashFollowupPath = join11(HOOKS_DIR, "cursor-bash-followup.ts");
6054
- const cursorSessionStartPath = join11(HOOKS_DIR, "cursor-session-start.ts");
6055
6244
  const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
6056
6245
  writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
6057
6246
  writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
@@ -6067,10 +6256,7 @@ function writeHookScripts() {
6067
6256
  writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
6068
6257
  writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
6069
6258
  writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
6070
- writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_TS, "utf-8");
6071
6259
  writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
6072
- writeFileSync7(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_TS, "utf-8");
6073
- writeFileSync7(cursorSessionStartPath, CURSOR_SESSION_START_TS, "utf-8");
6074
6260
  writeFileSync7(mcpLocalServerPath, `#!/usr/bin/env bun
6075
6261
  /**
6076
6262
  * Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
@@ -6891,10 +7077,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
6891
7077
  chmodSync2(commonScriptPath, 493);
6892
7078
  chmodSync2(commonBashScriptPath, 493);
6893
7079
  chmodSync2(cursorBashJudgePath, 493);
6894
- chmodSync2(cursorEditPrecheckPath, 493);
6895
7080
  chmodSync2(cursorEditCapturePath, 493);
6896
- chmodSync2(cursorBashFollowupPath, 493);
6897
- chmodSync2(cursorSessionStartPath, 493);
6898
7081
  chmodSync2(mcpLocalServerPath, 493);
6899
7082
  return {
6900
7083
  bashScript: bashScriptPath,
@@ -6909,10 +7092,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
6909
7092
  transcriptSyncScript: transcriptSyncScriptPath,
6910
7093
  userPromptSubmitScript: userPromptSubmitScriptPath,
6911
7094
  cursorBashJudgeScript: cursorBashJudgePath,
6912
- cursorEditPrecheckScript: cursorEditPrecheckPath,
6913
7095
  cursorEditCaptureScript: cursorEditCapturePath,
6914
- cursorBashFollowupScript: cursorBashFollowupPath,
6915
- cursorSessionStartScript: cursorSessionStartPath,
6916
7096
  mcpLocalServerScript: mcpLocalServerPath
6917
7097
  };
6918
7098
  }
@@ -6945,7 +7125,7 @@ function writeConfigEnv(opts) {
6945
7125
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6946
7126
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6947
7127
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6948
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.67")}`
7128
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.69")}`
6949
7129
  ];
6950
7130
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6951
7131
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7396,10 +7576,17 @@ async function installCommand(opts = {}) {
7396
7576
  hasCursor = true;
7397
7577
  installCursorHooks(agent.settingsPath, {
7398
7578
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
7399
- editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
7400
7579
  editCaptureScriptPath: scripts.cursorEditCaptureScript,
7401
- bashFollowupScriptPath: scripts.cursorBashFollowupScript,
7402
- sessionStartScriptPath: scripts.cursorSessionStartScript
7580
+ bashFollowupScriptPath: scripts.bashFollowupScript,
7581
+ editPrecheckScriptPath: scripts.editPrecheckScript,
7582
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
7583
+ cvePrecheckScriptPath: scripts.cvePrecheckScript,
7584
+ planJudgeScriptPath: scripts.planJudgeScript,
7585
+ agentJudgeScriptPath: scripts.agentJudgeScript,
7586
+ stopSummaryScriptPath: scripts.stopSummaryScript,
7587
+ sessionStartScriptPath: scripts.sessionStartScript,
7588
+ userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
7589
+ transcriptSyncScriptPath: scripts.transcriptSyncScript
7403
7590
  });
7404
7591
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
7405
7592
  }
@@ -7460,6 +7647,36 @@ async function installCommand(opts = {}) {
7460
7647
  }
7461
7648
  }
7462
7649
  }
7650
+ if (hasCursor && !opts.noMcp) {
7651
+ try {
7652
+ if (useLocalMcp) {
7653
+ const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
7654
+ console.log(`Registered local MCP guardrails server in ${mcp.path}`);
7655
+ console.log(` url: ${mcp.url}`);
7656
+ } else {
7657
+ const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
7658
+ method: "POST",
7659
+ headers: {
7660
+ "Authorization": `Bearer ${token}`,
7661
+ "Content-Type": "application/json"
7662
+ },
7663
+ body: "{}"
7664
+ });
7665
+ if (!mintResp.ok) {
7666
+ const errText = await mintResp.text().catch(() => "");
7667
+ throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
7668
+ }
7669
+ const minted = await mintResp.json();
7670
+ const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
7671
+ console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
7672
+ console.log(` url: ${mcp.url}`);
7673
+ }
7674
+ console.log();
7675
+ } catch (err) {
7676
+ console.warn(` \u26A0 Cursor MCP registration failed: ${err.message}`);
7677
+ console.log();
7678
+ }
7679
+ }
7463
7680
  const priorLocalFlag = (() => {
7464
7681
  try {
7465
7682
  const content = readFileSync10(CONFIG_PATH3, "utf-8");
@@ -7971,10 +8188,20 @@ async function statusCommand() {
7971
8188
  const hooks = inspectCursorHooks(a.settingsPath);
7972
8189
  console.log(` hooks installed: ${hooks.installed ? "\u2713" : "\u2717"}`);
7973
8190
  if (hooks.installed) {
8191
+ console.log(` \u2022 sessionStart: ${hooks.sessionStart ? "\u2713" : "\u2717"}`);
8192
+ console.log(` \u2022 sessionEnd: ${hooks.sessionEnd ? "\u2713" : "\u2717"}`);
8193
+ console.log(` \u2022 beforeSubmitPrompt: ${hooks.beforeSubmitPrompt ? "\u2713" : "\u2717"}`);
7974
8194
  console.log(` \u2022 beforeShellExecution: ${hooks.beforeShellExecution ? "\u2713" : "\u2717"}`);
7975
- console.log(` \u2022 preToolUse: ${hooks.preToolUse ? "\u2713" : "\u2717"}`);
8195
+ console.log(` \u2022 afterShellExecution: ${hooks.afterShellExecution ? "\u2713" : "\u2717"}`);
8196
+ console.log(` \u2022 PreToolUse Bash: ${hooks.preToolUseBash ? "\u2713" : "\u2717"}`);
8197
+ console.log(` \u2022 PreToolUse Edit: ${hooks.preToolUseEdit ? "\u2713" : "\u2717"}`);
8198
+ console.log(` \u2022 PreToolUse CWE: ${hooks.preToolUseCwe ? "\u2713" : "\u2717"}`);
8199
+ console.log(` \u2022 PreToolUse CVE: ${hooks.preToolUseCve ? "\u2713" : "\u2717"}`);
8200
+ console.log(` \u2022 PreToolUse Agent: ${hooks.preToolUseAgent ? "\u2713" : "\u2717"}`);
8201
+ console.log(` \u2022 PreToolUse Plan: ${hooks.preToolUsePlan ? "\u2713" : "\u2717"}`);
7976
8202
  console.log(` \u2022 afterFileEdit: ${hooks.afterFileEdit ? "\u2713" : "\u2717"}`);
7977
8203
  console.log(` \u2022 postToolUse: ${hooks.postToolUse ? "\u2713" : "\u2717"}`);
8204
+ console.log(` \u2022 stop (transcript): ${hooks.stop ? "\u2713" : "\u2717"}`);
7978
8205
  }
7979
8206
  }
7980
8207
  }
@@ -7988,6 +8215,7 @@ async function statusCommand() {
7988
8215
  "cc-cwe-precheck.ts",
7989
8216
  "cc-cve-precheck.ts",
7990
8217
  "cc-plan-judge.ts",
8218
+ "cc-agent-judge.ts",
7991
8219
  "cc-stop-summary.ts",
7992
8220
  "cc-session-start.ts",
7993
8221
  "cc-transcript-sync.ts",
@@ -7995,10 +8223,19 @@ async function statusCommand() {
7995
8223
  "_synkro-common.ts"
7996
8224
  ];
7997
8225
  const cursorHooks = [
7998
- "cursor-bash-judge.sh",
7999
- "cursor-edit-precheck.sh",
8000
- "cursor-bash-followup.sh",
8001
- "_synkro-common.sh"
8226
+ "cursor-bash-judge.ts",
8227
+ "cursor-edit-capture.ts",
8228
+ "cc-edit-precheck.ts",
8229
+ "cc-cwe-precheck.ts",
8230
+ "cc-cve-precheck.ts",
8231
+ "cc-agent-judge.ts",
8232
+ "cc-plan-judge.ts",
8233
+ "cc-session-start.ts",
8234
+ "cc-stop-summary.ts",
8235
+ "cc-bash-followup.ts",
8236
+ "cc-user-prompt-submit.ts",
8237
+ "cc-transcript-sync.ts",
8238
+ "_synkro-common.ts"
8002
8239
  ];
8003
8240
  console.log("Hook scripts (Claude Code):");
8004
8241
  for (const f of ccHooks) {
@@ -9018,7 +9255,11 @@ function disconnectCommand(args2 = []) {
9018
9255
  }
9019
9256
  if (sawClaudeCode) {
9020
9257
  const mcpRemoved = uninstallMcpConfig();
9021
- console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
9258
+ console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (CC): ${mcpRemoved ? "removed from ~/.claude.json" : "no entry found"}`);
9259
+ }
9260
+ {
9261
+ const cursorMcpRemoved = uninstallCursorMcpConfig();
9262
+ console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
9022
9263
  }
9023
9264
  if (purge) {
9024
9265
  if (existsSync14(SYNKRO_DIR5)) {