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 +15 -1
- package/dist/cli.js +13 -12
- package/dist/explain.js +4 -3
- package/dist/interactive.js +67 -12
- package/dist/renderer.js +1 -1
- package/dist/scanner.js +2 -6
- package/package.json +1 -1
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.
|
|
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
|
}
|
package/dist/interactive.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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', '
|
|
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;
|
|
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);
|
|
182
|
+
const nameColWidth = Math.min(maxName + typeColWidth, 35);
|
|
148
183
|
const nameWidth = nameColWidth - typeColWidth;
|
|
149
|
-
// content: marker(2) + nameCol
|
|
150
|
-
const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5;
|
|
151
|
-
const w = Math.min(cols, contentWidth + 4);
|
|
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
|
-
|
|
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
|
-
|
|
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', '
|
|
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', '
|
|
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
|
}
|