@synkro-sh/cli 1.4.67 → 1.4.68

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|Grep|Glob",
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"
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"
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"
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"
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
 
@@ -702,7 +795,7 @@ synkro_post_with_retry() {
702
795
  });
703
796
 
704
797
  // 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;
798
+ 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
799
  var init_hookScriptsTs = __esm({
707
800
  "cli/installer/hookScriptsTs.ts"() {
708
801
  "use strict";
@@ -739,7 +832,17 @@ if (existsSync(CONFIG_PATH)) {
739
832
  } catch {}
740
833
  }
741
834
 
742
- export const GATEWAY_URL = process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh';
835
+ const ALLOWED_GATEWAY_HOSTS = new Set(['api.synkro.sh', 'localhost', '127.0.0.1']);
836
+ function validateGatewayUrl(raw: string): string {
837
+ try {
838
+ const u = new URL(raw);
839
+ if (!ALLOWED_GATEWAY_HOSTS.has(u.hostname)) return 'https://api.synkro.sh';
840
+ return raw.replace(/\\/+$/, '');
841
+ } catch {
842
+ return 'https://api.synkro.sh';
843
+ }
844
+ }
845
+ export const GATEWAY_URL = validateGatewayUrl(process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh');
743
846
  export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
744
847
  const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
745
848
 
@@ -1083,11 +1186,11 @@ async function channelGrade(role: GradeRole, prompt: string, jwt: string, port:
1083
1186
  return String(data.result || '');
1084
1187
  }
1085
1188
 
1086
- export async function localGrade(surface: string, prompt: string): Promise<string> {
1189
+ export async function localGrade(surface: string, prompt: string, timeoutMs = 20000): Promise<string> {
1087
1190
  if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1088
1191
  const jwt = loadJwt();
1089
1192
  if (!jwt) throw new Error('NO_JWT');
1090
- return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929);
1193
+ return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929, timeoutMs);
1091
1194
  }
1092
1195
 
1093
1196
  export async function localGradeCwe(prompt: string): Promise<string> {
@@ -1101,6 +1204,7 @@ export async function localGradeCwe(prompt: string): Promise<string> {
1101
1204
  export interface Verdict {
1102
1205
  ok: boolean;
1103
1206
  reason: string;
1207
+ suggestedFix: string;
1104
1208
  ruleId: string;
1105
1209
  ruleMode: string;
1106
1210
  severity: string;
@@ -1111,6 +1215,7 @@ export function parseVerdict(resp: string): Verdict {
1111
1215
  const verdict: Verdict = {
1112
1216
  ok: true,
1113
1217
  reason: '',
1218
+ suggestedFix: '',
1114
1219
  ruleId: '',
1115
1220
  ruleMode: '',
1116
1221
  severity: 'low',
@@ -1129,6 +1234,9 @@ export function parseVerdict(resp: string): Verdict {
1129
1234
  const reasonMatch = inner.match(/<reason>(.*?)<\\/reason>/) || inner.match(/<reasoning>(.*?)<\\/reasoning>/);
1130
1235
  if (reasonMatch) verdict.reason = reasonMatch[1].trim();
1131
1236
 
1237
+ const fixMatch = inner.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
1238
+ if (fixMatch) verdict.suggestedFix = fixMatch[1].trim();
1239
+
1132
1240
  if (!verdict.ok) {
1133
1241
  const ruleIdMatch = inner.match(/<rule_id>(.*?)<\\/rule_id>/);
1134
1242
  const ruleModeMatch = inner.match(/<rule_mode>(.*?)<\\/rule_mode>/);
@@ -1147,6 +1255,10 @@ export function parseVerdict(resp: string): Verdict {
1147
1255
  const vReason = vBlock.match(/<reason>(.*?)<\\/reason>/);
1148
1256
  if (vReason) verdict.reason = vReason[1].trim();
1149
1257
  }
1258
+ if (!verdict.suggestedFix) {
1259
+ const vFix = vBlock.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
1260
+ if (vFix) verdict.suggestedFix = vFix[1].trim();
1261
+ }
1150
1262
  if (!sevMatch) {
1151
1263
  const vSev = vBlock.match(/<severity>(.*?)<\\/severity>/);
1152
1264
  if (vSev) verdict.severity = vSev[1].trim();
@@ -1301,8 +1413,10 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
1301
1413
  }
1302
1414
  case 'NotebookEdit':
1303
1415
  return toolInput.new_source || '';
1416
+ case 'StrReplace':
1417
+ return toolInput.new_string || toolInput.content || toolInput.code_edit || '';
1304
1418
  default:
1305
- return '';
1419
+ return toolInput.content || toolInput.new_string || toolInput.code_edit || '';
1306
1420
  }
1307
1421
  }
1308
1422
 
@@ -1616,13 +1730,90 @@ export function dispatchFinding(
1616
1730
  }).catch(() => {});
1617
1731
  }
1618
1732
 
1733
+ // \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
1734
+
1735
+ export const EDIT_TOOL_NAMES = new Set([
1736
+ 'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'StrReplace',
1737
+ ]);
1738
+ export const SHELL_TOOL_NAMES = new Set([
1739
+ 'Bash', 'Shell', 'Read', 'Grep', 'Glob', 'terminal', 'run_terminal_cmd', 'execute_command',
1740
+ ]);
1741
+ export const AGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
1742
+ export const PLAN_TOOL_NAMES = new Set(['ExitPlanMode', 'SwitchMode', 'CreatePlan']);
1743
+
1744
+ export function isEditTool(toolName: string): boolean {
1745
+ return EDIT_TOOL_NAMES.has(toolName);
1746
+ }
1747
+ export function isShellTool(toolName: string): boolean {
1748
+ return SHELL_TOOL_NAMES.has(toolName);
1749
+ }
1750
+ export function isAgentTool(toolName: string): boolean {
1751
+ return AGENT_TOOL_NAMES.has(toolName);
1752
+ }
1753
+ export function isPlanTool(toolName: string): boolean {
1754
+ return PLAN_TOOL_NAMES.has(toolName);
1755
+ }
1756
+
1757
+ export function hookSessionId(payload: Record<string, unknown>): string {
1758
+ return String(payload.session_id ?? payload.conversation_id ?? '');
1759
+ }
1760
+
1761
+ export function isCursorHookFormat(): boolean {
1762
+ return process.env.SYNKRO_HOOK_FORMAT === 'cursor';
1763
+ }
1764
+
1765
+ let cursorHookExited = false;
1766
+
1767
+ export function setupCursorHookSignals(): void {
1768
+ if (!isCursorHookFormat()) return;
1769
+ process.on('SIGTERM', () => outputEmpty());
1770
+ }
1771
+
1772
+ function cursorHookExit(): never {
1773
+ cursorHookExited = true;
1774
+ process.exit(0);
1775
+ }
1776
+
1619
1777
  // \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
1620
1778
 
1621
1779
  export function outputJson(obj: any): void {
1780
+ if (isCursorHookFormat()) {
1781
+ const hso = obj?.hookSpecificOutput;
1782
+ const sys = typeof obj?.systemMessage === 'string' ? obj.systemMessage : '';
1783
+ if (hso?.permissionDecision === 'deny') {
1784
+ const reason = hso.permissionDecisionReason || hso.additionalContext || sys;
1785
+ if (!cursorHookExited) {
1786
+ cursorHookExited = true;
1787
+ process.stdout.write(JSON.stringify({
1788
+ permission: 'deny',
1789
+ user_message: sys || reason,
1790
+ agent_message: hso.additionalContext || reason,
1791
+ }) + '\\n');
1792
+ }
1793
+ cursorHookExit();
1794
+ }
1795
+ const ctx = sys || hso?.additionalContext;
1796
+ if (ctx) {
1797
+ if (!cursorHookExited) {
1798
+ cursorHookExited = true;
1799
+ process.stdout.write(JSON.stringify({ additional_context: ctx }) + '\\n');
1800
+ }
1801
+ cursorHookExit();
1802
+ }
1803
+ outputEmpty();
1804
+ return;
1805
+ }
1622
1806
  console.log(JSON.stringify(obj));
1623
1807
  }
1624
1808
 
1625
1809
  export function outputEmpty(): void {
1810
+ if (isCursorHookFormat()) {
1811
+ if (!cursorHookExited) {
1812
+ cursorHookExited = true;
1813
+ try { process.stdout.write('{}\\n'); } catch {}
1814
+ }
1815
+ cursorHookExit();
1816
+ }
1626
1817
  console.log('{}');
1627
1818
  }
1628
1819
  `;
@@ -1631,26 +1822,27 @@ import {
1631
1822
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
1632
1823
  parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
1633
1824
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
1634
- outputJson, outputEmpty, GATEWAY_URL,
1825
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
1635
1826
  type HookConfig, type Rule,
1636
1827
  } from './_synkro-common.ts';
1637
1828
  import { existsSync, readFileSync } from 'node:fs';
1638
1829
  import { basename, dirname, join } from 'node:path';
1639
1830
 
1640
1831
  async function main() {
1832
+ setupCursorHookSignals();
1641
1833
  try {
1642
1834
  const input = await readStdin();
1643
1835
  if (!input.trim()) { outputEmpty(); return; }
1644
1836
 
1645
1837
  const payload = JSON.parse(input);
1646
1838
  const toolName = payload.tool_name || '';
1647
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
1839
+ if (!isEditTool(toolName)) {
1648
1840
  outputEmpty();
1649
1841
  return;
1650
1842
  }
1651
1843
 
1652
1844
  const toolInput = payload.tool_input || {};
1653
- const sessionId = payload.session_id || '';
1845
+ const sessionId = hookSessionId(payload);
1654
1846
  const toolUseId = payload.tool_use_id || '';
1655
1847
  const cwd = payload.cwd || '';
1656
1848
  const permissionMode = payload.permission_mode || '';
@@ -1841,24 +2033,25 @@ main();
1841
2033
  import {
1842
2034
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
1843
2035
  localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
1844
- outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
2036
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
1845
2037
  } from './_synkro-common.ts';
1846
2038
  import { basename, extname } from 'node:path';
1847
2039
 
1848
2040
  async function main() {
2041
+ setupCursorHookSignals();
1849
2042
  try {
1850
2043
  const input = await readStdin();
1851
2044
  if (!input.trim()) { outputEmpty(); return; }
1852
2045
 
1853
2046
  const payload = JSON.parse(input);
1854
2047
  const toolName = payload.tool_name || '';
1855
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
2048
+ if (!isEditTool(toolName)) {
1856
2049
  outputEmpty();
1857
2050
  return;
1858
2051
  }
1859
2052
 
1860
2053
  const toolInput = payload.tool_input || {};
1861
- const sessionId = payload.session_id || '';
2054
+ const sessionId = hookSessionId(payload);
1862
2055
  const cwd = payload.cwd || '';
1863
2056
  const gitRepo = detectRepo(cwd || '.');
1864
2057
 
@@ -1959,6 +2152,12 @@ async function main() {
1959
2152
  if (id && !cweIds.includes(id)) cweIds.push(id);
1960
2153
  }
1961
2154
 
2155
+ const fixMatches = gradeResp.match(/<suggested_fix>([^<]+)<\\/suggested_fix>/g) || [];
2156
+ const fixes: Record<string, string> = {};
2157
+ for (let i = 0; i < Math.min(cweIds.length, fixMatches.length); i++) {
2158
+ fixes[cweIds[i]] = fixMatches[i].replace(/<\\/?suggested_fix>/g, '').trim();
2159
+ }
2160
+
1962
2161
  // Filter out exempted CWEs for this file
1963
2162
  const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
1964
2163
 
@@ -1977,7 +2176,11 @@ async function main() {
1977
2176
  const label = count === 1 ? 'match' : 'matches';
1978
2177
  const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
1979
2178
  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.';
2179
+ const fixLines = activeCweIds
2180
+ .filter(id => fixes[id])
2181
+ .map(id => '[' + id + '] Fix: ' + fixes[id]);
2182
+ const fixHint = fixLines.length > 0 ? '\\n' + fixLines.join('\\n') : '';
2183
+ 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
2184
 
1982
2185
  for (const cweId of activeCweIds) {
1983
2186
  dispatchFinding(jwt, {
@@ -2026,7 +2229,7 @@ main();
2026
2229
  import {
2027
2230
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
2028
2231
  reconstructContent, readStdin, findNearestDeps, log,
2029
- outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
2232
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
2030
2233
  } from './_synkro-common.ts';
2031
2234
  import { basename } from 'node:path';
2032
2235
 
@@ -2044,19 +2247,20 @@ function isManifest(filename: string): boolean {
2044
2247
  }
2045
2248
 
2046
2249
  async function main() {
2250
+ setupCursorHookSignals();
2047
2251
  try {
2048
2252
  const input = await readStdin();
2049
2253
  if (!input.trim()) { outputEmpty(); return; }
2050
2254
 
2051
2255
  const payload = JSON.parse(input);
2052
2256
  const toolName = payload.tool_name || '';
2053
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
2257
+ if (!isEditTool(toolName)) {
2054
2258
  outputEmpty();
2055
2259
  return;
2056
2260
  }
2057
2261
 
2058
2262
  const toolInput = payload.tool_input || {};
2059
- const sessionId = payload.session_id || '';
2263
+ const sessionId = hookSessionId(payload);
2060
2264
  const cwd = payload.cwd || '';
2061
2265
 
2062
2266
  const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
@@ -2173,7 +2377,7 @@ import {
2173
2377
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2174
2378
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
2175
2379
  extractTranscript, readLastPrompt, log,
2176
- outputJson, outputEmpty, GATEWAY_URL,
2380
+ outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
2177
2381
  type HookConfig, type Rule,
2178
2382
  } from './_synkro-common.ts';
2179
2383
 
@@ -2249,19 +2453,20 @@ interface PkgMeta {
2249
2453
  }
2250
2454
 
2251
2455
  async function main() {
2456
+ setupCursorHookSignals();
2252
2457
  try {
2253
2458
  const input = await readStdin();
2254
2459
  if (!input.trim()) { outputEmpty(); return; }
2255
2460
 
2256
2461
  const payload = JSON.parse(input);
2257
2462
  const toolName = payload.tool_name || '';
2258
- if (!['Bash', 'Read', 'Grep', 'Glob'].includes(toolName)) {
2463
+ if (!isShellTool(toolName)) {
2259
2464
  outputEmpty();
2260
2465
  return;
2261
2466
  }
2262
2467
 
2263
2468
  const toolInput = payload.tool_input || {};
2264
- const sessionId = payload.session_id || '';
2469
+ const sessionId = hookSessionId(payload);
2265
2470
  const toolUseId = payload.tool_use_id || '';
2266
2471
  const cwd = payload.cwd || '';
2267
2472
  const permissionMode = payload.permission_mode || '';
@@ -2270,7 +2475,12 @@ async function main() {
2270
2475
 
2271
2476
  let command = '';
2272
2477
  switch (toolName) {
2273
- case 'Bash': command = toolInput.command || ''; break;
2478
+ case 'Bash':
2479
+ case 'Shell':
2480
+ case 'terminal':
2481
+ case 'run_terminal_cmd':
2482
+ case 'execute_command':
2483
+ command = toolInput.command || ''; break;
2274
2484
  case 'Read': command = 'cat ' + (toolInput.file_path || ''); break;
2275
2485
  case 'Grep': command = "grep -r '" + (toolInput.pattern || '') + "' " + (toolInput.path || '.'); break;
2276
2486
  case 'Glob': command = "find . -name '" + (toolInput.pattern || '') + "'"; break;
@@ -2601,24 +2811,25 @@ import {
2601
2811
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2602
2812
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
2603
2813
  extractTranscript, readLastPrompt, log,
2604
- outputJson, outputEmpty, GATEWAY_URL,
2814
+ outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
2605
2815
  type HookConfig, type Rule,
2606
2816
  } from './_synkro-common.ts';
2607
2817
 
2608
2818
  async function main() {
2819
+ setupCursorHookSignals();
2609
2820
  try {
2610
2821
  const input = await readStdin();
2611
2822
  if (!input.trim()) { outputEmpty(); return; }
2612
2823
 
2613
2824
  const payload = JSON.parse(input);
2614
2825
  const toolName = payload.tool_name || '';
2615
- if (toolName !== 'Agent') {
2826
+ if (!isAgentTool(toolName)) {
2616
2827
  outputEmpty();
2617
2828
  return;
2618
2829
  }
2619
2830
 
2620
2831
  const toolInput = payload.tool_input || {};
2621
- const sessionId = payload.session_id || '';
2832
+ const sessionId = hookSessionId(payload);
2622
2833
  const toolUseId = payload.tool_use_id || '';
2623
2834
  const cwd = payload.cwd || '';
2624
2835
  const permissionMode = payload.permission_mode || '';
@@ -2764,14 +2975,13 @@ main();
2764
2975
  import {
2765
2976
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2766
2977
  parseVerdict, dispatchCapture, postWithRetry, readStdin, log,
2767
- outputJson, outputEmpty, GATEWAY_URL,
2978
+ outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
2768
2979
  } from './_synkro-common.ts';
2769
2980
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
2770
2981
  import { join } from 'node:path';
2771
2982
  import { homedir } from 'node:os';
2772
2983
 
2773
- function findLatestPlan(): string | null {
2774
- const plansDir = join(homedir(), '.claude', 'plans');
2984
+ function findLatestPlanInDir(plansDir: string): string | null {
2775
2985
  if (!existsSync(plansDir)) return null;
2776
2986
  try {
2777
2987
  const files = readdirSync(plansDir)
@@ -2784,6 +2994,23 @@ function findLatestPlan(): string | null {
2784
2994
  }
2785
2995
  }
2786
2996
 
2997
+ function findLatestPlan(): string | null {
2998
+ const dirs = [
2999
+ join(homedir(), '.claude', 'plans'),
3000
+ join(homedir(), '.cursor', 'plans'),
3001
+ ];
3002
+ let best: { path: string; mtime: number } | null = null;
3003
+ for (const dir of dirs) {
3004
+ const p = findLatestPlanInDir(dir);
3005
+ if (!p) continue;
3006
+ try {
3007
+ const mtime = statSync(p).mtimeMs;
3008
+ if (!best || mtime > best.mtime) best = { path: p, mtime };
3009
+ } catch {}
3010
+ }
3011
+ return best?.path ?? null;
3012
+ }
3013
+
2787
3014
  function appendReviewToPlan(planFile: string, verdict: string): void {
2788
3015
  try {
2789
3016
  let content = readFileSync(planFile, 'utf-8');
@@ -2795,20 +3022,21 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
2795
3022
  }
2796
3023
 
2797
3024
  async function main() {
3025
+ setupCursorHookSignals();
2798
3026
  try {
2799
3027
  const input = await readStdin();
2800
3028
  if (!input.trim()) { outputEmpty(); return; }
2801
3029
 
2802
3030
  const payload = JSON.parse(input);
2803
3031
  const toolName = payload.tool_name || '';
2804
- if (toolName !== 'ExitPlanMode') { outputEmpty(); return; }
3032
+ if (!isPlanTool(toolName)) { outputEmpty(); return; }
2805
3033
 
2806
3034
  const planFile = findLatestPlan();
2807
3035
  if (!planFile) { outputEmpty(); return; }
2808
3036
  const plan = readFileSync(planFile, 'utf-8');
2809
3037
  if (plan.length < 20) { outputEmpty(); return; }
2810
3038
 
2811
- const sessionId = payload.session_id || '';
3039
+ const sessionId = hookSessionId(payload);
2812
3040
  const cwd = payload.cwd || '';
2813
3041
  const gitRepo = detectRepo(cwd || '.');
2814
3042
 
@@ -2913,16 +3141,17 @@ main();
2913
3141
  STOP_SUMMARY_TS = `#!/usr/bin/env bun
2914
3142
  import {
2915
3143
  loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
2916
- outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
3144
+ outputJson, outputEmpty, appendLocalTelemetry, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
2917
3145
  } from './_synkro-common.ts';
2918
3146
 
2919
3147
  async function main() {
3148
+ setupCursorHookSignals();
2920
3149
  try {
2921
3150
  const input = await readStdin();
2922
3151
  if (!input.trim()) { outputEmpty(); return; }
2923
3152
 
2924
3153
  const payload = JSON.parse(input);
2925
- const sessionId = payload.session_id || '';
3154
+ const sessionId = hookSessionId(payload);
2926
3155
  if (!sessionId) { outputEmpty(); return; }
2927
3156
 
2928
3157
  const cwd = payload.cwd || '';
@@ -3000,18 +3229,19 @@ main();
3000
3229
  SESSION_START_TS = `#!/usr/bin/env bun
3001
3230
  import {
3002
3231
  loadJwt, detectRepo, channelUp, tag, readStdin,
3003
- outputJson, outputEmpty, GATEWAY_URL,
3232
+ outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3004
3233
  type HookConfig,
3005
3234
  } from './_synkro-common.ts';
3006
3235
 
3007
3236
  async function main() {
3237
+ setupCursorHookSignals();
3008
3238
  try {
3009
3239
  const input = await readStdin();
3010
3240
  if (!input.trim()) { outputEmpty(); return; }
3011
3241
 
3012
3242
  const payload = JSON.parse(input);
3013
3243
  const cwd = payload.cwd || '';
3014
- const sessionId = payload.session_id || '';
3244
+ const sessionId = hookSessionId(payload);
3015
3245
  const gitRepo = detectRepo(cwd || '.');
3016
3246
 
3017
3247
  let jwt = loadJwt();
@@ -3064,27 +3294,33 @@ main();
3064
3294
  BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
3065
3295
  import {
3066
3296
  loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
3067
- outputEmpty, appendLocalTelemetry, GATEWAY_URL,
3297
+ outputEmpty, appendLocalTelemetry, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
3068
3298
  } from './_synkro-common.ts';
3069
3299
 
3070
3300
  async function main() {
3301
+ setupCursorHookSignals();
3071
3302
  try {
3072
3303
  const input = await readStdin();
3073
3304
  if (!input.trim()) { outputEmpty(); return; }
3074
3305
 
3075
3306
  const payload = JSON.parse(input);
3076
3307
  const toolName = payload.tool_name || '';
3077
- if (toolName !== 'Bash') { outputEmpty(); return; }
3308
+ const shellCmd = typeof payload.command === 'string' ? payload.command : (payload.tool_input?.command || '');
3309
+ if (!isShellTool(toolName) && !shellCmd) { outputEmpty(); return; }
3078
3310
 
3079
3311
  const jwt = loadJwt();
3080
3312
  if (!jwt) { outputEmpty(); return; }
3081
3313
 
3082
- const sessionId = payload.session_id || '';
3083
- const toolUseId = payload.tool_use_id || '';
3084
- if (!sessionId || !toolUseId) { outputEmpty(); return; }
3314
+ const sessionId = hookSessionId(payload);
3315
+ const toolUseId = payload.tool_use_id || payload.tool_call_id || 'cursor-shell';
3316
+ if (!sessionId) { outputEmpty(); return; }
3085
3317
 
3086
- const isError = payload.tool_result?.is_error === true;
3087
- const cmd = payload.tool_input?.command || '';
3318
+ let isError = payload.tool_result?.is_error === true;
3319
+ try {
3320
+ const out = JSON.parse(payload.tool_output || '{}');
3321
+ if (out.exitCode !== 0 || out.is_error === true) isError = true;
3322
+ } catch {}
3323
+ const cmd = shellCmd;
3088
3324
  const cmdHash = cmd ? hashCommand(cmd) : '';
3089
3325
 
3090
3326
  if (cmdHash && sessionId) {
@@ -3128,19 +3364,20 @@ main();
3128
3364
  TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
3129
3365
  import {
3130
3366
  loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
3131
- outputEmpty, GATEWAY_URL,
3367
+ outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3132
3368
  } from './_synkro-common.ts';
3133
3369
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3134
3370
  import { join, dirname } from 'node:path';
3135
3371
  import { homedir } from 'node:os';
3136
3372
 
3137
3373
  async function main() {
3374
+ setupCursorHookSignals();
3138
3375
  try {
3139
3376
  const input = await readStdin();
3140
3377
  if (!input.trim()) { outputEmpty(); return; }
3141
3378
 
3142
3379
  const payload = JSON.parse(input);
3143
- const sessionId = payload.session_id || '';
3380
+ const sessionId = hookSessionId(payload);
3144
3381
  const transcriptPath = payload.transcript_path || '';
3145
3382
  const cwd = payload.cwd || '';
3146
3383
 
@@ -3268,15 +3505,16 @@ async function main() {
3268
3505
  main();
3269
3506
  `;
3270
3507
  USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
3271
- import { readStdin, appendLocalTelemetry, aggregateUsage } from './_synkro-common.ts';
3508
+ import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId } from './_synkro-common.ts';
3272
3509
  import { writeFileSync, mkdirSync } from 'node:fs';
3273
3510
  import { join, dirname } from 'node:path';
3274
3511
  import { homedir } from 'node:os';
3275
3512
 
3276
3513
  async function main() {
3514
+ setupCursorHookSignals();
3277
3515
  try {
3278
3516
  const input = await readStdin();
3279
- if (!input.trim()) return;
3517
+ if (!input.trim()) { outputEmpty(); return; }
3280
3518
  const payload = JSON.parse(input);
3281
3519
  const msg = payload.message || payload.prompt || payload.content || '';
3282
3520
  if (msg) {
@@ -3285,7 +3523,7 @@ async function main() {
3285
3523
  writeFileSync(promptFile, msg, 'utf-8');
3286
3524
  }
3287
3525
 
3288
- const sessionId = payload.session_id || '';
3526
+ const sessionId = hookSessionId(payload);
3289
3527
  const transcriptPath = payload.transcript_path || '';
3290
3528
  if (sessionId && transcriptPath) {
3291
3529
  const usage = aggregateUsage(transcriptPath);
@@ -3306,7 +3544,10 @@ async function main() {
3306
3544
  });
3307
3545
  }
3308
3546
  }
3309
- } catch {}
3547
+ outputEmpty();
3548
+ } catch {
3549
+ outputEmpty();
3550
+ }
3310
3551
  }
3311
3552
 
3312
3553
  main();
@@ -3315,38 +3556,99 @@ main();
3315
3556
  import {
3316
3557
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
3317
3558
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
3318
- appendLocalTelemetry, log, GATEWAY_URL,
3319
- type HookConfig, type Rule,
3559
+ extractTranscript, readLastPrompt, log, GATEWAY_URL,
3560
+ type Rule,
3320
3561
  } from './_synkro-common.ts';
3321
3562
 
3563
+ // Cursor beforeShellExecution timeout is 15s; stay under it (JWT refresh + grade).
3564
+ const CURSOR_GRADE_TIMEOUT_MS = 7500;
3565
+ const CURSOR_CLOUD_TIMEOUT_MS = 6000;
3566
+
3567
+ let hookDone = false;
3568
+
3569
+ function finishAllow(): never {
3570
+ if (!hookDone) {
3571
+ hookDone = true;
3572
+ try { process.stdout.write('{}\\n'); } catch {}
3573
+ }
3574
+ process.exit(0);
3575
+ }
3576
+
3577
+ function finishWith(payload: Record<string, unknown>): never {
3578
+ hookDone = true;
3579
+ process.stdout.write(JSON.stringify(payload) + '\\n');
3580
+ process.exit(0);
3581
+ }
3582
+
3583
+ process.on('SIGTERM', () => finishAllow());
3584
+
3585
+ const SHELL_TOOL_NAMES = new Set(['Bash', 'Shell', 'terminal', 'run_terminal_cmd', 'execute_command']);
3586
+ const BASH_PRE_TOOL_NAMES = new Set(['Bash', 'Shell', 'Read', 'Grep', 'Glob', ...SHELL_TOOL_NAMES]);
3587
+
3588
+ function extractCommand(payload: Record<string, unknown>): { command: string; toolName: string } {
3589
+ const direct = typeof payload.command === 'string' ? payload.command : '';
3590
+ if (direct) return { command: direct, toolName: 'Bash' };
3591
+
3592
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
3593
+ if (!BASH_PRE_TOOL_NAMES.has(toolName)) return { command: '', toolName };
3594
+
3595
+ const toolInput = (payload.tool_input && typeof payload.tool_input === 'object')
3596
+ ? payload.tool_input as Record<string, unknown>
3597
+ : {};
3598
+
3599
+ let command = '';
3600
+ switch (toolName) {
3601
+ case 'Bash':
3602
+ case 'Shell':
3603
+ case 'terminal':
3604
+ case 'run_terminal_cmd':
3605
+ case 'execute_command':
3606
+ command = String(toolInput.command ?? '');
3607
+ break;
3608
+ case 'Read':
3609
+ command = 'cat ' + String(toolInput.file_path ?? toolInput.path ?? '');
3610
+ break;
3611
+ case 'Grep':
3612
+ command = "grep -r '" + String(toolInput.pattern ?? '') + "' " + String(toolInput.path ?? '.');
3613
+ break;
3614
+ case 'Glob':
3615
+ command = "find . -name '" + String(toolInput.pattern ?? '') + "'";
3616
+ break;
3617
+ }
3618
+ return { command, toolName: toolName || 'Bash' };
3619
+ }
3620
+
3322
3621
  async function main() {
3323
3622
  try {
3324
3623
  const input = await readStdin();
3325
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3624
+ if (!input.trim()) finishAllow();
3326
3625
 
3327
- const payload = JSON.parse(input);
3328
- const command = payload.command || '';
3329
- if (!command) { process.stdout.write('{}\\n'); return; }
3626
+ const payload = JSON.parse(input) as Record<string, unknown>;
3627
+ const { command, toolName } = extractCommand(payload);
3628
+ if (!command) finishAllow();
3330
3629
 
3331
- const cwd = payload.cwd || '';
3332
- const sessionId = payload.conversation_id || '';
3630
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
3631
+ const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
3632
+ const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
3333
3633
  const repo = detectRepo(cwd || '.');
3334
3634
 
3335
3635
  const cmdShort = command.slice(0, 80);
3336
3636
  log('bashGuard checking: ' + cmdShort);
3337
3637
 
3338
3638
  let jwt = loadJwt();
3339
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3639
+ if (!jwt) finishAllow();
3340
3640
  jwt = await ensureFreshJwt(jwt);
3341
3641
 
3642
+ const transcript = extractTranscript(transcriptPath);
3643
+ const lastPrompt = readLastPrompt();
3644
+
3342
3645
  const config = await loadConfig(jwt);
3343
- if (config.silent) { process.stdout.write('{}\\n'); return; }
3646
+ if (config.silent) finishAllow();
3344
3647
 
3345
3648
  const rt = await route(config);
3346
3649
  const tagStr = tag(rt, config);
3347
3650
 
3348
3651
  if (rt === 'local') {
3349
- // Build grading prompt with rules
3350
3652
  const rulesBlock = config.rules.map((r: Rule, i: number) =>
3351
3653
  (i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
3352
3654
  ).join('\\n');
@@ -3357,14 +3659,17 @@ async function main() {
3357
3659
  '',
3358
3660
  'COMMAND TO EVALUATE:',
3359
3661
  command,
3662
+ '',
3663
+ 'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
3664
+ 'Last user prompt: ' + (lastPrompt || 'none'),
3360
3665
  ].join('\\n');
3361
3666
 
3362
3667
  let gradeResp: string;
3363
3668
  try {
3364
- gradeResp = await localGrade('bash', graderPrompt);
3365
- } catch {
3366
- process.stdout.write('{}\\n');
3367
- return;
3669
+ gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS);
3670
+ } catch (e) {
3671
+ log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
3672
+ finishAllow();
3368
3673
  }
3369
3674
 
3370
3675
  const verdict = parseVerdict(gradeResp);
@@ -3379,16 +3684,13 @@ async function main() {
3379
3684
  command, reasoning: guardReason,
3380
3685
  rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3381
3686
  });
3382
- const result = {
3687
+ finishWith({
3383
3688
  permission: 'deny',
3384
- user_message: tagStr + ' bashGuard \\u2192 block: ' + guardReason,
3689
+ user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
3385
3690
  agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
3386
- };
3387
- process.stdout.write(JSON.stringify(result) + '\\n');
3388
- return;
3691
+ });
3389
3692
  }
3390
3693
 
3391
- // Audit mode \u2014 warn but allow
3392
3694
  dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3393
3695
  'Bash', repo, sessionId, config.captureDepth, {
3394
3696
  command, reasoning: guardReason,
@@ -3402,177 +3704,46 @@ async function main() {
3402
3704
  });
3403
3705
  }
3404
3706
 
3405
- process.stdout.write('{}\\n');
3406
- return;
3707
+ log('bashGuard ' + cmdShort + ' \u2192 pass');
3708
+ finishAllow();
3407
3709
  }
3408
3710
 
3409
- // \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
3410
- const body = {
3711
+ const body: Record<string, any> = {
3411
3712
  hook_event: 'PreToolUse',
3412
- tool_name: 'Bash',
3713
+ tool_name: toolName || 'Bash',
3413
3714
  tool_input: { command },
3414
3715
  response_format: 'cursor',
3716
+ user_intent: transcript.userIntent || null,
3717
+ last_user_message: lastPrompt || null,
3718
+ recent_user_messages: transcript.recentUserMessages,
3719
+ recent_messages: transcript.recentMessages,
3415
3720
  session_id: sessionId || null,
3416
3721
  cwd: cwd || null,
3417
3722
  repo: repo || null,
3418
3723
  };
3419
3724
 
3420
- const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 6000);
3421
-
3422
- if (!resp) {
3423
- log('bashGuard ' + cmdShort + ' \\u2192 error (timeout)');
3424
- process.stdout.write('{}\\n');
3425
- return;
3426
- }
3427
-
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');
3435
- }
3436
- }
3437
-
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
- async function main() {
3450
- try {
3451
- const input = await readStdin();
3452
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3453
-
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 || '';
3459
-
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; }
3463
-
3464
- const fileShort = basename(filePath);
3465
- log('editGuard checking: ' + fileShort);
3466
-
3467
- const repo = detectRepo(cwd || '.');
3468
-
3469
- let jwt = loadJwt();
3470
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3471
- jwt = await ensureFreshJwt(jwt);
3472
-
3473
- const config = await loadConfig(jwt);
3474
- if (config.silent) { process.stdout.write('{}\\n'); return; }
3475
-
3476
- const rt = await route(config);
3477
- const tagStr = tag(rt, config);
3478
-
3479
- if (rt === 'local') {
3480
- const contentShort = content.slice(0, 4000);
3481
- const rulesBlock = config.rules.map((r: Rule, i: number) =>
3482
- (i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
3483
- ).join('\\n');
3484
-
3485
- const graderPrompt = [
3486
- 'RULES:',
3487
- rulesBlock || '(none)',
3488
- '',
3489
- 'FILE: ' + filePath,
3490
- '',
3491
- 'CONTENT TO EVALUATE (first 4000 chars):',
3492
- contentShort,
3493
- ].join('\\n');
3494
-
3495
- let gradeResp: string;
3496
- try {
3497
- gradeResp = await localGrade('edit', graderPrompt);
3498
- } catch {
3499
- process.stdout.write('{}\\n');
3500
- return;
3501
- }
3502
-
3503
- const verdict = parseVerdict(gradeResp);
3504
- const editContent = 'file=' + filePath + ' content=' + content.slice(0, 2000);
3505
-
3506
- if (!verdict.ok) {
3507
- const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
3508
- const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
3509
-
3510
- 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,
3514
- rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3515
- });
3516
- const result = {
3517
- permission: 'deny',
3518
- user_message: tagStr + ' editGuard ' + fileShort + ' \\u2192 block: ' + guardReason,
3519
- agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
3520
- };
3521
- process.stdout.write(JSON.stringify(result) + '\\n');
3522
- return;
3523
- }
3524
-
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,
3529
- rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3530
- });
3531
- } 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',
3535
- rulesChecked: config.rules, violatedRules: [],
3536
- });
3537
- }
3538
-
3539
- process.stdout.write('{}\\n');
3540
- return;
3541
- }
3542
-
3543
- // \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
3544
- const body = {
3545
- hook_event: 'PreToolUse',
3546
- tool_name: toolName || 'Edit',
3547
- tool_input: { file_path: filePath, content },
3548
- file_path: filePath,
3549
- content,
3550
- response_format: 'cursor',
3551
- session_id: sessionId || null,
3552
- cwd: cwd || null,
3553
- repo: repo || null,
3554
- };
3555
-
3556
- const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 8000);
3725
+ const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, CURSOR_CLOUD_TIMEOUT_MS);
3557
3726
 
3558
3727
  if (!resp) {
3559
- log('editGuard ' + fileShort + ' \\u2192 error (timeout)');
3560
- process.stdout.write('{}\\n');
3561
- return;
3728
+ log('bashGuard ' + cmdShort + ' \u2192 pass (cloud timeout)');
3729
+ finishAllow();
3562
3730
  }
3563
3731
 
3564
3732
  if (resp.hook_response) {
3565
- process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
3566
- } else {
3567
- process.stdout.write('{}\\n');
3733
+ finishWith(resp.hook_response as Record<string, unknown>);
3568
3734
  }
3569
- } catch {
3570
- process.stdout.write('{}\\n');
3735
+ log('bashGuard ' + cmdShort + ' \u2192 pass (no hook_response)');
3736
+ finishAllow();
3737
+ } catch (e) {
3738
+ log('bashGuard error: ' + String(e));
3739
+ finishAllow();
3571
3740
  }
3572
3741
  }
3573
3742
 
3574
- main();
3575
- `;
3743
+ main().catch((e) => {
3744
+ log('bashGuard fatal: ' + String(e));
3745
+ finishAllow();
3746
+ });`;
3576
3747
  CURSOR_EDIT_CAPTURE_TS = `#!/usr/bin/env bun
3577
3748
  import {
3578
3749
  loadJwt, ensureFreshJwt, detectRepo, readStdin,
@@ -3582,26 +3753,37 @@ import { existsSync, readFileSync } from 'node:fs';
3582
3753
  import { basename, dirname, join } from 'node:path';
3583
3754
  import { homedir } from 'node:os';
3584
3755
 
3756
+ let hookDone = false;
3757
+
3758
+ function finish(): never {
3759
+ if (!hookDone) {
3760
+ hookDone = true;
3761
+ try { process.stdout.write('{}\\n'); } catch {}
3762
+ }
3763
+ process.exit(0);
3764
+ }
3765
+
3766
+ process.on('SIGTERM', () => finish());
3767
+
3585
3768
  async function main() {
3586
3769
  try {
3587
3770
  const input = await readStdin();
3588
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3771
+ if (!input.trim()) finish();
3589
3772
 
3590
3773
  const payload = JSON.parse(input);
3591
3774
  const filePath = payload.file_path || '';
3592
- if (!filePath) { process.stdout.write('{}\\n'); return; }
3775
+ if (!filePath) finish();
3593
3776
 
3594
- const cwd = payload.cwd || '';
3777
+ const cwd = payload.cwd || payload.workspace_roots?.[0] || '';
3595
3778
  const sessionId = payload.conversation_id || '';
3596
3779
  const repo = detectRepo(cwd || '.');
3597
3780
 
3598
3781
  log('editScan ' + basename(filePath));
3599
3782
 
3600
3783
  let jwt = loadJwt();
3601
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3784
+ if (!jwt) finish();
3602
3785
  jwt = await ensureFreshJwt(jwt);
3603
3786
 
3604
- // Read actual file content (up to 50KB)
3605
3787
  let fileContent = '';
3606
3788
  const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
3607
3789
  try {
@@ -3611,7 +3793,6 @@ async function main() {
3611
3793
  }
3612
3794
  } catch {}
3613
3795
 
3614
- // Walk up to find package.json dependencies
3615
3796
  let dependencies: Record<string, string> = {};
3616
3797
  let pkgDir = cwd || dirname(fullPath);
3617
3798
  while (pkgDir !== '/' && pkgDir !== '.') {
@@ -3638,12 +3819,10 @@ async function main() {
3638
3819
  if (cwd) captureBody.cwd = cwd;
3639
3820
  if (repo) captureBody.repo = repo;
3640
3821
 
3641
- // Check if local_only
3642
3822
  const rulesPath = join(homedir(), '.synkro', 'rules.json');
3643
3823
  if (existsSync(rulesPath)) {
3644
3824
  appendLocalTelemetry(captureBody);
3645
3825
  } else {
3646
- // Fire-and-forget to cloud
3647
3826
  fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3648
3827
  method: 'POST',
3649
3828
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
@@ -3653,157 +3832,17 @@ async function main() {
3653
3832
  appendLocalTelemetry(captureBody);
3654
3833
  }
3655
3834
 
3656
- process.stdout.write('{}\\n');
3657
- } catch {
3658
- process.stdout.write('{}\\n');
3835
+ finish();
3836
+ } catch (e) {
3837
+ log('editScan error: ' + String(e));
3838
+ finish();
3659
3839
  }
3660
3840
  }
3661
3841
 
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');
3802
- }
3803
- }
3804
-
3805
- main();
3806
- `;
3842
+ main().catch((e) => {
3843
+ log('editScan fatal: ' + String(e));
3844
+ finish();
3845
+ });`;
3807
3846
  }
3808
3847
  });
3809
3848
 
@@ -6048,10 +6087,7 @@ function writeHookScripts() {
6048
6087
  const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
6049
6088
  const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
6050
6089
  const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
6051
- const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.ts");
6052
6090
  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
6091
  const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
6056
6092
  writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
6057
6093
  writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
@@ -6067,10 +6103,7 @@ function writeHookScripts() {
6067
6103
  writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
6068
6104
  writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
6069
6105
  writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
6070
- writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_TS, "utf-8");
6071
6106
  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
6107
  writeFileSync7(mcpLocalServerPath, `#!/usr/bin/env bun
6075
6108
  /**
6076
6109
  * Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
@@ -6891,10 +6924,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
6891
6924
  chmodSync2(commonScriptPath, 493);
6892
6925
  chmodSync2(commonBashScriptPath, 493);
6893
6926
  chmodSync2(cursorBashJudgePath, 493);
6894
- chmodSync2(cursorEditPrecheckPath, 493);
6895
6927
  chmodSync2(cursorEditCapturePath, 493);
6896
- chmodSync2(cursorBashFollowupPath, 493);
6897
- chmodSync2(cursorSessionStartPath, 493);
6898
6928
  chmodSync2(mcpLocalServerPath, 493);
6899
6929
  return {
6900
6930
  bashScript: bashScriptPath,
@@ -6909,10 +6939,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
6909
6939
  transcriptSyncScript: transcriptSyncScriptPath,
6910
6940
  userPromptSubmitScript: userPromptSubmitScriptPath,
6911
6941
  cursorBashJudgeScript: cursorBashJudgePath,
6912
- cursorEditPrecheckScript: cursorEditPrecheckPath,
6913
6942
  cursorEditCaptureScript: cursorEditCapturePath,
6914
- cursorBashFollowupScript: cursorBashFollowupPath,
6915
- cursorSessionStartScript: cursorSessionStartPath,
6916
6943
  mcpLocalServerScript: mcpLocalServerPath
6917
6944
  };
6918
6945
  }
@@ -6945,7 +6972,7 @@ function writeConfigEnv(opts) {
6945
6972
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6946
6973
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6947
6974
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6948
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.67")}`
6975
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.68")}`
6949
6976
  ];
6950
6977
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6951
6978
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7396,10 +7423,17 @@ async function installCommand(opts = {}) {
7396
7423
  hasCursor = true;
7397
7424
  installCursorHooks(agent.settingsPath, {
7398
7425
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
7399
- editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
7400
7426
  editCaptureScriptPath: scripts.cursorEditCaptureScript,
7401
- bashFollowupScriptPath: scripts.cursorBashFollowupScript,
7402
- sessionStartScriptPath: scripts.cursorSessionStartScript
7427
+ bashFollowupScriptPath: scripts.bashFollowupScript,
7428
+ editPrecheckScriptPath: scripts.editPrecheckScript,
7429
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
7430
+ cvePrecheckScriptPath: scripts.cvePrecheckScript,
7431
+ planJudgeScriptPath: scripts.planJudgeScript,
7432
+ agentJudgeScriptPath: scripts.agentJudgeScript,
7433
+ stopSummaryScriptPath: scripts.stopSummaryScript,
7434
+ sessionStartScriptPath: scripts.sessionStartScript,
7435
+ userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
7436
+ transcriptSyncScriptPath: scripts.transcriptSyncScript
7403
7437
  });
7404
7438
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
7405
7439
  }
@@ -7971,10 +8005,20 @@ async function statusCommand() {
7971
8005
  const hooks = inspectCursorHooks(a.settingsPath);
7972
8006
  console.log(` hooks installed: ${hooks.installed ? "\u2713" : "\u2717"}`);
7973
8007
  if (hooks.installed) {
8008
+ console.log(` \u2022 sessionStart: ${hooks.sessionStart ? "\u2713" : "\u2717"}`);
8009
+ console.log(` \u2022 sessionEnd: ${hooks.sessionEnd ? "\u2713" : "\u2717"}`);
8010
+ console.log(` \u2022 beforeSubmitPrompt: ${hooks.beforeSubmitPrompt ? "\u2713" : "\u2717"}`);
7974
8011
  console.log(` \u2022 beforeShellExecution: ${hooks.beforeShellExecution ? "\u2713" : "\u2717"}`);
7975
- console.log(` \u2022 preToolUse: ${hooks.preToolUse ? "\u2713" : "\u2717"}`);
8012
+ console.log(` \u2022 afterShellExecution: ${hooks.afterShellExecution ? "\u2713" : "\u2717"}`);
8013
+ console.log(` \u2022 PreToolUse Bash: ${hooks.preToolUseBash ? "\u2713" : "\u2717"}`);
8014
+ console.log(` \u2022 PreToolUse Edit: ${hooks.preToolUseEdit ? "\u2713" : "\u2717"}`);
8015
+ console.log(` \u2022 PreToolUse CWE: ${hooks.preToolUseCwe ? "\u2713" : "\u2717"}`);
8016
+ console.log(` \u2022 PreToolUse CVE: ${hooks.preToolUseCve ? "\u2713" : "\u2717"}`);
8017
+ console.log(` \u2022 PreToolUse Agent: ${hooks.preToolUseAgent ? "\u2713" : "\u2717"}`);
8018
+ console.log(` \u2022 PreToolUse Plan: ${hooks.preToolUsePlan ? "\u2713" : "\u2717"}`);
7976
8019
  console.log(` \u2022 afterFileEdit: ${hooks.afterFileEdit ? "\u2713" : "\u2717"}`);
7977
8020
  console.log(` \u2022 postToolUse: ${hooks.postToolUse ? "\u2713" : "\u2717"}`);
8021
+ console.log(` \u2022 stop (transcript): ${hooks.stop ? "\u2713" : "\u2717"}`);
7978
8022
  }
7979
8023
  }
7980
8024
  }
@@ -7988,6 +8032,7 @@ async function statusCommand() {
7988
8032
  "cc-cwe-precheck.ts",
7989
8033
  "cc-cve-precheck.ts",
7990
8034
  "cc-plan-judge.ts",
8035
+ "cc-agent-judge.ts",
7991
8036
  "cc-stop-summary.ts",
7992
8037
  "cc-session-start.ts",
7993
8038
  "cc-transcript-sync.ts",
@@ -7995,10 +8040,19 @@ async function statusCommand() {
7995
8040
  "_synkro-common.ts"
7996
8041
  ];
7997
8042
  const cursorHooks = [
7998
- "cursor-bash-judge.sh",
7999
- "cursor-edit-precheck.sh",
8000
- "cursor-bash-followup.sh",
8001
- "_synkro-common.sh"
8043
+ "cursor-bash-judge.ts",
8044
+ "cursor-edit-capture.ts",
8045
+ "cc-edit-precheck.ts",
8046
+ "cc-cwe-precheck.ts",
8047
+ "cc-cve-precheck.ts",
8048
+ "cc-agent-judge.ts",
8049
+ "cc-plan-judge.ts",
8050
+ "cc-session-start.ts",
8051
+ "cc-stop-summary.ts",
8052
+ "cc-bash-followup.ts",
8053
+ "cc-user-prompt-submit.ts",
8054
+ "cc-transcript-sync.ts",
8055
+ "_synkro-common.ts"
8002
8056
  ];
8003
8057
  console.log("Hook scripts (Claude Code):");
8004
8058
  for (const f of ccHooks) {