ccperm 1.11.3 → 1.12.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/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
  }
@@ -27,8 +27,7 @@ function cleanLabel(label) {
27
27
  // Truncate inline scripts with \n
28
28
  if (s.includes('\\n'))
29
29
  s = s.replace(/\\n.*$/, '…');
30
- // Strip :* and trailing * suffix
31
- s = s.replace(/:\*$/, ' *');
30
+ // Show deprecated :* as-is (don't normalize to space)
32
31
  return s;
33
32
  }
34
33
  // strip ANSI escape codes for visible length
@@ -62,6 +61,8 @@ function startInteractive(merged, results) {
62
61
  const projects = merged.filter((r) => r.totalCount > 0 && !r.isGlobal).sort((a, b) => b.totalCount - a.totalCount);
63
62
  const withPerms = [...globals, ...projects];
64
63
  const emptyCount = merged.filter((r) => r.totalCount === 0 && !r.isGlobal).length;
64
+ const riskMap = buildRiskMap(results);
65
+ const depMap = buildDeprecatedMap(results);
65
66
  if (withPerms.length === 0) {
66
67
  console.log(`\n ${colors_js_1.GREEN}No projects with permissions found.${colors_js_1.NC}\n`);
67
68
  resolve();
@@ -85,7 +86,7 @@ function startInteractive(merged, results) {
85
86
  const render = () => {
86
87
  process.stdout.write('\x1b[2J\x1b[H\x1b[?25l');
87
88
  if (state.view === 'list')
88
- renderList(state, withPerms, emptyCount);
89
+ renderList(state, withPerms, emptyCount, riskMap, depMap);
89
90
  else
90
91
  renderDetail(state, withPerms, results);
91
92
  };
@@ -136,19 +137,53 @@ function startInteractive(merged, results) {
136
137
  render();
137
138
  });
138
139
  }
139
- function renderList(state, withPerms, emptyCount) {
140
+ function buildDeprecatedMap(results) {
141
+ const map = new Map();
142
+ for (const r of results) {
143
+ let count = 0;
144
+ for (const p of r.permissions) {
145
+ if (p.includes(':*'))
146
+ count++;
147
+ }
148
+ if (count > 0)
149
+ map.set(r.display, count);
150
+ }
151
+ return map;
152
+ }
153
+ function buildRiskMap(results) {
154
+ const map = new Map();
155
+ for (const r of results) {
156
+ let crit = 0, hi = 0;
157
+ for (const g of r.groups) {
158
+ for (const item of g.items) {
159
+ const info = (0, explain_js_1.explain)(g.category, item.name);
160
+ if (info.risk === 'critical')
161
+ crit++;
162
+ else if (info.risk === 'high')
163
+ hi++;
164
+ }
165
+ }
166
+ map.set(r.display, { critical: crit, high: hi });
167
+ }
168
+ return map;
169
+ }
170
+ function renderList(state, withPerms, emptyCount, riskMap, depMap) {
140
171
  const rows = process.stdout.rows || 24;
141
172
  const cols = process.stdout.columns || 80;
142
- const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
173
+ const cats = ['Bash', 'MCP', 'Tools'];
143
174
  const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
175
+ const hasRisk = [...riskMap.values()].some((v) => v.critical > 0 || v.high > 0);
176
+ const hasDep = depMap.size > 0;
177
+ const riskColWidth = hasRisk ? 3 : 0;
178
+ const depColWidth = hasDep ? 3 : 0;
144
179
  const catColWidth = catsPresent.length * 7;
145
- const typeColWidth = 7; // " local " or " shared"
180
+ const typeColWidth = 7;
146
181
  const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
147
- const nameColWidth = Math.min(maxName + typeColWidth, 35); // name + type combined
182
+ const nameColWidth = Math.min(maxName + typeColWidth, 35);
148
183
  const nameWidth = nameColWidth - typeColWidth;
149
- // content: marker(2) + nameCol(nameColWidth) + gap(2) + catCols + gap(2) + total(5)
150
- const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5;
151
- const w = Math.min(cols, contentWidth + 4); // +4 for box borders + padding
184
+ // content: marker(2) + nameCol + gap(2) + catCols + gap(2) + total(5) + riskCol(3) + depCol(3)
185
+ const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ? riskColWidth : 0) + (hasDep ? depColWidth : 0);
186
+ const w = Math.min(cols, contentWidth + 4);
152
187
  const inner = w - 4;
153
188
  const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
154
189
  // box takes: top(1) + header(2) + sep(1) + content + globalSep?(1) + emptyLine?(1) + bottom(1)
@@ -161,7 +196,9 @@ function renderList(state, withPerms, emptyCount) {
161
196
  const scrollInfo = withPerms.length > visibleRows ? `${state.cursor + 1}/${withPerms.length}` : '';
162
197
  const lines = [];
163
198
  lines.push(boxTop('ccperm', scrollInfo, w));
164
- lines.push(boxLine(`${colors_js_1.DIM}${pad('PROJECT', nameColWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`, w));
199
+ const riskHeader = hasRisk ? ` ${rpad('!', 2)}` : '';
200
+ const depHeader = hasDep ? ` ${rpad('†', 2)}` : '';
201
+ 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));
165
202
  lines.push(boxSep(w));
166
203
  const globalCount = withPerms.filter((r) => r.isGlobal).length;
167
204
  const end = Math.min(state.scrollOffset + visibleRows, withPerms.length);
@@ -180,7 +217,25 @@ function renderList(state, withPerms, emptyCount) {
180
217
  return `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
181
218
  }).join(' ');
182
219
  const totalCol = isCursor ? `${colors_js_1.BOLD}${rpad(r.totalCount, 5)}${colors_js_1.NC}` : rpad(r.totalCount, 5);
183
- lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}`, w));
220
+ let riskCol = '';
221
+ if (hasRisk) {
222
+ const risk = riskMap.get(r.display) || { critical: 0, high: 0 };
223
+ if (risk.critical > 0)
224
+ riskCol = ` ${colors_js_1.RED}${rpad(risk.critical, 2)}${colors_js_1.NC}`;
225
+ else if (risk.high > 0)
226
+ riskCol = ` ${colors_js_1.YELLOW}${rpad(risk.high, 2)}${colors_js_1.NC}`;
227
+ else
228
+ riskCol = ` ${colors_js_1.DIM}${rpad('·', 2)}${colors_js_1.NC}`;
229
+ }
230
+ let depCol = '';
231
+ if (hasDep) {
232
+ const dep = depMap.get(r.display) || 0;
233
+ if (dep > 0)
234
+ depCol = ` ${colors_js_1.DIM}${rpad(dep, 2)}${colors_js_1.NC}`;
235
+ else
236
+ depCol = ` ${colors_js_1.DIM}${rpad('·', 2)}${colors_js_1.NC}`;
237
+ }
238
+ lines.push(boxLine(`${nameCol} ${catCols} ${totalCol}${riskCol}${depCol}`, w));
184
239
  // separator after global section
185
240
  if (r.isGlobal && i + 1 < withPerms.length && !withPerms[i + 1].isGlobal) {
186
241
  lines.push(boxSep(w));
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
@@ -111,14 +111,10 @@ function categorize(perm) {
111
111
  const m = perm.match(/^Bash\((.+)\)$/) || perm.match(/^Bash\((.+)/);
112
112
  return { category: 'Bash', label: m ? m[1] : perm };
113
113
  }
114
- if (perm.startsWith('WebFetch')) {
115
- const m = perm.match(/^WebFetch\(domain:(.+)\)$/);
116
- return { category: 'WebFetch', label: m ? m[1] : perm };
117
- }
118
114
  if (perm.startsWith('mcp_') || perm.startsWith('mcp__')) {
119
115
  return { category: 'MCP', label: perm };
120
116
  }
121
- if (/^(Read|Write|Edit|Glob|Grep|WebSearch)/.test(perm)) {
117
+ if (/^(Read|Write|Edit|Glob|Grep|WebSearch|WebFetch)/.test(perm)) {
122
118
  return { category: 'Tools', label: perm };
123
119
  }
124
120
  return { category: 'Other', label: perm };
@@ -131,6 +127,6 @@ function groupPermissions(perms) {
131
127
  map.set(category, []);
132
128
  map.get(category).push({ name: label });
133
129
  }
134
- const order = ['Bash', 'WebFetch', 'MCP', 'Tools', 'Other'];
130
+ const order = ['Bash', 'MCP', 'Tools', 'Other'];
135
131
  return order.filter((c) => map.has(c)).map((c) => ({ category: c, items: map.get(c) }));
136
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.11.3",
3
+ "version": "1.12.1",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"