claude-slim 2.14.2 → 2.14.3
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/codex/index.js +7 -2
- package/dist/scanner/untrusted.d.ts +23 -11
- package/dist/scanner/untrusted.js +67 -68
- package/package.json +1 -1
- package/skills/claude-slim/SKILL.md +7 -7
package/dist/codex/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { countTokensCached } from '../tokenizer.js';
|
|
|
4
4
|
import { listingTokens, listingTokensFromContent, parseFrontmatterDescription } from '../scanner/skill-listing.js';
|
|
5
5
|
import { safeReadFile, safeReaddir, isDirectory, isBrokenSymlink } from '../scanner/fs-walk.js';
|
|
6
6
|
import { detectBackupArtifact } from '../scanner/backup-artifacts.js';
|
|
7
|
+
import { sanitizeUntrustedTree } from '../scanner/untrusted.js';
|
|
7
8
|
// Codex support.
|
|
8
9
|
//
|
|
9
10
|
// The Claude Code scanner is deliberately left untouched: it is the
|
|
@@ -183,7 +184,11 @@ export async function scanCodex(contents) {
|
|
|
183
184
|
: 0;
|
|
184
185
|
const skills = [...local, ...plugin];
|
|
185
186
|
const listing = (xs) => xs.reduce((sum, x) => sum + x.listingTokens, 0);
|
|
186
|
-
|
|
187
|
+
// ~/.codex labels are authored by whoever wrote those skills and agents, and
|
|
188
|
+
// the CLI merges this tree into `scan --json` and prints skill names to the
|
|
189
|
+
// terminal. It never passes through the ~/.claude scanner, so it sanitizes on
|
|
190
|
+
// its own way out.
|
|
191
|
+
return sanitizeUntrustedTree({
|
|
187
192
|
root: getCodexDir(),
|
|
188
193
|
skills,
|
|
189
194
|
agents,
|
|
@@ -192,7 +197,7 @@ export async function scanCodex(contents) {
|
|
|
192
197
|
totalTokens: listing(skills) + listing(agents) + instructionsTokens,
|
|
193
198
|
unusedDetectionAvailable: false,
|
|
194
199
|
unusedDetectionReason: UNUSED_DETECTION_REASON,
|
|
195
|
-
};
|
|
200
|
+
});
|
|
196
201
|
}
|
|
197
202
|
/** Re-exported so callers can reuse the frontmatter parser without a deep import. */
|
|
198
203
|
export { parseFrontmatterDescription };
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { ScanResult } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Names read off disk are written by whoever authored the skill, plugin,
|
|
4
|
-
* memory file — not by the
|
|
5
|
-
* into the agent's context, which makes them an indirect prompt
|
|
6
|
-
* surface: a
|
|
7
|
-
*
|
|
3
|
+
* Names read off disk are written by whoever authored the skill, plugin, agent,
|
|
4
|
+
* or memory file — not by the person running the scan. They flow through the
|
|
5
|
+
* report into the agent's context, which makes them an indirect prompt
|
|
6
|
+
* injection surface: a directory name can carry instructions aimed at the model
|
|
7
|
+
* rather than a label aimed at a human.
|
|
8
8
|
*
|
|
9
9
|
* Snyk's audit of this skill (W011, medium 0.30) is about exactly this path.
|
|
10
10
|
* The scan never emits file *bodies* — descriptions are measured for token cost
|
|
11
|
-
* and then discarded — so
|
|
12
|
-
*
|
|
11
|
+
* and then discarded — so labels are the whole exposed surface.
|
|
12
|
+
*
|
|
13
|
+
* v2.14.1 sanitized a hand-written list of fields and claimed that a single
|
|
14
|
+
* chokepoint could not be forgotten. That was wrong: the chokepoint was one
|
|
15
|
+
* function, but its contents were an enumeration, and the first version of it
|
|
16
|
+
* already missed `pluginSkills[].pluginName`, `pluginSkills[].plugin`, and the
|
|
17
|
+
* entire `codex` subtree — whose skill names are printed to the terminal too.
|
|
18
|
+
* So this walks the result instead of listing its fields. A field added later
|
|
19
|
+
* is covered because it exists, not because someone remembered it.
|
|
13
20
|
*/
|
|
14
21
|
/** Longest label we render. Real names are far shorter; payloads are not. */
|
|
15
22
|
export declare const MAX_NAME_LENGTH = 120;
|
|
@@ -21,10 +28,15 @@ export declare const MAX_NAME_LENGTH = 120;
|
|
|
21
28
|
*/
|
|
22
29
|
export declare function sanitizeUntrusted(value: string, max?: number): string;
|
|
23
30
|
/**
|
|
24
|
-
* Return a copy of
|
|
31
|
+
* Return a copy of any scanned tree with every label flattened and bounded.
|
|
32
|
+
*
|
|
33
|
+
* Numbers and booleans pass through untouched; the input is not mutated.
|
|
25
34
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
35
|
+
* Each scanner applies this at its own exit. There is more than one scanner:
|
|
36
|
+
* `scanCodex()` runs separately and the CLI merges it in at print time
|
|
37
|
+
* (`src/cli.ts`), so `~/.codex` labels never pass through the `~/.claude`
|
|
38
|
+
* scanner and have to be sanitized on their own way out.
|
|
29
39
|
*/
|
|
40
|
+
export declare function sanitizeUntrustedTree<T>(value: T): T;
|
|
41
|
+
/** The `~/.claude` scanner's exit. */
|
|
30
42
|
export declare function sanitizeScanResult(result: ScanResult): ScanResult;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Names read off disk are written by whoever authored the skill, plugin,
|
|
3
|
-
* memory file — not by the
|
|
4
|
-
* into the agent's context, which makes them an indirect prompt
|
|
5
|
-
* surface: a
|
|
6
|
-
*
|
|
2
|
+
* Names read off disk are written by whoever authored the skill, plugin, agent,
|
|
3
|
+
* or memory file — not by the person running the scan. They flow through the
|
|
4
|
+
* report into the agent's context, which makes them an indirect prompt
|
|
5
|
+
* injection surface: a directory name can carry instructions aimed at the model
|
|
6
|
+
* rather than a label aimed at a human.
|
|
7
7
|
*
|
|
8
8
|
* Snyk's audit of this skill (W011, medium 0.30) is about exactly this path.
|
|
9
9
|
* The scan never emits file *bodies* — descriptions are measured for token cost
|
|
10
|
-
* and then discarded — so
|
|
11
|
-
*
|
|
10
|
+
* and then discarded — so labels are the whole exposed surface.
|
|
11
|
+
*
|
|
12
|
+
* v2.14.1 sanitized a hand-written list of fields and claimed that a single
|
|
13
|
+
* chokepoint could not be forgotten. That was wrong: the chokepoint was one
|
|
14
|
+
* function, but its contents were an enumeration, and the first version of it
|
|
15
|
+
* already missed `pluginSkills[].pluginName`, `pluginSkills[].plugin`, and the
|
|
16
|
+
* entire `codex` subtree — whose skill names are printed to the terminal too.
|
|
17
|
+
* So this walks the result instead of listing its fields. A field added later
|
|
18
|
+
* is covered because it exists, not because someone remembered it.
|
|
12
19
|
*/
|
|
13
20
|
/** Longest label we render. Real names are far shorter; payloads are not. */
|
|
14
21
|
export const MAX_NAME_LENGTH = 120;
|
|
@@ -20,82 +27,74 @@ const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
|
20
27
|
*/
|
|
21
28
|
const INVISIBLE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/g;
|
|
22
29
|
/**
|
|
23
|
-
*
|
|
30
|
+
* Keys whose values are truncated only at their peril.
|
|
24
31
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
32
|
+
* Paths locate files that cleanup then acts on, so a shortened path is a wrong
|
|
33
|
+
* path. `currentProjectSlug` is matched against directory names. The two
|
|
34
|
+
* `*Reason` strings are our own prose and already run past the label bound.
|
|
35
|
+
* All of them are still flattened — that is the part that blocks injection.
|
|
27
36
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
const FLATTEN_ONLY_KEYS = new Set([
|
|
38
|
+
'path',
|
|
39
|
+
'root',
|
|
40
|
+
'target',
|
|
41
|
+
'currentProjectSlug',
|
|
42
|
+
'unusedDetectionReason',
|
|
43
|
+
]);
|
|
44
|
+
/** Strip anything that could forge a row or hide from a human reader. */
|
|
45
|
+
function flatten(value) {
|
|
46
|
+
return value
|
|
30
47
|
.replace(CONTROL_CHARS, ' ')
|
|
31
48
|
.replace(INVISIBLE, '')
|
|
32
49
|
.replace(/\s+/g, ' ')
|
|
33
50
|
.trim();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Collapse an untrusted label to a single bounded, printable line.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately not an escape or an encoding: the value is a display label, and
|
|
56
|
+
* a reversible transform would relocate a payload rather than remove it.
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeUntrusted(value, max = MAX_NAME_LENGTH) {
|
|
59
|
+
const flattened = flatten(value);
|
|
34
60
|
if (flattened.length <= max)
|
|
35
61
|
return flattened;
|
|
36
62
|
return `${flattened.slice(0, max)}…`;
|
|
37
63
|
}
|
|
38
64
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
65
|
+
* Walk any scan value, sanitizing every string it contains.
|
|
66
|
+
*
|
|
67
|
+
* `key` is the property name the string arrived under; for arrays it is
|
|
68
|
+
* inherited from the property holding the array, so `mcpServerNames[]` and
|
|
69
|
+
* `plugins[].skills[]` are treated as the labels they are.
|
|
41
70
|
*/
|
|
42
|
-
function
|
|
43
|
-
|
|
71
|
+
function sanitizeDeep(value, key) {
|
|
72
|
+
if (typeof value === 'string') {
|
|
73
|
+
return FLATTEN_ONLY_KEYS.has(key) ? flatten(value) : sanitizeUntrusted(value);
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
return value.map((entry) => sanitizeDeep(entry, key));
|
|
77
|
+
}
|
|
78
|
+
if (value !== null && typeof value === 'object') {
|
|
79
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, sanitizeDeep(v, k)]));
|
|
80
|
+
}
|
|
81
|
+
// Numbers, booleans, null, undefined — nothing to sanitize, nothing to copy.
|
|
82
|
+
return value;
|
|
44
83
|
}
|
|
45
84
|
/**
|
|
46
|
-
* Return a copy of
|
|
85
|
+
* Return a copy of any scanned tree with every label flattened and bounded.
|
|
47
86
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
87
|
+
* Numbers and booleans pass through untouched; the input is not mutated.
|
|
88
|
+
*
|
|
89
|
+
* Each scanner applies this at its own exit. There is more than one scanner:
|
|
90
|
+
* `scanCodex()` runs separately and the CLI merges it in at print time
|
|
91
|
+
* (`src/cli.ts`), so `~/.codex` labels never pass through the `~/.claude`
|
|
92
|
+
* scanner and have to be sanitized on their own way out.
|
|
51
93
|
*/
|
|
94
|
+
export function sanitizeUntrustedTree(value) {
|
|
95
|
+
return sanitizeDeep(value, '');
|
|
96
|
+
}
|
|
97
|
+
/** The `~/.claude` scanner's exit. */
|
|
52
98
|
export function sanitizeScanResult(result) {
|
|
53
|
-
|
|
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
|
-
};
|
|
99
|
+
return sanitizeUntrustedTree(result);
|
|
101
100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.3",
|
|
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": {
|
|
@@ -47,7 +47,7 @@ wrapper.
|
|
|
47
47
|
An outdated claude-slim does not merely lack features — it reports **wrong numbers**. Versions before 2.8.0 summed memory across every project on disk and inflated the startup estimate roughly 8×. Presenting those figures as fact is worse than not running at all, so check first:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
50
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
51
51
|
claude_slim check-update --json
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -80,7 +80,7 @@ If `"outdated": false`, say nothing and continue to Phase 1.
|
|
|
80
80
|
Run the CLI to collect environment data:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
83
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
84
84
|
claude_slim scan --json
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -200,20 +200,20 @@ If subcommand is `scan`, stop here. Ask a localized equivalent of "Proceed with
|
|
|
200
200
|
Run the interactive clean command:
|
|
201
201
|
|
|
202
202
|
```bash
|
|
203
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
203
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
204
204
|
claude_slim clean
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
Or with dry-run:
|
|
208
208
|
```bash
|
|
209
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
209
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
210
210
|
claude_slim clean --dry-run
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
After cleanup, re-run scan to get updated numbers, then show the savings report:
|
|
214
214
|
|
|
215
215
|
```bash
|
|
216
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
216
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
217
217
|
claude_slim report
|
|
218
218
|
```
|
|
219
219
|
|
|
@@ -226,7 +226,7 @@ Present the report box AND the before/after breakdown table to the user.
|
|
|
226
226
|
When `/claude-slim restore` is invoked:
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
229
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
230
230
|
claude_slim restore
|
|
231
231
|
```
|
|
232
232
|
|
|
@@ -235,7 +235,7 @@ claude_slim restore
|
|
|
235
235
|
When `/claude-slim doctor` is invoked:
|
|
236
236
|
|
|
237
237
|
```bash
|
|
238
|
-
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.
|
|
238
|
+
claude_slim(){ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/dist/cli.js" ]; then node "$CLAUDE_PLUGIN_ROOT/dist/cli.js" "$@"; elif command -v claude-slim >/dev/null 2>&1; then claude-slim "$@"; else npx -y 'claude-slim@^2.14.3' "$@"; fi; }
|
|
239
239
|
claude_slim doctor
|
|
240
240
|
```
|
|
241
241
|
|