ccperm 1.16.0 → 1.17.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
@@ -86,16 +86,29 @@ ccperm은 Claude Code 설정을 세 단계로 구분합니다:
86
86
 
87
87
  권한은 합산 방식 — global + shared + local이 런타임에 병합됩니다.
88
88
 
89
- ## Allow vs Deny
89
+ ## Allow vs Deny vs Ask
90
90
 
91
- Claude Code 설정은 `permissions.allow`와 `permissions.deny`를 모두 지원합니다. ccperm은 이를 명확히 분리합니다:
91
+ Claude Code 설정은 `permissions.allow`, `permissions.deny`, `permissions.ask`를 모두 지원합니다. ccperm은 이를 명확히 분리합니다:
92
92
 
93
93
  - **Allow** 권한은 메인 목록에 표시되며, 삭제(`[d]`)나 글로벌 복사(`[g]`)가 가능합니다
94
94
  - **Deny** 룰은 상세 뷰에서 접이식 **Deny** 섹션에 `DENY` 태그와 함께 표시됩니다
95
- - Deny 룰은 **삭제/복사 불가** 보안 가드레일입니다 (예: `Bash(rm -rf)`, `Write:.env*`)
96
- - `--verbose` 출력에서 프로젝트별 Deny 섹션이 분리 표시됩니다
95
+ - **Ask** 룰은 접이식 **Ask** 섹션에 표시됩니다 매번 확인을 요청하는 권한입니다
96
+ - Deny와 Ask 룰은 **삭제/복사 불가** 의도적으로 설정한 제어 규칙입니다
97
+ - `--verbose` 출력에서 프로젝트별 Deny, Ask 섹션이 분리 표시됩니다
97
98
  - `--hey-claude-witness-me`에서 deny 룰이 "Protected rules"로 나열됩니다
98
- - Allow 카운트에 deny 룰은 포함되지 않습니다
99
+ - Allow 카운트에 deny, ask 룰은 포함되지 않습니다
100
+
101
+ ## 추가 설정
102
+
103
+ ccperm은 다음 최상위 설정도 스캔하여 표시합니다:
104
+
105
+ | 필드 | 설명 |
106
+ |------|------|
107
+ | `allowedTools` | 사용이 허용된 도구 (예: `Bash`, `Read`, `Write`) |
108
+ | `deniedTools` | 사용이 차단된 도구 (예: `WebSearch`) |
109
+ | `additionalDirectories` | 프로젝트 루트 외 Claude Code가 접근할 수 있는 추가 디렉토리 |
110
+
111
+ TUI 상세 뷰에서 접이식 섹션으로, `--verbose` 출력에서 별도 섹션으로 표시됩니다. Deny 룰과 마찬가지로 삭제/복사가 불가합니다.
99
112
 
100
113
  ## 위험도 분류
101
114
 
package/README.md CHANGED
@@ -86,16 +86,29 @@ ccperm distinguishes three levels of Claude Code settings:
86
86
 
87
87
  Permissions are additive — global + shared + local are merged at runtime.
88
88
 
89
- ## Allow vs Deny
89
+ ## Allow vs Deny vs Ask
90
90
 
91
- Claude Code settings support both `permissions.allow` and `permissions.deny`. ccperm separates them clearly:
91
+ Claude Code settings support `permissions.allow`, `permissions.deny`, and `permissions.ask`. ccperm separates them clearly:
92
92
 
93
93
  - **Allow** permissions are shown in the main list and can be deleted (`[d]`) or copied to global (`[g]`)
94
94
  - **Deny** rules appear in a collapsible **Deny** section in detail view, tagged with `DENY` and dimmed
95
- - Deny rules **cannot be deleted or copied** — they are security guardrails (e.g. `Bash(rm -rf)`, `Write:.env*`)
96
- - `--verbose` output shows a separate Deny section per project
95
+ - **Ask** rules appear in a collapsible **Ask** section these are permissions that prompt for confirmation each time
96
+ - Deny and Ask rules **cannot be deleted or copied** — they are intentional controls
97
+ - `--verbose` output shows separate Deny and Ask sections per project
97
98
  - `--hey-claude-witness-me` lists deny rules under "Protected rules"
98
- - Allow counts never include deny rules
99
+ - Allow counts never include deny or ask rules
100
+
101
+ ## Additional Settings
102
+
103
+ ccperm also scans and displays these top-level settings when present:
104
+
105
+ | Field | Description |
106
+ |-------|-------------|
107
+ | `allowedTools` | Tools explicitly allowed for use (e.g. `Bash`, `Read`, `Write`) |
108
+ | `deniedTools` | Tools explicitly denied (e.g. `WebSearch`) |
109
+ | `additionalDirectories` | Extra directories Claude Code can access beyond the project root |
110
+
111
+ These appear as collapsible sections in the TUI detail view and in `--verbose` output. Like deny rules, they cannot be deleted or copied.
99
112
 
100
113
  ## Risk Classification
101
114
 
@@ -20,7 +20,7 @@ function toFileEntries(results) {
20
20
  }
21
21
  const fileType = r.isGlobal ? 'global' : r.display.includes('settings.local.json') ? 'local' : 'shared';
22
22
  const name = r.isGlobal ? '~/.claude' : shortPath(r.display);
23
- return { display: r.display, shortName: name, totalCount: r.totalCount, denyCount: r.denyCount, groups, isGlobal: r.isGlobal, fileType };
23
+ return { display: r.display, shortName: name, totalCount: r.totalCount, denyCount: r.denyCount, askCount: r.askCount, groups, isGlobal: r.isGlobal, fileType };
24
24
  });
25
25
  }
26
26
  function summarize(results) {
@@ -35,12 +35,14 @@ function summarize(results) {
35
35
  const projectsEmpty = results.filter((r) => r.totalCount === 0).length;
36
36
  const totalPerms = [...categoryTotals.values()].reduce((a, b) => a + b, 0);
37
37
  const totalDeny = results.reduce((sum, r) => sum + r.denyCount, 0);
38
+ const totalAsk = results.reduce((sum, r) => sum + r.askCount, 0);
38
39
  return {
39
40
  totalProjects: dirs.size,
40
41
  projectsWithPerms,
41
42
  projectsEmpty,
42
43
  totalPerms,
43
44
  totalDeny,
45
+ totalAsk,
44
46
  categoryTotals,
45
47
  };
46
48
  }
@@ -79,6 +79,7 @@ function refreshProject(results, withPerms, idx, filePath) {
79
79
  const entry = withPerms[idx];
80
80
  entry.totalCount = updated.totalCount;
81
81
  entry.denyCount = updated.denyCount;
82
+ entry.askCount = updated.askCount;
82
83
  entry.groups = new Map();
83
84
  for (const g of updated.groups)
84
85
  entry.groups.set(g.category, g.items.length);
@@ -594,6 +595,65 @@ function renderDetail(state, withPerms, results, dupMap) {
594
595
  }
595
596
  }
596
597
  }
598
+ // Ask section
599
+ if (fileResult.askCount > 0) {
600
+ const askKey = `${fileResult.path}:__ask__`;
601
+ const askOpen = state.expanded.has(askKey);
602
+ const askArrow = askOpen ? '▾' : '▸';
603
+ allNavRows.push({ text: `${colors_js_1.YELLOW}${askArrow} Ask${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.askCount})${colors_js_1.NC}`, key: askKey, isDeny: true });
604
+ if (askOpen) {
605
+ for (const group of fileResult.askGroups) {
606
+ for (const item of group.items) {
607
+ const clean = cleanLabel(item.name);
608
+ const maxLen = w - 16;
609
+ const name = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '…' : clean;
610
+ allNavRows.push({ text: ` ${colors_js_1.DIM}ASK ${name}${colors_js_1.NC}`, isDeny: true });
611
+ }
612
+ }
613
+ }
614
+ }
615
+ // AllowedTools section
616
+ if (fileResult.allowedTools.length > 0) {
617
+ const atKey = `${fileResult.path}:__allowedTools__`;
618
+ const atOpen = state.expanded.has(atKey);
619
+ const atArrow = atOpen ? '▾' : '▸';
620
+ allNavRows.push({ text: `${colors_js_1.CYAN}${atArrow} AllowedTools${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.allowedTools.length})${colors_js_1.NC}`, key: atKey, isDeny: true });
621
+ if (atOpen) {
622
+ for (const t of fileResult.allowedTools) {
623
+ const maxLen = w - 10;
624
+ const name = t.length > maxLen ? t.slice(0, maxLen - 1) + '…' : t;
625
+ allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, isDeny: true });
626
+ }
627
+ }
628
+ }
629
+ // DeniedTools section
630
+ if (fileResult.deniedTools.length > 0) {
631
+ const dtKey = `${fileResult.path}:__deniedTools__`;
632
+ const dtOpen = state.expanded.has(dtKey);
633
+ const dtArrow = dtOpen ? '▾' : '▸';
634
+ allNavRows.push({ text: `${colors_js_1.RED}${dtArrow} DeniedTools${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.deniedTools.length})${colors_js_1.NC}`, key: dtKey, isDeny: true });
635
+ if (dtOpen) {
636
+ for (const t of fileResult.deniedTools) {
637
+ const maxLen = w - 10;
638
+ const name = t.length > maxLen ? t.slice(0, maxLen - 1) + '…' : t;
639
+ allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, isDeny: true });
640
+ }
641
+ }
642
+ }
643
+ // AdditionalDirectories section
644
+ if (fileResult.additionalDirectories.length > 0) {
645
+ const adKey = `${fileResult.path}:__additionalDirs__`;
646
+ const adOpen = state.expanded.has(adKey);
647
+ const adArrow = adOpen ? '▾' : '▸';
648
+ allNavRows.push({ text: `${colors_js_1.DIM}${adArrow} AdditionalDirectories${colors_js_1.NC} ${colors_js_1.DIM}(${fileResult.additionalDirectories.length})${colors_js_1.NC}`, key: adKey, isDeny: true });
649
+ if (adOpen) {
650
+ for (const d of fileResult.additionalDirectories) {
651
+ const maxLen = w - 10;
652
+ const name = d.length > maxLen ? d.slice(0, maxLen - 1) + '…' : d;
653
+ allNavRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, isDeny: true });
654
+ }
655
+ }
656
+ }
597
657
  const navRows = allNavRows;
598
658
  // handle toggle
599
659
  if (state._toggle) {
package/dist/renderer.js CHANGED
@@ -67,6 +67,32 @@ function printVerbose(results, summary) {
67
67
  }
68
68
  }
69
69
  }
70
+ if (result.askCount > 0) {
71
+ console.log(` ${colors_js_1.YELLOW}Ask${colors_js_1.NC} ${colors_js_1.DIM}(${result.askCount})${colors_js_1.NC}`);
72
+ for (const group of result.askGroups) {
73
+ for (const item of group.items) {
74
+ console.log(` ${colors_js_1.DIM}ASK ${item.name}${colors_js_1.NC}`);
75
+ }
76
+ }
77
+ }
78
+ if (result.allowedTools.length > 0) {
79
+ console.log(` ${colors_js_1.CYAN}AllowedTools${colors_js_1.NC} ${colors_js_1.DIM}(${result.allowedTools.length})${colors_js_1.NC}`);
80
+ for (const t of result.allowedTools) {
81
+ console.log(` ${colors_js_1.DIM}${t}${colors_js_1.NC}`);
82
+ }
83
+ }
84
+ if (result.deniedTools.length > 0) {
85
+ console.log(` ${colors_js_1.RED}DeniedTools${colors_js_1.NC} ${colors_js_1.DIM}(${result.deniedTools.length})${colors_js_1.NC}`);
86
+ for (const t of result.deniedTools) {
87
+ console.log(` ${colors_js_1.DIM}${t}${colors_js_1.NC}`);
88
+ }
89
+ }
90
+ if (result.additionalDirectories.length > 0) {
91
+ console.log(` ${colors_js_1.DIM}AdditionalDirectories${colors_js_1.NC} ${colors_js_1.DIM}(${result.additionalDirectories.length})${colors_js_1.NC}`);
92
+ for (const d of result.additionalDirectories) {
93
+ console.log(` ${colors_js_1.DIM}${d}${colors_js_1.NC}`);
94
+ }
95
+ }
70
96
  console.log('');
71
97
  }
72
98
  if (projectsEmpty.length > 0) {
@@ -76,6 +102,7 @@ function printVerbose(results, summary) {
76
102
  function printFooter(summary) {
77
103
  console.log(`\n ${colors_js_1.DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors_js_1.NC}`);
78
104
  const catSummary = [...summary.categoryTotals.entries()].map(([k, v]) => `${k}: ${colors_js_1.BOLD}${v}${colors_js_1.NC}${colors_js_1.DIM}`).join(' ');
79
- const denySuffix = summary.totalDeny > 0 ? ` ${colors_js_1.BOLD}${summary.totalDeny}${colors_js_1.NC} deny rules` : '';
80
- console.log(` ${colors_js_1.BOLD}${summary.totalProjects}${colors_js_1.NC} projects ${colors_js_1.BOLD}${summary.totalPerms}${colors_js_1.NC} permissions${denySuffix} ${colors_js_1.DIM}(${catSummary})${colors_js_1.NC}\n`);
105
+ const denySuffix = summary.totalDeny > 0 ? ` ${colors_js_1.BOLD}${summary.totalDeny}${colors_js_1.NC} deny` : '';
106
+ const askSuffix = summary.totalAsk > 0 ? ` ${colors_js_1.BOLD}${summary.totalAsk}${colors_js_1.NC} ask` : '';
107
+ console.log(` ${colors_js_1.BOLD}${summary.totalProjects}${colors_js_1.NC} projects ${colors_js_1.BOLD}${summary.totalPerms}${colors_js_1.NC} permissions${denySuffix}${askSuffix} ${colors_js_1.DIM}(${catSummary})${colors_js_1.NC}\n`);
81
108
  }
package/dist/scanner.js CHANGED
@@ -210,11 +210,17 @@ function scanFile(filePath) {
210
210
  }
211
211
  const allowArr = Array.isArray(json?.permissions?.allow) ? json.permissions.allow : [];
212
212
  const denyArr = Array.isArray(json?.permissions?.deny) ? json.permissions.deny : [];
213
+ const askArr = Array.isArray(json?.permissions?.ask) ? json.permissions.ask : [];
213
214
  const perms = [...new Set(allowArr)].sort();
214
215
  const denyPerms = [...new Set(denyArr)].sort();
216
+ const askPerms = [...new Set(askArr)].sort();
215
217
  const groups = groupPermissions(perms);
216
218
  const denyGroups = groupPermissions(denyPerms);
217
- return { path: filePath, display, permissions: perms, groups, totalCount: perms.length, denyPermissions: denyPerms, denyGroups, denyCount: denyPerms.length, isGlobal };
219
+ const askGroups = groupPermissions(askPerms);
220
+ const allowedTools = Array.isArray(json?.allowedTools) ? [...new Set(json.allowedTools)].sort() : [];
221
+ const deniedTools = Array.isArray(json?.deniedTools) ? [...new Set(json.deniedTools)].sort() : [];
222
+ const additionalDirectories = Array.isArray(json?.additionalDirectories) ? [...new Set(json.additionalDirectories)].sort() : [];
223
+ return { path: filePath, display, permissions: perms, groups, totalCount: perms.length, denyPermissions: denyPerms, denyGroups, denyCount: denyPerms.length, askPermissions: askPerms, askGroups, askCount: askPerms.length, allowedTools, deniedTools, additionalDirectories, isGlobal };
218
224
  }
219
225
  function categorize(perm) {
220
226
  if (perm.startsWith('Bash')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"