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 +1 -1
- package/src/claude-context.mjs +4 -2
- package/src/glob-compat.mjs +84 -0
- package/src/instructions.mjs +30 -8
- package/types/glob-compat.d.mts +28 -0
- package/types/src/glob-compat.d.mts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.62.
|
|
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": {
|
package/src/claude-context.mjs
CHANGED
|
@@ -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}|[\\
|
|
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
|
+
}
|
package/src/instructions.mjs
CHANGED
|
@@ -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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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[];
|