ccperm 1.9.7 → 1.10.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 +128 -0
- package/dist/cli.js +7 -1
- package/package.json +1 -1
package/dist/advisor.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyze = analyze;
|
|
4
|
+
const aggregator_js_1 = require("./aggregator.js");
|
|
5
|
+
const RISKY = new Set(['rm', 'sudo', 'chmod', 'chown', 'kill', 'dd', 'ssh', 'scp', 'aws', 'gcloud', 'az', 'kubectl', 'terraform']);
|
|
6
|
+
function extractCmd(label) {
|
|
7
|
+
return label.replace(/__NEW_LINE_[a-f0-9]+__\s*/, '').replace(/[:]\*.*$/, '').replace(/\s\*.*$/, '').split(/[\s(]/)[0];
|
|
8
|
+
}
|
|
9
|
+
function analyze(results) {
|
|
10
|
+
const entries = (0, aggregator_js_1.toFileEntries)(results);
|
|
11
|
+
const withPerms = entries.filter((e) => e.totalCount > 0);
|
|
12
|
+
const hints = [];
|
|
13
|
+
// 1. Find frequently repeated bash commands → suggest global
|
|
14
|
+
const bashCmdProjects = new Map();
|
|
15
|
+
for (const r of results) {
|
|
16
|
+
const dir = (0, aggregator_js_1.projectDir)(r.display);
|
|
17
|
+
for (const g of r.groups) {
|
|
18
|
+
if (g.category !== 'Bash')
|
|
19
|
+
continue;
|
|
20
|
+
for (const item of g.items) {
|
|
21
|
+
const cmd = extractCmd(item.name);
|
|
22
|
+
if (!cmd)
|
|
23
|
+
continue;
|
|
24
|
+
if (!bashCmdProjects.has(cmd))
|
|
25
|
+
bashCmdProjects.set(cmd, new Set());
|
|
26
|
+
bashCmdProjects.get(cmd).add(dir);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const frequent = [...bashCmdProjects.entries()]
|
|
31
|
+
.filter(([, dirs]) => dirs.size >= 5)
|
|
32
|
+
.sort((a, b) => b[1].size - a[1].size)
|
|
33
|
+
.slice(0, 5);
|
|
34
|
+
if (frequent.length > 0) {
|
|
35
|
+
const cmds = frequent.map(([cmd, dirs]) => `${cmd} (${dirs.size} projects)`).join(', ');
|
|
36
|
+
hints.push({
|
|
37
|
+
type: 'consolidate',
|
|
38
|
+
message: `Common commands found across many projects: ${cmds}. Consider adding these to ~/.claude/settings.json to allow globally instead of per-project.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// 2. Find risky permissions
|
|
42
|
+
const riskyFound = [];
|
|
43
|
+
for (const r of results) {
|
|
44
|
+
for (const g of r.groups) {
|
|
45
|
+
if (g.category !== 'Bash')
|
|
46
|
+
continue;
|
|
47
|
+
for (const item of g.items) {
|
|
48
|
+
const cmd = extractCmd(item.name);
|
|
49
|
+
if (RISKY.has(cmd)) {
|
|
50
|
+
const dir = (0, aggregator_js_1.projectDir)(r.display);
|
|
51
|
+
const shortDir = dir.replace(/.*\//, '');
|
|
52
|
+
const file = r.display.includes('settings.local.json') ? 'local' : 'shared';
|
|
53
|
+
riskyFound.push({ cmd, project: shortDir, file });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (riskyFound.length > 0) {
|
|
59
|
+
const items = riskyFound.slice(0, 5).map((r) => `${r.cmd} in ${r.project} (${r.file})`).join(', ');
|
|
60
|
+
hints.push({
|
|
61
|
+
type: 'risk',
|
|
62
|
+
message: `High-risk commands found: ${items}. Review if these are still needed.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// 3. Find heredoc/one-time permissions
|
|
66
|
+
const heredocProjects = new Map();
|
|
67
|
+
for (const r of results) {
|
|
68
|
+
let count = 0;
|
|
69
|
+
for (const g of r.groups) {
|
|
70
|
+
if (g.category !== 'Bash')
|
|
71
|
+
continue;
|
|
72
|
+
for (const item of g.items) {
|
|
73
|
+
if (item.name.includes('__NEW_LINE_') || item.name.includes('<<') || item.name.includes('\\n'))
|
|
74
|
+
count++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (count > 0) {
|
|
78
|
+
const dir = (0, aggregator_js_1.projectDir)(r.display).replace(/.*\//, '');
|
|
79
|
+
heredocProjects.set(dir, (heredocProjects.get(dir) || 0) + count);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (heredocProjects.size > 0) {
|
|
83
|
+
const total = [...heredocProjects.values()].reduce((a, b) => a + b, 0);
|
|
84
|
+
const top = [...heredocProjects.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
85
|
+
const topStr = top.map(([p, c]) => `${p} (${c})`).join(', ');
|
|
86
|
+
hints.push({
|
|
87
|
+
type: 'cleanup',
|
|
88
|
+
message: `${total} one-time/heredoc permissions found (${topStr}). These were likely auto-allowed for single tasks and are safe to remove.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// 4. Global permissions check
|
|
92
|
+
const globalEntries = entries.filter((e) => e.isGlobal);
|
|
93
|
+
const globalPerms = globalEntries.reduce((sum, e) => sum + e.totalCount, 0);
|
|
94
|
+
if (globalPerms === 0 && frequent.length > 0) {
|
|
95
|
+
hints.push({
|
|
96
|
+
type: 'info',
|
|
97
|
+
message: `~/.claude/settings.json has no permissions. Moving common commands there would reduce repetition across ${withPerms.length} projects.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Build output
|
|
101
|
+
const lines = [];
|
|
102
|
+
const dirs = new Set(results.map((r) => (0, aggregator_js_1.projectDir)(r.display)));
|
|
103
|
+
const totalPerms = results.reduce((sum, r) => sum + r.totalCount, 0);
|
|
104
|
+
lines.push(`# ccperm: Permission Audit`);
|
|
105
|
+
lines.push(``);
|
|
106
|
+
lines.push(`Scanned ${results.length} settings files across ${dirs.size} projects. Found ${totalPerms} total permissions.`);
|
|
107
|
+
lines.push(``);
|
|
108
|
+
// Top projects
|
|
109
|
+
const sorted = [...withPerms].sort((a, b) => b.totalCount - a.totalCount).slice(0, 10);
|
|
110
|
+
lines.push(`## Top projects by permission count:`);
|
|
111
|
+
for (const e of sorted) {
|
|
112
|
+
lines.push(`- ${e.shortName} (${e.fileType}): ${e.totalCount} permissions`);
|
|
113
|
+
}
|
|
114
|
+
lines.push(``);
|
|
115
|
+
if (hints.length > 0) {
|
|
116
|
+
lines.push(`## Recommendations:`);
|
|
117
|
+
for (let i = 0; i < hints.length; i++) {
|
|
118
|
+
lines.push(`${i + 1}. ${hints[i].message}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(``);
|
|
121
|
+
}
|
|
122
|
+
lines.push(`## How to act:`);
|
|
123
|
+
lines.push(`- Global settings: ~/.claude/settings.json`);
|
|
124
|
+
lines.push(`- Project shared: <project>/.claude/settings.json (git tracked)`);
|
|
125
|
+
lines.push(`- Project local: <project>/.claude/settings.local.json (gitignored)`);
|
|
126
|
+
lines.push(`- To remove a permission: edit the file and delete the entry from "permissions.allow" array`);
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,8 @@ const updater_js_1 = require("./updater.js");
|
|
|
12
12
|
const aggregator_js_1 = require("./aggregator.js");
|
|
13
13
|
const renderer_js_1 = require("./renderer.js");
|
|
14
14
|
const interactive_js_1 = require("./interactive.js");
|
|
15
|
-
const
|
|
15
|
+
const advisor_js_1 = require("./advisor.js");
|
|
16
|
+
const KNOWN_FLAGS = new Set(['--cwd', '--verbose', '--static', '--update', '--fix', '--agent', '--debug', '--help', '-h', '--version', '-v']);
|
|
16
17
|
const HELP = `${colors_js_1.CYAN}ccperm${colors_js_1.NC} — Audit Claude Code permissions across projects
|
|
17
18
|
|
|
18
19
|
${colors_js_1.YELLOW}Usage:${colors_js_1.NC}
|
|
@@ -24,6 +25,7 @@ ${colors_js_1.YELLOW}Options:${colors_js_1.NC}
|
|
|
24
25
|
--verbose Show all permissions per project (static)
|
|
25
26
|
--static Force static output (default in non-TTY)
|
|
26
27
|
--update Update ccperm to latest version
|
|
28
|
+
--agent Briefing for your AI overlord 🤖
|
|
27
29
|
--help, -h Show this help
|
|
28
30
|
--version, -v Show version`;
|
|
29
31
|
async function main() {
|
|
@@ -90,6 +92,10 @@ async function main() {
|
|
|
90
92
|
const results = files.map(scanner_js_1.scanFile).filter((r) => r !== null);
|
|
91
93
|
const entries = (0, aggregator_js_1.toFileEntries)(results);
|
|
92
94
|
const summary = (0, aggregator_js_1.summarize)(results);
|
|
95
|
+
if (args.includes('--agent')) {
|
|
96
|
+
console.log((0, advisor_js_1.analyze)(results));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
93
99
|
if (!isStatic) {
|
|
94
100
|
await (0, interactive_js_1.startInteractive)(entries, results);
|
|
95
101
|
return;
|