@fml-inc/panopticon 0.1.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.
Files changed (124) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +5 -0
  3. package/README.md +363 -0
  4. package/bin/hook-handler +3 -0
  5. package/bin/mcp-server +3 -0
  6. package/bin/panopticon +3 -0
  7. package/bin/proxy +3 -0
  8. package/bin/server +3 -0
  9. package/dist/api/client.d.ts +67 -0
  10. package/dist/api/client.js +48 -0
  11. package/dist/api/client.js.map +1 -0
  12. package/dist/chunk-3BUJ7URA.js +387 -0
  13. package/dist/chunk-3BUJ7URA.js.map +1 -0
  14. package/dist/chunk-3TZAKV3M.js +158 -0
  15. package/dist/chunk-3TZAKV3M.js.map +1 -0
  16. package/dist/chunk-4SM2H22C.js +169 -0
  17. package/dist/chunk-4SM2H22C.js.map +1 -0
  18. package/dist/chunk-7Q3BJMLG.js +62 -0
  19. package/dist/chunk-7Q3BJMLG.js.map +1 -0
  20. package/dist/chunk-BVOE7A2Z.js +412 -0
  21. package/dist/chunk-BVOE7A2Z.js.map +1 -0
  22. package/dist/chunk-CF4GPWLI.js +170 -0
  23. package/dist/chunk-CF4GPWLI.js.map +1 -0
  24. package/dist/chunk-DZ5HJFB4.js +467 -0
  25. package/dist/chunk-DZ5HJFB4.js.map +1 -0
  26. package/dist/chunk-HQCY722C.js +428 -0
  27. package/dist/chunk-HQCY722C.js.map +1 -0
  28. package/dist/chunk-HRCEIYKU.js +134 -0
  29. package/dist/chunk-HRCEIYKU.js.map +1 -0
  30. package/dist/chunk-K7YUPLES.js +76 -0
  31. package/dist/chunk-K7YUPLES.js.map +1 -0
  32. package/dist/chunk-L7G27XWF.js +130 -0
  33. package/dist/chunk-L7G27XWF.js.map +1 -0
  34. package/dist/chunk-LWXF7YRG.js +626 -0
  35. package/dist/chunk-LWXF7YRG.js.map +1 -0
  36. package/dist/chunk-NXH7AONS.js +1120 -0
  37. package/dist/chunk-NXH7AONS.js.map +1 -0
  38. package/dist/chunk-QK5442ZP.js +55 -0
  39. package/dist/chunk-QK5442ZP.js.map +1 -0
  40. package/dist/chunk-QVK6VGCV.js +1703 -0
  41. package/dist/chunk-QVK6VGCV.js.map +1 -0
  42. package/dist/chunk-RX2RXHBH.js +1699 -0
  43. package/dist/chunk-RX2RXHBH.js.map +1 -0
  44. package/dist/chunk-SEXU2WYG.js +788 -0
  45. package/dist/chunk-SEXU2WYG.js.map +1 -0
  46. package/dist/chunk-SUGSQ4YI.js +264 -0
  47. package/dist/chunk-SUGSQ4YI.js.map +1 -0
  48. package/dist/chunk-TGXFVAID.js +138 -0
  49. package/dist/chunk-TGXFVAID.js.map +1 -0
  50. package/dist/chunk-WLBNFVIG.js +447 -0
  51. package/dist/chunk-WLBNFVIG.js.map +1 -0
  52. package/dist/chunk-XLTCUH5A.js +1072 -0
  53. package/dist/chunk-XLTCUH5A.js.map +1 -0
  54. package/dist/chunk-YVRWVDIA.js +146 -0
  55. package/dist/chunk-YVRWVDIA.js.map +1 -0
  56. package/dist/chunk-ZEC4LRKS.js +176 -0
  57. package/dist/chunk-ZEC4LRKS.js.map +1 -0
  58. package/dist/cli.d.ts +1 -0
  59. package/dist/cli.js +1084 -0
  60. package/dist/cli.js.map +1 -0
  61. package/dist/config-NwoZC-GM.d.ts +20 -0
  62. package/dist/db.d.ts +46 -0
  63. package/dist/db.js +15 -0
  64. package/dist/db.js.map +1 -0
  65. package/dist/doctor.d.ts +37 -0
  66. package/dist/doctor.js +14 -0
  67. package/dist/doctor.js.map +1 -0
  68. package/dist/hooks/handler.d.ts +23 -0
  69. package/dist/hooks/handler.js +295 -0
  70. package/dist/hooks/handler.js.map +1 -0
  71. package/dist/index.d.ts +57 -0
  72. package/dist/index.js +101 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/mcp/server.d.ts +1 -0
  75. package/dist/mcp/server.js +243 -0
  76. package/dist/mcp/server.js.map +1 -0
  77. package/dist/otlp/server.d.ts +7 -0
  78. package/dist/otlp/server.js +17 -0
  79. package/dist/otlp/server.js.map +1 -0
  80. package/dist/permissions.d.ts +33 -0
  81. package/dist/permissions.js +14 -0
  82. package/dist/permissions.js.map +1 -0
  83. package/dist/pricing.d.ts +29 -0
  84. package/dist/pricing.js +13 -0
  85. package/dist/pricing.js.map +1 -0
  86. package/dist/proxy/server.d.ts +10 -0
  87. package/dist/proxy/server.js +20 -0
  88. package/dist/proxy/server.js.map +1 -0
  89. package/dist/prune.d.ts +18 -0
  90. package/dist/prune.js +13 -0
  91. package/dist/prune.js.map +1 -0
  92. package/dist/query.d.ts +56 -0
  93. package/dist/query.js +27 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/reparse-636YZCE3.js +14 -0
  96. package/dist/reparse-636YZCE3.js.map +1 -0
  97. package/dist/repo.d.ts +17 -0
  98. package/dist/repo.js +9 -0
  99. package/dist/repo.js.map +1 -0
  100. package/dist/scanner.d.ts +73 -0
  101. package/dist/scanner.js +15 -0
  102. package/dist/scanner.js.map +1 -0
  103. package/dist/sdk.d.ts +82 -0
  104. package/dist/sdk.js +208 -0
  105. package/dist/sdk.js.map +1 -0
  106. package/dist/server.d.ts +5 -0
  107. package/dist/server.js +25 -0
  108. package/dist/server.js.map +1 -0
  109. package/dist/setup.d.ts +35 -0
  110. package/dist/setup.js +19 -0
  111. package/dist/setup.js.map +1 -0
  112. package/dist/sync/index.d.ts +29 -0
  113. package/dist/sync/index.js +32 -0
  114. package/dist/sync/index.js.map +1 -0
  115. package/dist/targets.d.ts +279 -0
  116. package/dist/targets.js +20 -0
  117. package/dist/targets.js.map +1 -0
  118. package/dist/types-D-MYCBol.d.ts +128 -0
  119. package/dist/types.d.ts +164 -0
  120. package/dist/types.js +1 -0
  121. package/dist/types.js.map +1 -0
  122. package/hooks/hooks.json +274 -0
  123. package/package.json +124 -0
  124. package/skills/panopticon-optimize/SKILL.md +222 -0
@@ -0,0 +1,274 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
9
+ }
10
+ ]
11
+ }
12
+ ],
13
+ "SessionEnd": [
14
+ {
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
19
+ }
20
+ ]
21
+ }
22
+ ],
23
+ "Setup": [
24
+ {
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
29
+ }
30
+ ]
31
+ }
32
+ ],
33
+ "UserPromptSubmit": [
34
+ {
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "PreToolUse": [
44
+ {
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
49
+ }
50
+ ]
51
+ }
52
+ ],
53
+ "PostToolUse": [
54
+ {
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
59
+ }
60
+ ]
61
+ }
62
+ ],
63
+ "PostToolUseFailure": [
64
+ {
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
69
+ }
70
+ ]
71
+ }
72
+ ],
73
+ "PermissionRequest": [
74
+ {
75
+ "hooks": [
76
+ {
77
+ "type": "command",
78
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
79
+ }
80
+ ]
81
+ }
82
+ ],
83
+ "PermissionDenied": [
84
+ {
85
+ "hooks": [
86
+ {
87
+ "type": "command",
88
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
89
+ }
90
+ ]
91
+ }
92
+ ],
93
+ "Stop": [
94
+ {
95
+ "hooks": [
96
+ {
97
+ "type": "command",
98
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
99
+ }
100
+ ]
101
+ }
102
+ ],
103
+ "StopFailure": [
104
+ {
105
+ "hooks": [
106
+ {
107
+ "type": "command",
108
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
109
+ }
110
+ ]
111
+ }
112
+ ],
113
+ "SubagentStart": [
114
+ {
115
+ "hooks": [
116
+ {
117
+ "type": "command",
118
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
119
+ }
120
+ ]
121
+ }
122
+ ],
123
+ "SubagentStop": [
124
+ {
125
+ "hooks": [
126
+ {
127
+ "type": "command",
128
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
129
+ }
130
+ ]
131
+ }
132
+ ],
133
+ "PreCompact": [
134
+ {
135
+ "hooks": [
136
+ {
137
+ "type": "command",
138
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
139
+ }
140
+ ]
141
+ }
142
+ ],
143
+ "PostCompact": [
144
+ {
145
+ "hooks": [
146
+ {
147
+ "type": "command",
148
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
149
+ }
150
+ ]
151
+ }
152
+ ],
153
+ "Notification": [
154
+ {
155
+ "hooks": [
156
+ {
157
+ "type": "command",
158
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
159
+ }
160
+ ]
161
+ }
162
+ ],
163
+ "TeammateIdle": [
164
+ {
165
+ "hooks": [
166
+ {
167
+ "type": "command",
168
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
169
+ }
170
+ ]
171
+ }
172
+ ],
173
+ "TaskCreated": [
174
+ {
175
+ "hooks": [
176
+ {
177
+ "type": "command",
178
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
179
+ }
180
+ ]
181
+ }
182
+ ],
183
+ "TaskCompleted": [
184
+ {
185
+ "hooks": [
186
+ {
187
+ "type": "command",
188
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
189
+ }
190
+ ]
191
+ }
192
+ ],
193
+ "Elicitation": [
194
+ {
195
+ "hooks": [
196
+ {
197
+ "type": "command",
198
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
199
+ }
200
+ ]
201
+ }
202
+ ],
203
+ "ElicitationResult": [
204
+ {
205
+ "hooks": [
206
+ {
207
+ "type": "command",
208
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
209
+ }
210
+ ]
211
+ }
212
+ ],
213
+ "ConfigChange": [
214
+ {
215
+ "hooks": [
216
+ {
217
+ "type": "command",
218
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
219
+ }
220
+ ]
221
+ }
222
+ ],
223
+ "InstructionsLoaded": [
224
+ {
225
+ "hooks": [
226
+ {
227
+ "type": "command",
228
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
229
+ }
230
+ ]
231
+ }
232
+ ],
233
+ "CwdChanged": [
234
+ {
235
+ "hooks": [
236
+ {
237
+ "type": "command",
238
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
239
+ }
240
+ ]
241
+ }
242
+ ],
243
+ "FileChanged": [
244
+ {
245
+ "hooks": [
246
+ {
247
+ "type": "command",
248
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
249
+ }
250
+ ]
251
+ }
252
+ ],
253
+ "WorktreeCreate": [
254
+ {
255
+ "hooks": [
256
+ {
257
+ "type": "command",
258
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
259
+ }
260
+ ]
261
+ }
262
+ ],
263
+ "WorktreeRemove": [
264
+ {
265
+ "hooks": [
266
+ {
267
+ "type": "command",
268
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/hook-handler claude"
269
+ }
270
+ ]
271
+ }
272
+ ]
273
+ }
274
+ }
package/package.json ADDED
@@ -0,0 +1,124 @@
1
+ {
2
+ "name": "@fml-inc/panopticon",
3
+ "version": "0.1.0",
4
+ "description": "Observability for Claude Code — captures OTel signals and hook events, queryable via MCP",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
9
+ "publishConfig": {
10
+ "registry": "https://registry.npmjs.org",
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/fml-inc/panopticon.git"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "dist/",
20
+ "hooks/",
21
+ "skills/",
22
+ ".claude-plugin/"
23
+ ],
24
+ "bin": {
25
+ "panopticon": "./bin/panopticon"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./sdk": {
33
+ "types": "./dist/sdk.d.ts",
34
+ "import": "./dist/sdk.js"
35
+ },
36
+ "./db": {
37
+ "types": "./dist/db.d.ts",
38
+ "import": "./dist/db.js"
39
+ },
40
+ "./types": {
41
+ "types": "./dist/types.d.ts",
42
+ "import": "./dist/types.js"
43
+ },
44
+ "./query": {
45
+ "types": "./dist/query.d.ts",
46
+ "import": "./dist/query.js"
47
+ },
48
+ "./server": {
49
+ "types": "./dist/server.d.ts",
50
+ "import": "./dist/server.js"
51
+ },
52
+ "./setup": {
53
+ "types": "./dist/setup.d.ts",
54
+ "import": "./dist/setup.js"
55
+ },
56
+ "./prune": {
57
+ "types": "./dist/prune.d.ts",
58
+ "import": "./dist/prune.js"
59
+ },
60
+ "./pricing": {
61
+ "types": "./dist/pricing.d.ts",
62
+ "import": "./dist/pricing.js"
63
+ },
64
+ "./permissions": {
65
+ "types": "./dist/permissions.d.ts",
66
+ "import": "./dist/permissions.js"
67
+ },
68
+ "./doctor": {
69
+ "types": "./dist/doctor.d.ts",
70
+ "import": "./dist/doctor.js"
71
+ },
72
+ "./repo": {
73
+ "types": "./dist/repo.d.ts",
74
+ "import": "./dist/repo.js"
75
+ },
76
+ "./scanner": {
77
+ "types": "./dist/scanner.d.ts",
78
+ "import": "./dist/scanner.js"
79
+ },
80
+ "./api": {
81
+ "types": "./dist/api/client.d.ts",
82
+ "import": "./dist/api/client.js"
83
+ },
84
+ "./sync": {
85
+ "types": "./dist/sync/index.d.ts",
86
+ "import": "./dist/sync/index.js"
87
+ },
88
+ "./targets": {
89
+ "types": "./dist/targets.d.ts",
90
+ "import": "./dist/targets.js"
91
+ }
92
+ },
93
+ "dependencies": {
94
+ "@modelcontextprotocol/sdk": "^1.0.0",
95
+ "@sentry/core": "^10.46.0",
96
+ "better-sqlite3": "^11.0.0",
97
+ "commander": "^14.0.3",
98
+ "protobufjs": "^7.0.0",
99
+ "smol-toml": "^1.3.0",
100
+ "tslog": "^4.10.2",
101
+ "zod": "^4.3.6"
102
+ },
103
+ "devDependencies": {
104
+ "@biomejs/biome": "^2.4.4",
105
+ "@types/better-sqlite3": "^7.0.0",
106
+ "@types/node": "^22.0.0",
107
+ "lefthook": "^2.1.1",
108
+ "tsup": "^8.0.0",
109
+ "typescript": "^5",
110
+ "vitest": "^4.1.0"
111
+ },
112
+ "scripts": {
113
+ "build": "tsup",
114
+ "dev": "tsup --watch",
115
+ "check": "biome check src/",
116
+ "check:fix": "biome check --write src/",
117
+ "format": "biome format --write src/",
118
+ "test": "vitest run --exclude '.claude/**'",
119
+ "test:watch": "vitest --exclude '.claude/**'",
120
+ "typecheck": "tsc --noEmit",
121
+ "pack:local": "pnpm build && pnpm pack",
122
+ "postinstall": "node ./bin/panopticon install && claude plugin update panopticon@local-plugins 2>/dev/null || true"
123
+ }
124
+ }
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: panopticon-optimize
3
+ description: "Analyze tool usage from panopticon, categorize by risk level, and generate optimized permission rules. Remembers approved categories across runs."
4
+ ---
5
+
6
+ # Panopticon Optimize
7
+
8
+ Analyze all tool usage captured by panopticon for the current repository, categorize each pattern by risk level, auto-allow safe patterns, and prompt for approval of higher-risk categories. Category approvals persist across runs.
9
+
10
+ ## Architecture
11
+
12
+ All permissions are enforced via panopticon's `PreToolUse` hook — panopticon does **not** write to `settings.local.json`.
13
+
14
+ - **Bash commands** → chain-aware enforcement: the hook splits chains (`&&`, `;`, `|`) and checks each component independently against approved base commands
15
+ - **Non-Bash tools** → exact name match against the allowed tools list
16
+
17
+ Both are stored in `~/.local/share/panopticon/permissions/allowed.json`. The hook returns `"permissionDecision": "allow"` only when the tool or all chain components match. Unmatched tools fall through to Claude Code's normal prompting.
18
+
19
+ ## MCP Tools
20
+
21
+ - **`panopticon_permissions_show`** — Load existing approvals + current allowed tools/commands. Call this first (no arguments needed).
22
+ - **`panopticon_permissions_apply`** — Write allowed.json (Bash commands + tool names), save approvals, and create backup. Call this at the end.
23
+
24
+ All analysis and querying uses the standard `panopticon_query` tool.
25
+
26
+ ---
27
+
28
+ ## Step 1 — Load State
29
+
30
+ Call `panopticon_permissions_show` (no arguments). It returns:
31
+ - `approvals` — previously approved/denied categories and custom overrides
32
+ - `allowed` — current `{ bash_commands, tools }` list
33
+ - File paths for reference
34
+
35
+ `"safe"` is always pre-approved and cannot be removed.
36
+
37
+ ## Step 2 — Identify Current Repository
38
+
39
+ Run `git remote get-url origin` and extract `org/repo` (strip `.git` suffix and host prefix). Used for backup metadata only — **not** for scoping queries, since the whitelist is global.
40
+
41
+ ## Step 3 — Query Panopticon
42
+
43
+ The allowed list is global (not per-repo), so queries must aggregate across **all** repositories.
44
+
45
+ Run via `panopticon_query`:
46
+
47
+ **Query A — Non-Bash tools:**
48
+ ```sql
49
+ SELECT tool_name, COUNT(*) as cnt
50
+ FROM hook_events
51
+ WHERE event_type = 'PreToolUse'
52
+ AND tool_name != 'Bash'
53
+ GROUP BY tool_name ORDER BY cnt DESC
54
+ ```
55
+
56
+ **Query B — All Bash commands (full command strings):**
57
+ ```sql
58
+ SELECT
59
+ json_extract(json_extract(decompress(payload), '$.tool_input'), '$.command') as cmd,
60
+ COUNT(*) as cnt
61
+ FROM hook_events
62
+ WHERE event_type = 'PreToolUse' AND tool_name = 'Bash'
63
+ GROUP BY cmd ORDER BY cnt DESC
64
+ LIMIT 500
65
+ ```
66
+
67
+ ## Step 4 — Classify
68
+
69
+ For each observed Bash command:
70
+ 1. Split on chain operators (`&&`, `;`, `|`) to extract individual commands
71
+ 2. For each individual command, extract the **base command** — the first token, or first two tokens for `git`/`gh`/`npx`/`pnpm` subcommands (e.g., `git status`, `npx tsup`)
72
+ 3. Classify each base command into a risk category (see below)
73
+ 4. Collect all unique base commands across all observed usage
74
+
75
+ ### Base command extraction
76
+
77
+ The hook uses the same algorithm, so patterns must match:
78
+ - Simple commands: first token → `ls`, `cat`, `rm`
79
+ - Compound CLI tools: first two tokens → `git status`, `npx tsup`, `pnpm install`, `gh pr`, `xargs grep`
80
+ - Transparent wrappers (compound): `env`, `nice`, `timeout`, `watch` — skip flags/positional args, extract delegated command → `timeout 30 rm` → `timeout rm`, `env NODE_ENV=prod node` → `env node`
81
+ - `find -exec` / `-execdir`: returns **both** `find` and the delegated command → `find . -exec rm {} \;` → `["find", "rm"]` — both must be allowed
82
+ - Shell re-entry: `bash -c` / `sh -c` → base command is `bash`/`sh` (classify as high_destructive)
83
+ - Env var prefixes stripped: `FOO=bar git push` → `git push`
84
+ - Redirections stripped: `ls 2>&1` → `ls`
85
+
86
+ ### Risk categories
87
+
88
+ #### Category: `safe` — Read-only, zero side effects
89
+ **Always auto-approved.**
90
+
91
+ Non-Bash tools: `Read`, `Grep`, `Glob`, `ToolSearch`, `Agent`, `EnterPlanMode`, `ExitPlanMode`, `TaskOutput`, `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet`, and any `mcp__plugin_panopticon_*` tool.
92
+
93
+ Bash base commands:
94
+ - **Read-only fs**: `find`, `ls`, `cat`, `head`, `tail`, `wc`, `pwd`, `echo`, `diff`, `readlink`, `file`, `which`, `type`, `env`, `printenv`, `basename`, `dirname`, `cd`, `mkdir`, `stat`, `du`, `realpath`
95
+ - **Read-only search/text**: `grep`, `rg`, `sort`, `uniq`, `tr`, `cut`, `jq`
96
+ - **Read-only system**: `date`, `uname`, `printf`, `true`, `false`, `test`
97
+ - **Read-only git**: `git status`, `git diff`, `git log`, `git show`, `git blame`, `git branch`, `git ls-tree`, `git merge-base`, `git fetch`, `git remote`, `git rev-parse`, `git describe`, `git tag`
98
+
99
+ #### Category: `low_check` — Lint & type-check (read-only analysis)
100
+
101
+ Base commands: `pnpm type-check`, `pnpm lint`, `npx eslint`, `npx tsc`
102
+
103
+ #### Category: `medium_build` — Local build artifacts only
104
+
105
+ Base commands: `npx tsup`, `npx prettier`, `pnpm exec`
106
+
107
+ #### Category: `medium_deps` — Dependency & formatting changes
108
+
109
+ Base commands: `pnpm format`, `pnpm install`, `pnpm rebuild`
110
+
111
+ #### Category: `medium_git_write` — Local git mutations
112
+
113
+ Base commands: `git add`, `git checkout`, `git stash`, `git commit`, `git rebase`, `git cherry-pick`, `git pull`, `git worktree`, `git merge`
114
+
115
+ #### Category: `high_git_remote` — Remote git & GitHub operations
116
+
117
+ Base commands: `git push`, `gh pr`, `gh run`, `gh api`
118
+
119
+ #### Category: `medium_fs_write` — Non-destructive file operations
120
+
121
+ Base commands: `cp`, `mv`, `touch`, `rmdir`
122
+
123
+ Note: `rmdir` only removes empty directories (fails on non-empty). Less risky than `rm`.
124
+
125
+ Compound commands (`xargs`, `env`, `nice`, `timeout`, `watch`) inherit the delegated command's category. `xargs grep` → safe (grep is safe), `timeout rm` → high_destructive (rm is destructive), `env node` → high_destructive (node is destructive). Classify `{wrapper} {subcmd}` into whatever category `{subcmd}` belongs to.
126
+
127
+ For `find -exec`/`-execdir`, both `find` (safe) and the delegated command must be allowed. If the delegated command is `rm`, the whole command falls into the highest-risk category of its components (high_destructive).
128
+
129
+ #### Category: `high_destructive` — Destructive or arbitrary execution
130
+
131
+ Base commands: `rm`, `pkill`, `kill`, `node`, `python3`, `python`, `sed`, `bash`, `sh`
132
+
133
+ **Default: deny.**
134
+
135
+ #### Category: `high_infra` — Infrastructure & deployment
136
+
137
+ Base commands: `npx convex`, `npx dotenvx`, `docker`, `fly`, `pnpm build`, `pnpm run`, `pnpm dev`, `curl`
138
+
139
+ **Default: deny.**
140
+
141
+ #### Category: `web` — Web access
142
+
143
+ `WebSearch`, `WebFetch`. For WebFetch, extract observed domains and generate domain-restricted patterns.
144
+
145
+ #### Category: `mcp_external` — Non-panopticon MCP/plugin tools
146
+
147
+ Any `mcp__plugin_fml_*`, `mcp__claude_ai_*`, `mcp__discjockey__*`, etc. Present per-plugin.
148
+
149
+ ## Step 5 — Generate Permission Patterns
150
+
151
+ For each approved category, generate permission patterns based on observed usage.
152
+
153
+ ### Non-Bash tools
154
+
155
+ Use the tool name directly (e.g., `WebSearch`, `mcp__plugin_panopticon_panopticon__panopticon_query`).
156
+
157
+ ### Bash commands
158
+
159
+ For each unique base command observed in panopticon data that falls within an approved category, generate `Bash({base_command} *)`. The `panopticon_permissions_apply` tool splits these into the `bash_commands` list in `allowed.json`.
160
+
161
+ ### Only generate for observed commands
162
+
163
+ Don't generate patterns for commands never seen in panopticon data. The patterns should reflect actual usage, not hypothetical commands.
164
+
165
+ ## Step 6 — Present to User
166
+
167
+ **Summary table first:**
168
+
169
+ ```
170
+ Category Risk Patterns Calls Status
171
+ ──────────────────────────────────────────────────────────
172
+ safe none 14 1,247 always approved
173
+ low_check low 4 50 previously approved
174
+ medium_build medium 3 9 ? pending
175
+ medium_deps medium 3 15 x previously denied
176
+ ```
177
+
178
+ **Then for each pending category**, show:
179
+ 1. Category name, risk level, one-line description
180
+ 2. Observed base commands and call counts
181
+ 3. Permission patterns that would be generated
182
+ 4. Ask: **"Approve `{category}`? (y = approve / n = deny / s = skip)"**
183
+
184
+ - **y**: Add to `approved_categories`
185
+ - **n**: Add to `denied_categories` (won't ask again)
186
+ - **s**: Skip (will ask next run)
187
+
188
+ For `denied_categories`, show as denied with option to re-evaluate.
189
+
190
+ ## Step 7 — Apply
191
+
192
+ Call `panopticon_permissions_apply` with:
193
+ - `repository` (optional) — org/repo slug, included in backup metadata
194
+ - `approved_categories` — all approved (including previously approved from state)
195
+ - `denied_categories` — all denied (including previously denied from state)
196
+ - `custom_overrides` — any per-pattern overrides
197
+ - `permissions` — the full permission patterns list (Bash and non-Bash mixed — the tool splits them)
198
+ - `categories` — full category breakdown (for backup)
199
+
200
+ The tool handles atomically:
201
+ 1. Writing `allowed.json` with Bash base commands + tool names (for hook enforcement)
202
+ 2. Saving approvals state
203
+ 3. Creating timestamped backup
204
+
205
+ ## Step 8 — Summary
206
+
207
+ Print:
208
+ ```
209
+ Panopticon Optimize — Complete
210
+ ═══════════════════════════════
211
+ Repository: org/repo
212
+ Analyzed: N tool calls across M sessions
213
+
214
+ allowed.json (hook enforcement):
215
+ X Bash base commands with chain-aware matching
216
+ Y non-Bash tools with exact name matching
217
+
218
+ Backup saved to ~/.local/share/panopticon/permissions/backups/...
219
+
220
+ Run /panopticon-optimize again to update as new patterns appear.
221
+ To reset all decisions: rm ~/.local/share/panopticon/permissions/approvals.json
222
+ ```