@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 +670 -429
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
file.hooks.preToolUse.push({
|
|
345
|
-
command: config.editPrecheckScriptPath,
|
|
370
|
+
pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
|
|
346
371
|
timeout: 15,
|
|
347
|
-
|
|
372
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
|
|
348
373
|
});
|
|
349
|
-
|
|
350
|
-
file.hooks.afterFileEdit.push({
|
|
351
|
-
command: config.editCaptureScriptPath,
|
|
374
|
+
pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
|
|
352
375
|
timeout: 15,
|
|
353
|
-
|
|
376
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
|
|
354
377
|
});
|
|
355
|
-
|
|
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 {
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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','
|
|
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 (!
|
|
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
|
|
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':
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3083
|
-
const toolUseId = payload.tool_use_id || '';
|
|
3084
|
-
if (!sessionId
|
|
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
|
-
|
|
3087
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3319
|
-
type
|
|
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
|
-
|
|
3323
|
-
|
|
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
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
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
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
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
|
-
|
|
3700
|
+
let hookDone = false;
|
|
3371
3701
|
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
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
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3406
|
-
|
|
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
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
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
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
}
|
|
3434
|
-
|
|
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())
|
|
3754
|
+
if (!input.trim()) finishAllow();
|
|
3453
3755
|
|
|
3454
|
-
const payload = JSON.parse(input)
|
|
3455
|
-
const toolName = payload
|
|
3456
|
-
|
|
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
|
|
3461
|
-
const
|
|
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
|
-
|
|
3465
|
-
|
|
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)
|
|
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)
|
|
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
|
-
'
|
|
3799
|
+
'COMMAND TO EVALUATE:',
|
|
3800
|
+
command,
|
|
3490
3801
|
'',
|
|
3491
|
-
'
|
|
3492
|
-
|
|
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('
|
|
3498
|
-
} catch {
|
|
3499
|
-
|
|
3500
|
-
|
|
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, '
|
|
3512
|
-
|
|
3513
|
-
command
|
|
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
|
-
|
|
3827
|
+
finishWith({
|
|
3517
3828
|
permission: 'deny',
|
|
3518
|
-
user_message: tagStr + '
|
|
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
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
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, '
|
|
3533
|
-
|
|
3534
|
-
command
|
|
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
|
-
|
|
3540
|
-
|
|
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
|
-
|
|
3544
|
-
const body = {
|
|
3856
|
+
const body: Record<string, any> = {
|
|
3545
3857
|
hook_event: 'PreToolUse',
|
|
3546
|
-
tool_name: toolName || '
|
|
3547
|
-
tool_input: {
|
|
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,
|
|
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('
|
|
3560
|
-
|
|
3561
|
-
return;
|
|
3873
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass (cloud timeout)');
|
|
3874
|
+
finishAllow();
|
|
3562
3875
|
}
|
|
3563
3876
|
|
|
3564
3877
|
if (resp.hook_response) {
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
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
|
-
|
|
3570
|
-
|
|
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())
|
|
3924
|
+
if (!input.trim()) finish();
|
|
3589
3925
|
|
|
3590
3926
|
const payload = JSON.parse(input);
|
|
3591
3927
|
const filePath = payload.file_path || '';
|
|
3592
|
-
if (!filePath)
|
|
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)
|
|
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
|
-
|
|
3657
|
-
} catch {
|
|
3658
|
-
|
|
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.
|
|
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.
|
|
7402
|
-
|
|
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
|
|
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.
|
|
7999
|
-
"cursor-edit-
|
|
8000
|
-
"
|
|
8001
|
-
"
|
|
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
|
|
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)) {
|