@synkro-sh/cli 1.6.88 → 1.6.90

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
@@ -1684,7 +1684,11 @@ export function combinedEditGradeEnabled(synkroFile?: SynkroFileConfig): boolean
1684
1684
  }
1685
1685
 
1686
1686
  async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
1687
- const body = JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind });
1687
+ // Defense-in-depth: redact any literal secret from the grade payload so a raw
1688
+ // credential never reaches the model. The grader still sees the SHAPE
1689
+ // (<redacted:...>) so its policy reasoning is unaffected.
1690
+ const safe = scrubSecrets(prompt);
1691
+ const body = JSON.stringify({ role, payload: safe, content: safe, agent_kind: agentKind });
1688
1692
 
1689
1693
  const resp = await fetch('http://127.0.0.1:' + port + '/submit', {
1690
1694
  method: 'POST',
@@ -1734,10 +1738,13 @@ const CONTAINERS_URL = process.env.SYNKRO_CONTAINERS_URL || 'https://containers.
1734
1738
  // FIRST so the caller's catch fails open cleanly before CC ever force-kills us.
1735
1739
  const CLOUD_GRADE_TIMEOUT_MS = 45000;
1736
1740
  async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeoutMs = CLOUD_GRADE_TIMEOUT_MS, agentKind: AgentKind = 'claude_code'): Promise<string> {
1741
+ // Defense-in-depth: redact any literal secret before the payload crosses the
1742
+ // network to the cloud grader \u2014 a raw credential never reaches the model.
1743
+ const safe = scrubSecrets(prompt);
1737
1744
  const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
1738
1745
  method: 'POST',
1739
1746
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1740
- body: JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind }),
1747
+ body: JSON.stringify({ role, payload: safe, content: safe, agent_kind: agentKind }),
1741
1748
  signal: AbortSignal.timeout(timeoutMs),
1742
1749
  });
1743
1750
  if (!resp.ok) {
@@ -2134,6 +2141,15 @@ function sessionLogPath(sessionId: string): string | null {
2134
2141
  }
2135
2142
 
2136
2143
  export function appendSessionAction(sessionId: string, entry: SessionAction): void {
2144
+ // Redact any secret in the action fields before they are persisted (local log
2145
+ // OR streamed to Timescale) \u2014 session diagnostics must never store a raw
2146
+ // credential. Tool name is a fixed identifier and left as-is.
2147
+ entry = {
2148
+ ...entry,
2149
+ summary: entry.summary ? scrubSecrets(entry.summary) : entry.summary,
2150
+ file: entry.file ? scrubSecrets(entry.file) : entry.file,
2151
+ outcome: entry.outcome ? scrubSecrets(entry.outcome) : entry.outcome,
2152
+ };
2137
2153
  const logPath = sessionLogPath(sessionId);
2138
2154
  if (!logPath) return;
2139
2155
  let step = 0;
@@ -2175,6 +2191,121 @@ export function readSessionLog(sessionId: string): SessionAction[] {
2175
2191
  } catch { return []; }
2176
2192
  }
2177
2193
 
2194
+ // \u2500\u2500\u2500 Secret/PII firewall \u2500\u2500\u2500
2195
+ // Detect LITERAL credentials in a proposed tool call so the hook can (a) BLOCK it
2196
+ // locally before the tool runs \u2014 the value never reaches a file, shell history,
2197
+ // OR the LLM grader (no leak to Anthropic/Cursor) \u2014 and (b) REDACT any secret
2198
+ // before it is persisted/logged. Patterns use character classes (not \\d / \\.)
2199
+ // to stay readable inside this template literal. HIGH-CONFIDENCE formats only \u2014
2200
+ // a false BLOCK stops the user's work, so we never block on generic entropy.
2201
+ interface SecretHit { type: string; value: string; }
2202
+ const HARD_SECRET_PATTERNS: Array<{ type: string; re: RegExp }> = [
2203
+ { type: 'anthropic-key', re: /sk-ant-[A-Za-z0-9_-]{24,}/g },
2204
+ { type: 'openai-key', re: /sk-(?:proj-)?[A-Za-z0-9]{32,}/g },
2205
+ { type: 'aws-access-key', re: /AKIA[0-9A-Z]{16}/g },
2206
+ { type: 'github-token', re: /gh[posru]_[A-Za-z0-9]{36,}/g },
2207
+ { type: 'google-api-key', re: /AIza[0-9A-Za-z_-]{35}/g },
2208
+ { type: 'slack-token', re: /xox[baprs]-[0-9A-Za-z-]{10,}/g },
2209
+ { type: 'stripe-key', re: /[rs]k_(?:live|test)_[A-Za-z0-9]{20,}/g },
2210
+ { type: 'jwt', re: /eyJ[A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}/g },
2211
+ { type: 'private-key', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
2212
+ { type: 'bearer-token', re: /[Bb]earer[ ]+[A-Za-z0-9._-]{20,}/g },
2213
+ ];
2214
+ // A match that is obviously a placeholder or env reference is NOT a real secret \u2014
2215
+ // never block on $VAR, <token>, {{ }}, or common dummy values.
2216
+ function isPlaceholderSecret(v: string): boolean {
2217
+ if (/[$<>{}]/.test(v)) return true;
2218
+ const tail = v.replace(/^(sk-ant-|[rs]k_(?:live|test)_|sk-|AKIA|gh[posru]_|AIza|xox[baprs]-|Bearer[ ]+)/, '');
2219
+ return /^(x+|0+|a+|placeholder|example|changeme|your[-_a-z0-9]*|dummy|test|fake|redacted|sample)$/i.test(tail);
2220
+ }
2221
+ export function detectHardSecrets(text: string): SecretHit[] {
2222
+ if (!text) return [];
2223
+ const hits: SecretHit[] = [];
2224
+ const seen = new Set<string>();
2225
+ for (const { type, re } of HARD_SECRET_PATTERNS) {
2226
+ re.lastIndex = 0;
2227
+ let m: RegExpExecArray | null;
2228
+ while ((m = re.exec(text)) !== null) {
2229
+ const v = m[0];
2230
+ if (isPlaceholderSecret(v) || seen.has(v)) continue;
2231
+ seen.add(v);
2232
+ hits.push({ type, value: v });
2233
+ }
2234
+ }
2235
+ return hits;
2236
+ }
2237
+ // Shape-preserving redaction for persist/log/grade boundaries: the reader still
2238
+ // sees that a secret of a given KIND was present, never the value. Broader than
2239
+ // the block set (over-redacting a log is harmless; missing one is not).
2240
+ export function scrubSecrets(text: string): string {
2241
+ if (!text) return text;
2242
+ let out = text;
2243
+ for (const { type, re } of HARD_SECRET_PATTERNS) {
2244
+ out = out.replace(re, (m) => isPlaceholderSecret(m) ? m : '<redacted:' + type + '>');
2245
+ }
2246
+ out = out.replace(/((?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key)["' ]*[=:][ ]*["']?)([^ "'\\n]{12,})/gi, (full, pfx, val) => {
2247
+ if (/[$<>{}]/.test(val)) return full;
2248
+ return pfx + '<redacted:credential>';
2249
+ });
2250
+ return out;
2251
+ }
2252
+ function guessEnvName(type: string, i: number): string {
2253
+ const base = type === 'anthropic-key' ? 'ANTHROPIC_API_KEY'
2254
+ : type === 'openai-key' ? 'OPENAI_API_KEY'
2255
+ : type === 'aws-access-key' ? 'AWS_ACCESS_KEY_ID'
2256
+ : type === 'github-token' ? 'GITHUB_TOKEN'
2257
+ : type === 'google-api-key' ? 'GOOGLE_API_KEY'
2258
+ : type === 'slack-token' ? 'SLACK_TOKEN'
2259
+ : type === 'stripe-key' ? 'STRIPE_API_KEY'
2260
+ : (type === 'jwt' || type === 'bearer-token') ? 'AUTH_TOKEN'
2261
+ : 'SECRET';
2262
+ return i === 0 ? base : base + '_' + (i + 1);
2263
+ }
2264
+ // Agent-facing remediation: show the SAME command with each literal secret swapped
2265
+ // for an env reference, so the agent can re-issue it securely without the value
2266
+ // ever appearing in the command, logs, or model context.
2267
+ export function secretRemediation(command: string, hits: SecretHit[]): string {
2268
+ let safe = command;
2269
+ const names: string[] = [];
2270
+ hits.forEach((h, i) => {
2271
+ const envName = guessEnvName(h.type, i);
2272
+ names.push(envName);
2273
+ safe = safe.split(h.value).join('$' + envName);
2274
+ });
2275
+ const typeList = hits.map(h => h.type).join(', ');
2276
+ return 'Synkro blocked this tool call: it contains a literal credential (' + typeList + '). '
2277
+ + 'A secret value must NEVER be inlined, printed, or echoed \u2014 that leaks it into shell history, logs, and the model context. '
2278
+ + 'Reference the credential from the environment so its value resolves at runtime and never appears in the command. '
2279
+ + 'Set it once out-of-band (export ' + names[0] + '=... in an untracked file, or load from your secret manager), then re-issue as:\\n ' + safe + '\\n'
2280
+ + 'Do NOT print the value to verify it; check the variable is non-empty instead, e.g. test -n "$' + names[0] + '". Re-issue with the literal removed \u2014 do not ask the user to paste it.';
2281
+ }
2282
+ // The NEW text an edit introduces, so we flag a secret the agent is ADDING \u2014 never
2283
+ // a pre-existing one already in the file (which would falsely block any edit to a
2284
+ // file that contains a credential). Write = whole new content; Edit = new_string;
2285
+ // MultiEdit = every edit's new_string.
2286
+ export function editedAddedText(toolName: string, toolInput: any): string {
2287
+ const ti = toolInput || {};
2288
+ if (toolName === 'MultiEdit' && Array.isArray(ti.edits)) {
2289
+ return ti.edits.map((e: any) => (e && typeof e.new_string === 'string') ? e.new_string : '').join('\\n');
2290
+ }
2291
+ return String(ti.content ?? ti.contents ?? ti.new_string ?? ti.new_source ?? ti.code_edit ?? ti.patch ?? '');
2292
+ }
2293
+ // Deny payload when an edit HARDCODES a literal credential (else null). Code-edit
2294
+ // remediation: load from env/secret manager at runtime, never inline the value.
2295
+ export function editSecretDenial(toolName: string, toolInput: any, fileShort: string, tagStr: string): { systemMessage: string; hookSpecificOutput: any } | null {
2296
+ const hits = detectHardSecrets(editedAddedText(toolName, toolInput));
2297
+ if (!hits.length) return null;
2298
+ const types = hits.map(h => h.type).join(', ');
2299
+ const rem = 'Synkro blocked this edit: it hardcodes a literal credential (' + types + ') into ' + fileShort + '. '
2300
+ + 'NEVER commit a secret value to a file \u2014 it leaks into source control, logs, and the model context. '
2301
+ + 'Load it at runtime from the environment or a secret manager and reference it instead (e.g. process.env.SECRET_NAME, os.environ["SECRET_NAME"], or your config/secret provider). '
2302
+ + 'Re-do the edit with the literal removed; do not ask the user to paste it.';
2303
+ return {
2304
+ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 BLOCKED: hardcoded credential (' + types + '). Not sent to the grader; reference it via the environment.',
2305
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: rem, additionalContext: rem },
2306
+ };
2307
+ }
2308
+
2178
2309
  const RETRIEVAL_RECENT_N = 40;
2179
2310
  const PROCESS_KW = ['test', 'typecheck', 'tsc', 'lint', 'build', 'migrate', 'migration', 'deploy', 'publish', 'push', 'seed'];
2180
2311
 
@@ -2578,6 +2709,11 @@ export function mintEventId(prefix = 'evt'): string {
2578
2709
  export function appendLocalTelemetry(body: Record<string, any>): void {
2579
2710
  if ((process.env.SYNKRO_STORAGE_MODE || 'local') !== 'local') return;
2580
2711
  const event = { ...body };
2712
+ // Redact secrets from free-text fields before they reach the spool/dashboard,
2713
+ // for any caller that passes raw tool-call content (idempotent if already scrubbed).
2714
+ for (const k of ['command', 'reasoning', 'summary', 'target', 'content']) {
2715
+ if (typeof event[k] === 'string') event[k] = scrubSecrets(event[k]);
2716
+ }
2581
2717
  if (!event.event_id) {
2582
2718
  const ct = String(event.capture_type || '');
2583
2719
  const prefix = ct === 'usage_tick' ? 'usage' : ct === 'edit_scan' ? 'edit' : 'evt';
@@ -2799,6 +2935,12 @@ export function filePathFromToolInput(toolInput: any): string {
2799
2935
  }
2800
2936
 
2801
2937
  export function reconstructContent(toolName: string, toolInput: any, filePath: string, cwd?: string): string {
2938
+ // Cap the grade payload (64KB ~= 16K tokens) so a huge Write/whole-file edit
2939
+ // cannot push grader inference past its 45s budget. Applies UNIFORMLY to every
2940
+ // path, including the previously-uncapped Write/StrReplace/default branches.
2941
+ return reconstructContentInner(toolName, toolInput, filePath, cwd).slice(0, 65536);
2942
+ }
2943
+ function reconstructContentInner(toolName: string, toolInput: any, filePath: string, cwd?: string): string {
2802
2944
  const canRead = filePath && cwd && isPathUnder(filePath, cwd);
2803
2945
  switch (toolName) {
2804
2946
  case 'ApplyPatch':
@@ -3947,6 +4089,13 @@ export async function logGraderUnavailable(
3947
4089
  gradeInput?: string, ctx?: { sessionId?: string; repo?: string },
3948
4090
  ): Promise<void> {
3949
4091
  const { category, why } = classifyGraderError(errorMessage);
4092
+ // Redact any secret from the diagnostic fields before they are persisted
4093
+ // (Timescale in cloud, on-device log in local). The grade input especially can
4094
+ // carry a credential (a .env edit, a bash command) \u2014 debug diagnostics must
4095
+ // never store a raw secret. classifyGraderError ran on the original above.
4096
+ target = scrubSecrets(target || '');
4097
+ errorMessage = scrubSecrets(errorMessage || '');
4098
+ gradeInput = gradeInput ? scrubSecrets(gradeInput) : gradeInput;
3950
4099
 
3951
4100
  if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
3952
4101
  try {
@@ -4054,7 +4203,7 @@ export function outputEmpty(): void {
4054
4203
  EDIT_PRECHECK_TS = `#!/usr/bin/env bun
4055
4204
  import {
4056
4205
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
4057
- parseVerdict, parseCombinedVerdict, combinedEditGradeEnabled, dispatchCapture, dispatchFinding, ruleMode, reconstructContent, isPathUnder, postWithRetry,
4206
+ parseVerdict, parseCombinedVerdict, combinedEditGradeEnabled, dispatchCapture, dispatchFinding, ruleMode, reconstructContent, editSecretDenial, isPathUnder, postWithRetry,
4058
4207
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
4059
4208
  appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, log,
4060
4209
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
@@ -4180,6 +4329,14 @@ async function main() {
4180
4329
  return;
4181
4330
  }
4182
4331
 
4332
+ // Secret firewall: block an edit that HARDCODES a literal credential before it
4333
+ // reaches the grader/model. Flags only the NEW content (never a pre-existing
4334
+ // secret already in the file).
4335
+ {
4336
+ const _den = editSecretDenial(toolName, toolInput, fileShort, tagStr);
4337
+ if (_den) { outputJson(_den); return; }
4338
+ }
4339
+
4183
4340
  if (rt === 'local') {
4184
4341
  // \u2500\u2500\u2500 Local grading: org rules ONLY (channel 1, port 18929) \u2500\u2500\u2500
4185
4342
  const proposedShort = proposed.slice(0, 4000);
@@ -4428,7 +4585,7 @@ main();
4428
4585
  CWE_PRECHECK_TS = String.raw`#!/usr/bin/env bun
4429
4586
  import {
4430
4587
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
4431
- localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
4588
+ localGradeCwe, parseVerdict, reconstructContent, editSecretDenial, readStdin, log,
4432
4589
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
4433
4590
  extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, cweSnippetLines, dispatchFinding, dispatchCapture, GATEWAY_URL,
4434
4591
  logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
@@ -4691,6 +4848,14 @@ async function main() {
4691
4848
  const graderLabel = graderPool === 'claude_code' ? 'claude' : graderPool;
4692
4849
  const cweTag = '[synkro:' + rt + ':cweScan:' + graderLabel + ']';
4693
4850
 
4851
+ // Secret firewall: a hardcoded credential in the edit is CWE-798 — block it
4852
+ // locally (deterministic, no model) before the CWE grade, flagging only the
4853
+ // NEW content the edit introduces.
4854
+ {
4855
+ const _den = editSecretDenial(scanToolName, scanToolInput, fileShort, cweTag);
4856
+ if (_den) { outputJson(_den); return; }
4857
+ }
4858
+
4694
4859
  if (rt === 'skip') {
4695
4860
  outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, 'channel down', graderPool) });
4696
4861
  return;
@@ -5452,6 +5617,7 @@ import {
5452
5617
  logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
5453
5618
  loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
5454
5619
  hashCommand, resolveTranscriptPath, isCursorHookFormat,
5620
+ detectHardSecrets, secretRemediation, scrubSecrets,
5455
5621
  type HookConfig, type Rule,
5456
5622
  } from './_synkro-common.ts';
5457
5623
  import { createHash } from 'node:crypto';
@@ -5591,6 +5757,35 @@ async function main() {
5591
5757
  return;
5592
5758
  }
5593
5759
 
5760
+ // ─── Secret firewall: BLOCK a literal credential in the command BEFORE it runs
5761
+ // or reaches the grader — the value never lands in a file, shell history, OR the
5762
+ // LLM provider (no leak to Anthropic/Cursor). Deterministic + local, no model
5763
+ // call. High-confidence formats only (a false block stops the user), and
5764
+ // $VAR / placeholder refs never trip it. ───
5765
+ const secretHits = detectHardSecrets(command);
5766
+ if (secretHits.length) {
5767
+ const types = secretHits.map(h => h.type).join(', ');
5768
+ const remediation = secretRemediation(command, secretHits);
5769
+ log('bashGuard ' + cmdShort + ' → BLOCKED: literal credential (' + types + ')');
5770
+ appendLocalTelemetry({
5771
+ capture_type: 'local_verdict',
5772
+ verdict: 'block',
5773
+ hook_type: 'bash',
5774
+ category: 'secret_exposure',
5775
+ tool_name: toolName,
5776
+ command: scrubSecrets(command).slice(0, 200),
5777
+ session_id: sessionId,
5778
+ repo: gitRepo || cwd,
5779
+ cc_model: transcript.ccModel,
5780
+ reasoning: 'Literal credential (' + types + ') in the proposed command — blocked locally before grading; never sent to the model.',
5781
+ });
5782
+ outputJson({
5783
+ systemMessage: tagStr + ' bashGuard → BLOCKED: literal credential in command (' + types + '). Not sent to the grader; use an env var reference instead.',
5784
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: remediation, additionalContext: remediation },
5785
+ });
5786
+ return;
5787
+ }
5788
+
5594
5789
  // ─── Install protection: install-scan runs first and owns block traces ───
5595
5790
  let scanConcern = '';
5596
5791
  let scanBlockContext = '';
@@ -6481,6 +6676,7 @@ import {
6481
6676
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
6482
6677
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
6483
6678
  isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
6679
+ detectHardSecrets, secretRemediation, scrubSecrets,
6484
6680
  extractTranscript, readLastPrompt, readSessionLog, compressSessionLog, sessionLogForGrade,
6485
6681
  appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
6486
6682
  loadSynkroFile, effectiveGraderPool, synkroFilePresent,
@@ -6637,6 +6833,23 @@ async function main() {
6637
6833
  const rt = await route(config, synkroFile);
6638
6834
  const tagStr = tag(rt, config, graderPool);
6639
6835
 
6836
+ // Secret firewall: BLOCK a literal credential in the command BEFORE it runs or
6837
+ // reaches the grader \u2014 never leaks to a file, shell history, or the model
6838
+ // (Anthropic/Cursor). Local + deterministic; $VAR / placeholder refs never trip it.
6839
+ const secretHits = detectHardSecrets(command);
6840
+ if (secretHits.length) {
6841
+ const types = secretHits.map(h => h.type).join(', ');
6842
+ const remediation = secretRemediation(command, secretHits);
6843
+ log('bashGuard ' + cmdShort + ' \u2192 BLOCKED: literal credential (' + types + ')');
6844
+ appendLocalTelemetry({
6845
+ capture_type: 'local_verdict', verdict: 'block', hook_type: 'bash',
6846
+ category: 'secret_exposure', tool_name: toolName, command: scrubSecrets(command).slice(0, 200),
6847
+ session_id: sessionId, repo: repo || cwd, cc_model: model,
6848
+ reasoning: 'Literal credential (' + types + ') in the proposed command \u2014 blocked locally before grading; never sent to the model.',
6849
+ });
6850
+ finishWith({ permission: 'deny', user_message: tagStr + ' bashGuard \u2192 BLOCKED: literal credential in command (' + types + '). Use an env var reference instead.', agent_message: remediation });
6851
+ }
6852
+
6640
6853
  // Install protection \u2014 install-scan runs first and owns block traces.
6641
6854
  let scanConcern = '';
6642
6855
  let scanBlockContext = '';
@@ -10951,7 +11164,7 @@ function writeConfigEnv(opts) {
10951
11164
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10952
11165
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10953
11166
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10954
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.88")}`
11167
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.90")}`
10955
11168
  ];
10956
11169
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10957
11170
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -14994,7 +15207,7 @@ var args = process.argv.slice(2);
14994
15207
  var cmd = args[0] || "";
14995
15208
  var subArgs = args.slice(1);
14996
15209
  function printVersion() {
14997
- console.log("1.6.88");
15210
+ console.log("1.6.90");
14998
15211
  }
14999
15212
  function printHelp2() {
15000
15213
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents