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 +15 -1
- package/dist/cli.js +13 -12
- package/dist/explain.js +4 -3
- package/dist/interactive.js +117 -16
- package/dist/renderer.js +1 -1
- package/dist/scanner.js +70 -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
|
@@ -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
|
-
//
|
|
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
|
|
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 (
|
|
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', '
|
|
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) +
|
|
170
|
-
const contentWidth = 2 + nameColWidth + 2 + catColWidth + 2 + 5 + (hasRisk ?
|
|
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 ? ` ${
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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', '
|
|
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', '
|
|
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
|
}
|