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 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` 한번 쳐봐. 너를 위한 플래그가 있어.</sub>
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.</sub>
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 RISKY = new Set(['rm', 'sudo', 'chmod', 'chown', 'kill', 'dd', 'ssh', 'scp', 'aws', 'gcloud', 'az', 'kubectl', 'terraform']);
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. Find frequently repeated bash commands → suggest global
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. 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
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 (heredocProjects.size > 0) {
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
- // 4. Global permissions check
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].message}`);
146
+ lines.push(`${i + 1}. ${hints[i]}`);
119
147
  }
120
148
  lines.push(``);
121
149
  }
122
- lines.push(`## How to act:`);
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
- // Input is the "label" from categorize(), not the raw permission string
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
- // [description, risk: green/yellow/red]
12
- 'git': ['Git version control', 'green'],
13
- 'npm': ['Package manager (can run scripts)', 'yellow'],
14
- 'npx': ['Run npm packages', 'yellow'],
15
- 'node': ['Run Node.js scripts', 'yellow'],
16
- 'bun': ['Bun runtime', 'yellow'],
17
- 'deno': ['Deno runtime', 'yellow'],
18
- 'python': ['Run Python scripts', 'yellow'],
19
- 'python3': ['Run Python scripts', 'yellow'],
20
- 'pip': ['Python package manager', 'yellow'],
21
- 'pip3': ['Python package manager', 'yellow'],
22
- 'docker': ['Container management', 'yellow'],
23
- 'docker-compose': ['Multi-container management', 'yellow'],
24
- 'curl': ['HTTP requests', 'yellow'],
25
- 'wget': ['Download files', 'yellow'],
26
- 'ssh': ['Remote shell access', 'red'],
27
- 'scp': ['Remote file copy', 'red'],
28
- 'rsync': ['File sync (local/remote)', 'yellow'],
29
- 'rm': ['Delete files', 'red'],
30
- 'chmod': ['Change permissions', 'yellow'],
31
- 'chown': ['Change ownership', 'red'],
32
- 'kill': ['Terminate processes', 'yellow'],
33
- 'sudo': ['Superuser access', 'red'],
34
- 'apt': ['System packages (Debian)', 'red'],
35
- 'apt-get': ['System packages (Debian)', 'red'],
36
- 'brew': ['Homebrew packages', 'yellow'],
37
- 'make': ['Build automation', 'yellow'],
38
- 'cargo': ['Rust build tool', 'yellow'],
39
- 'go': ['Go build tool', 'yellow'],
40
- 'mvn': ['Maven build', 'yellow'],
41
- 'gradle': ['Gradle build', 'yellow'],
42
- 'yarn': ['Package manager', 'yellow'],
43
- 'pnpm': ['Package manager', 'yellow'],
44
- 'tsc': ['TypeScript compiler', 'green'],
45
- 'eslint': ['Linter', 'green'],
46
- 'prettier': ['Formatter', 'green'],
47
- 'jest': ['Test runner', 'green'],
48
- 'vitest': ['Test runner', 'green'],
49
- 'cat': ['Read files', 'green'],
50
- 'ls': ['List directories', 'green'],
51
- 'find': ['Search files', 'green'],
52
- 'grep': ['Search text', 'green'],
53
- 'sed': ['Stream editor', 'yellow'],
54
- 'awk': ['Text processing', 'green'],
55
- 'wc': ['Count lines/words', 'green'],
56
- 'head': ['First lines of file', 'green'],
57
- 'tail': ['Last lines of file', 'green'],
58
- 'mkdir': ['Create directories', 'green'],
59
- 'cp': ['Copy files', 'green'],
60
- 'mv': ['Move/rename files', 'yellow'],
61
- 'echo': ['Print text', 'green'],
62
- 'env': ['Environment variables', 'green'],
63
- 'which': ['Locate command', 'green'],
64
- 'gh': ['GitHub CLI', 'yellow'],
65
- 'heroku': ['Heroku CLI', 'yellow'],
66
- 'vercel': ['Vercel CLI', 'yellow'],
67
- 'aws': ['AWS CLI', 'red'],
68
- 'gcloud': ['Google Cloud CLI', 'red'],
69
- 'az': ['Azure CLI', 'red'],
70
- 'kubectl': ['Kubernetes CLI', 'red'],
71
- 'terraform': ['Infrastructure as Code', 'red'],
72
- 'dd': ['Low-level disk copy', 'red'],
73
- 'jq': ['JSON processor', 'green'],
74
- 'bunx': ['Run bun packages', 'yellow'],
75
- 'claude': ['Claude Code CLI', 'green'],
76
- 'defaults': ['macOS defaults', 'yellow'],
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', 'green'],
80
- 'Write': ['Create/overwrite files', 'yellow'],
81
- 'Edit': ['Modify existing files', 'yellow'],
82
- 'Glob': ['Search files by pattern', 'green'],
83
- 'Grep': ['Search file contents', 'green'],
84
- 'WebSearch': ['Web search', 'green'],
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 like "git branch:*" or "npm run build"
180
+ // Extract first command word from a bash label
87
181
  function extractCmd(label) {
88
- // Remove :* or * suffix patterns
89
- const clean = label.replace(/[:]\*.*$/, '').replace(/\s\*.*$/, '');
90
- // Get first word
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
- return { description: '', risk: 'yellow' };
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: 'yellow' };
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: 'yellow' };
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: 'yellow' };
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: 'yellow' };
244
+ return { description: '', risk: 'medium' };
127
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.10.2",
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