claude-slim 2.14.0 → 2.14.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/dist/scanner/index.js +6 -2
- package/dist/scanner/untrusted.d.ts +30 -0
- package/dist/scanner/untrusted.js +101 -0
- package/package.json +1 -1
package/dist/scanner/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { scanPluginSurfaces } from './plugin-surfaces.js';
|
|
|
20
20
|
import { computePluginBreakdown } from './plugin-breakdown.js';
|
|
21
21
|
import { computePluginCosts } from './plugin-cost.js';
|
|
22
22
|
import { scanUserSurfaces } from './user-surfaces.js';
|
|
23
|
+
import { sanitizeScanResult } from './untrusted.js';
|
|
23
24
|
const DEFAULT_LOOKBACK_DAYS = 60;
|
|
24
25
|
export async function scan(opts = {}) {
|
|
25
26
|
const lookbackDays = opts.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
|
|
@@ -149,7 +150,10 @@ export async function scan(opts = {}) {
|
|
|
149
150
|
pluginSkillListingTokens.set(c.pluginName, (pluginSkillListingTokens.get(c.pluginName) ?? 0) + c.skillTokens);
|
|
150
151
|
}
|
|
151
152
|
const recoverableStartupTokens = sumRecoverableStartupTokens(issues, [...localSkills, ...pluginSkills], currentProjectSlug, pluginSkillListingTokens);
|
|
152
|
-
|
|
153
|
+
// Labels below are authored by whoever wrote each skill, plugin, or memory
|
|
154
|
+
// file, and they reach the agent's context through the report. Flatten them
|
|
155
|
+
// at this one exit rather than at each site that reads a name off disk.
|
|
156
|
+
return sanitizeScanResult({
|
|
153
157
|
localSkills,
|
|
154
158
|
pluginSkills,
|
|
155
159
|
plugins,
|
|
@@ -171,7 +175,7 @@ export async function scan(opts = {}) {
|
|
|
171
175
|
allProjectsMemoryTokens,
|
|
172
176
|
recoverableStartupTokens,
|
|
173
177
|
disabledPluginSkillTokens,
|
|
174
|
-
};
|
|
178
|
+
});
|
|
175
179
|
}
|
|
176
180
|
async function pathExists(p) {
|
|
177
181
|
try {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ScanResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Names read off disk are written by whoever authored the skill, plugin, or
|
|
4
|
+
* memory file — not by the user running the scan. They flow through the report
|
|
5
|
+
* into the agent's context, which makes them an indirect prompt injection
|
|
6
|
+
* surface: a skill directory or frontmatter `name:` can carry instructions
|
|
7
|
+
* aimed at the model rather than a label aimed at a human.
|
|
8
|
+
*
|
|
9
|
+
* Snyk's audit of this skill (W011, medium 0.30) is about exactly this path.
|
|
10
|
+
* The scan never emits file *bodies* — descriptions are measured for token cost
|
|
11
|
+
* and then discarded — so what this module covers is the whole exposed surface,
|
|
12
|
+
* not a sample of it.
|
|
13
|
+
*/
|
|
14
|
+
/** Longest label we render. Real names are far shorter; payloads are not. */
|
|
15
|
+
export declare const MAX_NAME_LENGTH = 120;
|
|
16
|
+
/**
|
|
17
|
+
* Collapse an untrusted label to a single bounded, printable line.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately not an escape or an encoding: the value is a display label, and
|
|
20
|
+
* a reversible transform would relocate a payload rather than remove it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sanitizeUntrusted(value: string, max?: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Return a copy of the scan with every outsider-authored label flattened.
|
|
25
|
+
*
|
|
26
|
+
* Applied once at the scanner's exit rather than at each of the dozen sites
|
|
27
|
+
* that read a name off disk: one chokepoint cannot be forgotten by whoever adds
|
|
28
|
+
* the next detector.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sanitizeScanResult(result: ScanResult): ScanResult;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names read off disk are written by whoever authored the skill, plugin, or
|
|
3
|
+
* memory file — not by the user running the scan. They flow through the report
|
|
4
|
+
* into the agent's context, which makes them an indirect prompt injection
|
|
5
|
+
* surface: a skill directory or frontmatter `name:` can carry instructions
|
|
6
|
+
* aimed at the model rather than a label aimed at a human.
|
|
7
|
+
*
|
|
8
|
+
* Snyk's audit of this skill (W011, medium 0.30) is about exactly this path.
|
|
9
|
+
* The scan never emits file *bodies* — descriptions are measured for token cost
|
|
10
|
+
* and then discarded — so what this module covers is the whole exposed surface,
|
|
11
|
+
* not a sample of it.
|
|
12
|
+
*/
|
|
13
|
+
/** Longest label we render. Real names are far shorter; payloads are not. */
|
|
14
|
+
export const MAX_NAME_LENGTH = 120;
|
|
15
|
+
/** C0/C1 controls, including the newlines that would forge new report rows. */
|
|
16
|
+
const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
17
|
+
/**
|
|
18
|
+
* Zero-width and bidi-override characters: invisible to the human reading the
|
|
19
|
+
* report, fully visible to the model reading the same string.
|
|
20
|
+
*/
|
|
21
|
+
const INVISIBLE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/g;
|
|
22
|
+
/**
|
|
23
|
+
* Collapse an untrusted label to a single bounded, printable line.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately not an escape or an encoding: the value is a display label, and
|
|
26
|
+
* a reversible transform would relocate a payload rather than remove it.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeUntrusted(value, max = MAX_NAME_LENGTH) {
|
|
29
|
+
const flattened = value
|
|
30
|
+
.replace(CONTROL_CHARS, ' ')
|
|
31
|
+
.replace(INVISIBLE, '')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim();
|
|
34
|
+
if (flattened.length <= max)
|
|
35
|
+
return flattened;
|
|
36
|
+
return `${flattened.slice(0, max)}…`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Paths are shown to the user and used to locate files for cleanup, so they are
|
|
40
|
+
* flattened but never truncated — a shortened path would be a wrong path.
|
|
41
|
+
*/
|
|
42
|
+
function sanitizePath(value) {
|
|
43
|
+
return value.replace(CONTROL_CHARS, ' ').replace(INVISIBLE, '').trim();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Return a copy of the scan with every outsider-authored label flattened.
|
|
47
|
+
*
|
|
48
|
+
* Applied once at the scanner's exit rather than at each of the dozen sites
|
|
49
|
+
* that read a name off disk: one chokepoint cannot be forgotten by whoever adds
|
|
50
|
+
* the next detector.
|
|
51
|
+
*/
|
|
52
|
+
export function sanitizeScanResult(result) {
|
|
53
|
+
const skill = (s) => ({
|
|
54
|
+
...s,
|
|
55
|
+
name: sanitizeUntrusted(s.name),
|
|
56
|
+
path: sanitizePath(s.path),
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
...result,
|
|
60
|
+
localSkills: result.localSkills.map(skill),
|
|
61
|
+
pluginSkills: result.pluginSkills.map(skill),
|
|
62
|
+
plugins: result.plugins.map((p) => ({
|
|
63
|
+
...p,
|
|
64
|
+
name: sanitizeUntrusted(p.name),
|
|
65
|
+
skills: p.skills.map((s) => sanitizeUntrusted(s)),
|
|
66
|
+
})),
|
|
67
|
+
brokenSymlinks: result.brokenSymlinks.map((b) => ({
|
|
68
|
+
...b,
|
|
69
|
+
name: sanitizeUntrusted(b.name),
|
|
70
|
+
path: sanitizePath(b.path),
|
|
71
|
+
target: sanitizeUntrusted(b.target),
|
|
72
|
+
})),
|
|
73
|
+
memoryFiles: result.memoryFiles.map((m) => ({
|
|
74
|
+
...m,
|
|
75
|
+
project: sanitizeUntrusted(m.project),
|
|
76
|
+
name: sanitizeUntrusted(m.name),
|
|
77
|
+
path: sanitizePath(m.path),
|
|
78
|
+
})),
|
|
79
|
+
claudeMdSections: result.claudeMdSections.map((s) => ({
|
|
80
|
+
...s,
|
|
81
|
+
name: sanitizeUntrusted(s.name),
|
|
82
|
+
})),
|
|
83
|
+
mcpServerNames: result.mcpServerNames.map((n) => sanitizeUntrusted(n)),
|
|
84
|
+
issues: result.issues.map((i) => ({
|
|
85
|
+
...i,
|
|
86
|
+
name: sanitizeUntrusted(i.name),
|
|
87
|
+
path: sanitizePath(i.path),
|
|
88
|
+
...(i.detail === undefined ? {} : { detail: sanitizeUntrusted(i.detail) }),
|
|
89
|
+
...(i.marketplace === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: { marketplace: sanitizeUntrusted(i.marketplace) }),
|
|
92
|
+
})),
|
|
93
|
+
pluginBreakdown: result.pluginBreakdown.map((p) => ({
|
|
94
|
+
...p,
|
|
95
|
+
name: sanitizeUntrusted(p.name),
|
|
96
|
+
marketplace: sanitizeUntrusted(p.marketplace),
|
|
97
|
+
})),
|
|
98
|
+
userAgents: result.userAgents.map((a) => ({ ...a, name: sanitizeUntrusted(a.name) })),
|
|
99
|
+
userCommands: result.userCommands.map((c) => ({ ...c, name: sanitizeUntrusted(c.name) })),
|
|
100
|
+
};
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "Audit and shrink your Claude Code startup context. Measures what every skill, plugin, agent, command, and memory file costs in the system prompt, then reversibly disables the dead weight. Non-destructive scan, tiered proposals, one-command restore — no proxy, no compression.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|