agent-sanitizer 2.62.0 → 2.62.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sanitizer",
3
- "version": "2.62.0",
3
+ "version": "2.62.1",
4
4
  "description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -225,9 +225,11 @@ export function isInsideDir(dir, file) {
225
225
 
226
226
  // What rendered untrusted text must never carry raw: what Layer 1 strips, C0/C1
227
227
  // controls (ESC opens a terminal escape, a newline forges a report line),
228
- // line/paragraph separators, lone surrogates.
228
+ // line/paragraph separators, lone surrogates. Spelled as ranges, not runtime
229
+ // `\p{}` lookups: Cc and Cs are immutable under Unicode's stability policy, and
230
+ // Zl/Zp are the single code points U+2028/U+2029.
229
231
  const DISPLAY_UNSAFE = new RegExp(
230
- `${STRIP.source}|[\\p{Cc}\\p{Zl}\\p{Zp}\\p{Cs}]`,
232
+ `${STRIP.source}|[\\u0000-\\u001F\\u007F-\\u009F\\u2028\\u2029\\uD800-\\uDFFF]`,
231
233
  "gu",
232
234
  );
233
235
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Glob patterns rewritten for the Node releases whose `fs.globSync` cannot see
3
+ * a dot-named segment after a `**`.
4
+ *
5
+ * Before 22.22.1, 24.13.1 and 25.4.0, and on every 23.x, globSync matches
6
+ * NOTHING for a `**` directly followed by a dot-named segment
7
+ * (nodejs/node#61012): `**\/.claude/*.md` finds no file, so an instruction scan
8
+ * reports clean over files it never read. On those releases each such pattern
9
+ * is rewritten into forms the broken walker answers; on every other release
10
+ * patterns pass through untouched, so a fixed Node keeps its own semantics.
11
+ */
12
+
13
+ // The first release of each affected line that carries the fix. 23.x never got
14
+ // one; every line from 26 on was cut after it landed.
15
+ const FIXED_IN = new Map([
16
+ [22, [22, 1]],
17
+ [24, [13, 1]],
18
+ [25, [4, 0]],
19
+ ]);
20
+ const FIRST_FIXED_MAJOR = 26;
21
+
22
+ // A `**` segment directly followed by a dot-named one, at the pattern start or
23
+ // after a `/`, or opening a brace alternative.
24
+ const DOT_AFTER_GLOBSTAR = /(?:^|[/{,])\*\*\/(?=\.)/;
25
+
26
+ // A dot segment after `**/` that the rewrite cannot reach: escaped, or opening
27
+ // an alternative of a brace set.
28
+ const HIDDEN_DOT_AFTER_GLOBSTAR = /\*\*\/(?:\\\.|\{(?:[^{}]*,)?\.)/;
29
+
30
+ /**
31
+ * Whether the globSync of Node `version` (`process.versions.node`'s shape)
32
+ * misses a dot-named segment after a `**`.
33
+ * @param {string} version
34
+ * @returns {boolean}
35
+ */
36
+ export function globstarSkipsDotSegments(version) {
37
+ const [major, minor, patch] = version.split(".").map(Number);
38
+ if (major >= FIRST_FIXED_MAJOR) return false;
39
+ const fixed = FIXED_IN.get(major);
40
+ if (!fixed) return true;
41
+ const [fixedMinor, fixedPatch] = fixed;
42
+ return minor < fixedMinor || (minor === fixedMinor && patch < fixedPatch);
43
+ }
44
+
45
+ /**
46
+ * `pattern` as patterns the broken globSync answers.
47
+ *
48
+ * Each `**\/` before a dot segment spans zero directories or one or more:
49
+ * dropping it is the first case and `**\/*\/` the second, and neither puts a
50
+ * dot segment straight after a `**`. A `*` never matches a dot-named directory,
51
+ * so the halves can match less than a fixed Node does (which enters a dot
52
+ * directory named like the segment after `**`), never more. A dot segment the
53
+ * rewrite cannot reach throws rather than silently matching nothing.
54
+ * @param {string} pattern
55
+ * @returns {string[]}
56
+ */
57
+ export function expandDotGlobstar(pattern) {
58
+ const match = DOT_AFTER_GLOBSTAR.exec(pattern);
59
+ if (match) {
60
+ const head = pattern.slice(0, match.index + match[0].indexOf("*"));
61
+ const tail = pattern.slice(match.index + match[0].length);
62
+ return [`${head}${tail}`, `${head}**/*/${tail}`].flatMap(expandDotGlobstar);
63
+ }
64
+ if (HIDDEN_DOT_AFTER_GLOBSTAR.test(pattern))
65
+ throw new Error(
66
+ `glob ${JSON.stringify(pattern)} puts a dot-named segment after "**" in a ` +
67
+ `form this Node's globSync matches nothing for (nodejs/node#61012); ` +
68
+ `spell the segment plainly or run Node >= 22.22.1, 24.13.1 or 25.4.0.`,
69
+ );
70
+ return [pattern];
71
+ }
72
+
73
+ /**
74
+ * `patterns` ready for this runtime's globSync: rewritten by
75
+ * {@link expandDotGlobstar} on an affected Node, untouched otherwise.
76
+ * @param {string[]} patterns
77
+ * @param {string} [version] defaults to the running Node
78
+ * @returns {string[]}
79
+ */
80
+ export function compatGlobs(patterns, version = process.versions.node) {
81
+ return globstarSkipsDotSegments(version)
82
+ ? patterns.flatMap(expandDotGlobstar)
83
+ : patterns;
84
+ }
@@ -40,6 +40,7 @@ import {
40
40
  stripInvisible,
41
41
  } from "./invisible.mjs";
42
42
  import { excludeNodeModules, isInsideDir } from "./claude-context.mjs";
43
+ import { compatGlobs } from "./glob-compat.mjs";
43
44
 
44
45
  // The library's public door onto Claude Code's context scope. The definitions
45
46
  // live in a dependency-free data module because the SessionStart hook imports
@@ -377,14 +378,35 @@ export function walkContextGlobs(globs, cwd, exclude) {
377
378
  // under a `.claude/worktrees/` checkout.
378
379
  /** @param {import("node:fs").Dirent} dirent @returns {string} */
379
380
  const absolute = (dirent) => resolve(root, dirent.parentPath, dirent.name);
380
- return globSync(globs, {
381
- cwd,
382
- withFileTypes: true,
383
- exclude: (dirent) => {
384
- const entry = relative(root, absolute(dirent)).split(sep).join("/");
385
- return excludeNodeModules(entry) || (exclude?.(entry) ?? false);
386
- },
387
- }).map(absolute);
381
+ /** @param {string} entry @returns {boolean} */
382
+ const excluded = (entry) =>
383
+ excludeNodeModules(entry) || (exclude?.(entry) ?? false);
384
+ return (
385
+ globSync(compatGlobs(globs), {
386
+ cwd,
387
+ withFileTypes: true,
388
+ exclude: (dirent) =>
389
+ excluded(relative(root, absolute(dirent)).split(sep).join("/")),
390
+ })
391
+ .map(absolute)
392
+ // Node never consults `exclude` on some literal segments (the `.claude`
393
+ // compatGlobs spells out on an affected Node is one), so a match under a
394
+ // directory the caller pruned is dropped here instead.
395
+ .filter((path) => !ancestorEntries(relative(root, path)).some(excluded))
396
+ );
397
+ }
398
+
399
+ /**
400
+ * Every directory above `rel` (a root-relative path), as `/`-separated
401
+ * entries outermost first: `a/b/c.md` gives `a` and `a/b`.
402
+ * @param {string} rel
403
+ * @returns {string[]}
404
+ */
405
+ function ancestorEntries(rel) {
406
+ const segments = rel.split(sep);
407
+ return segments
408
+ .slice(0, -1)
409
+ .map((_, i) => segments.slice(0, i + 1).join("/"));
388
410
  }
389
411
 
390
412
  /**
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether the globSync of Node `version` (`process.versions.node`'s shape)
3
+ * misses a dot-named segment after a `**`.
4
+ * @param {string} version
5
+ * @returns {boolean}
6
+ */
7
+ export function globstarSkipsDotSegments(version: string): boolean;
8
+ /**
9
+ * `pattern` as patterns the broken globSync answers.
10
+ *
11
+ * Each `**\/` before a dot segment spans zero directories or one or more:
12
+ * dropping it is the first case and `**\/*\/` the second, and neither puts a
13
+ * dot segment straight after a `**`. A `*` never matches a dot-named directory,
14
+ * so the halves can match less than a fixed Node does (which enters a dot
15
+ * directory named like the segment after `**`), never more. A dot segment the
16
+ * rewrite cannot reach throws rather than silently matching nothing.
17
+ * @param {string} pattern
18
+ * @returns {string[]}
19
+ */
20
+ export function expandDotGlobstar(pattern: string): string[];
21
+ /**
22
+ * `patterns` ready for this runtime's globSync: rewritten by
23
+ * {@link expandDotGlobstar} on an affected Node, untouched otherwise.
24
+ * @param {string[]} patterns
25
+ * @param {string} [version] defaults to the running Node
26
+ * @returns {string[]}
27
+ */
28
+ export function compatGlobs(patterns: string[], version?: string): string[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether the globSync of Node `version` (`process.versions.node`'s shape)
3
+ * misses a dot-named segment after a `**`.
4
+ * @param {string} version
5
+ * @returns {boolean}
6
+ */
7
+ export function globstarSkipsDotSegments(version: string): boolean;
8
+ /**
9
+ * `pattern` as patterns the broken globSync answers.
10
+ *
11
+ * Each `**\/` before a dot segment spans zero directories or one or more:
12
+ * dropping it is the first case and `**\/*\/` the second, and neither puts a
13
+ * dot segment straight after a `**`. A `*` never matches a dot-named directory,
14
+ * so the halves can match less than a fixed Node does (which enters a dot
15
+ * directory named like the segment after `**`), never more. A dot segment the
16
+ * rewrite cannot reach throws rather than silently matching nothing.
17
+ * @param {string} pattern
18
+ * @returns {string[]}
19
+ */
20
+ export function expandDotGlobstar(pattern: string): string[];
21
+ /**
22
+ * `patterns` ready for this runtime's globSync: rewritten by
23
+ * {@link expandDotGlobstar} on an affected Node, untouched otherwise.
24
+ * @param {string[]} patterns
25
+ * @param {string} [version] defaults to the running Node
26
+ * @returns {string[]}
27
+ */
28
+ export function compatGlobs(patterns: string[], version?: string): string[];