@synkro-sh/cli 1.6.15 → 1.6.16

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
@@ -116,6 +116,17 @@ function installCCHooks(settingsPath, config) {
116
116
  settings.hooks.SessionEnd = settings.hooks.SessionEnd ?? [];
117
117
  settings.hooks.SessionStart = settings.hooks.SessionStart ?? [];
118
118
  settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit ?? [];
119
+ settings.hooks.PreToolUse.push({
120
+ matcher: "Bash",
121
+ hooks: [
122
+ {
123
+ type: "command",
124
+ command: config.installScanScriptPath,
125
+ timeout: 8
126
+ }
127
+ ],
128
+ [SYNKRO_MARKER]: true
129
+ });
119
130
  settings.hooks.PreToolUse.push({
120
131
  matcher: "Bash|Read|Grep|Glob",
121
132
  hooks: [
@@ -319,6 +330,12 @@ function installCursorHooks(hooksJsonPath, config) {
319
330
  pushCcHook(h, "beforeSubmitPrompt", config.userPromptSubmitScriptPath, { timeout: 5 });
320
331
  pushCcHook(h, "stop", config.transcriptSyncScriptPath, { timeout: 3 });
321
332
  h.beforeShellExecution = h.beforeShellExecution ?? [];
333
+ h.beforeShellExecution.push({
334
+ command: cursorCcCmd(config.installScanScriptPath),
335
+ timeout: 8,
336
+ failClosed: false,
337
+ [SYNKRO_MARKER2]: true
338
+ });
322
339
  h.beforeShellExecution.push({
323
340
  command: bunRunCmd(config.bashJudgeScriptPath),
324
341
  timeout: 15,
@@ -327,6 +344,13 @@ function installCursorHooks(hooksJsonPath, config) {
327
344
  });
328
345
  pushCcHook(h, "afterShellExecution", config.bashFollowupScriptPath, { timeout: 10 });
329
346
  h.preToolUse = h.preToolUse ?? [];
347
+ h.preToolUse.push({
348
+ command: cursorCcCmd(config.installScanScriptPath),
349
+ timeout: 8,
350
+ failClosed: false,
351
+ matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command",
352
+ [SYNKRO_MARKER2]: true
353
+ });
330
354
  h.preToolUse.push({
331
355
  command: bunRunCmd(config.bashJudgeScriptPath),
332
356
  timeout: 15,
@@ -727,7 +751,7 @@ synkro_post_with_retry() {
727
751
  });
728
752
 
729
753
  // cli/installer/hookScriptsTs.ts
730
- 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;
754
+ var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_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;
731
755
  var init_hookScriptsTs = __esm({
732
756
  "cli/installer/hookScriptsTs.ts"() {
733
757
  "use strict";
@@ -975,12 +999,19 @@ export function normalizeMode(m?: string): 'ask' | 'fix' {
975
999
 
976
1000
  // \u2500\u2500\u2500 Config Loading \u2500\u2500\u2500
977
1001
 
1002
+ export interface RuleExample {
1003
+ text: string;
1004
+ verdict: 'violation' | 'ok';
1005
+ note?: string;
1006
+ }
1007
+
978
1008
  export interface Rule {
979
1009
  rule_id: string;
980
1010
  text: string;
981
1011
  severity: string;
982
1012
  category: string;
983
1013
  mode: string;
1014
+ examples?: RuleExample[];
984
1015
  }
985
1016
 
986
1017
  export interface HookConfig {
@@ -1032,6 +1063,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1032
1063
  severity: r.severity || '',
1033
1064
  category: r.category || '',
1034
1065
  mode: normalizeMode(r.mode),
1066
+ examples: Array.isArray(r.examples) ? r.examples : undefined,
1035
1067
  }));
1036
1068
  }
1037
1069
  config.silent = raw.silent === true;
@@ -1072,6 +1104,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1072
1104
  severity: r.severity || '',
1073
1105
  category: r.category || '',
1074
1106
  mode: normalizeMode(r.mode),
1107
+ examples: Array.isArray(r.examples) ? r.examples : undefined,
1075
1108
  }));
1076
1109
  }
1077
1110
  } catch {}
@@ -2271,6 +2304,8 @@ import {
2271
2304
  import { existsSync, readFileSync } from 'node:fs';
2272
2305
  import { basename, dirname, join } from 'node:path';
2273
2306
 
2307
+ const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
2308
+
2274
2309
  async function main() {
2275
2310
  setupCursorHookSignals();
2276
2311
  try {
@@ -2362,7 +2397,7 @@ async function main() {
2362
2397
 
2363
2398
  let gradeResp: string;
2364
2399
  try {
2365
- gradeResp = await localGrade('edit', graderPrompt);
2400
+ gradeResp = await localGrade('edit', graderPrompt, undefined, agentKind);
2366
2401
  } catch (err) {
2367
2402
  logGraderUnavailable('editGuard', fileShort, (err as Error).message || String(err));
2368
2403
  outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 local grader unavailable, skipped' });
@@ -2483,6 +2518,7 @@ import {
2483
2518
  import { basename, extname, resolve, join, dirname } from 'node:path';
2484
2519
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
2485
2520
 
2521
+ const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
2486
2522
 
2487
2523
  interface PackageCapability {
2488
2524
  name: string;
@@ -2743,8 +2779,8 @@ async function main() {
2743
2779
  const chunk2 = cweContent.slice(mid - OVERLAP);
2744
2780
  try {
2745
2781
  const [resp1, resp2] = await Promise.all([
2746
- localGradeCwe(buildCwePrompt(chunk1)),
2747
- localGradeCwe(buildCwePrompt(chunk2)),
2782
+ localGradeCwe(buildCwePrompt(chunk1), agentKind),
2783
+ localGradeCwe(buildCwePrompt(chunk2), agentKind),
2748
2784
  ]);
2749
2785
  gradeResponses = [resp1, resp2];
2750
2786
  } catch (gradeErr: any) {
@@ -2755,7 +2791,7 @@ async function main() {
2755
2791
  }
2756
2792
  } else {
2757
2793
  try {
2758
- gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent))];
2794
+ gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), agentKind)];
2759
2795
  } catch (gradeErr: any) {
2760
2796
  const reason = gradeErr?.message || String(gradeErr);
2761
2797
  logGraderUnavailable('cweGuard', fileShort, reason);
@@ -3116,6 +3152,96 @@ async function main() {
3116
3152
  }
3117
3153
  }
3118
3154
 
3155
+ main();
3156
+ `;
3157
+ INSTALL_SCAN_TS = `#!/usr/bin/env bun
3158
+ import {
3159
+ loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
3160
+ readStdin, runInstallScan, dispatchFinding, dispatchCapture, hashCommand,
3161
+ outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
3162
+ } from './_synkro-common.ts';
3163
+ import { writeFileSync, mkdirSync } from 'node:fs';
3164
+ import { join } from 'node:path';
3165
+
3166
+ const SCAN_CACHE_DIR = (process.env.HOME || '/tmp') + '/.synkro/.scan-cache';
3167
+
3168
+ async function main() {
3169
+ setupCursorHookSignals();
3170
+ try {
3171
+ const input = await readStdin();
3172
+ if (!input.trim()) { outputEmpty(); return; }
3173
+
3174
+ const payload = JSON.parse(input);
3175
+ const toolName = payload.tool_name || '';
3176
+ if (!isShellTool(toolName)) { outputEmpty(); return; }
3177
+
3178
+ const toolInput = payload.tool_input || {};
3179
+ const command = typeof payload.command === 'string' ? payload.command : (toolInput.command || '');
3180
+ if (!command) { outputEmpty(); return; }
3181
+
3182
+ let jwt = loadJwt();
3183
+ if (!jwt) { outputEmpty(); return; }
3184
+ jwt = await ensureFreshJwt(jwt);
3185
+
3186
+ const scan = await runInstallScan(command, jwt);
3187
+
3188
+ if (scan.scanned) {
3189
+ try {
3190
+ mkdirSync(SCAN_CACHE_DIR, { recursive: true });
3191
+ writeFileSync(join(SCAN_CACHE_DIR, hashCommand(command)), JSON.stringify(scan), 'utf-8');
3192
+ } catch {}
3193
+ }
3194
+
3195
+ if (!scan.scanned) { outputEmpty(); return; }
3196
+
3197
+ const sessionId = hookSessionId(payload);
3198
+ const cwd = payload.cwd || '';
3199
+ const repo = detectRepo(cwd);
3200
+ const config = await loadConfig(jwt);
3201
+ const rt = await route(config);
3202
+ const tagStr = tag(rt, config);
3203
+
3204
+ if (scan.action === 'block') {
3205
+ for (const f of scan.findings) {
3206
+ dispatchFinding(jwt, {
3207
+ session_id: sessionId,
3208
+ file_path: command,
3209
+ finding_type: 'cve' as const,
3210
+ finding_id: f.advisoryId + ':' + f.name,
3211
+ severity: f.severity,
3212
+ status: 'open',
3213
+ detail: f.detail,
3214
+ package_name: f.name,
3215
+ package_version: f.version,
3216
+ }, config.captureDepth);
3217
+ }
3218
+ dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
3219
+ 'Bash', repo, sessionId, config.captureDepth, {
3220
+ command, reasoning: scan.blockContext.slice(0, 200),
3221
+ violatedRules: scan.violatedIds,
3222
+ });
3223
+ outputJson({
3224
+ systemMessage: '[synkro:installScan] ' + scan.summary,
3225
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '[synkro:installScan] ' + scan.summary },
3226
+ });
3227
+ } else if (scan.action === 'warn') {
3228
+ outputJson({
3229
+ systemMessage: '[synkro:installScan] ' + scan.summary,
3230
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '[synkro:installScan] ' + scan.summary },
3231
+ });
3232
+ } else {
3233
+ const label = scan.scannedLabel || command.slice(0, 80);
3234
+ outputJson({
3235
+ systemMessage: '[synkro:installScan] ' + label + ' \\u2192 clean',
3236
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '[synkro:installScan] ' + label + ' \\u2192 clean' },
3237
+ });
3238
+ }
3239
+ } catch (err) {
3240
+ process.stderr.write('[synkro] installScan error: ' + String(err) + '\\n');
3241
+ outputEmpty();
3242
+ }
3243
+ }
3244
+
3119
3245
  main();
3120
3246
  `;
3121
3247
  BASH_JUDGE_TS = String.raw`#!/usr/bin/env bun
@@ -3126,9 +3252,43 @@ import {
3126
3252
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
3127
3253
  outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
3128
3254
  logGraderUnavailable, filterRules, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
3129
- runInstallScan,
3255
+ hashCommand,
3130
3256
  type HookConfig, type Rule,
3131
3257
  } from './_synkro-common.ts';
3258
+ import { createHash } from 'node:crypto';
3259
+ import { existsSync, statSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
3260
+ import { join } from 'node:path';
3261
+
3262
+ const SCAN_CACHE_DIR = (process.env.HOME || '/tmp') + '/.synkro/.scan-cache';
3263
+
3264
+ function readCachedScan(command: string): any | null {
3265
+ try {
3266
+ const path = join(SCAN_CACHE_DIR, hashCommand(command));
3267
+ if (!existsSync(path)) return null;
3268
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
3269
+ unlinkSync(path);
3270
+ return data;
3271
+ } catch { return null; }
3272
+ }
3273
+
3274
+ const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
3275
+ const DEDUP_TTL_MS = 3000;
3276
+
3277
+ function isDuplicate(command: string, sessionId: string): boolean {
3278
+ const hash = createHash('md5').update(sessionId + ':' + command).digest('hex').slice(0, 12);
3279
+ const marker = DEDUP_DIR + '/' + hash;
3280
+ try {
3281
+ if (existsSync(marker)) {
3282
+ const age = Date.now() - statSync(marker).mtimeMs;
3283
+ if (age < DEDUP_TTL_MS) return true;
3284
+ }
3285
+ } catch {}
3286
+ try {
3287
+ mkdirSync(DEDUP_DIR, { recursive: true });
3288
+ writeFileSync(marker, '', { flag: 'w' });
3289
+ } catch {}
3290
+ return false;
3291
+ }
3132
3292
 
3133
3293
  async function main() {
3134
3294
  setupCursorHookSignals();
@@ -3166,6 +3326,12 @@ async function main() {
3166
3326
  }
3167
3327
  if (!command) { outputEmpty(); return; }
3168
3328
 
3329
+ if (isDuplicate(command, sessionId)) {
3330
+ log('bashGuard skip (dedup): ' + command.slice(0, 80));
3331
+ outputEmpty();
3332
+ return;
3333
+ }
3334
+
3169
3335
  const cmdShort = command.slice(0, 80);
3170
3336
  log('bashGuard checking: ' + cmdShort);
3171
3337
 
@@ -3211,45 +3377,20 @@ async function main() {
3211
3377
  return;
3212
3378
  }
3213
3379
 
3214
- // ─── Install protection: server-side pkg-scan (CVE + typosquat + tarball + reputation) ───
3215
- // Detection + extraction happen server-side (runInstallScan); the hook
3216
- // relays the verdict. A block is handed to the grader as an authoritative
3217
- // concern so the normal ask + consent-carryover flow lets the user
3218
- // override it on the next turn.
3219
- let installScanMsg = '';
3380
+ // ─── Install protection: read cached scan from the install-scan hook ───
3381
+ // The install-scan hook (INSTALL_SCAN_TS) runs before this hook, calls
3382
+ // runInstallScan(), outputs its own system message, and caches the result.
3383
+ // We just read the cache to feed scanConcern into the grader prompt so
3384
+ // the consent-carryover flow works.
3220
3385
  let scanConcern = '';
3221
3386
  let scanBlockContext = '';
3222
3387
  if (toolName === 'Bash') {
3223
- const scan = await runInstallScan(command, jwt);
3224
- if (scan.action === 'block') {
3225
- for (const f of scan.findings) {
3226
- dispatchFinding(jwt, {
3227
- session_id: sessionId,
3228
- file_path: command,
3229
- finding_type: 'cve' as const,
3230
- finding_id: f.advisoryId + ':' + f.name,
3231
- severity: f.severity,
3232
- status: 'open',
3233
- detail: f.detail,
3234
- package_name: f.name,
3235
- package_version: f.version,
3236
- }, config.captureDepth);
3237
- }
3238
- dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
3239
- 'Bash', gitRepo, sessionId, config.captureDepth, {
3240
- command,
3241
- reasoning: scan.blockContext.slice(0, 200),
3242
- violatedRules: scan.violatedIds,
3243
- ccModel: transcript.ccModel,
3244
- });
3245
- scanBlockContext = scan.blockContext;
3388
+ const scan = readCachedScan(command);
3389
+ if (scan && scan.action === 'block') {
3390
+ scanBlockContext = scan.blockContext || '';
3246
3391
  scanConcern = 'PACKAGE SCANNER FLAG (authoritative — do NOT re-evaluate whether the vulnerability is real): '
3247
- + scan.blockContext
3392
+ + scanBlockContext
3248
3393
  + ' For this concern you MUST return ok=false with rule_id "SYNKRO_PKGSCAN", rule_mode "ask", and the reason above — UNLESS the user has explicitly consented in this conversation to installing this despite the warning, in which case return ok=true.';
3249
- } else if (scan.scanned && scan.action === 'warn') {
3250
- installScanMsg = '[synkro:installScan] ' + scan.summary;
3251
- } else if (scan.scanned) {
3252
- installScanMsg = '[synkro:installScan] ' + (scan.scannedLabel || cmdShort) + ' → clean';
3253
3394
  }
3254
3395
  }
3255
3396
 
@@ -3305,10 +3446,9 @@ async function main() {
3305
3446
  const blockMsg = mode === 'fix'
3306
3447
  ? tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before retrying — do not ask the user.'
3307
3448
  : tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
3308
- const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
3309
3449
  outputJson({
3310
- systemMessage: combined,
3311
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
3450
+ systemMessage: blockMsg,
3451
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: blockMsg },
3312
3452
  });
3313
3453
  dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
3314
3454
  toolName, gitRepo, sessionId, config.captureDepth, {
@@ -3317,8 +3457,10 @@ async function main() {
3317
3457
  });
3318
3458
  } else {
3319
3459
  const reason = tagStr + ' bashGuard → pass: ' + (verdict.reason || 'no policy violations detected');
3320
- const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
3321
- outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
3460
+ outputJson({
3461
+ systemMessage: reason,
3462
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: reason },
3463
+ });
3322
3464
  dispatchCapture(jwt, 'bash', 'pass', 'clean', verdict.category || 'trivial_utility',
3323
3465
  toolName, gitRepo, sessionId, config.captureDepth, {
3324
3466
  command, reasoning: verdict.reason || 'no policy violations detected',
@@ -3370,30 +3512,16 @@ async function main() {
3370
3512
 
3371
3513
  if (!resp) {
3372
3514
  log('bashGuard ' + cmdShort + ' → error (timeout)');
3373
- if (installScanMsg) {
3374
- outputJson({ systemMessage: installScanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: installScanMsg } });
3375
- } else { outputEmpty(); }
3515
+ outputEmpty();
3376
3516
  return;
3377
3517
  }
3378
3518
 
3379
3519
  if (!resp.hook_response || typeof resp.hook_response !== 'object') {
3380
3520
  log('bashGuard ' + cmdShort + ' → pass (no hook_response)');
3381
- if (installScanMsg) {
3382
- outputJson({ systemMessage: installScanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: installScanMsg } });
3383
- } else { outputEmpty(); }
3521
+ outputEmpty();
3384
3522
  return;
3385
3523
  }
3386
3524
 
3387
- if (installScanMsg) {
3388
- const existing = resp.hook_response.systemMessage || '';
3389
- resp.hook_response.systemMessage = installScanMsg + (existing ? '\\n' + existing : '');
3390
- if (resp.hook_response.hookSpecificOutput) {
3391
- const existingCtx = resp.hook_response.hookSpecificOutput.additionalContext || '';
3392
- resp.hook_response.hookSpecificOutput.additionalContext = installScanMsg + (existingCtx ? '\\n' + existingCtx : '');
3393
- } else {
3394
- resp.hook_response.hookSpecificOutput = { hookEventName: 'PreToolUse', additionalContext: resp.hook_response.systemMessage };
3395
- }
3396
- }
3397
3525
  outputJson(resp.hook_response);
3398
3526
  } catch (err) {
3399
3527
  process.stderr.write('[synkro] bashGuard error: ' + String(err) + '\\n');
@@ -3413,6 +3541,8 @@ import {
3413
3541
  type HookConfig, type Rule,
3414
3542
  } from './_synkro-common.ts';
3415
3543
 
3544
+ const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
3545
+
3416
3546
  async function main() {
3417
3547
  setupCursorHookSignals();
3418
3548
  try {
@@ -3482,7 +3612,7 @@ async function main() {
3482
3612
 
3483
3613
  let gradeResp: string;
3484
3614
  try {
3485
- gradeResp = await localGrade('bash', graderPrompt);
3615
+ gradeResp = await localGrade('bash', graderPrompt, undefined, agentKind);
3486
3616
  } catch (err) {
3487
3617
  logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), (err as Error).message || String(err));
3488
3618
  outputJson({ systemMessage: tagStr + ' agentGuard \u2192 local grader unavailable, skipped' });
@@ -3581,6 +3711,8 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from '
3581
3711
  import { join } from 'node:path';
3582
3712
  import { homedir } from 'node:os';
3583
3713
 
3714
+ const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
3715
+
3584
3716
  function findLatestPlanInDir(plansDir: string): string | null {
3585
3717
  if (!existsSync(plansDir)) return null;
3586
3718
  try {
@@ -3672,7 +3804,7 @@ async function main() {
3672
3804
 
3673
3805
  let gradeResp: string;
3674
3806
  try {
3675
- gradeResp = await localGrade('plan', graderPrompt);
3807
+ gradeResp = await localGrade('plan', graderPrompt, undefined, agentKind);
3676
3808
  } catch {
3677
3809
  outputJson({ systemMessage: tagStr + ' planReview \u2192 local grader unavailable, skipped' });
3678
3810
  return;
@@ -4163,13 +4295,26 @@ main();
4163
4295
  import {
4164
4296
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
4165
4297
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules,
4166
- isSafeInRepoRead, runInstallScan, postWithRetry, readStdin,
4298
+ isSafeInRepoRead, postWithRetry, readStdin, hashCommand,
4167
4299
  extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
4168
4300
  appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
4169
4301
  type Rule,
4170
4302
  } from './_synkro-common.ts';
4171
4303
  import { createHash } from 'node:crypto';
4172
- import { existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
4304
+ import { existsSync, statSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
4305
+ import { join } from 'node:path';
4306
+
4307
+ const SCAN_CACHE_DIR = (process.env.HOME || '/tmp') + '/.synkro/.scan-cache';
4308
+
4309
+ function readCachedScan(command: string): any | null {
4310
+ try {
4311
+ const path = join(SCAN_CACHE_DIR, hashCommand(command));
4312
+ if (!existsSync(path)) return null;
4313
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
4314
+ unlinkSync(path);
4315
+ return data;
4316
+ } catch { return null; }
4317
+ }
4173
4318
 
4174
4319
  const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
4175
4320
  const DEDUP_TTL_MS = 3000;
@@ -4298,33 +4443,16 @@ async function main() {
4298
4443
  const rt = await route(config);
4299
4444
  const tagStr = tag(rt, config);
4300
4445
 
4301
- // Install protection \u2014 scan packages before any npm/pip/cargo/etc. install.
4302
- // A block is handed to the grader as an authoritative concern so the
4303
- // normal ask + consent-carryover flow can let the user override it.
4446
+ // Install protection \u2014 read cached scan from the install-scan hook.
4304
4447
  let scanConcern = '';
4305
4448
  let scanBlockContext = '';
4306
4449
  if (SHELL_TOOL_NAMES.has(toolName)) {
4307
- const scan = await runInstallScan(command, jwt);
4308
- if (scan.action === 'block') {
4309
- for (const f of scan.findings) {
4310
- dispatchFinding(jwt, {
4311
- session_id: sessionId, file_path: command,
4312
- finding_type: 'cve' as const, finding_id: f.advisoryId + ':' + f.name,
4313
- severity: f.severity, status: 'open', detail: f.detail,
4314
- package_name: f.name, package_version: f.version,
4315
- }, config.captureDepth);
4316
- }
4317
- dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
4318
- 'Bash', repo, sessionId, config.captureDepth, {
4319
- command, reasoning: scan.blockContext.slice(0, 200),
4320
- violatedRules: scan.violatedIds, ccModel: model,
4321
- });
4322
- scanBlockContext = scan.blockContext;
4450
+ const scan = readCachedScan(command);
4451
+ if (scan && scan.action === 'block') {
4452
+ scanBlockContext = scan.blockContext || '';
4323
4453
  scanConcern = 'PACKAGE SCANNER FLAG (authoritative \u2014 do NOT re-evaluate whether the vulnerability is real): '
4324
- + scan.blockContext
4454
+ + scanBlockContext
4325
4455
  + ' For this concern you MUST return ok=false with rule_id "SYNKRO_PKGSCAN", rule_mode "ask", and the reason above \u2014 UNLESS the user has explicitly consented in this conversation to installing this despite the warning, in which case return ok=true.';
4326
- } else if (scan.scanned && scan.action === 'warn') {
4327
- log('bashGuard installScan warn: ' + scan.summary);
4328
4456
  }
4329
4457
  }
4330
4458
 
@@ -5680,8 +5808,6 @@ async function dockerInstall(opts = {}) {
5680
5808
  `127.0.0.1:${HOST_GRADER_PORT}:8929`,
5681
5809
  "-p",
5682
5810
  `127.0.0.1:${HOST_CWE_PORT}:8930`,
5683
- "-p",
5684
- `127.0.0.1:${HOST_PG_PORT}:5433`,
5685
5811
  "-v",
5686
5812
  `${PGDATA_PATH}:/data/pgdata`,
5687
5813
  "-v",
@@ -5910,7 +6036,7 @@ function checkPgdata() {
5910
6036
  if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
5911
6037
  return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
5912
6038
  }
5913
- var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PG_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
6039
+ var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
5914
6040
  var init_dockerInstall = __esm({
5915
6041
  "cli/local-cc/dockerInstall.ts"() {
5916
6042
  "use strict";
@@ -5924,7 +6050,6 @@ var init_dockerInstall = __esm({
5924
6050
  HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
5925
6051
  HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
5926
6052
  HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
5927
- HOST_PG_PORT = parseInt(process.env.SYNKRO_HOST_PG_PORT || "15433", 10);
5928
6053
  CONTAINER_NAME = "synkro-server";
5929
6054
  DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
5930
6055
  DockerInstallError = class extends Error {
@@ -6440,6 +6565,7 @@ function writeHookScripts() {
6440
6565
  const userPromptSubmitScriptPath = join8(HOOKS_DIR, "cc-user-prompt-submit.ts");
6441
6566
  const commonScriptPath = join8(HOOKS_DIR, "_synkro-common.ts");
6442
6567
  const commonBashScriptPath = join8(HOOKS_DIR, "_synkro-common.sh");
6568
+ const installScanScriptPath = join8(HOOKS_DIR, "cc-install-scan.ts");
6443
6569
  const cursorBashJudgePath = join8(HOOKS_DIR, "cursor-bash-judge.ts");
6444
6570
  const cursorEditCapturePath = join8(HOOKS_DIR, "cursor-edit-capture.ts");
6445
6571
  const mcpStdioProxyPath = join8(HOOKS_DIR, "mcp-stdio-proxy.ts");
@@ -6456,6 +6582,7 @@ function writeHookScripts() {
6456
6582
  writeFileSync7(userPromptSubmitScriptPath, USER_PROMPT_SUBMIT_TS, "utf-8");
6457
6583
  writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
6458
6584
  writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
6585
+ writeFileSync7(installScanScriptPath, INSTALL_SCAN_TS, "utf-8");
6459
6586
  writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
6460
6587
  writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
6461
6588
  writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
@@ -6472,6 +6599,7 @@ function writeHookScripts() {
6472
6599
  chmodSync2(userPromptSubmitScriptPath, 493);
6473
6600
  chmodSync2(commonScriptPath, 493);
6474
6601
  chmodSync2(commonBashScriptPath, 493);
6602
+ chmodSync2(installScanScriptPath, 493);
6475
6603
  chmodSync2(cursorBashJudgePath, 493);
6476
6604
  chmodSync2(cursorEditCapturePath, 493);
6477
6605
  chmodSync2(mcpStdioProxyPath, 493);
@@ -6487,6 +6615,7 @@ function writeHookScripts() {
6487
6615
  sessionStartScript: sessionStartScriptPath,
6488
6616
  transcriptSyncScript: transcriptSyncScriptPath,
6489
6617
  userPromptSubmitScript: userPromptSubmitScriptPath,
6618
+ installScanScript: installScanScriptPath,
6490
6619
  cursorBashJudgeScript: cursorBashJudgePath,
6491
6620
  cursorEditCaptureScript: cursorEditCapturePath
6492
6621
  };
@@ -6520,7 +6649,7 @@ function writeConfigEnv(opts) {
6520
6649
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6521
6650
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6522
6651
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6523
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.15")}`
6652
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.16")}`
6524
6653
  ];
6525
6654
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6526
6655
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -6762,6 +6891,7 @@ async function installCommand(opts = {}) {
6762
6891
  sessionStartScriptPath: scripts.sessionStartScript,
6763
6892
  transcriptSyncScriptPath: scripts.transcriptSyncScript,
6764
6893
  userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
6894
+ installScanScriptPath: scripts.installScanScript,
6765
6895
  skipTranscriptSync: !transcriptConsent
6766
6896
  });
6767
6897
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
@@ -6779,7 +6909,8 @@ async function installCommand(opts = {}) {
6779
6909
  stopSummaryScriptPath: scripts.stopSummaryScript,
6780
6910
  sessionStartScriptPath: scripts.sessionStartScript,
6781
6911
  userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
6782
- transcriptSyncScriptPath: scripts.transcriptSyncScript
6912
+ transcriptSyncScriptPath: scripts.transcriptSyncScript,
6913
+ installScanScriptPath: scripts.installScanScript
6783
6914
  });
6784
6915
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
6785
6916
  }
@@ -9185,7 +9316,7 @@ var args = process.argv.slice(2);
9185
9316
  var cmd = args[0] || "";
9186
9317
  var subArgs = args.slice(1);
9187
9318
  function printVersion() {
9188
- console.log("1.6.15");
9319
+ console.log("1.6.16");
9189
9320
  }
9190
9321
  function printHelp2() {
9191
9322
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents