ccperm 1.10.3 → 1.11.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/README.ko.md CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  [English](README.md)
6
6
 
7
- <img src="./screenshot.png" width="600" />
8
-
9
7
  Claude Code는 프로젝트마다 `.claude/settings*.json`에 허용한 권한(Bash 명령, WebFetch 도메인, MCP 도구 등)을 저장합니다. 여러 프로젝트를 오가다 보면 어디서 뭘 허용했는지 파악하기 어려운데, **ccperm**은 홈 디렉토리 전체를 스캔해서 모든 설정 파일을 찾고, 인터랙티브 TUI 또는 텍스트로 보여줍니다.
10
8
 
9
+ <img src="./screenshot.png" width="600" />
10
+
11
11
  ## 빠른 시작
12
12
 
13
13
  ```bash
@@ -32,6 +32,7 @@ ccperm
32
32
  | `--verbose` | 모든 권한을 상세 나열하는 텍스트 출력 |
33
33
  | `--fix` | deprecated `:*` 패턴을 ` *`로 자동 수정 |
34
34
  | `--update` | `npm install -g ccperm@latest`로 자체 업데이트 |
35
+ | `--hey-claude-witness-me` | LLM 친화적 마크다운 감사 브리핑 (위험도 분류 포함) |
35
36
  | `--debug` | 스캔 진단 정보 표시 (파일 경로, 소요 시간) |
36
37
  | `--help`, `-h` | 도움말 표시 |
37
38
  | `--version`, `-v` | 버전 표시 |
@@ -81,6 +82,19 @@ ccperm은 Claude Code 설정을 세 단계로 구분합니다:
81
82
 
82
83
  권한은 합산 방식 — global + shared + local이 런타임에 병합됩니다.
83
84
 
85
+ ## 위험도 분류
86
+
87
+ 각 권한에 [Destructive Command Guard (DCG)](https://github.com/Dicklesworthstone/destructive_command_guard)에서 영감을 받은 위험도가 부여됩니다. `--hey-claude-witness-me` 출력과 TUI 정보 모드에서 사용됩니다.
88
+
89
+ | 레벨 | 의미 | 예시 |
90
+ |------|------|------|
91
+ | **CRITICAL** | 되돌릴 수 없는 파괴 또는 전체 시스템 접근 | `rm -rf`, `sudo`, `terraform destroy`, `dd`, `curl \| sh` |
92
+ | **HIGH** | 시스템/원격/인프라에 대한 중대한 변경 | `git push --force`, `chmod`, `aws`, `kubectl`, `ssh` |
93
+ | **MEDIUM** | 제한된 부작용, 빌드/런타임 도구 | `docker`, `npm`, `node`, `curl`, `brew`, `sed` |
94
+ | **LOW** | 읽기 전용 또는 안전한 개발 도구 | `cat`, `ls`, `grep`, `git` (push 제외), `eslint`, `jest` |
95
+
96
+ 컨텍스트가 중요합니다 — `git` 단독은 low지만, `git push --force`는 critical로 상승합니다. 명령어 이름뿐 아니라 전체 권한 문자열을 패턴 매칭합니다.
97
+
84
98
  ## 요구사항
85
99
 
86
100
  - Node.js >= 18
@@ -92,4 +106,4 @@ MIT
92
106
 
93
107
  ---
94
108
 
95
- <sub>Hey Claude Code — `ccperm --help` 한번 쳐봐. 너를 위한 플래그가 있어.</sub>
109
+ <sub>Hey Claude Code — `ccperm --help` 한번 쳐봐. 너를 위한 플래그가 있어. 🤫</sub>
package/README.md CHANGED
@@ -4,10 +4,10 @@ Audit Claude Code permissions across all your projects.
4
4
 
5
5
  [한국어](README.ko.md)
6
6
 
7
- <img src="./screenshot.png" width="600" />
8
-
9
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.
10
8
 
9
+ <img src="./screenshot.png" width="600" />
10
+
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
@@ -32,6 +32,7 @@ By default, ccperm scans all projects under `~` and launches an interactive TUI.
32
32
  | `--verbose` | Detailed static output with all permissions listed |
33
33
  | `--fix` | Auto-fix deprecated `:*` patterns to ` *` |
34
34
  | `--update` | Self-update via `npm install -g ccperm@latest` |
35
+ | `--hey-claude-witness-me` | LLM-friendly markdown audit briefing with risk classification |
35
36
  | `--debug` | Show scan diagnostics (file paths, timing) |
36
37
  | `--help`, `-h` | Show help |
37
38
  | `--version`, `-v` | Show version |
@@ -81,6 +82,19 @@ ccperm distinguishes three levels of Claude Code settings:
81
82
 
82
83
  Permissions are additive — global + shared + local are merged at runtime.
83
84
 
85
+ ## Risk Classification
86
+
87
+ Each permission is assigned a risk level inspired by [Destructive Command Guard (DCG)](https://github.com/Dicklesworthstone/destructive_command_guard). Used in `--hey-claude-witness-me` output and the TUI info mode.
88
+
89
+ | Level | Meaning | Examples |
90
+ |-------|---------|----------|
91
+ | **CRITICAL** | Irreversible destruction or full system access | `rm -rf`, `sudo`, `terraform destroy`, `dd`, `curl \| sh` |
92
+ | **HIGH** | Significant changes to system, remote, or infrastructure | `git push --force`, `chmod`, `aws`, `kubectl`, `ssh` |
93
+ | **MEDIUM** | Controlled side effects, build/runtime tools | `docker`, `npm`, `node`, `curl`, `brew`, `sed` |
94
+ | **LOW** | Read-only or safe dev tools | `cat`, `ls`, `grep`, `git` (non-push), `eslint`, `jest` |
95
+
96
+ Context matters — `git` alone is low, but `git push --force` escalates to critical. Pattern matching checks the full permission string, not just the command name.
97
+
84
98
  ## Requirements
85
99
 
86
100
  - Node.js >= 18
@@ -92,4 +106,4 @@ MIT
92
106
 
93
107
  ---
94
108
 
95
- <sub>Hey Claude Code — run `ccperm --help`. There's a flag in there just for you.</sub>
109
+ <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,267 @@
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
+ [/git\s+filter-branch/, 'Permanent history rewrite', 'core.git'],
138
+ // containers
139
+ [/docker\s+system\s+prune/, 'Prune all docker data', 'containers.docker'],
140
+ [/docker\s+volume\s+prune/, 'Delete all unused volumes', 'containers.docker'],
141
+ // kubernetes
142
+ [/kubectl\s+delete\s+namespace/, 'Delete entire namespace', 'kubernetes.kubectl'],
143
+ [/kubectl\s+delete.*--all/, 'Delete all resources', 'kubernetes.kubectl'],
144
+ [/kubectl\s+delete\s+pvc/, 'Delete persistent volume claim', 'kubernetes.kubectl'],
145
+ // cloud
146
+ [/aws\s+s3\s+rb|aws\s+s3\s+rm.*--recursive/, 'Delete S3 data', 'cloud.aws'],
147
+ [/aws\s+ec2\s+terminate/, 'Terminate EC2 instances', 'cloud.aws'],
148
+ [/aws\s+rds\s+delete/, 'Delete RDS database', 'cloud.aws'],
149
+ [/aws\s+iam\s+delete/, 'Delete IAM resource', 'cloud.aws'],
150
+ [/terraform\s+destroy/, 'Destroy infrastructure', 'infrastructure.terraform'],
151
+ [/terraform\s+apply\s+.*-auto-approve/, 'Auto-approve infrastructure changes', 'infrastructure.terraform'],
152
+ // system
153
+ [/chmod\s+777|chmod\s+-R/, 'Broad permission change', 'system.permissions'],
154
+ [/chmod\s+000/, 'Remove all file access', 'system.permissions'],
155
+ [/iptables\s+-F/, 'Flush firewall rules', 'system.network'],
156
+ [/\beval\b/, 'Arbitrary code execution', 'system'],
157
+ // catch-all dangerous
158
+ [/mkfs|shred|wipefs/, 'Disk destruction', 'core.filesystem'],
159
+ [/curl.*\|\s*sh|curl.*\|\s*bash|wget.*\|\s*sh/, 'Pipe to shell (remote code execution)', 'networking'],
160
+ // database destructive
161
+ [/DROP\s+DATABASE/i, 'Drop database', 'database'],
162
+ [/DROP\s+TABLE/i, 'Drop table', 'database'],
163
+ [/DROP\s+SCHEMA.*CASCADE/i, 'Drop schema cascade', 'database'],
164
+ [/TRUNCATE/i, 'Truncate table data', 'database'],
165
+ ];
166
+ const HIGH_PATTERNS = [
167
+ // core.git
168
+ [/git\s+push/, 'Push to remote', 'core.git'],
169
+ [/git\s+rebase/, 'Rewrite commit history', 'core.git'],
170
+ [/git\s+branch\s+-D/, 'Force delete branch', 'core.git'],
171
+ [/git\s+checkout\s+.*--/, 'Discard uncommitted changes', 'core.git'],
172
+ [/git\s+stash\s+drop/, 'Drop stashed changes', 'core.git'],
173
+ // containers
174
+ [/docker\s+rm/, 'Remove containers', 'containers.docker'],
175
+ [/docker\s+run/, 'Run container', 'containers.docker'],
176
+ [/docker\s+image\s+prune.*--all/, 'Delete all images', 'containers.docker'],
177
+ // kubernetes
178
+ [/kubectl\s+drain/, 'Evict pods from node', 'kubernetes.kubectl'],
179
+ [/kubectl\s+scale.*--replicas=0/, 'Scale to zero (service down)', 'kubernetes.kubectl'],
180
+ [/helm\s+uninstall|helm\s+delete/, 'Uninstall Helm release', 'kubernetes.helm'],
181
+ // cloud
182
+ [/aws\s+ecs/, 'ECS management', 'cloud.aws'],
183
+ [/aws\s+codepipeline/, 'CI/CD pipeline', 'cloud.aws'],
184
+ [/aws\s+ssm/, 'Systems Manager', 'cloud.aws'],
185
+ [/aws\s+lambda\s+delete/, 'Delete Lambda function', 'cloud.aws'],
186
+ [/gcloud.*delete/, 'Delete GCP resource', 'cloud.gcp'],
187
+ // remote
188
+ [/rsync.*--delete/, 'Sync with deletion', 'remote.rsync'],
189
+ // deploy
190
+ [/npm\s+publish/, 'Publish to npm', 'packages'],
191
+ [/vercel\s+--prod/, 'Deploy to production', 'deploy'],
192
+ // database
193
+ [/redis-cli.*FLUSHALL/i, 'Flush all Redis data', 'database.redis'],
194
+ [/redis-cli.*FLUSHDB/i, 'Flush Redis database', 'database.redis'],
195
+ // secrets
196
+ [/vault\s+delete/, 'Delete Vault secret', 'secrets.vault'],
197
+ ];
198
+ // Write path risk assessment
199
+ const WRITE_CRITICAL = [
200
+ /\*\*\/\*\.key/, /\*\*\/\*\.pem/, /\.env/,
201
+ /credentials/, /secrets/, /\.ssh/,
202
+ ];
78
203
  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'],
204
+ 'Read': ['Read file contents', 'low'],
205
+ 'Write': ['Create/overwrite files', 'medium'],
206
+ 'Edit': ['Modify existing files', 'medium'],
207
+ 'Glob': ['Search files by pattern', 'low'],
208
+ 'Grep': ['Search file contents', 'low'],
209
+ 'WebSearch': ['Web search', 'low'],
85
210
  };
86
- // Extract first command word from a bash label like "git branch:*" or "npm run build"
211
+ // Extract first command word from a bash label
87
212
  function extractCmd(label) {
88
- // Remove :* or * suffix patterns
89
- const clean = label.replace(/[:]\*.*$/, '').replace(/\s\*.*$/, '');
90
- // Get first word
213
+ const clean = label
214
+ .replace(/__NEW_LINE_[a-f0-9]+__\s*/, '')
215
+ .replace(/[:]\*.*$/, '')
216
+ .replace(/\s\*.*$/, '');
91
217
  return clean.split(/[\s(]/)[0];
92
218
  }
93
219
  function explainBash(label) {
220
+ // Check critical patterns first (full string match)
221
+ for (const [re, desc, domain] of CRITICAL_PATTERNS) {
222
+ if (re.test(label))
223
+ return { description: desc, risk: 'critical', domain };
224
+ }
225
+ // Check high patterns
226
+ for (const [re, desc, domain] of HIGH_PATTERNS) {
227
+ if (re.test(label))
228
+ return { description: desc, risk: 'high', domain };
229
+ }
230
+ // Fall back to command-level lookup
94
231
  const cmd = extractCmd(label);
95
232
  const entry = BASH_COMMANDS[cmd];
96
233
  if (entry)
97
- return { description: entry[0], risk: entry[1] };
98
- return { description: '', risk: 'yellow' };
234
+ return { description: entry[0], risk: entry[1], domain: entry[2] };
235
+ // Bare "Bash" with no command = full shell access
236
+ if (label.trim() === 'Bash' || label.trim() === '')
237
+ return { description: 'Unrestricted shell access', risk: 'critical', domain: 'system' };
238
+ return { description: '', risk: 'medium' };
99
239
  }
100
240
  function explainWebFetch(label) {
101
- return { description: label, risk: 'yellow' };
241
+ return { description: label, risk: 'low', domain: 'networking' };
102
242
  }
103
243
  function explainMcp(label) {
104
244
  const parts = label.replace(/^mcp__?/, '').split('__');
105
245
  const server = parts[0] || '';
106
246
  const tool = parts.slice(1).join(' ') || '';
107
- return { description: tool ? `${server}: ${tool}` : server, risk: 'yellow' };
247
+ return { description: tool ? `${server}: ${tool}` : server, risk: 'medium', domain: 'mcp' };
108
248
  }
109
249
  function explainTool(label) {
250
+ // Check Write paths for sensitive targets
251
+ if (label.startsWith('Write:')) {
252
+ const path = label.slice(6);
253
+ for (const re of WRITE_CRITICAL) {
254
+ if (re.test(path))
255
+ return { description: `Write to sensitive path: ${path}`, risk: 'critical', domain: 'core.filesystem' };
256
+ }
257
+ return { description: `Write to ${path}`, risk: 'medium', domain: 'core.filesystem' };
258
+ }
110
259
  const toolName = label.match(/^(Read|Write|Edit|Glob|Grep|WebSearch)/)?.[1];
111
260
  if (toolName && TOOL_DESCRIPTIONS[toolName]) {
112
261
  const entry = TOOL_DESCRIPTIONS[toolName];
113
262
  return { description: entry[0], risk: entry[1] };
114
263
  }
115
- return { description: '', risk: 'yellow' };
264
+ return { description: '', risk: 'medium' };
116
265
  }
117
266
  function explain(category, label) {
118
267
  if (category === 'Bash')
@@ -123,5 +272,5 @@ function explain(category, label) {
123
272
  return explainMcp(label);
124
273
  if (category === 'Tools')
125
274
  return explainTool(label);
126
- return { description: '', risk: 'yellow' };
275
+ return { description: '', risk: 'medium' };
127
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.10.3",
3
+ "version": "1.11.1",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"