claude-slim 2.10.0 → 2.11.0

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
@@ -202,7 +202,7 @@ claude-slim never updates itself — that's your package manager's job, and writ
202
202
  - **`~/.claude/agents/` and `~/.claude/commands/`** — measured and reported since v2.8, never moved or deleted. There is no restore path for them yet, and a destructive action without its undo isn't worth shipping.
203
203
  - **Plugin internals** (`~/.claude/plugins/config.json`, individual `plugin.json` files) — left alone; use `claude plugin` to manage plugins.
204
204
  - **Git / project sources** — claude-slim only looks inside `~/.claude/`, never at your code.
205
- - **`~/.codex/`** — scanned and reported when Codex is installed, never modified. Unused-skill detection is not offered there: Codex session logs record the skill catalog, not invocations, so there is no honest usage signal to act on.
205
+ - **`~/.codex/`** — scanned and, since v2.11, cleanable under the same tiers. Moves go to `~/.codex/skills.disabled/` and reverse with `restore`. The path guard is per-agent, so a Codex item can never resolve into `~/.claude/`. Unused-skill detection is still not offered there: Codex session logs record the skill catalog, not invocations, so there is no honest usage signal to act on.
206
206
  - **Anything outside `~/.claude/`** — a path-containment guard refuses destructive ops anywhere else, even if a tampered manifest asked it to.
207
207
 
208
208
  Only touched: entries under `~/.claude/skills/`, `~/.claude/plugins/cache/temp_local_*`, and `~/.claude/projects/*/memory/`. Skill and memory entries are moved to `skills.disabled/`; broken symlink files are unlinked and `temp_local_*` failed-install caches are removed outright.
@@ -247,18 +247,16 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
247
247
 
248
248
  ---
249
249
 
250
- ## v2.10.0 — What's new
250
+ ## v2.11.0 — What's new
251
251
 
252
- Codex support, scoped to what Codex can actually be asked.
252
+ Codex gets the same tiered propose-and-choose cleanup Claude Code has, minus the two categories Codex cannot honestly support.
253
253
 
254
- - **`~/.codex/` is scanned when present.** `scan` auto-detects a Codex install and reports its startup cost local skills, plugin skills, agents, and `AGENTS.md`. `scan --json` gains a `codex` key; `--no-codex` skips it. On the dev machine this surfaced **10,926 tokens** nothing was measuring, including a `.bak` copy of a skill still costing 285 tokens every session.
255
- - Codex `SKILL.md` frontmatter matches Claude Code's exactly, so the existing listing parser is reused unchanged. Agents differ (`<name>.toml` with `description = "…"`) and get a small parser, verified against all 18 installed agents.
256
- - **Unused-skill detection is not offered for Codex, and `scan` says so.** Codex session logs record the skill *catalog* injected into each prompt, not invocations every skill shows up in nearly every session, so using them as a usage signal would mark everything "used". Checked against 408 session files, a 56,724-row log database, and the tool-registry table before concluding.
257
- - **`~/.codex/` is read-only.** Nothing is moved or deleted there, same as `~/.claude/agents/`.
254
+ - **Codex cleanup, tiered.** Five of seven categories carry over because they are filesystem facts, not usage inferences: broken symlinks and unfilled templates (Tier 1), install leftovers in `~/.codex/.tmp` (Tier 1 146MB on the dev machine), backup copies and plugin-shadowed duplicates (Tier 2), oversized skills (Tier 3). Codex items join the same numbered list tagged `[codex]`. `--no-codex` opts out.
255
+ - **`~/.codex/skills.disabled/`** moves are reversible through the same `restore`, and manifest entries record which agent they came from.
256
+ - **The path guard is now per-agent.** Not "inside any known root" a Codex issue must not resolve into `~/.claude/` and vice versa, so a tampered manifest cannot cross between agents.
257
+ - `unused_skill` and `oversized_memory` remain unavailable for Codex: no invocation record, and no `~/.codex/projects/*/memory/`.
258
258
 
259
- - **Backup-artifact detection, on both agents.** `foo.bak.20260711`, `foo (1)`, `foo~` and similar are flaggedTier 2 on Claude Code (movable, restorable), report-only on Codex. Matching is limited to artifact *shapes*, so `backup-manager` and `test-engineer` are never touched.
260
-
261
- Tests: 279 → 347 (+68).
259
+ Tests: 347 374 (+27), the guard suite being the point cross-agent isolation both directions, traversal escapes, and a `~/.claude-backup` sibling a naive `startsWith` would have allowed.
262
260
 
263
261
  For older release notes, see [CHANGELOG.md](CHANGELOG.md).
264
262
 
package/dist/cleaner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { rename, readdir, rmdir, rm, unlink, lstat, mkdir } from 'node:fs/promises';
2
2
  import { join, dirname, resolve, sep } from 'node:path';
3
- import { appendManifest, ensureDisabledDir, getDisabledDir, removeEntry, recordDisabledPlugin, removeDisabledPlugin } from './manifest.js';
4
- import { assertInsideClaudeDir, getSkillsDir, getProjectsDir } from './paths.js';
3
+ import { appendManifest, ensureDisabledDir, removeEntry, recordDisabledPlugin, removeDisabledPlugin } from './manifest.js';
4
+ import { assertInsideAgentRoot, getAgentRoot, getAgentDisabledDir, getSkillsDir, getProjectsDir } from './paths.js';
5
5
  import { disablePlugin, enablePlugin, isClaudeCliAvailable, ClaudeCliMissingError } from './plugin-runtime.js';
6
6
  // Restrict a restore target to a specific subtree of ~/.claude/. Complements
7
7
  // assertInsideClaudeDir: a tampered manifest could still name a legal
@@ -40,8 +40,6 @@ async function recordOrRollback(entry, rollback) {
40
40
  }
41
41
  }
42
42
  export async function cleanIssues(issues) {
43
- await ensureDisabledDir();
44
- const disabledDir = getDisabledDir();
45
43
  const moved = [];
46
44
  const skipped = [];
47
45
  const errors = [];
@@ -54,15 +52,20 @@ export async function cleanIssues(issues) {
54
52
  claudeCliMissing = true;
55
53
  }
56
54
  for (const issue of issues) {
55
+ // Scope every path decision to the issue's own agent, so a Codex issue can
56
+ // never resolve into ~/.claude/ (or the reverse).
57
+ const agent = issue.agent ?? 'claude';
58
+ const disabledDir = await ensureDisabledDir(agent);
57
59
  try {
58
60
  // unused_plugin and report-only types don't touch filesystem paths directly
59
61
  if (issue.type !== 'unused_plugin' && issue.type !== 'oversized_memory' && issue.type !== 'disabled_plugin') {
60
- assertInsideClaudeDir(issue.path);
62
+ assertInsideAgentRoot(issue.path, agent);
61
63
  }
62
64
  if (issue.type === 'broken_symlink') {
63
65
  await unlink(issue.path);
64
66
  const entry = {
65
67
  date: new Date().toISOString(),
68
+ agent,
66
69
  name: issue.name,
67
70
  from: issue.path,
68
71
  type: issue.type,
@@ -85,6 +88,7 @@ export async function cleanIssues(issues) {
85
88
  await rename(issue.path, dest);
86
89
  const entry = {
87
90
  date: new Date().toISOString(),
91
+ agent,
88
92
  name: issue.name,
89
93
  from: issue.path,
90
94
  type: issue.type,
@@ -108,6 +112,7 @@ export async function cleanIssues(issues) {
108
112
  }
109
113
  const entry = {
110
114
  date: new Date().toISOString(),
115
+ agent,
111
116
  name: issue.name,
112
117
  from: issue.path,
113
118
  type: issue.type,
@@ -131,6 +136,7 @@ export async function cleanIssues(issues) {
131
136
  await rename(issue.path, backupDir);
132
137
  const entry = {
133
138
  date: new Date().toISOString(),
139
+ agent,
134
140
  name: issue.name,
135
141
  from: issue.path,
136
142
  type: issue.type,
@@ -210,7 +216,7 @@ export async function restoreItem(entry) {
210
216
  return;
211
217
  }
212
218
  const legacyEntry = entry;
213
- assertInsideClaudeDir(legacyEntry.from);
219
+ assertInsideAgentRoot(legacyEntry.from, legacyEntry.agent ?? 'claude');
214
220
  if (legacyEntry.type === 'broken_symlink') {
215
221
  throw new Error(`Broken symlinks cannot be restored (${legacyEntry.name})`);
216
222
  }
@@ -220,7 +226,9 @@ export async function restoreItem(entry) {
220
226
  if (legacyEntry.type === 'temp_cache') {
221
227
  throw new Error(`Temp caches were deleted and cannot be restored (${legacyEntry.name})`);
222
228
  }
223
- const disabledDir = getDisabledDir();
229
+ // Resolve the store from the entry's own agent — a Codex skill was parked
230
+ // under ~/.codex/skills.disabled and must be looked for there.
231
+ const disabledDir = getAgentDisabledDir(('agent' in entry ? entry.agent : undefined) ?? 'claude');
224
232
  if (legacyEntry.type === 'stale_project') {
225
233
  // Type-scoped path guard: stale-project backups must restore under
226
234
  // ~/.claude/projects/. Prevents a tampered manifest from redirecting a
@@ -239,7 +247,9 @@ export async function restoreItem(entry) {
239
247
  }
240
248
  else {
241
249
  // Type-scoped path guard: skill restores must land under ~/.claude/skills/.
242
- assertInsideSubtree(legacyEntry.from, getSkillsDir(), 'skill');
250
+ // Per-agent skills/ subtree: a Codex entry restores under ~/.codex/skills,
251
+ // never ~/.claude/skills.
252
+ assertInsideSubtree(legacyEntry.from, join(getAgentRoot(legacyEntry.agent ?? 'claude'), 'skills'), 'skill');
243
253
  // Restore skill directory using the same naming as cleanIssues
244
254
  const safeName = legacyEntry.name.replace(/\//g, '--');
245
255
  const src = join(disabledDir, safeName);
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { collectDoctorReport, formatDoctorReport } from './doctor.js';
13
13
  import { checkForUpdate, formatUpdateNotice } from './update-check.js';
14
14
  import { scanCodex } from './codex/index.js';
15
15
  import { formatCodexSummary } from './codex/report.js';
16
+ import { classifyCodexIssues } from './codex/detectors.js';
16
17
  import { resolveSelection, resolveRestoreSelection } from './selection.js';
17
18
  const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
18
19
  const { version: PKG_VERSION } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
@@ -108,12 +109,14 @@ program
108
109
  .option('--auto', 'Non-interactive: auto-select Tier 1 items only')
109
110
  .option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
110
111
  .option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
112
+ .option('--no-codex', 'Skip ~/.codex entirely')
111
113
  .action(async (opts) => {
112
114
  await runCleanPipeline({
113
115
  dryRun: !!opts.dryRun,
114
116
  auto: !!opts.auto,
115
117
  sessionsPerDay: parseNonNegativeInt(opts.sessionsPerDay, 2),
116
118
  lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
119
+ codex: opts.codex !== false,
117
120
  });
118
121
  });
119
122
  // --- restore ---
@@ -269,12 +272,20 @@ program.action(async () => {
269
272
  process.exitCode = 1;
270
273
  return;
271
274
  }
272
- await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60 });
275
+ await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60, codex: true });
273
276
  });
274
277
  // --- shared clean pipeline ---
275
278
  async function runCleanPipeline(opts) {
276
279
  await initTokenizer();
277
280
  const result = await scan({ lookbackDays: opts.lookbackDays });
281
+ // Codex issues join the same tiered list. They carry `agent: 'codex'`, which
282
+ // is what keeps the cleaner's path guard pointed at ~/.codex/.
283
+ const codexContents = new Map();
284
+ const codexScan = opts.codex ? await scanCodex(codexContents) : null;
285
+ if (codexScan) {
286
+ result.issues.push(...(await classifyCodexIssues({ scan: codexScan, contents: codexContents })));
287
+ result.issues.sort((a, b) => a.tier - b.tier || b.tokens - a.tokens);
288
+ }
278
289
  if (result.issues.length === 0) {
279
290
  console.log('\n \x1b[32mAlready slim!\x1b[0m No issues found.\n');
280
291
  await flushCache();
@@ -0,0 +1,26 @@
1
+ import type { Issue } from '../types.js';
2
+ import type { CodexScanResult } from './index.js';
3
+ export interface CodexDetectorContext {
4
+ scan: CodexScanResult;
5
+ /** SKILL.md contents keyed by absolute path, so detectors need not re-read. */
6
+ contents: Map<string, string>;
7
+ }
8
+ /** Tier 1 — a SKILL.md symlink whose target is gone. Contributes nothing. */
9
+ export declare function detectBrokenSymlinks(ctx: CodexDetectorContext): Promise<Issue[]>;
10
+ /** Tier 1 — a scaffold nobody filled in. */
11
+ export declare function detectTemplates(ctx: CodexDetectorContext): Issue[];
12
+ /**
13
+ * Tier 1 — `~/.codex/.tmp`, where interrupted plugin installs accumulate.
14
+ * Sized rather than token-counted: this is disk, not context.
15
+ */
16
+ export declare function detectInstallLeftovers(): Promise<Issue[]>;
17
+ /** Tier 2 — a leftover copy, recognised by name shape alone. */
18
+ export declare function detectBackups(ctx: CodexDetectorContext): Issue[];
19
+ /**
20
+ * Tier 2 — a local skill shadowed by a plugin-provided one of the same name.
21
+ * Removing the local copy leaves the plugin version in place.
22
+ */
23
+ export declare function detectDuplicates(ctx: CodexDetectorContext): Issue[];
24
+ /** Tier 3 — large enough to be worth a look, but possibly still in use. */
25
+ export declare function detectOversized(ctx: CodexDetectorContext): Issue[];
26
+ export declare function classifyCodexIssues(ctx: CodexDetectorContext): Promise<Issue[]>;
@@ -0,0 +1,110 @@
1
+ import { join } from 'node:path';
2
+ import { getCodexDir } from '../paths.js';
3
+ import { OVERSIZED_SKILL_BYTES } from '../scanner/constants.js';
4
+ import { isDirectory, safeReaddir, getDirSize, isBrokenSymlink } from '../scanner/fs-walk.js';
5
+ // Codex cleanup candidates, in the same three tiers the Claude path uses.
6
+ //
7
+ // Five of Claude's seven categories carry over unchanged, because they are
8
+ // filesystem facts rather than usage inferences:
9
+ //
10
+ // broken symlinks · empty templates · duplicates · oversized · install leftovers
11
+ //
12
+ // The two that do not:
13
+ // unused_skill — no invocation record exists in Codex session logs
14
+ // oversized_memory— Codex has no ~/.codex/projects/*/memory/ equivalent
15
+ //
16
+ // Every issue produced here carries `agent: 'codex'`, which is what keeps the
17
+ // cleaner's path guard scoped to ~/.codex/ and unable to reach ~/.claude/.
18
+ const TEMPLATE_MARKER = 'Replace with description';
19
+ /** MB once it is worth calling MB; KB below that, so a 3KB leftover is not "0MB". */
20
+ function formatBytes(bytes) {
21
+ const mb = bytes / 1024 / 1024;
22
+ return mb >= 1 ? `${mb.toFixed(0)}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`;
23
+ }
24
+ function issue(type, tier, skill, detail) {
25
+ return { type, tier, agent: 'codex', name: skill.name, path: skill.path, tokens: skill.tokens, detail };
26
+ }
27
+ /** Tier 1 — a SKILL.md symlink whose target is gone. Contributes nothing. */
28
+ export async function detectBrokenSymlinks(ctx) {
29
+ const out = [];
30
+ const skillsDir = join(getCodexDir(), 'skills');
31
+ for (const entry of await safeReaddir(skillsDir)) {
32
+ if (entry.startsWith('.'))
33
+ continue;
34
+ const dir = join(skillsDir, entry);
35
+ if (!(await isDirectory(dir)))
36
+ continue;
37
+ const md = join(dir, 'SKILL.md');
38
+ if (await isBrokenSymlink(md)) {
39
+ out.push({
40
+ type: 'broken_symlink', tier: 1, agent: 'codex',
41
+ name: entry, path: md, tokens: 0, detail: 'dead symlink',
42
+ });
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ /** Tier 1 — a scaffold nobody filled in. */
48
+ export function detectTemplates(ctx) {
49
+ return ctx.scan.skills
50
+ .filter((s) => s.source === 'local')
51
+ .filter((s) => ctx.contents.get(join(s.path, 'SKILL.md'))?.includes(TEMPLATE_MARKER))
52
+ .map((s) => issue('template', 1, s, 'unfilled template'));
53
+ }
54
+ /**
55
+ * Tier 1 — `~/.codex/.tmp`, where interrupted plugin installs accumulate.
56
+ * Sized rather than token-counted: this is disk, not context.
57
+ */
58
+ export async function detectInstallLeftovers() {
59
+ const out = [];
60
+ for (const name of ['.tmp', '.remote-plugin-install-staging']) {
61
+ const path = join(getCodexDir(), name);
62
+ if (!(await isDirectory(path)))
63
+ continue;
64
+ const bytes = await getDirSize(path);
65
+ if (bytes === 0)
66
+ continue;
67
+ out.push({
68
+ type: 'temp_cache', tier: 1, agent: 'codex',
69
+ name, path, tokens: 0,
70
+ detail: `${formatBytes(bytes)} of install leftovers`,
71
+ });
72
+ }
73
+ return out;
74
+ }
75
+ /** Tier 2 — a leftover copy, recognised by name shape alone. */
76
+ export function detectBackups(ctx) {
77
+ return ctx.scan.skills
78
+ .filter((s) => s.source === 'local' && s.backupArtifact)
79
+ .map((s) => issue('backup_artifact', 2, s, `looks like a backup copy (${s.backupArtifact})`));
80
+ }
81
+ /**
82
+ * Tier 2 — a local skill shadowed by a plugin-provided one of the same name.
83
+ * Removing the local copy leaves the plugin version in place.
84
+ */
85
+ export function detectDuplicates(ctx) {
86
+ const fromPlugins = new Set(ctx.scan.skills.filter((s) => s.source === 'plugin').map((s) => s.name));
87
+ return ctx.scan.skills
88
+ .filter((s) => s.source === 'local' && fromPlugins.has(s.name))
89
+ .map((s) => issue('duplicate', 2, s, 'also provided by a plugin'));
90
+ }
91
+ /** Tier 3 — large enough to be worth a look, but possibly still in use. */
92
+ export function detectOversized(ctx) {
93
+ return ctx.scan.skills
94
+ .filter((s) => s.source === 'local' && s.sizeBytes > OVERSIZED_SKILL_BYTES)
95
+ .map((s) => issue('oversized_skill', 3, s, `${Math.round(s.sizeBytes / 1024)}KB`));
96
+ }
97
+ export async function classifyCodexIssues(ctx) {
98
+ const [symlinks, leftovers] = await Promise.all([
99
+ detectBrokenSymlinks(ctx),
100
+ detectInstallLeftovers(),
101
+ ]);
102
+ return [
103
+ ...symlinks,
104
+ ...detectTemplates(ctx),
105
+ ...leftovers,
106
+ ...detectBackups(ctx),
107
+ ...detectDuplicates(ctx),
108
+ ...detectOversized(ctx),
109
+ ].sort((a, b) => a.tier - b.tier || b.tokens - a.tokens);
110
+ }
@@ -41,6 +41,11 @@ export declare function isCodexInstalled(): Promise<boolean>;
41
41
  * back to the flat estimate.
42
42
  */
43
43
  export declare function parseTomlDescription(content: string): string | null;
44
- export declare function scanCodex(): Promise<CodexScanResult | null>;
44
+ /**
45
+ * @param contents optional sink for SKILL.md bodies, so detectors can inspect
46
+ * them without a second pass over the filesystem. Deliberately an out-param
47
+ * rather than part of the result: it must not land in `scan --json`.
48
+ */
49
+ export declare function scanCodex(contents?: Map<string, string>): Promise<CodexScanResult | null>;
45
50
  /** Re-exported so callers can reuse the frontmatter parser without a deep import. */
46
51
  export { parseFrontmatterDescription };
@@ -60,7 +60,7 @@ export function parseTomlDescription(content) {
60
60
  const unescaped = single[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\').trim();
61
61
  return unescaped || null;
62
62
  }
63
- async function scanLocalSkills() {
63
+ async function scanLocalSkills(contents) {
64
64
  const dir = join(getCodexDir(), 'skills');
65
65
  const entries = await safeReaddir(dir);
66
66
  const results = [];
@@ -77,6 +77,7 @@ async function scanLocalSkills() {
77
77
  const content = await safeReadFile(md);
78
78
  if (content === null)
79
79
  continue;
80
+ contents?.set(md, content);
80
81
  results.push({
81
82
  name: entry,
82
83
  path: skillDir,
@@ -161,11 +162,16 @@ async function scanAgents() {
161
162
  }
162
163
  return results;
163
164
  }
164
- export async function scanCodex() {
165
+ /**
166
+ * @param contents optional sink for SKILL.md bodies, so detectors can inspect
167
+ * them without a second pass over the filesystem. Deliberately an out-param
168
+ * rather than part of the result: it must not land in `scan --json`.
169
+ */
170
+ export async function scanCodex(contents) {
165
171
  if (!(await isCodexInstalled()))
166
172
  return null;
167
173
  const [local, plugin, agents] = await Promise.all([
168
- scanLocalSkills(),
174
+ scanLocalSkills(contents),
169
175
  scanPluginSkills(),
170
176
  scanAgents(),
171
177
  ]);
@@ -1,6 +1,8 @@
1
1
  import type { Manifest, ManifestEntry, AnyManifestEntry, DisabledPluginEntry } from './types.js';
2
+ import type { AgentId } from './paths.js';
2
3
  export declare function getDisabledDir(): string;
3
- export declare function ensureDisabledDir(): Promise<void>;
4
+ /** Create (if needed) and return the disabled-skill store for one agent. */
5
+ export declare function ensureDisabledDir(agent?: AgentId): Promise<string>;
4
6
  export declare function migrateLegacyIfNeeded(): Promise<void>;
5
7
  export declare function readManifestV2(): Promise<Manifest>;
6
8
  export declare function writeManifestV2(manifest: Manifest): Promise<void>;
package/dist/manifest.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { readFile, writeFile, mkdir, rename, access } from 'node:fs/promises';
2
- import { getDisabledDir as getDir, getManifestPath, getLegacyManifestPath, } from './paths.js';
2
+ import { getDisabledDir as getDir, getAgentDisabledDir, getManifestPath, getLegacyManifestPath, } from './paths.js';
3
3
  export function getDisabledDir() {
4
4
  return getDir();
5
5
  }
6
- export async function ensureDisabledDir() {
7
- await mkdir(getDisabledDir(), { recursive: true });
6
+ /** Create (if needed) and return the disabled-skill store for one agent. */
7
+ export async function ensureDisabledDir(agent = 'claude') {
8
+ const dir = getAgentDisabledDir(agent);
9
+ await mkdir(dir, { recursive: true });
10
+ return dir;
8
11
  }
9
12
  async function pathExists(p) {
10
13
  try {
package/dist/paths.d.ts CHANGED
@@ -14,4 +14,23 @@ export declare function getDisabledDir(): string;
14
14
  export declare function getCurrentProjectSlug(cwd?: string): string;
15
15
  export declare function getManifestPath(): string;
16
16
  export declare function getLegacyManifestPath(): string;
17
+ /** The agents claude-slim is allowed to touch. Adding one widens what every
18
+ * destructive operation may reach, so this list is the security boundary. */
19
+ export type AgentId = 'claude' | 'codex';
20
+ export declare function getCodexDir(): string;
21
+ export declare function getAgentRoot(agent: AgentId): string;
22
+ export declare function getAgentDisabledDir(agent: AgentId): string;
23
+ /**
24
+ * Refuse to operate on a path outside the given agent's root. Guards destructive
25
+ * operations (rename/rm/unlink) against tampered manifests and scanner bugs.
26
+ *
27
+ * Deliberately per-agent rather than "inside any known root": a Codex issue must
28
+ * not be able to reach into ~/.claude/ and vice versa. Widening this to a single
29
+ * combined check would let one bad manifest entry cross between agents, which is
30
+ * exactly the failure this exists to prevent.
31
+ */
32
+ export declare function assertInsideAgentRoot(p: string, agent: AgentId): void;
33
+ /** Back-compat wrapper — the Claude path is by far the most common caller. */
17
34
  export declare function assertInsideClaudeDir(p: string): void;
35
+ /** Which agent owns this path, or null if it belongs to neither. */
36
+ export declare function agentForPath(p: string): AgentId | null;
package/dist/paths.js CHANGED
@@ -32,12 +32,44 @@ export function getManifestPath() {
32
32
  export function getLegacyManifestPath() {
33
33
  return join(getDisabledDir(), '.claude-slim-manifest.jsonl');
34
34
  }
35
- // Refuse to operate on a path outside ~/.claude/. Guards destructive operations
36
- // (rename/rm/unlink) from acting on attacker-tampered manifests or scanner bugs.
37
- export function assertInsideClaudeDir(p) {
38
- const resolved = resolve(p);
39
- const root = resolve(getClaudeDir());
40
- if (resolved !== root && !resolved.startsWith(root + sep)) {
41
- throw new Error(`Refusing to operate on path outside ~/.claude/: ${p}`);
35
+ export function getCodexDir() {
36
+ return join(homedir(), '.codex');
37
+ }
38
+ export function getAgentRoot(agent) {
39
+ return agent === 'claude' ? getClaudeDir() : getCodexDir();
40
+ }
41
+ export function getAgentDisabledDir(agent) {
42
+ return join(getAgentRoot(agent), 'skills.disabled');
43
+ }
44
+ function isInside(child, root) {
45
+ const c = resolve(child);
46
+ const r = resolve(root);
47
+ return c === r || c.startsWith(r + sep);
48
+ }
49
+ /**
50
+ * Refuse to operate on a path outside the given agent's root. Guards destructive
51
+ * operations (rename/rm/unlink) against tampered manifests and scanner bugs.
52
+ *
53
+ * Deliberately per-agent rather than "inside any known root": a Codex issue must
54
+ * not be able to reach into ~/.claude/ and vice versa. Widening this to a single
55
+ * combined check would let one bad manifest entry cross between agents, which is
56
+ * exactly the failure this exists to prevent.
57
+ */
58
+ export function assertInsideAgentRoot(p, agent) {
59
+ if (!isInside(p, getAgentRoot(agent))) {
60
+ const label = agent === 'claude' ? '~/.claude/' : '~/.codex/';
61
+ throw new Error(`Refusing to operate on path outside ${label}: ${p}`);
42
62
  }
43
63
  }
64
+ /** Back-compat wrapper — the Claude path is by far the most common caller. */
65
+ export function assertInsideClaudeDir(p) {
66
+ assertInsideAgentRoot(p, 'claude');
67
+ }
68
+ /** Which agent owns this path, or null if it belongs to neither. */
69
+ export function agentForPath(p) {
70
+ if (isInside(p, getClaudeDir()))
71
+ return 'claude';
72
+ if (isInside(p, getCodexDir()))
73
+ return 'codex';
74
+ return null;
75
+ }
package/dist/report.js CHANGED
@@ -261,7 +261,10 @@ export function formatScanSummary(result) {
261
261
  const detail = issue.detail ? ` (${issue.detail})` : '';
262
262
  const tokStr = issue.tokens > 0 ? ` ~${issue.tokens.toLocaleString()} tok` : '';
263
263
  const permanent = permanentTypes.has(issue.type) ? ' \x1b[31m(permanent)\x1b[0m' : '';
264
- lines.push(` ${selected} ${i + 1}. \x1b[${color}m[${tierLabel}]\x1b[0m ${issue.type}: ${issue.name}${detail}${tokStr}${permanent}`);
264
+ // Tag the agent when it is not Claude Code, so a ~/.codex item is never
265
+ // mistaken for one under ~/.claude.
266
+ const agentTag = issue.agent && issue.agent !== 'claude' ? ` \x1b[36m[${issue.agent}]\x1b[0m` : '';
267
+ lines.push(` ${selected} ${i + 1}. \x1b[${color}m[${tierLabel}]\x1b[0m${agentTag} ${issue.type}: ${issue.name}${detail}${tokStr}${permanent}`);
265
268
  }
266
269
  }
267
270
  // --- PLUGINS BREAKDOWN ---
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { UserSurfaceEntry } from './scanner/user-surfaces.js';
2
- export type { UserSurfaceEntry };
2
+ import type { AgentId } from './paths.js';
3
+ export type { UserSurfaceEntry, AgentId };
3
4
  export interface SkillInfo {
4
5
  name: string;
5
6
  path: string;
@@ -37,6 +38,8 @@ export type IssueTier = 1 | 2 | 3;
37
38
  export type IssueType = 'broken_symlink' | 'template' | 'skill_dup' | 'duplicate' | 'oversized_memory' | 'oversized_skill' | 'unused_skill' | 'unused_plugin' | 'disabled_plugin' | 'stale_project' | 'temp_cache' | 'backup_artifact';
38
39
  export interface Issue {
39
40
  type: IssueType;
41
+ /** Which agent root this issue lives under. Absent means Claude Code. */
42
+ agent?: AgentId;
40
43
  tier: IssueTier;
41
44
  name: string;
42
45
  detail?: string;
@@ -85,6 +88,8 @@ export interface ScanResult {
85
88
  }
86
89
  export interface ManifestEntry {
87
90
  date: string;
91
+ /** Which agent root `from` belongs to. Absent means Claude Code (pre-2.11 entries). */
92
+ agent?: AgentId;
88
93
  name: string;
89
94
  from: string;
90
95
  type: IssueType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
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": {