claude-slim 2.13.1 → 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/README.md CHANGED
@@ -139,6 +139,14 @@ claude plugin install claude-slim
139
139
 
140
140
  Then just type `/claude-slim` in any session.
141
141
 
142
+ Or install it through the [skills.sh](https://skills.sh) directory, which can also place
143
+ the skill into Codex, Cursor, and OpenCode. What claude-slim *analyzes* is unchanged:
144
+ `~/.claude/` and `~/.codex/`.
145
+
146
+ ```bash
147
+ npx skills add iops-leo/claude-slim
148
+ ```
149
+
142
150
  ---
143
151
 
144
152
  ## Usage
@@ -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
- return {
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.13.1",
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": {
@@ -18,16 +18,38 @@ Analyze the user's Claude Code environment for token waste and perform non-destr
18
18
 
19
19
  ---
20
20
 
21
+ ## Running the CLI
22
+
23
+ Every command block below opens with a `claude_slim()` resolver. It is repeated in full each
24
+ time on purpose: each Bash invocation is a fresh shell, so a function defined in one
25
+ block does not survive into the next. It resolves in three tiers — the plugin's own
26
+ `dist/cli.js` when `CLAUDE_PLUGIN_ROOT` is set, then a `claude-slim` on `PATH`, then
27
+ `npx`. That last tier is what makes the skill work when it was installed by
28
+ `npx skills add` rather than `claude plugin install`, where no plugin root exists.
29
+
30
+ Do not shorten the name. A two-letter `cs` collides with claude-squad's binary, and a
31
+ shell function is invisible to `timeout`, `env`, and `xargs` — a wrapped call would
32
+ silently run that other program instead. Call `claude_slim` directly, never through a
33
+ wrapper.
34
+
35
+ ---
36
+
21
37
  ## Phase 0 — Version gate (run before every scan)
22
38
 
23
39
  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:
24
40
 
25
41
  ```bash
26
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" check-update --json
42
+ 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' "$@"; fi; }
43
+ claude_slim check-update --json
27
44
  ```
28
45
 
29
46
  The check is cached for 24h and fails open — if it errors, times out, or returns `"latest": null`, **proceed silently**. Never block the user because a version lookup failed.
30
47
 
48
+ The response carries `installMethod`. When it is `"npx"` the gate is structurally
49
+ inert — the CLI was just fetched at the tag it is being compared against, so `outdated`
50
+ can never be true. Note that the version was resolved at run time and move on; do not
51
+ present the gate as a safeguard it is not.
52
+
31
53
  If `"outdated": true`, stop and tell the user before scanning:
32
54
 
33
55
  > 설치된 claude-slim이 {installed}이고 최신은 {latest}입니다.
@@ -39,7 +61,7 @@ If `"outdated": true`, stop and tell the user before scanning:
39
61
 
40
62
  Then honour their answer. If they choose to continue, run the scan but **label the numbers as coming from an outdated version** in your report.
41
63
 
42
- **Plugin installs need a restart.** `claude plugin update` writes the new version to disk, but the running session keeps the loaded copy. Tell the user this explicitly — otherwise they update, re-run, and see the same stale numbers with no idea why.
64
+ **Plugin installs need a restart** (only when `installMethod` is `"plugin"`). `claude plugin update` writes the new version to disk, but the running session keeps the loaded copy. Tell the user this explicitly — otherwise they update, re-run, and see the same stale numbers with no idea why.
43
65
 
44
66
  If `"outdated": false`, say nothing and continue to Phase 1.
45
67
 
@@ -50,20 +72,27 @@ If `"outdated": false`, say nothing and continue to Phase 1.
50
72
  Run the CLI to collect environment data:
51
73
 
52
74
  ```bash
53
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" scan --json
75
+ 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' "$@"; fi; }
76
+ claude_slim scan --json
54
77
  ```
55
78
 
56
- If `CLAUDE_PLUGIN_ROOT` is not set:
57
- ```bash
58
- PLUGIN_DIR=$(find ~/.claude/plugins -path "*/claude-slim/dist/cli.js" -type f 2>/dev/null | head -1 | xargs dirname | xargs dirname)
59
- node "$PLUGIN_DIR/dist/cli.js" scan --json
60
- ```
79
+ If that command fails for **any** reason — `node` missing, or the `npx` tier unable to
80
+ reach the npm registry because the machine is offline or behind a proxy — fall back to
81
+ the legacy bash scanner. It needs neither node nor a network:
61
82
 
62
- If `node` is not available, fall back to the legacy bash scanner:
63
83
  ```bash
64
- bash "${CLAUDE_PLUGIN_ROOT}/skills/claude-slim/scripts/scan.sh"
84
+ for d in "${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/claude-slim}" "$HOME/.claude/skills/claude-slim" "./.claude/skills/claude-slim"; do
85
+ [ -n "$d" ] || continue
86
+ if [ -f "$d/scripts/scan.sh" ]; then bash "$d/scripts/scan.sh" json; exit $?; fi
87
+ done
88
+ echo "claude-slim: no scan.sh found in any known skill directory" >&2
89
+ exit 1
65
90
  ```
66
91
 
92
+ If this fails too, tell the user the scan could not run and **stop**. Never continue to
93
+ Phase 2 without data: an empty scan is indistinguishable from a clean environment, and
94
+ reporting zeros as fact is the one outcome worse than reporting nothing.
95
+
67
96
  ---
68
97
 
69
98
  ## Phase 2 — Interpret & Present
@@ -163,18 +192,21 @@ If subcommand is `scan`, stop here. Ask a localized equivalent of "Proceed with
163
192
  Run the interactive clean command:
164
193
 
165
194
  ```bash
166
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" clean
195
+ 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' "$@"; fi; }
196
+ claude_slim clean
167
197
  ```
168
198
 
169
199
  Or with dry-run:
170
200
  ```bash
171
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" clean --dry-run
201
+ 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' "$@"; fi; }
202
+ claude_slim clean --dry-run
172
203
  ```
173
204
 
174
205
  After cleanup, re-run scan to get updated numbers, then show the savings report:
175
206
 
176
207
  ```bash
177
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" report
208
+ 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' "$@"; fi; }
209
+ claude_slim report
178
210
  ```
179
211
 
180
212
  Present the report box AND the before/after breakdown table to the user.
@@ -186,7 +218,8 @@ Present the report box AND the before/after breakdown table to the user.
186
218
  When `/claude-slim restore` is invoked:
187
219
 
188
220
  ```bash
189
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" restore
221
+ 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' "$@"; fi; }
222
+ claude_slim restore
190
223
  ```
191
224
 
192
225
  ## Doctor
@@ -194,7 +227,8 @@ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" restore
194
227
  When `/claude-slim doctor` is invoked:
195
228
 
196
229
  ```bash
197
- node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" doctor
230
+ 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' "$@"; fi; }
231
+ claude_slim doctor
198
232
  ```
199
233
 
200
234
  Explain warnings in the user's language. Pay special attention to session-log warnings because they explain why unused-skill detection may be suppressed.