ccperm 1.12.0 → 1.13.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/dist/advisor.js CHANGED
@@ -134,7 +134,21 @@ function analyze(results) {
134
134
  const topStr = top.map(([p, c]) => `${p} (${c})`).join(', ');
135
135
  hints.push(`${heredocTotal} one-time/heredoc permissions found (${topStr}). Safe to remove.`);
136
136
  }
137
- // 3. Global check
137
+ // 3. WebFetch → suggest global wildcard
138
+ let webFetchTotal = 0;
139
+ let webFetchProjects = 0;
140
+ for (const r of results) {
141
+ const wf = r.groups.find((g) => g.category === 'WebFetch');
142
+ if (wf) {
143
+ webFetchTotal += wf.items.length;
144
+ webFetchProjects++;
145
+ }
146
+ }
147
+ const hasGlobalWebFetch = results.some((r) => r.isGlobal && r.groups.some((g) => g.category === 'WebFetch'));
148
+ if (webFetchProjects >= 3 && !hasGlobalWebFetch) {
149
+ hints.push(`WebFetch permissions found in ${webFetchProjects} projects (${webFetchTotal} domains total). WebFetch is read-only — add \`"WebFetch(*)"\` to ~/.claude/settings.json to skip per-domain approval globally.`);
150
+ }
151
+ // 4. Global check
138
152
  const globalEntries = entries.filter((e) => e.isGlobal);
139
153
  const globalPerms = globalEntries.reduce((sum, e) => sum + e.totalCount, 0);
140
154
  if (globalPerms === 0 && frequent.length > 0) {
package/dist/cli.js CHANGED
@@ -95,12 +95,23 @@ async function main() {
95
95
  }
96
96
  console.log(` ${colors_js_1.GREEN}✔${colors_js_1.NC} Found ${colors_js_1.CYAN}${files.length}${colors_js_1.NC} settings files\n`);
97
97
  const results = files.map(scanner_js_1.scanFile).filter((r) => r !== null);
98
- const entries = (0, aggregator_js_1.toFileEntries)(results);
99
- const summary = (0, aggregator_js_1.summarize)(results);
100
98
  if (args.includes('--hey-claude-witness-me')) {
101
99
  console.log((0, advisor_js_1.analyze)(results));
102
100
  return;
103
101
  }
102
+ const entries = (0, aggregator_js_1.toFileEntries)(results);
103
+ const summary = (0, aggregator_js_1.summarize)(results);
104
+ if (args.includes('--fix')) {
105
+ const affected = (0, scanner_js_1.countDeprecated)(results);
106
+ if (affected.length === 0) {
107
+ console.log(` ${colors_js_1.GREEN}✔ Nothing to fix.${colors_js_1.NC}\n`);
108
+ return;
109
+ }
110
+ const { totalPatterns, fixedFiles } = (0, fixer_js_1.fixFiles)(affected);
111
+ console.log(` ${colors_js_1.GREEN}✔ Fixed ${totalPatterns} patterns in ${fixedFiles} files.${colors_js_1.NC}`);
112
+ console.log(` ${colors_js_1.DIM}Restart Claude Code for changes to take effect.${colors_js_1.NC}\n`);
113
+ return;
114
+ }
104
115
  if (!isStatic) {
105
116
  await (0, interactive_js_1.startInteractive)(entries, results);
106
117
  return;
@@ -112,16 +123,6 @@ async function main() {
112
123
  (0, renderer_js_1.printCompact)(entries, summary);
113
124
  }
114
125
  (0, renderer_js_1.printFooter)(summary);
115
- if (args.includes('--fix')) {
116
- const affected = (0, scanner_js_1.countDeprecated)(results);
117
- if (affected.length === 0) {
118
- console.log(`\n ${colors_js_1.GREEN}✔ Nothing to fix.${colors_js_1.NC}\n`);
119
- return;
120
- }
121
- const { totalPatterns, fixedFiles } = (0, fixer_js_1.fixFiles)(affected);
122
- console.log(`\n ${colors_js_1.GREEN}✔ Fixed ${totalPatterns} patterns in ${fixedFiles} files.${colors_js_1.NC}`);
123
- console.log(` ${colors_js_1.DIM}Restart Claude Code for changes to take effect.${colors_js_1.NC}\n`);
124
- }
125
126
  }
126
127
  main();
127
128
  (0, updater_js_1.notifyUpdate)();
package/dist/explain.js CHANGED
@@ -266,11 +266,12 @@ function explainTool(label) {
266
266
  function explain(category, label) {
267
267
  if (category === 'Bash')
268
268
  return explainBash(label);
269
- if (category === 'WebFetch')
270
- return explainWebFetch(label);
271
269
  if (category === 'MCP')
272
270
  return explainMcp(label);
273
- if (category === 'Tools')
271
+ if (category === 'Tools') {
272
+ if (label.startsWith('WebFetch'))
273
+ return explainWebFetch(label);
274
274
  return explainTool(label);
275
+ }
275
276
  return { description: '', risk: 'medium' };
276
277
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startInteractive = startInteractive;
7
7
  const node_readline_1 = __importDefault(require("node:readline"));
8
8
  const colors_js_1 = require("./colors.js");
9
+ const scanner_js_1 = require("./scanner.js");
9
10
  const explain_js_1 = require("./explain.js");
10
11
  function severityTag(s) {
11
12
  const labels = {
@@ -27,8 +28,7 @@ function cleanLabel(label) {
27
28
  // Truncate inline scripts with \n
28
29
  if (s.includes('\\n'))
29
30
  s = s.replace(/\\n.*$/, '…');
30
- // Strip :* and trailing * suffix
31
- s = s.replace(/:\*$/, ' *');
31
+ // Show deprecated :* as-is (don't normalize to space)
32
32
  return s;
33
33
  }
34
34
  // strip ANSI escape codes for visible length
@@ -50,7 +50,7 @@ function boxTop(title, info, width) {
50
50
  function boxBottom(hint, width) {
51
51
  const inner = width - 2;
52
52
  const hintPart = ` ${hint} `;
53
- const fill = Math.max(0, inner - hintPart.length);
53
+ const fill = Math.max(0, inner - visLen(hintPart));
54
54
  return `${colors_js_1.DIM}└${'─'.repeat(fill)}${hintPart}┘${colors_js_1.NC}`;
55
55
  }
56
56
  function boxSep(width) {
@@ -63,6 +63,7 @@ function startInteractive(merged, results) {
63
63
  const withPerms = [...globals, ...projects];
64
64
  const emptyCount = merged.filter((r) => r.totalCount === 0 && !r.isGlobal).length;
65
65
  const riskMap = buildRiskMap(results);
66
+ const depMap = buildDeprecatedMap(results);
66
67
  if (withPerms.length === 0) {
67
68
  console.log(`\n ${colors_js_1.GREEN}No projects with permissions found.${colors_js_1.NC}\n`);
68
69
  resolve();
@@ -86,7 +87,7 @@ function startInteractive(merged, results) {
86
87
  const render = () => {
87
88
  process.stdout.write('\x1b[2J\x1b[H\x1b[?25l');
88
89
  if (state.view === 'list')
89
- renderList(state, withPerms, emptyCount, riskMap);
90
+ renderList(state, withPerms, emptyCount, riskMap, depMap);
90
91
  else
91
92
  renderDetail(state, withPerms, results);
92
93
  };
@@ -113,7 +114,42 @@ function startInteractive(merged, results) {
113
114
  }
114
115
  }
115
116
  else {
116
- if (key.name === 'escape' || key.name === 'backspace') {
117
+ if (state.confirmDelete) {
118
+ if (key.name === 'y') {
119
+ const { rawPerm, filePath } = state.confirmDelete;
120
+ if ((0, scanner_js_1.removePerm)(filePath, rawPerm)) {
121
+ const idx = results.findIndex((r) => r.path === filePath);
122
+ if (idx >= 0) {
123
+ const updated = (0, scanner_js_1.scanFile)(filePath);
124
+ if (updated) {
125
+ results[idx] = updated;
126
+ const entry = withPerms[state.selectedProject];
127
+ entry.totalCount = updated.totalCount;
128
+ entry.groups = new Map();
129
+ for (const g of updated.groups)
130
+ entry.groups.set(g.category, g.items.length);
131
+ }
132
+ }
133
+ state.flash = `${colors_js_1.GREEN}✔ Deleted${colors_js_1.NC}`;
134
+ }
135
+ state.confirmDelete = undefined;
136
+ }
137
+ else {
138
+ state.confirmDelete = undefined;
139
+ }
140
+ }
141
+ else if (state.confirmGlobal) {
142
+ if (key.name === 'y') {
143
+ if ((0, scanner_js_1.addPermToGlobal)(state.confirmGlobal.rawPerm)) {
144
+ state.flash = `${colors_js_1.GREEN}✔ Copied to global${colors_js_1.NC}`;
145
+ }
146
+ else {
147
+ state.flash = `${colors_js_1.DIM}· Already in global${colors_js_1.NC}`;
148
+ }
149
+ }
150
+ state.confirmGlobal = undefined;
151
+ }
152
+ else if (key.name === 'escape' || key.name === 'backspace') {
117
153
  state.view = 'list';
118
154
  state.detailCursor = 0;
119
155
  state.detailScroll = 0;
@@ -130,6 +166,12 @@ function startInteractive(merged, results) {
130
166
  else if (key.name === 'i') {
131
167
  state.showInfo = !state.showInfo;
132
168
  }
169
+ else if (key.name === 'd') {
170
+ state._delete = true;
171
+ }
172
+ else if (key.name === 'g') {
173
+ state._global = true;
174
+ }
133
175
  }
134
176
  render();
135
177
  };
@@ -137,6 +179,19 @@ function startInteractive(merged, results) {
137
179
  render();
138
180
  });
139
181
  }
182
+ function buildDeprecatedMap(results) {
183
+ const map = new Map();
184
+ for (const r of results) {
185
+ let count = 0;
186
+ for (const p of r.permissions) {
187
+ if (p.includes(':*'))
188
+ count++;
189
+ }
190
+ if (count > 0)
191
+ map.set(r.display, count);
192
+ }
193
+ return map;
194
+ }
140
195
  function buildRiskMap(results) {
141
196
  const map = new Map();
142
197
  for (const r of results) {
@@ -154,20 +209,22 @@ function buildRiskMap(results) {
154
209
  }
155
210
  return map;
156
211
  }
157
- function renderList(state, withPerms, emptyCount, riskMap) {
212
+ function renderList(state, withPerms, emptyCount, riskMap, depMap) {
158
213
  const rows = process.stdout.rows || 24;
159
214
  const cols = process.stdout.columns || 80;
160
- const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
215
+ const cats = ['Bash', 'MCP', 'Tools'];
161
216
  const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
162
217
  const hasRisk = [...riskMap.values()].some((v) => v.critical > 0 || v.high > 0);
218
+ const hasDep = depMap.size > 0;
163
219
  const riskColWidth = hasRisk ? 3 : 0;
220
+ const depColWidth = hasDep ? 3 : 0;
164
221
  const catColWidth = catsPresent.length * 7;
165
222
  const typeColWidth = 7;
166
223
  const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
167
224
  const nameColWidth = Math.min(maxName + typeColWidth, 35);
168
225
  const nameWidth = nameColWidth - typeColWidth;
169
- // content: marker(2) + nameCol + gap(2) + catCols + gap(2) + total(5) + gap(1) + riskCol
170
- const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ? 1 + riskColWidth : 0);
226
+ // content: marker(2) + nameCol + gap(2) + catCols + gap(2) + total(5) + riskCol(3) + depCol(3)
227
+ const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ? riskColWidth : 0) + (hasDep ? depColWidth : 0);
171
228
  const w = Math.min(cols, contentWidth + 4);
172
229
  const inner = w - 4;
173
230
  const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
@@ -181,8 +238,9 @@ function renderList(state, withPerms, emptyCount, riskMap) {
181
238
  const scrollInfo = withPerms.length > visibleRows ? `${state.cursor + 1}/${withPerms.length}` : '';
182
239
  const lines = [];
183
240
  lines.push(boxTop('ccperm', scrollInfo, w));
184
- const riskHeader = hasRisk ? ` ${colors_js_1.DIM}⚠${colors_js_1.NC}` : '';
185
- lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameColWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}${riskHeader}`, w));
241
+ const riskHeader = hasRisk ? ` ${rpad('!', 2)}` : '';
242
+ const depHeader = hasDep ? ` ${rpad('', 2)}` : '';
243
+ lines.push(boxLine(`${colors_js_1.DIM} ${pad('PROJECT', nameColWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} ${rpad('TOTAL', 5)}${riskHeader}${depHeader}${colors_js_1.NC}`, w));
186
244
  lines.push(boxSep(w));
187
245
  const globalCount = withPerms.filter((r) => r.isGlobal).length;
188
246
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
@@ -211,7 +269,15 @@ function renderList(state, withPerms, emptyCount, riskMap) {
211
269
  else
212
270
  riskCol = ` ${colors_js_1.DIM}${rpad('·', 2)}${colors_js_1.NC}`;
213
271
  }
214
- lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}${riskCol}`, w));
272
+ let depCol = '';
273
+ if (hasDep) {
274
+ const dep = depMap.get(r.display) || 0;
275
+ if (dep > 0)
276
+ depCol = ` ${colors_js_1.DIM}${rpad(dep, 2)}${colors_js_1.NC}`;
277
+ else
278
+ depCol = ` ${colors_js_1.DIM}${rpad('·', 2)}${colors_js_1.NC}`;
279
+ }
280
+ lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}${riskCol}${depCol}`, w));
215
281
  // separator after global section
216
282
  if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
217
283
  lines.push(boxSep(w));
@@ -243,6 +309,8 @@ function renderDetail(state, withPerms, results) {
243
309
  if (isOpen) {
244
310
  for (const item of group.items) {
245
311
  const clean = cleanLabel(item.name);
312
+ // Find the raw permission string from the original permissions array
313
+ const rawPerm = fileResult.permissions.find((p) => p.includes(item.name)) || '';
246
314
  if (state.showInfo) {
247
315
  const info = (0, explain_js_1.explain)(group.category, item.name);
248
316
  const tag = severityTag(info.risk);
@@ -250,12 +318,12 @@ function renderDetail(state, withPerms, results) {
250
318
  const nameMax = Math.min(30, w - tagLen - 14);
251
319
  const name = clean.length > nameMax ? clean.slice(0, nameMax - 1) + '…' : clean;
252
320
  const desc = info.description ? `${colors_js_1.DIM}${info.description}${colors_js_1.NC}` : '';
253
- navRows.push({ text: ` ${pad(name, nameMax)} ${tag} ${desc}`, perm: item.name });
321
+ navRows.push({ text: ` ${pad(name, nameMax)} ${tag} ${desc}`, perm: item.name, rawPerm });
254
322
  }
255
323
  else {
256
324
  const maxLen = w - 8;
257
325
  const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
258
- navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name });
326
+ navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name, rawPerm });
259
327
  }
260
328
  }
261
329
  }
@@ -273,6 +341,22 @@ function renderDetail(state, withPerms, results) {
273
341
  return;
274
342
  }
275
343
  }
344
+ // handle delete
345
+ if (state._delete) {
346
+ delete state._delete;
347
+ const row = navRows[state.detailCursor];
348
+ if (row?.rawPerm) {
349
+ state.confirmDelete = { perm: row.perm, rawPerm: row.rawPerm, filePath: fileResult.path };
350
+ }
351
+ }
352
+ // handle global copy
353
+ if (state._global) {
354
+ delete state._global;
355
+ const row = navRows[state.detailCursor];
356
+ if (row?.rawPerm && !project.isGlobal) {
357
+ state.confirmGlobal = { perm: row.perm, rawPerm: row.rawPerm };
358
+ }
359
+ }
276
360
  if (state.detailCursor >= navRows.length)
277
361
  state.detailCursor = Math.max(0, navRows.length - 1);
278
362
  // box chrome: top(1) + sep(1) + bottom(1) = 3
@@ -293,8 +377,25 @@ function renderDetail(state, withPerms, results) {
293
377
  const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
294
378
  lines.push(boxLine(`${prefix}${row.text}`, w));
295
379
  }
296
- const infoHint = state.showInfo ? '[i] hide info' : '[i] info';
297
- lines.push(boxBottom(`[↑↓] navigate [Enter] expand ${infoHint} [Esc] back [q] quit`, w));
380
+ if (state.flash) {
381
+ lines.push(boxBottom(state.flash, w));
382
+ state.flash = undefined;
383
+ }
384
+ else if (state.confirmDelete) {
385
+ const name = cleanLabel(state.confirmDelete.perm);
386
+ const truncName = name.length > 30 ? name.slice(0, 29) + '…' : name;
387
+ lines.push(boxBottom(`${colors_js_1.RED}Delete "${truncName}"? [y/N]${colors_js_1.NC}`, w));
388
+ }
389
+ else if (state.confirmGlobal) {
390
+ const name = cleanLabel(state.confirmGlobal.perm);
391
+ const truncName = name.length > 30 ? name.slice(0, 29) + '…' : name;
392
+ lines.push(boxBottom(`${colors_js_1.CYAN}Copy "${truncName}" to global? [y/N]${colors_js_1.NC}`, w));
393
+ }
394
+ else {
395
+ const infoHint = state.showInfo ? '[i] hide info' : '[i] info';
396
+ const globalHint = project.isGlobal ? '' : ' [g] global';
397
+ lines.push(boxBottom(`[↑↓] navigate [Enter] expand ${infoHint} [d] delete${globalHint} [Esc] back [q] quit`, w));
398
+ }
298
399
  process.stdout.write(lines.join('\n') + '\n');
299
400
  }
300
401
  function pad(s, n) {
package/dist/renderer.js CHANGED
@@ -12,7 +12,7 @@ function rpad(s, n) {
12
12
  return str.length >= n ? str : ' '.repeat(n - str.length) + str;
13
13
  }
14
14
  function printCompact(entries, summary) {
15
- const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
15
+ const cats = ['Bash', 'MCP', 'Tools'];
16
16
  const catsPresent = cats.filter((c) => entries.some((r) => r.groups.has(c)));
17
17
  const globals = entries.filter((r) => r.isGlobal);
18
18
  const projects = entries.filter((r) => r.totalCount > 0 && !r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
package/dist/scanner.js CHANGED
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.removePerm = removePerm;
7
+ exports.addPermToGlobal = addPermToGlobal;
6
8
  exports.countDeprecated = countDeprecated;
7
9
  exports.findSettingsFiles = findSettingsFiles;
8
10
  exports.scanFile = scanFile;
@@ -11,6 +13,72 @@ const node_path_1 = __importDefault(require("node:path"));
11
13
  const node_os_1 = __importDefault(require("node:os"));
12
14
  const PERM_RE = /"(Bash|Write|Edit|Read|Glob|Grep|WebSearch|WebFetch|mcp_)[^"]*"/g;
13
15
  const DEPRECATED_RE = /:\*\)|:\*"/g;
16
+ const AUDIT_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.ccperm', 'audit');
17
+ function writeAudit(action, filePath, perm, before, after) {
18
+ try {
19
+ node_fs_1.default.mkdirSync(AUDIT_DIR, { recursive: true });
20
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
21
+ const entry = { action, file: filePath, perm, before, after, timestamp: new Date().toISOString() };
22
+ node_fs_1.default.writeFileSync(node_path_1.default.join(AUDIT_DIR, `${ts}_${action}.json`), JSON.stringify(entry, null, 2) + '\n');
23
+ }
24
+ catch { /* audit is best-effort */ }
25
+ }
26
+ function removePerm(filePath, rawPerm) {
27
+ let content;
28
+ try {
29
+ content = node_fs_1.default.readFileSync(filePath, 'utf8');
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ let json;
35
+ try {
36
+ json = JSON.parse(content);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ const allow = json?.permissions?.allow;
42
+ if (!Array.isArray(allow))
43
+ return false;
44
+ const idx = allow.indexOf(rawPerm);
45
+ if (idx === -1)
46
+ return false;
47
+ const before = [...allow];
48
+ allow.splice(idx, 1);
49
+ node_fs_1.default.writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n', 'utf8');
50
+ writeAudit('DELETE', filePath, rawPerm, before, allow);
51
+ return true;
52
+ }
53
+ function addPermToGlobal(rawPerm) {
54
+ const globalPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'settings.json');
55
+ let content;
56
+ try {
57
+ content = node_fs_1.default.readFileSync(globalPath, 'utf8');
58
+ }
59
+ catch {
60
+ content = '{}';
61
+ }
62
+ let json;
63
+ try {
64
+ json = JSON.parse(content);
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ if (!json.permissions)
70
+ json.permissions = {};
71
+ if (!Array.isArray(json.permissions.allow))
72
+ json.permissions.allow = [];
73
+ const allow = json.permissions.allow;
74
+ if (allow.includes(rawPerm))
75
+ return false; // already exists
76
+ const before = [...allow];
77
+ allow.push(rawPerm);
78
+ node_fs_1.default.writeFileSync(globalPath, JSON.stringify(json, null, 2) + '\n', 'utf8');
79
+ writeAudit('COPY_TO_GLOBAL', globalPath, rawPerm, before, allow);
80
+ return true;
81
+ }
14
82
  function countDeprecated(results) {
15
83
  const out = [];
16
84
  for (const r of results) {
@@ -111,14 +179,10 @@ function categorize(perm) {
111
179
  const m = perm.match(/^Bash\((.+)\)$/) || perm.match(/^Bash\((.+)/);
112
180
  return { category: 'Bash', label: m ? m[1] : perm };
113
181
  }
114
- if (perm.startsWith('WebFetch')) {
115
- const m = perm.match(/^WebFetch\(domain:(.+)\)$/);
116
- return { category: 'WebFetch', label: m ? m[1] : perm };
117
- }
118
182
  if (perm.startsWith('mcp_') || perm.startsWith('mcp__')) {
119
183
  return { category: 'MCP', label: perm };
120
184
  }
121
- if (/^(Read|Write|Edit|Glob|Grep|WebSearch)/.test(perm)) {
185
+ if (/^(Read|Write|Edit|Glob|Grep|WebSearch|WebFetch)/.test(perm)) {
122
186
  return { category: 'Tools', label: perm };
123
187
  }
124
188
  return { category: 'Other', label: perm };
@@ -131,6 +195,6 @@ function groupPermissions(perms) {
131
195
  map.set(category, []);
132
196
  map.get(category).push({ name: label });
133
197
  }
134
- const order = ['Bash', 'WebFetch', 'MCP', 'Tools', 'Other'];
198
+ const order = ['Bash', 'MCP', 'Tools', 'Other'];
135
199
  return order.filter((c) => map.has(c)).map((c) => ({ category: c, items: map.get(c) }));
136
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"