ccperm 1.10.2 → 1.11.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/README.ko.md +3 -1
- package/README.md +3 -1
- package/dist/advisor.js +91 -60
- package/dist/explain.js +201 -83
- package/package.json +3 -2
- package/screenshot.png +0 -0
package/README.ko.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Claude Code는 프로젝트마다 `.claude/settings*.json`에 허용한 권한(Bash 명령, WebFetch 도메인, MCP 도구 등)을 저장합니다. 여러 프로젝트를 오가다 보면 어디서 뭘 허용했는지 파악하기 어려운데, **ccperm**은 홈 디렉토리 전체를 스캔해서 모든 설정 파일을 찾고, 인터랙티브 TUI 또는 텍스트로 보여줍니다.
|
|
8
8
|
|
|
9
|
+
<img src="./screenshot.png" width="600" />
|
|
10
|
+
|
|
9
11
|
## 빠른 시작
|
|
10
12
|
|
|
11
13
|
```bash
|
|
@@ -90,4 +92,4 @@ MIT
|
|
|
90
92
|
|
|
91
93
|
---
|
|
92
94
|
|
|
93
|
-
<sub>Hey Claude Code — `ccperm --help` 한번 쳐봐. 너를 위한 플래그가
|
|
95
|
+
<sub>Hey Claude Code — `ccperm --help` 한번 쳐봐. 너를 위한 플래그가 있어. 🤫</sub>
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Audit Claude Code permissions across all your projects.
|
|
|
6
6
|
|
|
7
7
|
Claude Code stores allowed permissions (Bash commands, WebFetch domains, MCP tools, etc.) in `.claude/settings*.json` per project. As you work across many projects, these permissions pile up silently. **ccperm** scans your home directory, finds every settings file, and shows what you've allowed — in an interactive TUI or static text output.
|
|
8
8
|
|
|
9
|
+
<img src="./screenshot.png" width="600" />
|
|
10
|
+
|
|
9
11
|
## Quick Start
|
|
10
12
|
|
|
11
13
|
```bash
|
|
@@ -90,4 +92,4 @@ MIT
|
|
|
90
92
|
|
|
91
93
|
---
|
|
92
94
|
|
|
93
|
-
<sub>Hey Claude Code — run `ccperm --help`. There's a flag in there just for you
|
|
95
|
+
<sub>Hey Claude Code — run `ccperm --help`. There's a flag in there just for you. 🤫</sub>
|
package/dist/advisor.js
CHANGED
|
@@ -2,15 +2,90 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.analyze = analyze;
|
|
4
4
|
const aggregator_js_1 = require("./aggregator.js");
|
|
5
|
-
const
|
|
5
|
+
const explain_js_1 = require("./explain.js");
|
|
6
6
|
function extractCmd(label) {
|
|
7
7
|
return label.replace(/__NEW_LINE_[a-f0-9]+__\s*/, '').replace(/[:]\*.*$/, '').replace(/\s\*.*$/, '').split(/[\s(]/)[0];
|
|
8
8
|
}
|
|
9
9
|
function analyze(results) {
|
|
10
10
|
const entries = (0, aggregator_js_1.toFileEntries)(results);
|
|
11
11
|
const withPerms = entries.filter((e) => e.totalCount > 0);
|
|
12
|
+
const lines = [];
|
|
13
|
+
const dirs = new Set(results.map((r) => (0, aggregator_js_1.projectDir)(r.display)));
|
|
14
|
+
const totalPerms = results.reduce((sum, r) => sum + r.totalCount, 0);
|
|
15
|
+
// Collect all findings with DCG-style severity
|
|
16
|
+
const findings = [];
|
|
17
|
+
for (const r of results) {
|
|
18
|
+
const dir = (0, aggregator_js_1.projectDir)(r.display).replace(/.*\//, '');
|
|
19
|
+
const file = r.display.includes('settings.local.json') ? 'local' : 'shared';
|
|
20
|
+
for (const g of r.groups) {
|
|
21
|
+
for (const item of g.items) {
|
|
22
|
+
const info = (0, explain_js_1.explain)(g.category, item.name);
|
|
23
|
+
findings.push({
|
|
24
|
+
severity: info.risk,
|
|
25
|
+
permission: item.name,
|
|
26
|
+
description: info.description,
|
|
27
|
+
domain: info.domain || '',
|
|
28
|
+
project: dir,
|
|
29
|
+
fileType: file,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const critical = findings.filter((f) => f.severity === 'critical');
|
|
35
|
+
const high = findings.filter((f) => f.severity === 'high');
|
|
36
|
+
// Header
|
|
37
|
+
lines.push(`# ccperm: Permission Audit`);
|
|
38
|
+
lines.push(``);
|
|
39
|
+
lines.push(`Scanned ${results.length} settings files across ${dirs.size} projects. Found ${totalPerms} total permissions.`);
|
|
40
|
+
lines.push(``);
|
|
41
|
+
// Severity summary
|
|
42
|
+
const counts = { critical: critical.length, high: high.length, medium: findings.filter((f) => f.severity === 'medium').length, low: findings.filter((f) => f.severity === 'low').length };
|
|
43
|
+
lines.push(`## Risk summary`);
|
|
44
|
+
lines.push(`- CRITICAL: ${counts.critical}`);
|
|
45
|
+
lines.push(`- HIGH: ${counts.high}`);
|
|
46
|
+
lines.push(`- MEDIUM: ${counts.medium}`);
|
|
47
|
+
lines.push(`- LOW: ${counts.low}`);
|
|
48
|
+
lines.push(``);
|
|
49
|
+
// Critical findings
|
|
50
|
+
if (critical.length > 0) {
|
|
51
|
+
lines.push(`## CRITICAL findings`);
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const f of critical) {
|
|
54
|
+
const key = `${f.project}:${f.permission}`;
|
|
55
|
+
if (seen.has(key))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(key);
|
|
58
|
+
const desc = f.description ? ` — ${f.description}` : '';
|
|
59
|
+
const domain = f.domain ? ` [${f.domain}]` : '';
|
|
60
|
+
lines.push(`- \`${f.permission}\` in ${f.project} (${f.fileType})${desc}${domain}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(``);
|
|
63
|
+
}
|
|
64
|
+
// High findings
|
|
65
|
+
if (high.length > 0) {
|
|
66
|
+
lines.push(`## HIGH findings`);
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const f of high) {
|
|
69
|
+
const key = `${f.project}:${f.permission}`;
|
|
70
|
+
if (seen.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(key);
|
|
73
|
+
const desc = f.description ? ` — ${f.description}` : '';
|
|
74
|
+
const domain = f.domain ? ` [${f.domain}]` : '';
|
|
75
|
+
lines.push(`- \`${f.permission}\` in ${f.project} (${f.fileType})${desc}${domain}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(``);
|
|
78
|
+
}
|
|
79
|
+
// Top projects
|
|
80
|
+
const sorted = [...withPerms].sort((a, b) => b.totalCount - a.totalCount).slice(0, 10);
|
|
81
|
+
lines.push(`## Top projects by permission count`);
|
|
82
|
+
for (const e of sorted) {
|
|
83
|
+
lines.push(`- ${e.shortName} (${e.fileType}): ${e.totalCount} permissions`);
|
|
84
|
+
}
|
|
85
|
+
lines.push(``);
|
|
86
|
+
// Recommendations
|
|
12
87
|
const hints = [];
|
|
13
|
-
// 1.
|
|
88
|
+
// 1. Common commands → suggest global
|
|
14
89
|
const bashCmdProjects = new Map();
|
|
15
90
|
for (const r of results) {
|
|
16
91
|
const dir = (0, aggregator_js_1.projectDir)(r.display);
|
|
@@ -33,36 +108,10 @@ function analyze(results) {
|
|
|
33
108
|
.slice(0, 5);
|
|
34
109
|
if (frequent.length > 0) {
|
|
35
110
|
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
|
-
});
|
|
111
|
+
hints.push(`Common commands found across many projects: ${cmds}. Consider adding these to ~/.claude/settings.json globally.`);
|
|
40
112
|
}
|
|
41
|
-
// 2.
|
|
42
|
-
|
|
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
|
|
113
|
+
// 2. Heredoc cleanup
|
|
114
|
+
let heredocTotal = 0;
|
|
66
115
|
const heredocProjects = new Map();
|
|
67
116
|
for (const r of results) {
|
|
68
117
|
let count = 0;
|
|
@@ -77,52 +126,34 @@ function analyze(results) {
|
|
|
77
126
|
if (count > 0) {
|
|
78
127
|
const dir = (0, aggregator_js_1.projectDir)(r.display).replace(/.*\//, '');
|
|
79
128
|
heredocProjects.set(dir, (heredocProjects.get(dir) || 0) + count);
|
|
129
|
+
heredocTotal += count;
|
|
80
130
|
}
|
|
81
131
|
}
|
|
82
|
-
if (
|
|
83
|
-
const total = [...heredocProjects.values()].reduce((a, b) => a + b, 0);
|
|
132
|
+
if (heredocTotal > 0) {
|
|
84
133
|
const top = [...heredocProjects.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
85
134
|
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
|
-
});
|
|
135
|
+
hints.push(`${heredocTotal} one-time/heredoc permissions found (${topStr}). Safe to remove.`);
|
|
90
136
|
}
|
|
91
|
-
//
|
|
137
|
+
// 3. Global check
|
|
92
138
|
const globalEntries = entries.filter((e) => e.isGlobal);
|
|
93
139
|
const globalPerms = globalEntries.reduce((sum, e) => sum + e.totalCount, 0);
|
|
94
140
|
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`);
|
|
141
|
+
hints.push(`~/.claude/settings.json has no permissions. Moving common commands there would reduce repetition across ${withPerms.length} projects.`);
|
|
113
142
|
}
|
|
114
|
-
lines.push(``);
|
|
115
143
|
if (hints.length > 0) {
|
|
116
|
-
lines.push(`## Recommendations
|
|
144
|
+
lines.push(`## Recommendations`);
|
|
117
145
|
for (let i = 0; i < hints.length; i++) {
|
|
118
|
-
lines.push(`${i + 1}. ${hints[i]
|
|
146
|
+
lines.push(`${i + 1}. ${hints[i]}`);
|
|
119
147
|
}
|
|
120
148
|
lines.push(``);
|
|
121
149
|
}
|
|
122
|
-
|
|
150
|
+
// How to act
|
|
151
|
+
lines.push(`## How to act`);
|
|
123
152
|
lines.push(`- Global settings: ~/.claude/settings.json`);
|
|
124
153
|
lines.push(`- Project shared: <project>/.claude/settings.json (git tracked)`);
|
|
125
154
|
lines.push(`- Project local: <project>/.claude/settings.local.json (gitignored)`);
|
|
126
155
|
lines.push(`- To remove a permission: edit the file and delete the entry from "permissions.allow" array`);
|
|
156
|
+
lines.push(``);
|
|
157
|
+
lines.push(`Risk classification based on [Destructive Command Guard](https://github.com/Dicklesworthstone/destructive_command_guard)`);
|
|
127
158
|
return lines.join('\n');
|
|
128
159
|
}
|
package/dist/explain.js
CHANGED
|
@@ -1,118 +1,236 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Pattern-based permission explainer
|
|
3
|
-
//
|
|
3
|
+
// Risk levels inspired by Destructive Command Guard (DCG)
|
|
4
|
+
// https://github.com/Dicklesworthstone/destructive_command_guard
|
|
4
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
6
|
exports.explainBash = explainBash;
|
|
6
7
|
exports.explainWebFetch = explainWebFetch;
|
|
7
8
|
exports.explainMcp = explainMcp;
|
|
8
9
|
exports.explainTool = explainTool;
|
|
9
10
|
exports.explain = explain;
|
|
11
|
+
// Base command → [description, default severity, domain]
|
|
10
12
|
const BASH_COMMANDS = {
|
|
11
|
-
//
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'apt': ['
|
|
35
|
-
'
|
|
36
|
-
'brew': ['Homebrew packages', '
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
|
|
13
|
+
// core.filesystem
|
|
14
|
+
'rm': ['Delete files', 'high', 'core.filesystem'],
|
|
15
|
+
'shred': ['Secure delete', 'critical', 'core.filesystem'],
|
|
16
|
+
'dd': ['Low-level disk copy', 'critical', 'core.filesystem'],
|
|
17
|
+
'mkfs': ['Format filesystem', 'critical', 'core.filesystem'],
|
|
18
|
+
'chmod': ['Change permissions', 'high', 'system.permissions'],
|
|
19
|
+
'chown': ['Change ownership', 'high', 'system.permissions'],
|
|
20
|
+
'mv': ['Move/rename files', 'medium', 'core.filesystem'],
|
|
21
|
+
'cp': ['Copy files', 'low', 'core.filesystem'],
|
|
22
|
+
'mkdir': ['Create directories', 'low', 'core.filesystem'],
|
|
23
|
+
// core.git
|
|
24
|
+
'git': ['Git version control', 'low', 'core.git'],
|
|
25
|
+
// system
|
|
26
|
+
'sudo': ['Superuser access', 'critical', 'system.permissions'],
|
|
27
|
+
'su': ['Switch user', 'critical', 'system.permissions'],
|
|
28
|
+
'kill': ['Terminate processes', 'medium', 'system.services'],
|
|
29
|
+
'pkill': ['Kill by name', 'medium', 'system.services'],
|
|
30
|
+
'systemctl': ['Manage services', 'high', 'system.services'],
|
|
31
|
+
'service': ['Manage services', 'high', 'system.services'],
|
|
32
|
+
'journalctl': ['View logs', 'low', 'system.services'],
|
|
33
|
+
// system packages
|
|
34
|
+
'apt': ['System packages (Debian)', 'high', 'system.packages'],
|
|
35
|
+
'apt-get': ['System packages (Debian)', 'high', 'system.packages'],
|
|
36
|
+
'apt-cache': ['Package cache query', 'low', 'system.packages'],
|
|
37
|
+
'dpkg': ['Debian packages', 'high', 'system.packages'],
|
|
38
|
+
'brew': ['Homebrew packages', 'medium', 'system.packages'],
|
|
39
|
+
'snap': ['Snap packages', 'medium', 'system.packages'],
|
|
40
|
+
// remote
|
|
41
|
+
'ssh': ['Remote shell access', 'high', 'remote.ssh'],
|
|
42
|
+
'scp': ['Remote file copy', 'high', 'remote.scp'],
|
|
43
|
+
'rsync': ['File sync (local/remote)', 'medium', 'remote.rsync'],
|
|
44
|
+
// containers
|
|
45
|
+
'docker': ['Container management', 'medium', 'containers.docker'],
|
|
46
|
+
'docker-compose': ['Multi-container management', 'medium', 'containers.compose'],
|
|
47
|
+
'podman': ['Container management', 'medium', 'containers.podman'],
|
|
48
|
+
// kubernetes
|
|
49
|
+
'kubectl': ['Kubernetes CLI', 'high', 'kubernetes.kubectl'],
|
|
50
|
+
'helm': ['Kubernetes packages', 'high', 'kubernetes.helm'],
|
|
51
|
+
// cloud
|
|
52
|
+
'aws': ['AWS CLI', 'high', 'cloud.aws'],
|
|
53
|
+
'gcloud': ['Google Cloud CLI', 'high', 'cloud.gcp'],
|
|
54
|
+
'az': ['Azure CLI', 'high', 'cloud.azure'],
|
|
55
|
+
'terraform': ['Infrastructure as Code', 'critical', 'infrastructure.terraform'],
|
|
56
|
+
'pulumi': ['Infrastructure as Code', 'high', 'infrastructure.pulumi'],
|
|
57
|
+
'ansible': ['Configuration management', 'high', 'infrastructure.ansible'],
|
|
58
|
+
// networking
|
|
59
|
+
'curl': ['HTTP requests', 'medium', 'networking'],
|
|
60
|
+
'wget': ['Download files', 'medium', 'networking'],
|
|
61
|
+
'tailscale': ['Tailscale VPN', 'medium', 'networking'],
|
|
62
|
+
'cloudflared': ['Cloudflare tunnel', 'medium', 'networking'],
|
|
63
|
+
// databases
|
|
64
|
+
'psql': ['PostgreSQL client', 'high', 'database.postgresql'],
|
|
65
|
+
'mysql': ['MySQL client', 'high', 'database.mysql'],
|
|
66
|
+
'sqlite3': ['SQLite client', 'medium', 'database.sqlite'],
|
|
67
|
+
'redis-cli': ['Redis client', 'high', 'database.redis'],
|
|
68
|
+
'mongosh': ['MongoDB shell', 'high', 'database.mongodb'],
|
|
69
|
+
// runtimes
|
|
70
|
+
'node': ['Run Node.js scripts', 'medium', 'runtime'],
|
|
71
|
+
'python': ['Run Python scripts', 'medium', 'runtime'],
|
|
72
|
+
'python3': ['Run Python scripts', 'medium', 'runtime'],
|
|
73
|
+
'bun': ['Bun runtime', 'medium', 'runtime'],
|
|
74
|
+
'deno': ['Deno runtime', 'medium', 'runtime'],
|
|
75
|
+
'go': ['Go build tool', 'medium', 'runtime'],
|
|
76
|
+
'cargo': ['Rust build tool', 'medium', 'runtime'],
|
|
77
|
+
'rustc': ['Rust compiler', 'low', 'runtime'],
|
|
78
|
+
// package managers
|
|
79
|
+
'npm': ['Package manager (can run scripts)', 'medium', 'packages'],
|
|
80
|
+
'npx': ['Run npm packages', 'medium', 'packages'],
|
|
81
|
+
'bunx': ['Run bun packages', 'medium', 'packages'],
|
|
82
|
+
'yarn': ['Package manager', 'medium', 'packages'],
|
|
83
|
+
'pnpm': ['Package manager', 'medium', 'packages'],
|
|
84
|
+
'pip': ['Python package manager', 'medium', 'packages'],
|
|
85
|
+
'pip3': ['Python package manager', 'medium', 'packages'],
|
|
86
|
+
'uv': ['Python package manager', 'medium', 'packages'],
|
|
87
|
+
// build tools
|
|
88
|
+
'make': ['Build automation', 'medium', 'build'],
|
|
89
|
+
'tsc': ['TypeScript compiler', 'low', 'build'],
|
|
90
|
+
'mvn': ['Maven build', 'medium', 'build'],
|
|
91
|
+
'gradle': ['Gradle build', 'medium', 'build'],
|
|
92
|
+
// deploy
|
|
93
|
+
'vercel': ['Vercel CLI', 'medium', 'deploy'],
|
|
94
|
+
'heroku': ['Heroku CLI', 'medium', 'deploy'],
|
|
95
|
+
'rclone': ['Cloud storage sync', 'medium', 'storage'],
|
|
96
|
+
// safe tools
|
|
97
|
+
'cat': ['Read files', 'low', 'read'],
|
|
98
|
+
'ls': ['List directories', 'low', 'read'],
|
|
99
|
+
'find': ['Search files', 'low', 'read'],
|
|
100
|
+
'grep': ['Search text', 'low', 'read'],
|
|
101
|
+
'head': ['First lines of file', 'low', 'read'],
|
|
102
|
+
'tail': ['Last lines of file', 'low', 'read'],
|
|
103
|
+
'wc': ['Count lines/words', 'low', 'read'],
|
|
104
|
+
'sort': ['Sort lines', 'low', 'read'],
|
|
105
|
+
'tree': ['Directory tree', 'low', 'read'],
|
|
106
|
+
'echo': ['Print text', 'low', 'read'],
|
|
107
|
+
'env': ['Environment variables', 'low', 'read'],
|
|
108
|
+
'which': ['Locate command', 'low', 'read'],
|
|
109
|
+
'jq': ['JSON processor', 'low', 'read'],
|
|
110
|
+
'sed': ['Stream editor', 'medium', 'text'],
|
|
111
|
+
'awk': ['Text processing', 'low', 'text'],
|
|
112
|
+
'xargs': ['Build commands from stdin', 'medium', 'text'],
|
|
113
|
+
'source': ['Run shell script', 'medium', 'shell'],
|
|
114
|
+
'bash': ['Run shell', 'medium', 'shell'],
|
|
115
|
+
'sh': ['Run shell', 'medium', 'shell'],
|
|
116
|
+
// linters/formatters
|
|
117
|
+
'eslint': ['Linter', 'low', 'dev'],
|
|
118
|
+
'prettier': ['Formatter', 'low', 'dev'],
|
|
119
|
+
'jest': ['Test runner', 'low', 'dev'],
|
|
120
|
+
'vitest': ['Test runner', 'low', 'dev'],
|
|
121
|
+
'pytest': ['Python test runner', 'low', 'dev'],
|
|
122
|
+
// platform
|
|
123
|
+
'gh': ['GitHub CLI', 'medium', 'platform.github'],
|
|
124
|
+
'claude': ['Claude Code CLI', 'low', 'platform'],
|
|
77
125
|
};
|
|
126
|
+
// Context-aware patterns that UPGRADE severity
|
|
127
|
+
// Regex matched against the FULL permission string (not just command name)
|
|
128
|
+
const CRITICAL_PATTERNS = [
|
|
129
|
+
// core.filesystem
|
|
130
|
+
[/rm\s+.*-[a-z]*r[a-z]*f|rm\s+.*-[a-z]*f[a-z]*r|rm\s+-rf/, 'Recursive force delete', 'core.filesystem'],
|
|
131
|
+
[/rm\s+.*\/\*|rm\s+~/, 'Delete broad path', 'core.filesystem'],
|
|
132
|
+
// core.git
|
|
133
|
+
[/git\s+push\s+.*--force|git\s+push\s+.*-f\b/, 'Force push (destroys remote history)', 'core.git'],
|
|
134
|
+
[/git\s+reset\s+--hard/, 'Hard reset (destroys uncommitted changes)', 'core.git'],
|
|
135
|
+
[/git\s+clean\s+-f/, 'Remove untracked files permanently', 'core.git'],
|
|
136
|
+
[/git\s+stash\s+clear/, 'Delete all stashed changes', 'core.git'],
|
|
137
|
+
// containers
|
|
138
|
+
[/docker\s+system\s+prune/, 'Prune all docker data', 'containers.docker'],
|
|
139
|
+
// kubernetes
|
|
140
|
+
[/kubectl\s+delete\s+namespace/, 'Delete entire namespace', 'kubernetes.kubectl'],
|
|
141
|
+
[/kubectl\s+delete.*--all/, 'Delete all resources', 'kubernetes.kubectl'],
|
|
142
|
+
// cloud
|
|
143
|
+
[/aws\s+s3\s+rb|aws\s+s3\s+rm.*--recursive/, 'Delete S3 data', 'cloud.aws'],
|
|
144
|
+
[/terraform\s+destroy/, 'Destroy infrastructure', 'infrastructure.terraform'],
|
|
145
|
+
// system
|
|
146
|
+
[/chmod\s+777|chmod\s+-R/, 'Broad permission change', 'system.permissions'],
|
|
147
|
+
// catch-all dangerous
|
|
148
|
+
[/mkfs|shred|wipefs/, 'Disk destruction', 'core.filesystem'],
|
|
149
|
+
[/curl.*\|\s*sh|curl.*\|\s*bash|wget.*\|\s*sh/, 'Pipe to shell (remote code execution)', 'networking'],
|
|
150
|
+
];
|
|
151
|
+
const HIGH_PATTERNS = [
|
|
152
|
+
// core.git
|
|
153
|
+
[/git\s+push/, 'Push to remote', 'core.git'],
|
|
154
|
+
[/git\s+rebase/, 'Rewrite commit history', 'core.git'],
|
|
155
|
+
[/git\s+branch\s+-D/, 'Force delete branch', 'core.git'],
|
|
156
|
+
// containers
|
|
157
|
+
[/docker\s+rm/, 'Remove containers', 'containers.docker'],
|
|
158
|
+
[/docker\s+run/, 'Run container', 'containers.docker'],
|
|
159
|
+
// cloud
|
|
160
|
+
[/aws\s+ecs/, 'ECS management', 'cloud.aws'],
|
|
161
|
+
[/aws\s+codepipeline/, 'CI/CD pipeline', 'cloud.aws'],
|
|
162
|
+
[/aws\s+ssm/, 'Systems Manager', 'cloud.aws'],
|
|
163
|
+
// deploy
|
|
164
|
+
[/npm\s+publish/, 'Publish to npm', 'packages'],
|
|
165
|
+
[/vercel\s+--prod/, 'Deploy to production', 'deploy'],
|
|
166
|
+
];
|
|
167
|
+
// Write path risk assessment
|
|
168
|
+
const WRITE_CRITICAL = [
|
|
169
|
+
/\*\*\/\*\.key/, /\*\*\/\*\.pem/, /\.env/,
|
|
170
|
+
/credentials/, /secrets/, /\.ssh/,
|
|
171
|
+
];
|
|
78
172
|
const TOOL_DESCRIPTIONS = {
|
|
79
|
-
'Read': ['Read file contents', '
|
|
80
|
-
'Write': ['Create/overwrite files', '
|
|
81
|
-
'Edit': ['Modify existing files', '
|
|
82
|
-
'Glob': ['Search files by pattern', '
|
|
83
|
-
'Grep': ['Search file contents', '
|
|
84
|
-
'WebSearch': ['Web search', '
|
|
173
|
+
'Read': ['Read file contents', 'low'],
|
|
174
|
+
'Write': ['Create/overwrite files', 'medium'],
|
|
175
|
+
'Edit': ['Modify existing files', 'medium'],
|
|
176
|
+
'Glob': ['Search files by pattern', 'low'],
|
|
177
|
+
'Grep': ['Search file contents', 'low'],
|
|
178
|
+
'WebSearch': ['Web search', 'low'],
|
|
85
179
|
};
|
|
86
|
-
// Extract first command word from a bash label
|
|
180
|
+
// Extract first command word from a bash label
|
|
87
181
|
function extractCmd(label) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
182
|
+
const clean = label
|
|
183
|
+
.replace(/__NEW_LINE_[a-f0-9]+__\s*/, '')
|
|
184
|
+
.replace(/[:]\*.*$/, '')
|
|
185
|
+
.replace(/\s\*.*$/, '');
|
|
91
186
|
return clean.split(/[\s(]/)[0];
|
|
92
187
|
}
|
|
93
188
|
function explainBash(label) {
|
|
189
|
+
// Check critical patterns first (full string match)
|
|
190
|
+
for (const [re, desc, domain] of CRITICAL_PATTERNS) {
|
|
191
|
+
if (re.test(label))
|
|
192
|
+
return { description: desc, risk: 'critical', domain };
|
|
193
|
+
}
|
|
194
|
+
// Check high patterns
|
|
195
|
+
for (const [re, desc, domain] of HIGH_PATTERNS) {
|
|
196
|
+
if (re.test(label))
|
|
197
|
+
return { description: desc, risk: 'high', domain };
|
|
198
|
+
}
|
|
199
|
+
// Fall back to command-level lookup
|
|
94
200
|
const cmd = extractCmd(label);
|
|
95
201
|
const entry = BASH_COMMANDS[cmd];
|
|
96
202
|
if (entry)
|
|
97
|
-
return { description: entry[0], risk: entry[1] };
|
|
98
|
-
|
|
203
|
+
return { description: entry[0], risk: entry[1], domain: entry[2] };
|
|
204
|
+
// Bare "Bash" with no command = full shell access
|
|
205
|
+
if (label.trim() === 'Bash' || label.trim() === '')
|
|
206
|
+
return { description: 'Unrestricted shell access', risk: 'critical', domain: 'system' };
|
|
207
|
+
return { description: '', risk: 'medium' };
|
|
99
208
|
}
|
|
100
209
|
function explainWebFetch(label) {
|
|
101
|
-
return { description: label, risk: '
|
|
210
|
+
return { description: label, risk: 'low', domain: 'networking' };
|
|
102
211
|
}
|
|
103
212
|
function explainMcp(label) {
|
|
104
213
|
const parts = label.replace(/^mcp__?/, '').split('__');
|
|
105
214
|
const server = parts[0] || '';
|
|
106
215
|
const tool = parts.slice(1).join(' ') || '';
|
|
107
|
-
return { description: tool ? `${server}: ${tool}` : server, risk: '
|
|
216
|
+
return { description: tool ? `${server}: ${tool}` : server, risk: 'medium', domain: 'mcp' };
|
|
108
217
|
}
|
|
109
218
|
function explainTool(label) {
|
|
219
|
+
// Check Write paths for sensitive targets
|
|
220
|
+
if (label.startsWith('Write:')) {
|
|
221
|
+
const path = label.slice(6);
|
|
222
|
+
for (const re of WRITE_CRITICAL) {
|
|
223
|
+
if (re.test(path))
|
|
224
|
+
return { description: `Write to sensitive path: ${path}`, risk: 'critical', domain: 'core.filesystem' };
|
|
225
|
+
}
|
|
226
|
+
return { description: `Write to ${path}`, risk: 'medium', domain: 'core.filesystem' };
|
|
227
|
+
}
|
|
110
228
|
const toolName = label.match(/^(Read|Write|Edit|Glob|Grep|WebSearch)/)?.[1];
|
|
111
229
|
if (toolName && TOOL_DESCRIPTIONS[toolName]) {
|
|
112
230
|
const entry = TOOL_DESCRIPTIONS[toolName];
|
|
113
231
|
return { description: entry[0], risk: entry[1] };
|
|
114
232
|
}
|
|
115
|
-
return { description: '', risk: '
|
|
233
|
+
return { description: '', risk: 'medium' };
|
|
116
234
|
}
|
|
117
235
|
function explain(category, label) {
|
|
118
236
|
if (category === 'Bash')
|
|
@@ -123,5 +241,5 @@ function explain(category, label) {
|
|
|
123
241
|
return explainMcp(label);
|
|
124
242
|
if (category === 'Tools')
|
|
125
243
|
return explainTool(label);
|
|
126
|
-
return { description: '', risk: '
|
|
244
|
+
return { description: '', risk: 'medium' };
|
|
127
245
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccperm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Audit Claude Code permissions across all your projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ccperm": "bin/ccperm.js"
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/ccperm.js",
|
|
26
|
-
"dist"
|
|
26
|
+
"dist",
|
|
27
|
+
"screenshot.png"
|
|
27
28
|
],
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/node": "^25.3.0",
|
package/screenshot.png
ADDED
|
Binary file
|