token-goat 2.9.9 → 2.9.11

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.
Files changed (27) hide show
  1. package/README.md +54 -11
  2. package/SECURITY.md +17 -19
  3. package/THIRD_PARTY_NOTICES.md +667 -0
  4. package/dist/token-goat-chunk-2ZUPQYLO.mjs +3728 -0
  5. package/dist/{token-goat-chunk-LQ3SGWIW.mjs → token-goat-chunk-3ZALKJ23.mjs} +69 -37
  6. package/dist/{token-goat-chunk-WBPVYAEO.mjs → token-goat-chunk-4NZUXC4F.mjs} +3 -2
  7. package/dist/{token-goat-chunk-KIEOFWLL.mjs → token-goat-chunk-AAEYU2U2.mjs} +5307 -6350
  8. package/dist/{token-goat-chunk-OBDTBOQA.mjs → token-goat-chunk-FDURVZQD.mjs} +10 -2
  9. package/dist/{token-goat-chunk-DRH4CVGF.mjs → token-goat-chunk-HDL77BN3.mjs} +11 -7
  10. package/dist/{token-goat-chunk-LYWIRYCF.mjs → token-goat-chunk-LHLQFGWQ.mjs} +1 -1
  11. package/dist/{token-goat-chunk-L6Q2XM5X.mjs → token-goat-chunk-LKOYKSID.mjs} +1275 -655
  12. package/dist/{token-goat-chunk-P5SQX2VK.mjs → token-goat-chunk-MKITQ5RY.mjs} +354 -180
  13. package/dist/{token-goat-chunk-PNMGVJ4C.mjs → token-goat-chunk-MRZ555B3.mjs} +2699 -2175
  14. package/dist/{token-goat-chunk-K5GKX6ND.mjs → token-goat-chunk-PJPFOOGM.mjs} +6 -5
  15. package/dist/{token-goat-chunk-FQD3OB5W.mjs → token-goat-chunk-QCQUIPIP.mjs} +6068 -1687
  16. package/dist/{token-goat-chunk-BYDDWQ2T.mjs → token-goat-chunk-SY7WTZMW.mjs} +5043 -7721
  17. package/dist/{token-goat-chunk-PXWBHSFB.mjs → token-goat-chunk-T6M7DAW3.mjs} +31 -4
  18. package/dist/{token-goat-chunk-QCHD2ENV.mjs → token-goat-chunk-V53Z47ZH.mjs} +85 -328
  19. package/dist/token-goat-chunk-Y5VKNLVD.mjs +5942 -0
  20. package/dist/token-goat-chunk-YCKCFYTM.mjs +3564 -0
  21. package/dist/token-goat-hook.mjs +6 -5
  22. package/dist/token-goat.core.mjs +8 -6
  23. package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
  24. package/docs/cli.md +4 -1
  25. package/docs/install.md +131 -11
  26. package/docs/security.md +3 -1
  27. package/package.json +8 -3
@@ -1,5 +1,10 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ canRunWrappedShell,
5
+ clipToDeliveryCap,
6
+ deliveredOutputBytes
7
+ } from "./token-goat-chunk-FDURVZQD.mjs";
3
8
  import {
4
9
  DEFAULT_RECONCILE_BUDGET_MS,
5
10
  HOOK_EVENTS,
@@ -10,7 +15,6 @@ import {
10
15
  getWebOutput,
11
16
  isReconcileClean,
12
17
  lineMayCarryResidentSignal,
13
- normalizePayload,
14
18
  readStdinJson,
15
19
  readTranscriptTail,
16
20
  reconcileProject,
@@ -18,7 +22,7 @@ import {
18
22
  storeWebOutput,
19
23
  summarizeResidentContext,
20
24
  taskListPruneHint
21
- } from "./token-goat-chunk-QCHD2ENV.mjs";
25
+ } from "./token-goat-chunk-V53Z47ZH.mjs";
22
26
  import {
23
27
  BASH_OUTPUT_SUBDIR,
24
28
  GENERIC_SERVED_OUTPUT_KEY,
@@ -38,10 +42,10 @@ import {
38
42
  countSymbols,
39
43
  enqueueDirtyPathSafe,
40
44
  ensureWorkerAlive,
41
- estimateTokens,
42
45
  extractCompactFromMarker,
43
46
  extractMarkdownHeadings,
44
47
  fenceUntrusted,
48
+ fenceUntrustedSpans,
45
49
  fenceWithMatches,
46
50
  foldDelivery,
47
51
  foldDetail,
@@ -90,6 +94,7 @@ import {
90
94
  planMarkdownOutline,
91
95
  planServedElisions,
92
96
  planSourceSkeleton,
97
+ preToolPathDeclined,
93
98
  probeImageMeta,
94
99
  recordBashOutput,
95
100
  recordBashRerun,
@@ -122,68 +127,51 @@ import {
122
127
  takePendingLargeFileHint,
123
128
  visionTokens,
124
129
  visionTokensSaved,
130
+ vscodePathAllowed,
125
131
  wasCliReadThisSession,
126
132
  wasFileReadThisSession,
127
133
  wasHintShown
128
- } from "./token-goat-chunk-BYDDWQ2T.mjs";
129
- import {
130
- bashOutputCapBytes,
131
- canRunWrappedShell,
132
- deliveredOutputBytes
133
- } from "./token-goat-chunk-OBDTBOQA.mjs";
134
+ } from "./token-goat-chunk-SY7WTZMW.mjs";
134
135
  import {
135
136
  BODY_FIRST_TOOL_RESPONSE_KEYS,
136
- ENV_KEYS,
137
- IDENTICAL_READ_MIN_BODY_BYTES,
138
137
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
139
- PER_FILE_COUNTERFACTUAL_CEILING,
140
- UNTRUSTED_TOOL_TAG,
141
- VERSION,
142
138
  compressOutput,
143
- containsLineRun,
144
139
  contextOutput,
145
- countNoun,
146
140
  dedupeConsecutive,
147
141
  denyOutput,
148
142
  detectFromCommand,
149
143
  detectHarness,
150
144
  detectLanguage,
151
- displaySafePath,
152
- displaySafeText,
153
145
  emitRewrite,
154
146
  emitRewriteIfChanged,
155
147
  envBool,
156
148
  envInt,
157
- extractErrorMessage,
149
+ estimateTokens,
158
150
  extractToolResponseField,
159
151
  extractToolResultText,
160
- fenceUntrustedFileContent,
161
152
  filterByName,
162
153
  findProject,
163
- foldPath,
164
154
  getCwd,
165
155
  getDb,
166
156
  getFilePath,
167
157
  getHarnessName,
168
158
  getToolInput,
169
159
  getToolName,
170
- globalDbPath,
171
160
  hasBareBackgroundOrNewline,
172
161
  hasUnquotedOperator,
173
162
  isMcpErrorResponse,
174
163
  isRewriteWorthwhile,
175
164
  isUnderSystemTemp,
165
+ languageHasFlag,
176
166
  loadConfig,
177
167
  makeDedupHintHandlers,
178
- neutralizeSpokenMarkers,
179
- normalizePath,
168
+ normalizePayload,
180
169
  passOutput,
170
+ readCopilotHooksOwners,
181
171
  recordStat,
182
172
  redactSecrets,
183
173
  registerHook,
184
- resolveIndexPath,
185
174
  resolveMinNetSavingsBytes,
186
- runGit,
187
175
  runHook,
188
176
  savedTokensFromBytes,
189
177
  serializeOutput,
@@ -191,10 +179,35 @@ import {
191
179
  shlexSplit,
192
180
  shortFingerprint,
193
181
  splitOwnTrailingNotices,
194
- stripAnsiEscapes,
182
+ stripAnsiEscapes
183
+ } from "./token-goat-chunk-AAEYU2U2.mjs";
184
+ import {
185
+ ENV_KEYS,
186
+ IDENTICAL_READ_MIN_BODY_BYTES,
187
+ PER_FILE_COUNTERFACTUAL_CEILING,
188
+ UNTRUSTED_TOOL_TAG,
189
+ VERSION,
190
+ atomicWriteText,
191
+ containsLineRun,
192
+ countNoun,
193
+ dataDir,
194
+ displaySafePath,
195
+ displaySafeText,
196
+ ensureDirSync,
197
+ escapesOntoNetworkThroughLinks,
198
+ extractErrorMessage,
199
+ fenceUntrustedFileContent,
200
+ foldPath,
201
+ globalDbPath,
202
+ hasCreatedConfig,
203
+ isInsideRoot,
204
+ neutralizeSpokenMarkers,
205
+ normalizePath,
206
+ resolveIndexPath,
207
+ runGit,
195
208
  toDisplayPath,
196
209
  toKB
197
- } from "./token-goat-chunk-KIEOFWLL.mjs";
210
+ } from "./token-goat-chunk-YCKCFYTM.mjs";
198
211
  import {
199
212
  init_define_import_meta_env
200
213
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -244,6 +257,120 @@ function stripUnsafeSuggestions(text) {
244
257
  }
245
258
  }
246
259
 
260
+ // src/vscode_duplicate.ts
261
+ init_define_import_meta_env();
262
+ import * as fs from "node:fs";
263
+ import * as os from "node:os";
264
+ import * as path from "node:path";
265
+ var VSCODE_HOOKS_DIR_ENV = "TOKEN_GOAT_VSCODE_HOOKS_DIR";
266
+ var MARKER_MAX_AGE_MS = 60 * 60 * 1e3;
267
+ var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
268
+ function markerDir() {
269
+ return path.join(dataDir(), "vscode-dedupe");
270
+ }
271
+ function foldDir(p) {
272
+ const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
273
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
274
+ }
275
+ function shimHooksDir() {
276
+ const raw = process.env[VSCODE_HOOKS_DIR_ENV];
277
+ return typeof raw === "string" && raw !== "" ? raw : void 0;
278
+ }
279
+ function userHooksDir() {
280
+ return path.join(os.homedir(), ".copilot", "hooks");
281
+ }
282
+ function userScopeCopyIsRedundant(event) {
283
+ const hooksDir = shimHooksDir();
284
+ if (hooksDir === void 0) return false;
285
+ if (foldDir(hooksDir) !== foldDir(userHooksDir())) return false;
286
+ const cwd = getCwd(event);
287
+ if (cwd === void 0 || cwd === "") return false;
288
+ const projectHooksDir = path.join(cwd, ".github", "hooks");
289
+ if (!vscodePathAllowed(projectHooksDir, cwd)) return false;
290
+ if (!readCopilotHooksOwners(projectHooksDir).has("vscode")) return false;
291
+ return hasCreatedConfig(path.join(projectHooksDir, "token-goat.json"));
292
+ }
293
+ function logStandDown(reason, event) {
294
+ if (process.env["TOKEN_GOAT_LOG"] === void 0 || process.env["TOKEN_GOAT_LOG"] === "") return;
295
+ try {
296
+ process.stderr.write(displaySafeText(`token-goat: vscode hook stood down (${reason}) event=${event.eventName} cwd=${getCwd(event) ?? "<none>"}`) + "\n");
297
+ } catch {
298
+ }
299
+ }
300
+ function markerName(sessionId, eventName, timestamp) {
301
+ const raw = `${sessionId}\0${eventName}\0${timestamp}`;
302
+ let h1 = 2166136261;
303
+ let h2 = 16777619;
304
+ for (let i = 0; i < raw.length; i++) {
305
+ h1 = Math.imul(h1 ^ raw.charCodeAt(i), 16777619) >>> 0;
306
+ h2 = Math.imul(h2 + raw.charCodeAt(i), 2246822507) >>> 0;
307
+ }
308
+ return `${h1.toString(16).padStart(8, "0")}${h2.toString(16).padStart(8, "0")}`;
309
+ }
310
+ function pruneMarkers(dir) {
311
+ const stamp = path.join(dir, ".pruned");
312
+ try {
313
+ const last = fs.statSync(stamp).mtimeMs;
314
+ if (Date.now() - last < PRUNE_INTERVAL_MS) return;
315
+ } catch {
316
+ }
317
+ try {
318
+ atomicWriteText(stamp, "");
319
+ } catch {
320
+ }
321
+ const cutoff = Date.now() - MARKER_MAX_AGE_MS;
322
+ let entries;
323
+ try {
324
+ entries = fs.readdirSync(dir);
325
+ } catch {
326
+ return;
327
+ }
328
+ for (const entry of entries) {
329
+ if (entry === ".pruned") continue;
330
+ const full = path.join(dir, entry);
331
+ try {
332
+ if (fs.statSync(full).mtimeMs < cutoff) fs.rmSync(full, { force: true });
333
+ } catch {
334
+ }
335
+ }
336
+ }
337
+ function alreadyClaimed(sessionId, eventName, timestamp) {
338
+ const dir = markerDir();
339
+ try {
340
+ ensureDirSync(dir);
341
+ } catch {
342
+ return false;
343
+ }
344
+ const marker = path.join(dir, markerName(sessionId, eventName, timestamp));
345
+ try {
346
+ fs.closeSync(fs.openSync(marker, "wx"));
347
+ } catch (e) {
348
+ if (e.code === "EEXIST") return true;
349
+ return false;
350
+ }
351
+ pruneMarkers(dir);
352
+ return false;
353
+ }
354
+ function shouldSuppressDuplicateVscodeHook(event, harness) {
355
+ if (harness !== "vscode") return false;
356
+ try {
357
+ if (userScopeCopyIsRedundant(event)) {
358
+ logStandDown("a project-scope install owns this workspace", event);
359
+ return true;
360
+ }
361
+ if (getFilePath(event) !== void 0) return false;
362
+ const sessionId = event.sessionId;
363
+ if (typeof sessionId !== "string" || sessionId === "") return false;
364
+ const timestamp = event.raw["timestamp"];
365
+ if (typeof timestamp !== "string" || timestamp === "") return false;
366
+ if (!alreadyClaimed(sessionId, event.eventName, timestamp)) return false;
367
+ logStandDown("another copy already claimed this event", event);
368
+ return true;
369
+ } catch {
370
+ return false;
371
+ }
372
+ }
373
+
247
374
  // src/hooks_grep.ts
248
375
  init_define_import_meta_env();
249
376
  function grepIntInput(toolInput, key) {
@@ -258,7 +385,7 @@ function grepIntInput(toolInput, key) {
258
385
  function grepSignature(toolInput) {
259
386
  const pattern = toolInput["pattern"];
260
387
  if (typeof pattern !== "string" || pattern === "") return null;
261
- const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
388
+ const path4 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
262
389
  const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
263
390
  const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
264
391
  const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
@@ -273,7 +400,7 @@ function grepSignature(toolInput) {
273
400
  const offset = grepIntInput(toolInput, "offset");
274
401
  return JSON.stringify([
275
402
  pattern,
276
- path3,
403
+ path4,
277
404
  outputMode,
278
405
  glob,
279
406
  type,
@@ -356,7 +483,6 @@ function foldGrepContentHandler(event) {
356
483
  }
357
484
  }
358
485
  var DOC_EXT_RE = /\.(?:md|mdx|rst|txt)$/i;
359
- var SOURCE_EXT_RE = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less)$/i;
360
486
  var STRUCTURAL_DOC_PATTERN_RE = /^(?:\^)?#+\s*/;
361
487
  var STRUCTURAL_SOURCE_PATTERN_RE = /^(?:\^|\s)*(?:def|class|function|async\s+def|async\s+function|export\s+(?:default\s+)?(?:class|function|interface|type|const|enum)|func|fn|struct|interface|impl|type)\b/i;
362
488
  function extractGrepStructuralSearch(toolInput) {
@@ -375,7 +501,7 @@ function extractGrepStructuralSearch(toolInput) {
375
501
  if (DOC_EXT_RE.test(rawPath) && STRUCTURAL_DOC_PATTERN_RE.test(pattern)) {
376
502
  return { filePath: rawPath, isDoc: true, isSource: false };
377
503
  }
378
- if (SOURCE_EXT_RE.test(rawPath) && STRUCTURAL_SOURCE_PATTERN_RE.test(pattern)) {
504
+ if (languageHasFlag(detectLanguage(rawPath), "grepSource") && STRUCTURAL_SOURCE_PATTERN_RE.test(pattern)) {
379
505
  return { filePath: rawPath, isDoc: false, isSource: true };
380
506
  }
381
507
  return null;
@@ -418,8 +544,8 @@ function isBroadCatchAllGlob(pattern, pathArg) {
418
544
  function globSignature(toolInput) {
419
545
  const pattern = toolInput["pattern"];
420
546
  if (typeof pattern !== "string" || pattern === "") return null;
421
- const path3 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
422
- return JSON.stringify([pattern, path3]);
547
+ const path4 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
548
+ return JSON.stringify([pattern, path4]);
423
549
  }
424
550
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
425
551
  toolName: "Glob",
@@ -451,8 +577,8 @@ registerHook("post_tool_use", postGlobHandler, { toolName: "Glob" });
451
577
 
452
578
  // src/hooks_edit.ts
453
579
  init_define_import_meta_env();
454
- import * as path from "node:path";
455
- import { statSync } from "node:fs";
580
+ import * as path2 from "node:path";
581
+ import { statSync as statSync2 } from "node:fs";
456
582
  function postEditHandlerInner(event) {
457
583
  const filePath = getFilePath(event);
458
584
  if (filePath === void 0) return passOutput();
@@ -479,11 +605,11 @@ function postEditHandlerInner(event) {
479
605
  if (loadConfig().hints.stable_doc_compacts) {
480
606
  markCompactStale(compactPathFor(normalized));
481
607
  }
482
- const editedBasename = path.basename(normalized);
608
+ const editedBasename = path2.basename(normalized);
483
609
  if (/\.(md|mdx|markdown|rst)$/i.test(editedBasename)) {
484
610
  let editedSize = Infinity;
485
611
  try {
486
- editedSize = statSync(normalized).size;
612
+ editedSize = statSync2(normalized).size;
487
613
  } catch {
488
614
  }
489
615
  if (meetsSavingsFloor(editedSize)) {
@@ -505,7 +631,7 @@ registerHook("post_tool_use", postEditHandler, { toolName: "NotebookEdit" });
505
631
 
506
632
  // src/hooks_write.ts
507
633
  init_define_import_meta_env();
508
- import { readFileSync, statSync as statSync2 } from "node:fs";
634
+ import { readFileSync, statSync as statSync3 } from "node:fs";
509
635
  var MAX_LINES_FOR_DIFF = 4e3;
510
636
  var MAX_OLD_FILE_BYTES_FOR_DIFF = 4 * 1024 * 1024;
511
637
  function splitLines(text) {
@@ -533,11 +659,12 @@ function preWriteRewriteHandler(event) {
533
659
  if (getToolName(event) !== "Write") return passOutput();
534
660
  const filePath = getFilePath(event);
535
661
  if (filePath === void 0) return passOutput();
662
+ if (preToolPathDeclined(event, filePath)) return passOutput();
536
663
  const newContent = getToolInput(event)["content"];
537
664
  if (typeof newContent !== "string") return passOutput();
538
665
  let stat;
539
666
  try {
540
- stat = statSync2(filePath);
667
+ stat = statSync3(filePath);
541
668
  } catch {
542
669
  return passOutput();
543
670
  }
@@ -600,13 +727,15 @@ function mergeManifestFiles(parent, siblingFiles) {
600
727
  byPath.set(key, f);
601
728
  continue;
602
729
  }
730
+ const mergedSymbols = [.../* @__PURE__ */ new Set([...prev.symbols_read ?? [], ...f.symbols_read ?? []])];
603
731
  byPath.set(key, {
604
732
  path: prev.path,
605
733
  readCount: Math.max(prev.readCount, f.readCount),
606
734
  lastReadAt: Math.max(prev.lastReadAt, f.lastReadAt),
607
735
  wasEdited: prev.wasEdited || f.wasEdited,
608
736
  sizeBytes: f.lastReadAt >= prev.lastReadAt ? f.sizeBytes : prev.sizeBytes,
609
- ...prev.wasTruncated || f.wasTruncated ? { wasTruncated: true } : {}
737
+ ...prev.wasTruncated || f.wasTruncated ? { wasTruncated: true } : {},
738
+ ...mergedSymbols.length > 0 ? { symbols_read: mergedSymbols } : {}
610
739
  });
611
740
  }
612
741
  return Array.from(byPath.values());
@@ -693,14 +822,14 @@ function buildSafeToDiscardSection(files) {
693
822
  if (id === void 0) continue;
694
823
  const entry = getBashOutput(id);
695
824
  if (entry === null) continue;
696
- const flatCommand = entry.command.replace(/[\t\r\n]+/g, " ");
825
+ const flatCommand = neutralizeSpokenMarkers(entry.command.replace(/[\t\r\n]+/g, " "));
697
826
  rerunRows.push("- `" + flatCommand + "` \u2014 an older run of this exact command was superseded; recall the surviving copy with `bash-output " + id + " --full`");
698
827
  }
699
828
  const supersededReadRows = [];
700
829
  for (const f of files) {
701
830
  if (f.readCount > 1 || f.wasEdited) {
702
831
  const reason = f.wasEdited ? "edited after being read" : "re-read " + f.readCount + "x";
703
- supersededReadRows.push("- " + f.path + " (" + reason + " \u2014 only the latest content already in context is current)");
832
+ supersededReadRows.push("- " + displaySafePath(f.path) + " (" + reason + " \u2014 only the latest content already in context is current)");
704
833
  }
705
834
  }
706
835
  const cachedOutputRows = [];
@@ -708,7 +837,7 @@ function buildSafeToDiscardSection(files) {
708
837
  if (rerunHashSet.has(hash)) continue;
709
838
  const entry = getBashOutput(id);
710
839
  if (entry === null) continue;
711
- const flatCommand = entry.command.replace(/[\t\r\n]+/g, " ");
840
+ const flatCommand = neutralizeSpokenMarkers(entry.command.replace(/[\t\r\n]+/g, " "));
712
841
  cachedOutputRows.push("- `" + flatCommand + "` \u2014 recallable via `bash-output " + id + " --full`");
713
842
  }
714
843
  const total = rerunRows.length + supersededReadRows.length + cachedOutputRows.length;
@@ -798,7 +927,7 @@ init_define_import_meta_env();
798
927
  var TRACKED_SKILL = "token-goat";
799
928
  var MAX_COMMANDS_SHOWN = 8;
800
929
  async function currentCommandNames() {
801
- const { buildProgram } = await import("./token-goat-chunk-DRH4CVGF.mjs");
930
+ const { buildProgram } = await import("./token-goat-chunk-HDL77BN3.mjs");
802
931
  return flattenCommandNames(buildCommandManifest(buildProgram()));
803
932
  }
804
933
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -852,7 +981,7 @@ async function checkSkillVersionDrift(sessionId) {
852
981
 
853
982
  // src/pending_context.ts
854
983
  init_define_import_meta_env();
855
- import { readFileSync as readFileSync2, rmSync, writeFileSync, mkdirSync } from "node:fs";
984
+ import { readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync } from "node:fs";
856
985
  import { dirname } from "node:path";
857
986
  var PENDING_SUFFIX = ".pending-context.txt";
858
987
  var MAX_PENDING_CONTEXT_BYTES = 4096;
@@ -866,7 +995,7 @@ function queuePendingContext(sessionId, text) {
866
995
  const merged = existing === null ? trimmed : `${existing}
867
996
  ${trimmed}`;
868
997
  const capped = merged.length <= MAX_PENDING_CONTEXT_BYTES ? merged : merged.slice(merged.length - MAX_PENDING_CONTEXT_BYTES);
869
- mkdirSync(dirname(target), { recursive: true });
998
+ ensureDirSync(dirname(target));
870
999
  writeFileSync(target, capped, "utf8");
871
1000
  } catch {
872
1001
  }
@@ -876,7 +1005,7 @@ function drainPendingContext(sessionId) {
876
1005
  if (target === null) return null;
877
1006
  const text = readPending(target);
878
1007
  try {
879
- rmSync(target, { force: true });
1008
+ rmSync2(target, { force: true });
880
1009
  } catch {
881
1010
  }
882
1011
  return text;
@@ -1016,6 +1145,19 @@ async function userPromptSubmitHandler(event) {
1016
1145
  }
1017
1146
  var CLAIMED_CHANGE_VERBS_RE = /\b(fix(?:ed|es|ing)?|implement(?:ed|s|ing)?|add(?:ed|s|ing)?|creat(?:e|ed|es|ing)|writ(?:e|ten|es|ing)|refactor(?:ed|s|ing)?|updat(?:e|ed|es|ing)|chang(?:e|ed|es|ing)|edit(?:ed|s|ing)?|modif(?:y|ied|ies|ying)|delet(?:e|ed|es|ing)|remov(?:e|ed|es|ing)|patch(?:ed|es|ing)?|resolv(?:e|ed|es|ing)|replac(?:e|ed|es|ing)|improv(?:e|ed|es|ing)|appl(?:y|ied|ies|ying))\b/i;
1018
1147
  var CLAIMED_COMMIT_VERBS_RE = /\b(commit(?:ted|s|ting)?|push(?:ed|es|ing)?)\b/i;
1148
+ var NEGATION_RE = /\b(no|not|n't|never|none|nothing|without)\b/i;
1149
+ function claimsUnnegatedChange(message) {
1150
+ const clauses = message.split(/[.;\n]+/);
1151
+ return clauses.some((clause) => {
1152
+ const match = CLAIMED_CHANGE_VERBS_RE.exec(clause);
1153
+ if (!match) {
1154
+ return false;
1155
+ }
1156
+ const wordsBefore = clause.slice(0, match.index).trim().split(/\s+/).filter(Boolean);
1157
+ const nearbyWords = wordsBefore.slice(-4).join(" ");
1158
+ return !NEGATION_RE.test(nearbyWords);
1159
+ });
1160
+ }
1019
1161
  function subagentStopHandler(event) {
1020
1162
  try {
1021
1163
  if (!event.sessionId) {
@@ -1031,7 +1173,7 @@ function subagentStopHandler(event) {
1031
1173
  const gitOutput = result.stdout.trim();
1032
1174
  if (!gitOutput) {
1033
1175
  const lastAssistantMessage = event.raw["last_assistant_message"] || "";
1034
- const claimsChanges = CLAIMED_CHANGE_VERBS_RE.test(lastAssistantMessage);
1176
+ const claimsChanges = claimsUnnegatedChange(lastAssistantMessage);
1035
1177
  const claimsCommitted = CLAIMED_COMMIT_VERBS_RE.test(lastAssistantMessage);
1036
1178
  if (claimsChanges && !claimsCommitted) {
1037
1179
  console.warn(
@@ -1073,7 +1215,7 @@ function reconcileNote(cwd, indexed) {
1073
1215
  try {
1074
1216
  const budgetMs = envInt(ENV_KEYS.RECONCILE_BUDGET_MS, DEFAULT_RECONCILE_BUDGET_MS, 0, 6e4);
1075
1217
  const result = reconcileProject({ cwd, budgetMs });
1076
- if (isReconcileClean(result)) return null;
1218
+ if (isReconcileClean(result) && !result.budgetExhausted) return null;
1077
1219
  const parts = [];
1078
1220
  if (result.changed.length > 0) parts.push(`${result.changed.length} changed`);
1079
1221
  if (result.added.length > 0) parts.push(`${result.added.length} new`);
@@ -1081,6 +1223,9 @@ function reconcileNote(cwd, indexed) {
1081
1223
  const breakdown = parts.length > 1 ? ` (${parts.join(", ")})` : "";
1082
1224
  const truncated = result.budgetExhausted ? ` (sweep stopped at its time budget with ${countNoun(result.unscanned, "file")} unchecked, so there may be more)` : "";
1083
1225
  const total = result.changed.length + result.added.length + result.removed.length;
1226
+ if (total === 0) {
1227
+ return `token-goat: index drift check${truncated} found nothing in the part it had time to scan. Symbol lookups may be briefly stale.`;
1228
+ }
1084
1229
  return `token-goat: reindexing ${countNoun(total, "file")} that changed outside this session${breakdown}${truncated}. Symbol lookups may be briefly stale.`;
1085
1230
  } catch {
1086
1231
  return null;
@@ -1693,11 +1838,11 @@ function mapInner(r, f) {
1693
1838
  function mapOuter(r, f) {
1694
1839
  return r.matched ? f(r) : r;
1695
1840
  }
1696
- function ab(pa, pb, join2) {
1697
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join2(ma.value, vb, data, i, j)));
1841
+ function ab(pa, pb, join3) {
1842
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join3(ma.value, vb, data, i, j)));
1698
1843
  }
1699
- function abc(pa, pb, pc, join2) {
1700
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join2(ma.value, mb.value, vc, data, i, j))));
1844
+ function abc(pa, pb, pc, join3) {
1845
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join3(ma.value, mb.value, vc, data, i, j))));
1701
1846
  }
1702
1847
  function ahead(p) {
1703
1848
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5674,8 +5819,8 @@ function trimCharacterEnd(str, char) {
5674
5819
  function unicodeEscape(str) {
5675
5820
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5676
5821
  }
5677
- function get(obj, path3) {
5678
- for (const key of path3) {
5822
+ function get(obj, path4) {
5823
+ for (const key of path4) {
5679
5824
  if (!obj) {
5680
5825
  return void 0;
5681
5826
  }
@@ -6876,8 +7021,8 @@ function withBrackets(str, brackets) {
6876
7021
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
6877
7022
  return lbr + str + rbr;
6878
7023
  }
6879
- function pathRewrite(path3, rewriter, baseUrl, metadata, elem) {
6880
- const modifiedPath = typeof rewriter === "function" ? rewriter(path3, metadata, elem) : path3;
7024
+ function pathRewrite(path4, rewriter, baseUrl, metadata, elem) {
7025
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path4, metadata, elem) : path4;
6881
7026
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
6882
7027
  }
6883
7028
  function formatImage(elem, walk, builder, formatOptions) {
@@ -7201,9 +7346,9 @@ function handleDeprecatedOptions(options) {
7201
7346
  options.selectors.push(...tagDefinitions);
7202
7347
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
7203
7348
  }
7204
- function set(obj, path3, value) {
7205
- const valueKey = path3.pop();
7206
- for (const key of path3) {
7349
+ function set(obj, path4, value) {
7350
+ const valueKey = path4.pop();
7351
+ for (const key of path4) {
7207
7352
  let nested = obj[key];
7208
7353
  if (!nested) {
7209
7354
  nested = {};
@@ -7493,7 +7638,7 @@ registerHook("post_tool_use", postSkillHandler, { toolName: "Skill" });
7493
7638
 
7494
7639
  // src/hooks_bash.ts
7495
7640
  init_define_import_meta_env();
7496
- import { statSync as statSync3, existsSync, openSync, readSync, closeSync } from "node:fs";
7641
+ import { statSync as statSync4, existsSync, openSync as openSync2, readSync, closeSync as closeSync2 } from "node:fs";
7497
7642
  var CD_PREFIX_RE = /^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)[ \t]*(?:&&|;|\r?\n)\s*)+/;
7498
7643
  function stripCdPrefix(cmd) {
7499
7644
  const stripped = cmd.replace(CD_PREFIX_RE, "");
@@ -7747,7 +7892,7 @@ function workingTreeStatusPaths(gitDir) {
7747
7892
  function enqueueRewrittenPath(absPath) {
7748
7893
  if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
7749
7894
  try {
7750
- if (!statSync3(absPath).isFile()) return;
7895
+ if (!statSync4(absPath).isFile()) return;
7751
7896
  } catch {
7752
7897
  return;
7753
7898
  }
@@ -7772,7 +7917,11 @@ function enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd) {
7772
7917
  }
7773
7918
  function isTempPath(fp) {
7774
7919
  const norm = fp.replace(/\\/g, "/");
7775
- return /^\/tmp\//i.test(norm) || /\/var\/folders\//i.test(norm) || /AppData\/Local\/Temp\//i.test(norm) || norm.startsWith("/c/Users/") && norm.includes("/AppData/Local/Temp/") || isUnderSystemTemp(fp);
7920
+ return /^\/tmp\//i.test(norm) || /\/var\/folders\//i.test(norm) || // Anchored to a local drive root on purpose. Unanchored, this clause said yes to
7921
+ // `//host/share/AppData/Local/Temp/x.md`, and the caller's answer to yes is to stat the path --
7922
+ // which on Windows opens an SMB session to whatever host a command named. A temp directory is
7923
+ // somewhere on a local volume; nothing that starts with two separators is one.
7924
+ /^[a-z]:\/(?:[^/]+\/)*AppData\/Local\/Temp\//i.test(norm) || norm.startsWith("/c/Users/") && norm.includes("/AppData/Local/Temp/") || isUnderSystemTemp(fp);
7776
7925
  }
7777
7926
  function isOrchestratorStateFile(filePath) {
7778
7927
  const basename3 = (filePath.includes("/") ? filePath.split("/").at(-1) : filePath.split("\\").at(-1)) ?? filePath;
@@ -7832,12 +7981,16 @@ var PS_FILE_METHOD_RE = /\[(?:System\.)?IO\.File\]::(?:ReadAllText|ReadAllLines|
7832
7981
  var PS_TEMP_READ_FLOOD_BYTES = 16 * 1024;
7833
7982
  function isLargeFileOnDisk(filePath, floor) {
7834
7983
  try {
7835
- return statSync3(filePath).size >= floor;
7984
+ return statSync4(filePath).size >= floor;
7836
7985
  } catch {
7837
7986
  return false;
7838
7987
  }
7839
7988
  }
7840
- function extractPowerShellWrappedGetContent(cmd) {
7989
+ function commandPathIsTouchable(filePath, event) {
7990
+ if (event === void 0) return !escapesOntoNetworkThroughLinks(filePath);
7991
+ return !preToolPathDeclined(event, filePath);
7992
+ }
7993
+ function extractPowerShellWrappedGetContent(cmd, event) {
7841
7994
  const w = POWERSHELL_WRAP_RE.exec(cmd);
7842
7995
  if (!w) return null;
7843
7996
  const inner = (w[1] ?? w[2] ?? "").trim();
@@ -7848,10 +8001,13 @@ function extractPowerShellWrappedGetContent(cmd) {
7848
8001
  if (filePath === void 0) return null;
7849
8002
  const flags = classifyFileExtensions(filePath);
7850
8003
  if (flags === null) return null;
7851
- if (isTempPath(filePath) && !isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
8004
+ if (isTempPath(filePath)) {
8005
+ if (!commandPathIsTouchable(filePath, event)) return null;
8006
+ if (!isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
8007
+ }
7852
8008
  return { filePath, ...flags };
7853
8009
  }
7854
- function extractPowerShellFileMethodRead(cmd) {
8010
+ function extractPowerShellFileMethodRead(cmd, event) {
7855
8011
  let inner = cmd.trim();
7856
8012
  const w = POWERSHELL_WRAP_RE.exec(inner);
7857
8013
  if (w) {
@@ -7861,13 +8017,16 @@ function extractPowerShellFileMethodRead(cmd) {
7861
8017
  if (!m?.[1]) return null;
7862
8018
  const filePath = m[1];
7863
8019
  if (isOrchestratorStateFile(filePath)) return null;
7864
- if (isTempPath(filePath) && !isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
8020
+ if (isTempPath(filePath)) {
8021
+ if (!commandPathIsTouchable(filePath, event)) return null;
8022
+ if (!isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
8023
+ }
7865
8024
  const flags = classifyFileExtensions(filePath);
7866
8025
  if (flags === null) {
7867
8026
  const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
7868
- return { filePath, isDoc, isConfig, isSql };
8027
+ return { filePath, isDoc, isEnv: false, isConfig, isSql };
7869
8028
  }
7870
- return { filePath, isDoc: flags.isDoc, isConfig: flags.isConfig, isSql: flags.isSql };
8029
+ return { filePath, isDoc: flags.isDoc, isEnv: flags.isEnv, isConfig: flags.isConfig, isSql: flags.isSql };
7871
8030
  }
7872
8031
  function extractRgSymbolSearch(cmd) {
7873
8032
  if (!/^(?:rg|grep)\s+/.test(cmd)) return null;
@@ -8190,36 +8349,6 @@ function extractLineRangeReadsCompound(cmd) {
8190
8349
  }
8191
8350
  return [...merged.values()];
8192
8351
  }
8193
- var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
8194
- "python",
8195
- "typescript",
8196
- "javascript",
8197
- "rust",
8198
- "go",
8199
- "c",
8200
- "cpp",
8201
- "ruby",
8202
- "java",
8203
- "csharp",
8204
- "php",
8205
- "kotlin",
8206
- "swift",
8207
- "scala",
8208
- "lua",
8209
- "elixir",
8210
- "dart",
8211
- "zig",
8212
- "r",
8213
- "sql",
8214
- "graphql",
8215
- "proto",
8216
- "terraform",
8217
- "bash",
8218
- "powershell",
8219
- "apex",
8220
- "salesforce_metadata",
8221
- "salesforce_markup"
8222
- ]);
8223
8352
  function sedRangeHint(filePath, ranges, tool) {
8224
8353
  const lang = detectLanguage(filePath);
8225
8354
  const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
@@ -8231,7 +8360,7 @@ function sedRangeHint(filePath, ranges, tool) {
8231
8360
  if (lang === "toml" || lang === "json" || lang === "yaml" || lang === "ini") {
8232
8361
  return prefix + 'For config, `token-goat config-get "' + filePath + '" <key>` or `token-goat section "' + filePath + '::<block>"` extracts one value; or ' + allReads + " for exactly those lines.";
8233
8362
  }
8234
- if (SYMBOL_BEARING_LANGUAGES.has(lang)) {
8363
+ if (languageHasFlag(lang, "symbolBearing")) {
8235
8364
  return prefix + 'For a whole function/class, `token-goat symbol <name>` or `token-goat read "' + filePath + '::<Symbol>"` is robust to line shifts; or ' + allReads + " for exactly those lines.";
8236
8365
  }
8237
8366
  return prefix + "Use " + allReads + " to read exactly those lines.";
@@ -8308,7 +8437,7 @@ function extractGetContentTail(cmd) {
8308
8437
  if (n <= 10) return null;
8309
8438
  const getnMatch = /^(Get-Content|gc)\s+/i.exec(cmd);
8310
8439
  if (!getnMatch) return null;
8311
- const afterCmd = cmd.slice(getnMatch[0].length);
8440
+ const afterCmd = cmd.slice(getnMatch[0].length).replace(/-Path\s+/i, "");
8312
8441
  const beforeTail = afterCmd.split(/-Tail/i)[0]?.trim() ?? "";
8313
8442
  const afterTail = afterCmd.split(/-Tail\s+\d+/i)[1]?.trim() ?? "";
8314
8443
  const filePath = (beforeTail || afterTail).replace(/^["']|["']$/g, "");
@@ -8321,7 +8450,7 @@ function extractGetContentTail(cmd) {
8321
8450
  function extractGetContentSelectFirst(cmd) {
8322
8451
  const m = /^(Get-Content|gc)\s+([^|]+)\s*\|\s*(Select-Object|select)\s+(-First\s+(\d+))/i.exec(cmd);
8323
8452
  if (!m) return null;
8324
- const filePath = (m[2]?.trim() ?? "").replace(/^["']|["']$/g, "");
8453
+ const filePath = (m[2]?.trim() ?? "").replace(/^-Path\s+/i, "").replace(/^["']|["']$/g, "");
8325
8454
  const n = parseInt(m[5] ?? "0", 10);
8326
8455
  if (n <= 10) return null;
8327
8456
  if (!filePath) return null;
@@ -8333,7 +8462,7 @@ function extractGetContentSelectFirst(cmd) {
8333
8462
  function taskOutputIsJsonlTranscript(outPath) {
8334
8463
  let fd = null;
8335
8464
  try {
8336
- fd = openSync(normalizePath(outPath), "r");
8465
+ fd = openSync2(normalizePath(outPath), "r");
8337
8466
  const buf = Buffer.alloc(64);
8338
8467
  const read = readSync(fd, buf, 0, buf.length, 0);
8339
8468
  return buf.subarray(0, read).toString("utf-8").trim().startsWith("{");
@@ -8342,7 +8471,7 @@ function taskOutputIsJsonlTranscript(outPath) {
8342
8471
  } finally {
8343
8472
  if (fd !== null) {
8344
8473
  try {
8345
- closeSync(fd);
8474
+ closeSync2(fd);
8346
8475
  } catch {
8347
8476
  }
8348
8477
  }
@@ -8698,14 +8827,14 @@ function shellQuoteSingle(s) {
8698
8827
  }
8699
8828
  function isCompressibleSingleCommand(cmd) {
8700
8829
  if (!cmd || cmd.length > 65536) return false;
8701
- if (["&&", "||", "$(", "`"].some((op) => cmd.includes(op))) return false;
8702
- if (cmd.includes("|") || cmd.includes(";")) return false;
8703
- if (/[<>]/.test(cmd)) return false;
8830
+ if (["$(", "`"].some((op) => cmd.includes(op))) return false;
8831
+ if (hasUnquotedOperator(cmd, ["&&", "||", "|", ";", "<", ">"])) return false;
8704
8832
  if (hasBareBackgroundOrNewline(cmd)) return false;
8705
8833
  return true;
8706
8834
  }
8707
8835
  function maybeCompressRewrite(event, rawCmd, cmd) {
8708
8836
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8837
+ if (event.raw["_tg_harness"] === "vscode") return null;
8709
8838
  let cfg;
8710
8839
  try {
8711
8840
  cfg = loadConfig().bash_compress;
@@ -8833,18 +8962,24 @@ function elideServedShellLines(cmd, output, priorIds, unknownLineNumbers = false
8833
8962
  }
8834
8963
  const cuts = planServedElisions(rows, bodies);
8835
8964
  if (cuts.length === 0) return null;
8836
- const out = [];
8965
+ const spans = [];
8966
+ const push = (text, own) => {
8967
+ const sep = spans.length === 0 ? "" : "\n";
8968
+ const prev = spans[spans.length - 1];
8969
+ if (prev !== void 0 && prev.own === true === own) spans[spans.length - 1] = { text: prev.text + sep + text, own };
8970
+ else spans.push({ text: sep + text, own });
8971
+ };
8837
8972
  let at = 0;
8838
8973
  for (const cut of cuts) {
8839
- for (let i = at; i < cut.start; i++) out.push(rows[i]?.raw ?? "");
8974
+ for (let i = at; i < cut.start; i++) push(rows[i]?.raw ?? "", false);
8840
8975
  const first2 = rows[cut.start];
8841
8976
  const last = rows[cut.start + cut.len - 1];
8842
8977
  if (first2 === void 0 || last === void 0) return null;
8843
- out.push(servedRunNotice(first2.no, last.no, cut.id, cut.len));
8978
+ push(servedRunNotice(first2.no, last.no, cut.id, cut.len), true);
8844
8979
  at = cut.start + cut.len;
8845
8980
  }
8846
- for (let i = at; i < rows.length; i++) out.push(rows[i]?.raw ?? "");
8847
- return out.join("\n");
8981
+ for (let i = at; i < rows.length; i++) push(rows[i]?.raw ?? "", false);
8982
+ return spans;
8848
8983
  }
8849
8984
  function foldShellReadBodies(cmd, output, fileKey, cwd) {
8850
8985
  if (!foldingEnabled()) return null;
@@ -8876,7 +9011,7 @@ function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
8876
9011
  const originalBytes = Buffer.byteLength(output, "utf-8");
8877
9012
  let onDisk;
8878
9013
  try {
8879
- onDisk = statSync3(fileKey).size;
9014
+ onDisk = statSync4(fileKey).size;
8880
9015
  } catch {
8881
9016
  return null;
8882
9017
  }
@@ -8892,6 +9027,23 @@ function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
8892
9027
  if (!isStructuralRewriteAccepted(originalBytes, Buffer.byteLength(text, "utf-8"), fold.ratioCap)) return null;
8893
9028
  return { text, kind: fold.kind === "read:markdown_outline" ? "bash_compress:markdown-outline" : "bash_compress:source-skeleton", detail: fold.detail };
8894
9029
  }
9030
+ var REWRITE_CLIP_NOTE = "\n[token-goat: rewrite clipped to the harness delivery cap; the pointer above recalls the untouched output]";
9031
+ function fenceRewriteWithinCap(spans) {
9032
+ const joined = spans.map((s) => s.text).join("");
9033
+ const fenced = fenceUntrustedSpans(spans, UNTRUSTED_TOOL_TAG);
9034
+ if (fenced === joined) return joined;
9035
+ const overhead = Buffer.byteLength(fenced, "utf-8") - Buffer.byteLength(joined, "utf-8") + Buffer.byteLength(REWRITE_CLIP_NOTE, "utf-8");
9036
+ const cut = clipToDeliveryCap(joined, overhead);
9037
+ if (!cut.clipped) return fenced;
9038
+ const kept = [];
9039
+ let left2 = cut.text.length;
9040
+ for (const s of spans) {
9041
+ if (left2 <= 0) break;
9042
+ kept.push(s.text.length <= left2 ? s : { ...s, text: s.text.slice(0, left2) });
9043
+ left2 -= s.text.length;
9044
+ }
9045
+ return fenceUntrustedSpans(kept, UNTRUSTED_TOOL_TAG) + REWRITE_CLIP_NOTE;
9046
+ }
8895
9047
  async function maybeCollapseIdenticalRead(cmd, rawCmd, output, exitCode, cwd, cacheMinBytes) {
8896
9048
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8897
9049
  if (exitCode !== null && exitCode !== 0) return null;
@@ -8918,7 +9070,8 @@ async function maybeCollapseIdenticalRead(cmd, rawCmd, output, exitCode, cwd, ca
8918
9070
  const priced = (text) => isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(text, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() });
8919
9071
  let rewrite = null;
8920
9072
  if (elided !== null) {
8921
- if (priced(elided)) rewrite = { text: elided, reason: "already-served file lines withheld", kind: "bash_compress:served-elide" };
9073
+ const fenced = fenceRewriteWithinCap(elided);
9074
+ if (priced(fenced)) rewrite = { text: fenced, reason: "already-served file lines withheld", kind: "bash_compress:served-elide" };
8922
9075
  } else {
8923
9076
  const structural = foldShellReadStructure(cmd, filePath, output, fileKey, cwd);
8924
9077
  if (structural !== null && priced(structural.text)) {
@@ -8926,7 +9079,10 @@ async function maybeCollapseIdenticalRead(cmd, rawCmd, output, exitCode, cwd, ca
8926
9079
  }
8927
9080
  if (rewrite === null) {
8928
9081
  const folded = foldShellReadBodies(cmd, output, fileKey, cwd);
8929
- if (folded !== null && priced(folded.text)) rewrite = { text: folded.text, reason: "code bodies folded", kind: "bash_compress:body-fold", detail: foldDetail(fileKey, folded.folds) };
9082
+ if (folded !== null) {
9083
+ const fenced = fenceRewriteWithinCap([{ text: folded.text }]);
9084
+ if (priced(fenced)) rewrite = { text: fenced, reason: "code bodies folded", kind: "bash_compress:body-fold", detail: foldDetail(fileKey, folded.folds) };
9085
+ }
8930
9086
  }
8931
9087
  }
8932
9088
  const storedId = await storeBashOutput(cmd, rewrite?.text ?? output, exitCode ?? 0, cwd);
@@ -8950,8 +9106,9 @@ async function maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMi
8950
9106
  let rewrittenText = null;
8951
9107
  if (priorIds.length > 0) {
8952
9108
  const elided = elideServedShellLines(cmd, output, priorIds, true);
8953
- if (elided !== null && isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(elided, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) {
8954
- rewrittenText = elided;
9109
+ const fenced = elided === null ? null : fenceRewriteWithinCap(elided);
9110
+ if (fenced !== null && isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(fenced, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) {
9111
+ rewrittenText = fenced;
8955
9112
  }
8956
9113
  }
8957
9114
  const storedId = await storeBashOutput(cmd, rewrittenText ?? output, exitCode ?? 0, cwd);
@@ -9037,17 +9194,10 @@ function maybeFoldCurlHtml(cmd, output, id) {
9037
9194
  `;
9038
9195
  const noticeBytes = Buffer.byteLength(notice, "utf-8");
9039
9196
  const clipNote = "\n[token-goat: cleaned text clipped to the harness delivery cap; the notice at the top of this output recalls the full raw markup]";
9040
- const cap = bashOutputCapBytes();
9041
9197
  const fenceOverhead = Buffer.byteLength(fenceUntrusted(cleaned, UNTRUSTED_TOOL_TAG), "utf-8") - Buffer.byteLength(cleaned, "utf-8");
9042
- const room = cap === null ? null : cap - noticeBytes - fenceOverhead - Buffer.byteLength(clipNote, "utf-8");
9043
- let body = cleaned;
9044
- let clipped = "";
9045
- if (room !== null && room > 0 && Buffer.byteLength(cleaned, "utf-8") > room) {
9046
- const sliced = Buffer.from(cleaned, "utf-8").subarray(0, room).toString("utf-8");
9047
- const lastNewline = sliced.lastIndexOf("\n");
9048
- body = lastNewline > 0 ? sliced.slice(0, lastNewline) : sliced;
9049
- clipped = clipNote;
9050
- }
9198
+ const cut = clipToDeliveryCap(cleaned, noticeBytes + fenceOverhead + Buffer.byteLength(clipNote, "utf-8"));
9199
+ const body = cut.text;
9200
+ const clipped = cut.clipped ? clipNote : "";
9051
9201
  const fenced = fenceUntrusted(body, UNTRUSTED_TOOL_TAG) + clipped;
9052
9202
  if (!isRewriteWorthwhile({
9053
9203
  originalBytes,
@@ -9307,6 +9457,20 @@ function preBashHandlerInner(event) {
9307
9457
  }
9308
9458
  return contextOutput(hints.join(" "));
9309
9459
  }
9460
+ const gcTailResult = extractGetContentTail(cmd);
9461
+ if (gcTailResult !== null) {
9462
+ const { filePath, isDoc, isConfig, isSql } = gcTailResult;
9463
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9464
+ recordStat("session_hint", 0, 0);
9465
+ return contextOutput("`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
9466
+ }
9467
+ const gcSelectResult = extractGetContentSelectFirst(cmd);
9468
+ if (gcSelectResult !== null) {
9469
+ const { filePath, isDoc, isConfig, isSql, n } = gcSelectResult;
9470
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9471
+ recordStat("session_hint", 0, 0);
9472
+ return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9473
+ }
9310
9474
  const catJsonPipe = extractCatJsonPipe(cmd);
9311
9475
  if (catJsonPipe !== null) {
9312
9476
  const { filePath } = catJsonPipe;
@@ -9341,7 +9505,7 @@ function preBashHandlerInner(event) {
9341
9505
  const msg = "`" + cmd0 + "` on multiple files loads them all into context. Read each surgically instead:\n" + perPath.join("\n");
9342
9506
  return cdStripped ? contextOutput(msg) : denyOutput(msg);
9343
9507
  }
9344
- const psGetContentResult = extractPowerShellWrappedGetContent(cmd);
9508
+ const psGetContentResult = extractPowerShellWrappedGetContent(cmd, event);
9345
9509
  if (psGetContentResult !== null) {
9346
9510
  const { filePath, isDoc, isEnv, isConfig, isSql } = psGetContentResult;
9347
9511
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
@@ -9402,20 +9566,6 @@ function preBashHandlerInner(event) {
9402
9566
  recordStat("session_hint", 0, 0);
9403
9567
  return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9404
9568
  }
9405
- const gcTailResult = extractGetContentTail(cmd);
9406
- if (gcTailResult !== null) {
9407
- const { filePath, isDoc, isConfig, isSql } = gcTailResult;
9408
- const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9409
- recordStat("session_hint", 0, 0);
9410
- return contextOutput("`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
9411
- }
9412
- const gcSelectResult = extractGetContentSelectFirst(cmd);
9413
- if (gcSelectResult !== null) {
9414
- const { filePath, isDoc, isConfig, isSql, n } = gcSelectResult;
9415
- const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9416
- recordStat("session_hint", 0, 0);
9417
- return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9418
- }
9419
9569
  const nodeRead = extractNodeFileRead(cmd);
9420
9570
  if (nodeRead !== null) {
9421
9571
  const { filePath, isDoc, isConfig, isSql } = nodeRead;
@@ -9429,16 +9579,16 @@ function preBashHandlerInner(event) {
9429
9579
  recordStat("session_hint", 0, 0);
9430
9580
  return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9431
9581
  }
9432
- const psMethodRead = extractPowerShellFileMethodRead(cmd);
9582
+ const psMethodRead = extractPowerShellFileMethodRead(cmd, event);
9433
9583
  if (psMethodRead !== null) {
9434
- const { filePath, isDoc, isConfig, isSql } = psMethodRead;
9584
+ const { filePath, isDoc, isEnv, isConfig, isSql } = psMethodRead;
9435
9585
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9436
9586
  const lead = "PowerShell `[IO.File]::ReadAllText()` bypasses read hooks. ";
9437
9587
  if (isSql) {
9438
9588
  recordStat("session_hint", 0, 0);
9439
9589
  return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9440
9590
  }
9441
- const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : isConfig ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
9591
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
9442
9592
  recordStat("session_hint", 0, 0);
9443
9593
  return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9444
9594
  }
@@ -9499,10 +9649,10 @@ function preBashHandlerInner(event) {
9499
9649
  const curlDl = extractCurlDownload(cmd);
9500
9650
  if (curlDl !== null) {
9501
9651
  const prevPath = getCurlDownloadPath(curlDl.url);
9502
- const prevResolvedPath = prevPath !== null ? resolveIndexPath(prevPath, preHookCwd ?? process.cwd()) : null;
9652
+ const prevResolvedPath = prevPath !== null && commandPathIsTouchable(prevPath, event) ? resolveIndexPath(prevPath, preHookCwd ?? process.cwd()) : null;
9503
9653
  if (prevPath !== null && prevResolvedPath !== null && !existsSync(prevResolvedPath)) {
9504
9654
  clearCurlDownload(curlDl.url);
9505
- } else if (prevPath !== null && prevResolvedPath !== null && statSync3(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
9655
+ } else if (prevPath !== null && prevResolvedPath !== null && statSync4(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
9506
9656
  recordStat("session_hint", 0, 0);
9507
9657
  return denyOutput(
9508
9658
  "Already downloaded to " + prevPath + " earlier this session. Use `rg '<pattern>' " + prevPath + '` to search it, or `token-goat read "' + prevPath + '::SectionName"` to read a part of it.'
@@ -9636,55 +9786,55 @@ function buildGhApiHint(cmd, stdout, exitCode) {
9636
9786
  return hints.length > 0 ? hints.join(" ") : null;
9637
9787
  }
9638
9788
  function recordBashFileReadsForSessionCache(cmd, cwd) {
9639
- const resolve = (p) => resolveIndexPath(p, cwd ?? process.cwd());
9789
+ const resolve2 = (p) => resolveIndexPath(p, cwd ?? process.cwd());
9640
9790
  const gcTail = extractGetContentTail(cmd);
9641
9791
  if (gcTail !== null) {
9642
- markFileTruncated(resolve(gcTail.filePath));
9792
+ markFileTruncated(resolve2(gcTail.filePath));
9643
9793
  return;
9644
9794
  }
9645
9795
  const tail = extractTailFile(cmd);
9646
9796
  if (tail !== null) {
9647
- markFileTruncated(resolve(tail.filePath));
9797
+ markFileTruncated(resolve2(tail.filePath));
9648
9798
  return;
9649
9799
  }
9650
9800
  const gcSelect = extractGetContentSelectFirst(cmd);
9651
9801
  if (gcSelect !== null) {
9652
- recordFileLineRange(resolve(gcSelect.filePath), 1, gcSelect.n);
9802
+ recordFileLineRange(resolve2(gcSelect.filePath), 1, gcSelect.n);
9653
9803
  return;
9654
9804
  }
9655
9805
  const head = extractHeadFile(cmd);
9656
9806
  if (head !== null) {
9657
- recordFileLineRange(resolve(head.filePath), 1, head.n);
9807
+ recordFileLineRange(resolve2(head.filePath), 1, head.n);
9658
9808
  return;
9659
9809
  }
9660
9810
  const cat = extractCatFile(cmd);
9661
9811
  if (cat !== null) {
9662
- recordFileRead(resolve(cat.filePath));
9812
+ recordFileRead(resolve2(cat.filePath));
9663
9813
  return;
9664
9814
  }
9665
9815
  const catMulti = extractCatFilesMulti(cmd);
9666
9816
  if (catMulti !== null) {
9667
- for (const r of catMulti) recordFileRead(resolve(r.filePath));
9817
+ for (const r of catMulti) recordFileRead(resolve2(r.filePath));
9668
9818
  return;
9669
9819
  }
9670
9820
  const wslCat = extractWslCatFile(cmd);
9671
9821
  if (wslCat !== null) {
9672
- recordFileRead(resolve(wslCat.filePath));
9822
+ recordFileRead(resolve2(wslCat.filePath));
9673
9823
  return;
9674
9824
  }
9675
9825
  const nodeRead = extractNodeFileRead(cmd);
9676
9826
  if (nodeRead !== null) {
9677
- recordFileRead(resolve(nodeRead.filePath));
9827
+ recordFileRead(resolve2(nodeRead.filePath));
9678
9828
  return;
9679
9829
  }
9680
9830
  const psMethod = extractPowerShellFileMethodRead(cmd);
9681
9831
  if (psMethod !== null) {
9682
- recordFileRead(resolve(psMethod.filePath));
9832
+ recordFileRead(resolve2(psMethod.filePath));
9683
9833
  return;
9684
9834
  }
9685
9835
  const pyRead = extractPythonFileRead(cmd);
9686
9836
  if (pyRead !== null && !pyRead.isOutputFile) {
9687
- recordFileRead(resolve(pyRead.filePath));
9837
+ recordFileRead(resolve2(pyRead.filePath));
9688
9838
  return;
9689
9839
  }
9690
9840
  }
@@ -9994,7 +10144,7 @@ registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" })
9994
10144
 
9995
10145
  // src/hooks_tool_failure.ts
9996
10146
  init_define_import_meta_env();
9997
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
10147
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
9998
10148
  import { dirname as dirname2, relative } from "node:path";
9999
10149
  var FAILURE_SUFFIX = ".tool-failures.json";
10000
10150
  var MAX_TRACKED_FAILURES = 64;
@@ -10036,7 +10186,7 @@ function readLedger(target) {
10036
10186
  }
10037
10187
  function writeLedger(target, ledger) {
10038
10188
  try {
10039
- mkdirSync2(dirname2(target), { recursive: true });
10189
+ ensureDirSync(dirname2(target));
10040
10190
  writeFileSync2(target, JSON.stringify(ledger), "utf8");
10041
10191
  } catch {
10042
10192
  }
@@ -10063,7 +10213,7 @@ function diagnoseEditFailure(event, errorText) {
10063
10213
  const absPath = normalizePath(filePath);
10064
10214
  if (!existsSync2(absPath)) return null;
10065
10215
  try {
10066
- const stat = statSync4(absPath);
10216
+ const stat = statSync5(absPath);
10067
10217
  if (stat.size > MAX_EDIT_DIAGNOSE_BYTES) return null;
10068
10218
  const fileContent = readFileSync3(absPath, "utf8");
10069
10219
  const relDisplay = displaySafeText(relative(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
@@ -10118,7 +10268,7 @@ function postToolUseFailureHandler(event) {
10118
10268
  const target = ledgerPath(event.sessionId);
10119
10269
  if (target === null) return passOutput();
10120
10270
  const toolName = getToolName(event);
10121
- const signature = failureSignature(toolName, errorText);
10271
+ const signature = failureSignature(toolName, redactSecrets(errorText).text);
10122
10272
  const ledger = readLedger(target);
10123
10273
  const priorState = ledger.seen[signature];
10124
10274
  const editDiagnostic = diagnoseEditFailure(event, errorText);
@@ -10222,7 +10372,7 @@ function mcpOutputId(sessionId, hash) {
10222
10372
  }
10223
10373
  function mcpInputPreview(toolInput) {
10224
10374
  try {
10225
- return JSON.stringify(toolInput).slice(0, 120);
10375
+ return redactSecrets(JSON.stringify(toolInput)).text.slice(0, 120);
10226
10376
  } catch {
10227
10377
  return "";
10228
10378
  }
@@ -10611,7 +10761,7 @@ function postMcpHandler(event) {
10611
10761
  id = storeMcpOutput(event.sessionId, toolName, toolInput, resultText);
10612
10762
  }
10613
10763
  if (resultText.length >= MCP_COMPRESS_MIN_BYTES && process.env["TOKEN_GOAT_MCP_COMPRESS"] !== "0") {
10614
- const compressed = compressMcpResultWithPacks(toolName, resultText) ?? compressMcpResult(resultText);
10764
+ const compressed = compressMcpResultWithPacks(toolName, redactedResult.text) ?? compressMcpResult(redactedResult.text);
10615
10765
  if (compressed !== null) {
10616
10766
  if (id === null) id = storeMcpOutput(event.sessionId, toolName, toolInput, resultText);
10617
10767
  if (id !== null) {
@@ -10840,9 +10990,9 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
10840
10990
  // src/hooks_agent_spawn.ts
10841
10991
  init_define_import_meta_env();
10842
10992
  import { createHash as createHash2 } from "node:crypto";
10843
- import * as fs from "node:fs";
10844
- import * as os from "node:os";
10845
- import * as path2 from "node:path";
10993
+ import * as fs2 from "node:fs";
10994
+ import * as os2 from "node:os";
10995
+ import * as path3 from "node:path";
10846
10996
  var BRIEFING_TARGET_TOKENS = 550;
10847
10997
  function buildSubagentBriefing() {
10848
10998
  try {
@@ -10928,7 +11078,7 @@ function preAgentHandler(event) {
10928
11078
  const briefing = buildSubagentBriefing();
10929
11079
  const advisory = duplicateOf ? `
10930
11080
 
10931
- [token-goat] A similar subagent spawn already appears to be outstanding this session (prompt starts: "${truncateForWarning(duplicateOf, 80)}"). This is advisory only -- proceeding is fine if intentional.` : "";
11081
+ &#91;token-goat] A similar subagent spawn already appears to be outstanding this session (prompt starts: "${neutralizeSpokenMarkers(truncateForWarning(duplicateOf, 80))}"). This is advisory only -- proceeding is fine if intentional.` : "";
10932
11082
  if (!briefing && !advisory) return passOutput();
10933
11083
  const updatedPrompt = prompt + briefing + advisory;
10934
11084
  const updatedInput = { ...toolInput, prompt: updatedPrompt };
@@ -11067,53 +11217,75 @@ function parseAgentDefinition(text, fallbackName) {
11067
11217
  return { name: name2, restricted: false };
11068
11218
  }
11069
11219
  function findRestrictedAgentNames(roots) {
11070
- const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents"), path2.join(process.cwd(), ".claude", "agents")];
11220
+ const projectAgentsRoot = path3.join(process.cwd(), ".claude", "agents");
11221
+ const scanRoots = roots ?? [path3.join(os2.homedir(), ".claude", "agents"), projectAgentsRoot];
11071
11222
  const names = /* @__PURE__ */ new Set();
11072
11223
  const visited = /* @__PURE__ */ new Set();
11073
11224
  let filesSeen = 0;
11074
- const walk = (dir, depth) => {
11225
+ const walk = (dir, depth, confineTo) => {
11075
11226
  if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
11076
11227
  let real;
11077
11228
  try {
11078
- real = fs.realpathSync(dir);
11229
+ real = fs2.realpathSync(dir);
11079
11230
  } catch {
11080
11231
  return;
11081
11232
  }
11233
+ if (confineTo !== null && !isInsideRoot(real, confineTo)) return;
11082
11234
  const key = process.platform === "win32" ? real.toLocaleLowerCase() : real;
11083
11235
  if (visited.has(key)) return;
11084
11236
  visited.add(key);
11085
11237
  let entries;
11086
11238
  try {
11087
- entries = fs.readdirSync(dir, { withFileTypes: true });
11239
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
11088
11240
  } catch {
11089
11241
  return;
11090
11242
  }
11091
11243
  for (const entry of entries) {
11092
11244
  if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
11093
- const full = path2.join(dir, entry.name);
11245
+ const full = path3.join(dir, entry.name);
11094
11246
  let stat;
11095
11247
  try {
11096
- stat = fs.statSync(full);
11248
+ stat = fs2.statSync(full);
11097
11249
  } catch {
11098
11250
  continue;
11099
11251
  }
11100
11252
  if (stat.isDirectory()) {
11101
- walk(full, depth + 1);
11253
+ walk(full, depth + 1, confineTo);
11102
11254
  continue;
11103
11255
  }
11104
11256
  if (!stat.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
11105
11257
  filesSeen++;
11106
11258
  let text;
11107
11259
  try {
11108
- text = fs.readFileSync(full, "utf-8");
11260
+ text = fs2.readFileSync(full, "utf-8");
11109
11261
  } catch {
11110
11262
  continue;
11111
11263
  }
11112
- const parsed = parseAgentDefinition(text, path2.basename(entry.name, path2.extname(entry.name)));
11264
+ const parsed = parseAgentDefinition(text, path3.basename(entry.name, path3.extname(entry.name)));
11113
11265
  if (parsed !== null && parsed.restricted) names.add(parsed.name);
11114
11266
  }
11115
11267
  };
11116
- for (const root of scanRoots) walk(root, 0);
11268
+ for (const root of scanRoots) {
11269
+ const isProjectRoot = roots === void 0 && path3.resolve(root) === path3.resolve(projectAgentsRoot);
11270
+ if (!isProjectRoot) {
11271
+ walk(root, 0, null);
11272
+ continue;
11273
+ }
11274
+ let rootIsLink;
11275
+ try {
11276
+ rootIsLink = fs2.lstatSync(root).isSymbolicLink();
11277
+ } catch {
11278
+ continue;
11279
+ }
11280
+ if (rootIsLink) continue;
11281
+ let confineTo;
11282
+ try {
11283
+ confineTo = fs2.realpathSync(root);
11284
+ } catch {
11285
+ continue;
11286
+ }
11287
+ walk(root, 0, confineTo);
11288
+ }
11117
11289
  return Array.from(names).sort();
11118
11290
  }
11119
11291
  function buildUnrestrictedSpawnAdvisory(toolInput) {
@@ -11216,6 +11388,7 @@ function harnessForNormalization() {
11216
11388
  if (detected === "kimi") return "kimi";
11217
11389
  if (detected === "qwen") return "qwen";
11218
11390
  if (detected === "copilot_cli") return "copilot_cli";
11391
+ if (detected === "vscode") return "vscode";
11219
11392
  return "claude";
11220
11393
  }
11221
11394
  function safeSuggestions(output) {
@@ -11231,6 +11404,7 @@ async function relayInProcess(eventName, rawPayload) {
11231
11404
  const harness = detectHarness();
11232
11405
  const payload = eventName === "pre_tool_use" || eventName === "post_tool_use" ? normalizePayload(rawPayload, harnessForNormalization()) : rawPayload;
11233
11406
  const event = buildEvent(eventName, payload);
11407
+ if (shouldSuppressDuplicateVscodeHook(event, harness)) return "{}";
11234
11408
  if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
11235
11409
  process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
11236
11410
  }