@sureshsankaran/opencode-destructive-check 1.0.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 (3) hide show
  1. package/README.md +64 -0
  2. package/package.json +35 -0
  3. package/src/index.ts +391 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @sureshsankaran/opencode-destructive-check
2
+
3
+ An OpenCode plugin that automatically checks for destructive commands before any tool/bash call and asks for user permission before executing them.
4
+
5
+ ## Features
6
+
7
+ - Detects destructive commands across multiple categories
8
+ - Asks for permission for all severity levels (critical, high, medium)
9
+ - Works automatically for all sessions and agents
10
+ - Provides a status tool to check plugin statistics
11
+
12
+ ## Detected Destructive Patterns
13
+
14
+ | Category | Examples | Severity |
15
+ | --------------- | ------------------------------------------------------------------- | -------- |
16
+ | **rmDangerous** | `rm -rf /`, `rm -rf ~`, `rm .git` | Critical |
17
+ | **sudo** | `sudo rm -rf /`, `sudo chmod`, `sudo chown` | Critical |
18
+ | **system** | `chmod 777 /`, `dd of=/dev/`, `mkfs`, `fdisk` | Critical |
19
+ | **git** | `git push --force`, `git reset --hard`, `git clean -f` | High |
20
+ | **database** | `DROP TABLE`, `DELETE FROM` (without WHERE), `TRUNCATE` | High |
21
+ | **container** | `kubectl delete namespace`, `docker rm -f`, `aws s3 rm --recursive` | High |
22
+ | **packages** | `npm cache clean --force`, `pip uninstall -y` | Medium |
23
+ | **network** | `iptables -F`, `ufw reset` | Medium |
24
+
25
+ ## Installation
26
+
27
+ Add the plugin to your `opencode.json` or `.opencode/opencode.jsonc` config:
28
+
29
+ ```json
30
+ {
31
+ "plugin": ["@sureshsankaran/opencode-destructive-check"]
32
+ }
33
+ ```
34
+
35
+ ## How It Works
36
+
37
+ 1. **`tool.execute.before` hook**: Inspects every tool call before execution, logging warnings for dangerous patterns
38
+ 2. **`permission.ask` hook**: Requires user confirmation for all detected destructive operations
39
+ 3. **`tool.execute.after` hook**: Logs when the system blocks dangerous operations
40
+
41
+ When a destructive command is detected, the user will be prompted to approve or deny the operation, giving full control over whether to proceed.
42
+
43
+ ## Available Tools
44
+
45
+ ### `destructive-check-status`
46
+
47
+ Returns the current status of the plugin including:
48
+
49
+ - Number of commands checked
50
+ - Number of permissions requested
51
+ - Last matched destructive pattern
52
+ - Pattern categories and counts
53
+
54
+ ## Dangerous Paths
55
+
56
+ The plugin also protects against file operations on dangerous paths:
57
+
58
+ - System directories: `/`, `/home`, `/etc`, `/var`, `/usr`, `/bin`, etc.
59
+ - User directories: `~`, `$HOME`
60
+ - Project files: `.git`, `.env`, `.ssh`, `package.json`, `Cargo.toml`, etc.
61
+
62
+ ## License
63
+
64
+ MIT
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@sureshsankaran/opencode-destructive-check",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode plugin that checks for destructive commands before any tool/bash call and asks for permission",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "type": "module",
8
+ "keywords": [
9
+ "opencode",
10
+ "opencode-plugin",
11
+ "security",
12
+ "destructive-commands",
13
+ "permission",
14
+ "safety"
15
+ ],
16
+ "author": "Suresh Sankaran",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/anomalyco/opencode"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/anomalyco/opencode/issues"
24
+ },
25
+ "files": [
26
+ "src"
27
+ ],
28
+ "peerDependencies": {
29
+ "@opencode-ai/plugin": ">=1.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@opencode-ai/plugin": "^1.2.2",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Destructive Command Check Plugin for OpenCode
3
+ *
4
+ * Automatically checks for destructive commands before any tool/bash call.
5
+ * This plugin runs for all sessions and agents, protecting against potentially
6
+ * harmful operations by asking for user permission.
7
+ *
8
+ * Destructive patterns detected:
9
+ * - rm -rf, rm -fr, rm -r with dangerous paths
10
+ * - git push --force, git reset --hard
11
+ * - DROP TABLE, DELETE FROM, TRUNCATE
12
+ * - chmod 777, chown on system dirs
13
+ * - dd commands
14
+ * - format/mkfs commands
15
+ * - sudo rm, sudo chmod, sudo chown
16
+ * - kubectl delete, docker rm -f
17
+ * - aws s3 rm --recursive
18
+ *
19
+ * The plugin will:
20
+ * 1. Detect destructive patterns in bash commands
21
+ * 2. Detect destructive file operations (deleting important files)
22
+ * 3. Ask for user permission before executing dangerous operations
23
+ */
24
+
25
+ import type { Plugin } from "@opencode-ai/plugin"
26
+
27
+ // Destructive command patterns to check
28
+ const DESTRUCTIVE_PATTERNS = {
29
+ // File deletion - dangerous patterns
30
+ rmDangerous: [
31
+ /\brm\s+(-[rRf]+\s+)*[\/~]\s*$/i, // rm / or rm ~
32
+ /\brm\s+(-[rRf]+\s+)*\/\*/, // rm /*
33
+ /\brm\s+(-[rRf]+\s+)*~\/\*/, // rm ~/*
34
+ /\brm\s+(-[rRf]+\s+)*\$HOME\b/i, // rm $HOME
35
+ /\brm\s+(-[rRf]+\s+)*\/home\b/i, // rm /home
36
+ /\brm\s+(-[rRf]+\s+)*\/etc\b/i, // rm /etc
37
+ /\brm\s+(-[rRf]+\s+)*\/var\b/i, // rm /var
38
+ /\brm\s+(-[rRf]+\s+)*\/usr\b/i, // rm /usr
39
+ /\brm\s+(-[rRf]+\s+)*\/bin\b/i, // rm /bin
40
+ /\brm\s+(-[rRf]+\s+)*\/sbin\b/i, // rm /sbin
41
+ /\brm\s+(-[rRf]+\s+)*\/boot\b/i, // rm /boot
42
+ /\brm\s+(-[rRf]+\s+)*\/lib\b/i, // rm /lib
43
+ /\brm\s+(-[rRf]+\s+)*\/opt\b/i, // rm /opt
44
+ /\brm\s+(-[rRf]+\s+)*\/root\b/i, // rm /root
45
+ /\brm\s+(-[rRf]+\s+)*\/sys\b/i, // rm /sys
46
+ /\brm\s+(-[rRf]+\s+)*\/proc\b/i, // rm /proc
47
+ /\brm\s+(-[rRf]+\s+)*\/dev\b/i, // rm /dev
48
+ /\brm\s+(-[rRf]+\s+)*\/mnt\b/i, // rm /mnt
49
+ /\brm\s+(-[rRf]+\s+)*\/tmp\b/i, // rm /tmp
50
+ /\brm\s+(-[rRf]+\s+)*\.git\b/i, // rm .git
51
+ /\brm\s+(-[rRf]+\s+)*node_modules\b/i, // rm node_modules (dangerous in wrong dir)
52
+ ],
53
+
54
+ // Git destructive operations
55
+ git: [
56
+ /\bgit\s+push\s+.*--force\b/i, // git push --force
57
+ /\bgit\s+push\s+.*-f\b/i, // git push -f
58
+ /\bgit\s+reset\s+--hard\b/i, // git reset --hard
59
+ /\bgit\s+clean\s+.*-f/i, // git clean -f
60
+ /\bgit\s+checkout\s+--\s+\./i, // git checkout -- .
61
+ /\bgit\s+stash\s+drop/i, // git stash drop
62
+ /\bgit\s+branch\s+.*-D\b/i, // git branch -D
63
+ /\bgit\s+reflog\s+expire/i, // git reflog expire
64
+ /\bgit\s+gc\s+--prune/i, // git gc --prune
65
+ ],
66
+
67
+ // Database destructive operations
68
+ database: [
69
+ /\bDROP\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i,
70
+ /\bTRUNCATE\s+TABLE\b/i,
71
+ /\bDELETE\s+FROM\s+\S+\s*(;|\s*$)/i, // DELETE without WHERE
72
+ /\bALTER\s+TABLE\s+\S+\s+DROP\b/i,
73
+ ],
74
+
75
+ // System destructive operations
76
+ system: [
77
+ /\bchmod\s+(-R\s+)?777\s+\//i, // chmod 777 /
78
+ /\bchown\s+(-R\s+)?\S+\s+\//i, // chown on root
79
+ /\bdd\s+.*of=\/dev\//i, // dd to device
80
+ /\bmkfs\b/i, // Format filesystem
81
+ /\bformat\s+[a-z]:/i, // Windows format
82
+ /\bfdisk\b/i, // Partition tool
83
+ /\bparted\b/i, // Partition tool
84
+ ],
85
+
86
+ // Elevated privileges with destructive commands
87
+ sudo: [
88
+ /\bsudo\s+rm\s+(-[rRf]+\s+)*\//i, // sudo rm on root
89
+ /\bsudo\s+chmod\b/i, // sudo chmod
90
+ /\bsudo\s+chown\b/i, // sudo chown
91
+ /\bsudo\s+dd\b/i, // sudo dd
92
+ /\bsudo\s+mkfs\b/i, // sudo mkfs
93
+ ],
94
+
95
+ // Container/cloud destructive operations
96
+ container: [
97
+ /\bkubectl\s+delete\s+(namespace|ns|pod|deployment|service)\b/i,
98
+ /\bdocker\s+rm\s+.*-f/i, // docker rm -f
99
+ /\bdocker\s+system\s+prune\s+.*-a/i, // docker system prune -a
100
+ /\bdocker\s+volume\s+rm\b/i, // docker volume rm
101
+ /\baws\s+s3\s+rm\s+.*--recursive\b/i, // aws s3 rm --recursive
102
+ /\baws\s+ec2\s+terminate-instances\b/i, // terminate EC2
103
+ /\bgcloud\s+.*delete\b/i, // gcloud delete operations
104
+ ],
105
+
106
+ // Package manager destructive operations
107
+ packages: [
108
+ /\bnpm\s+cache\s+clean\s+--force\b/i, // npm cache clean --force
109
+ /\byarn\s+cache\s+clean\b/i, // yarn cache clean
110
+ /\bpip\s+uninstall\s+.*-y\b/i, // pip uninstall -y (auto-confirm)
111
+ /\bbrew\s+uninstall\s+--force\b/i, // brew uninstall --force
112
+ ],
113
+
114
+ // Network destructive operations
115
+ network: [
116
+ /\biptables\s+.*-F\b/i, // Flush iptables
117
+ /\biptables\s+.*--flush\b/i, // Flush iptables
118
+ /\bufw\s+reset\b/i, // Reset firewall
119
+ ],
120
+ }
121
+
122
+ // File paths that are dangerous to delete/modify
123
+ const DANGEROUS_PATHS = [
124
+ "/",
125
+ "/*",
126
+ "/home",
127
+ "/etc",
128
+ "/var",
129
+ "/usr",
130
+ "/bin",
131
+ "/sbin",
132
+ "/boot",
133
+ "/lib",
134
+ "/opt",
135
+ "/root",
136
+ "/sys",
137
+ "/proc",
138
+ "/dev",
139
+ "~",
140
+ "~/",
141
+ "$HOME",
142
+ ".git",
143
+ ".env",
144
+ ".ssh",
145
+ "package.json",
146
+ "package-lock.json",
147
+ "yarn.lock",
148
+ "bun.lockb",
149
+ "Cargo.toml",
150
+ "go.mod",
151
+ "pyproject.toml",
152
+ "requirements.txt",
153
+ ]
154
+
155
+ type DestructiveMatch = {
156
+ category: string
157
+ pattern: string
158
+ severity: "critical" | "high" | "medium"
159
+ command: string
160
+ }
161
+
162
+ // Check if a command matches any destructive pattern
163
+ function checkCommand(command: string): DestructiveMatch | null {
164
+ for (const [category, patterns] of Object.entries(DESTRUCTIVE_PATTERNS)) {
165
+ for (const pattern of patterns) {
166
+ if (pattern.test(command)) {
167
+ const severity = getSeverity(category)
168
+ return {
169
+ category,
170
+ pattern: pattern.toString(),
171
+ severity,
172
+ command,
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return null
178
+ }
179
+
180
+ // Determine severity based on category
181
+ function getSeverity(category: string): "critical" | "high" | "medium" {
182
+ if (category === "rmDangerous" || category === "sudo" || category === "system") {
183
+ return "critical"
184
+ }
185
+ if (category === "git" || category === "database" || category === "container") {
186
+ return "high"
187
+ }
188
+ return "medium"
189
+ }
190
+
191
+ // Check if a file path is dangerous
192
+ function isDangerousPath(path: string): boolean {
193
+ const normalized = path.replace(/\\/g, "/").toLowerCase()
194
+ return DANGEROUS_PATHS.some((dangerous) => {
195
+ const normalizedDangerous = dangerous.toLowerCase()
196
+ return (
197
+ normalized === normalizedDangerous ||
198
+ normalized.startsWith(normalizedDangerous + "/") ||
199
+ normalized.endsWith("/" + normalizedDangerous)
200
+ )
201
+ })
202
+ }
203
+
204
+ // Statistics tracking
205
+ type Stats = {
206
+ checked: number
207
+ permissionsRequested: number
208
+ lastMatch?: DestructiveMatch
209
+ }
210
+
211
+ // Plugin state per session
212
+ const sessions: Record<string, Stats> = {}
213
+
214
+ function getStats(sessionID: string): Stats {
215
+ if (!sessions[sessionID]) {
216
+ sessions[sessionID] = { checked: 0, permissionsRequested: 0 }
217
+ }
218
+ return sessions[sessionID]
219
+ }
220
+
221
+ // Global stats
222
+ const globalStats: Stats = { checked: 0, permissionsRequested: 0 }
223
+
224
+ /**
225
+ * Destructive Command Check Plugin
226
+ */
227
+ const destructiveCheck: Plugin = async () => {
228
+ return {
229
+ // Tool for checking plugin status
230
+ tool: {
231
+ "destructive-check-status": {
232
+ description: "Get the status of the destructive command check plugin",
233
+ args: {},
234
+ async execute(_args: {}, ctx: { sessionID: string }) {
235
+ const stats = getStats(ctx.sessionID)
236
+ return JSON.stringify(
237
+ {
238
+ enabled: true,
239
+ session: {
240
+ id: ctx.sessionID,
241
+ ...stats,
242
+ },
243
+ global: globalStats,
244
+ patterns: {
245
+ categories: Object.keys(DESTRUCTIVE_PATTERNS),
246
+ total: Object.values(DESTRUCTIVE_PATTERNS).flat().length,
247
+ },
248
+ dangerousPaths: DANGEROUS_PATHS.length,
249
+ },
250
+ null,
251
+ 2,
252
+ )
253
+ },
254
+ },
255
+ },
256
+
257
+ // Check before any tool executes - logs warnings for awareness
258
+ async ["tool.execute.before"](
259
+ hookInput: { tool: string; sessionID: string; callID: string },
260
+ output: { args: Record<string, unknown> },
261
+ ): Promise<void> {
262
+ const stats = getStats(hookInput.sessionID)
263
+ stats.checked++
264
+ globalStats.checked++
265
+
266
+ const tool = hookInput.tool.toLowerCase()
267
+ const args = output.args
268
+
269
+ // Check bash/shell commands
270
+ if (tool === "bash" || tool === "shell" || tool === "execute") {
271
+ const command = (args?.command as string) || ""
272
+ if (command) {
273
+ const match = checkCommand(command)
274
+ if (match) {
275
+ console.warn(
276
+ `[destructive-check] Detected ${match.severity.toUpperCase()} destructive command - permission will be requested`,
277
+ )
278
+ console.warn(` Category: ${match.category}`)
279
+ console.warn(` Command: ${match.command.slice(0, 100)}${match.command.length > 100 ? "..." : ""}`)
280
+ }
281
+ }
282
+ }
283
+
284
+ // Check file write/delete operations
285
+ if (tool === "write" || tool === "edit" || tool === "delete" || tool === "remove") {
286
+ const filePath = (args?.filePath as string) || (args?.path as string) || ""
287
+ if (filePath && isDangerousPath(filePath)) {
288
+ console.warn(`[destructive-check] Dangerous file operation detected - permission will be requested`)
289
+ console.warn(` Tool: ${tool}`)
290
+ console.warn(` Path: ${filePath}`)
291
+ }
292
+ }
293
+
294
+ // Check git operations via tool
295
+ if (tool === "git") {
296
+ const subcommand = (args?.subcommand as string) || (args?.command as string) || ""
297
+ const fullCommand = `git ${subcommand}`
298
+ const match = checkCommand(fullCommand)
299
+ if (match) {
300
+ console.warn(`[destructive-check] Destructive git operation detected - permission will be requested`)
301
+ console.warn(` Severity: ${match.severity.toUpperCase()}`)
302
+ console.warn(` Command: ${fullCommand}`)
303
+ }
304
+ }
305
+ },
306
+
307
+ // Permission hook to require user confirmation for destructive operations
308
+ async ["permission.ask"](
309
+ input: {
310
+ id: string
311
+ type: string
312
+ pattern?: string | string[]
313
+ sessionID: string
314
+ messageID: string
315
+ callID?: string
316
+ title: string
317
+ metadata: Record<string, unknown>
318
+ time: { created: number }
319
+ },
320
+ output: { status: "ask" | "deny" | "allow" },
321
+ ): Promise<void> {
322
+ const stats = getStats(input.sessionID)
323
+
324
+ // Check if this is a bash/command execution permission
325
+ if (input.type === "bash" || input.type === "command" || input.type === "shell") {
326
+ const command = (input.metadata?.command as string) || input.title || ""
327
+
328
+ if (command) {
329
+ const match = checkCommand(command)
330
+ if (match) {
331
+ stats.permissionsRequested++
332
+ globalStats.permissionsRequested++
333
+ stats.lastMatch = match
334
+
335
+ console.warn(
336
+ `[destructive-check] PERMISSION REQUIRED: ${match.severity.toUpperCase()} destructive command detected`,
337
+ )
338
+ console.warn(` Category: ${match.category}`)
339
+ console.warn(` Severity: ${match.severity}`)
340
+ console.warn(` Command: ${command.slice(0, 100)}${command.length > 100 ? "..." : ""}`)
341
+
342
+ // Ask for permission for all severity levels
343
+ output.status = "ask"
344
+ return
345
+ }
346
+ }
347
+ }
348
+
349
+ // Check file operations
350
+ if (input.type === "write" || input.type === "edit" || input.type === "delete") {
351
+ const patterns = Array.isArray(input.pattern) ? input.pattern : input.pattern ? [input.pattern] : []
352
+ for (const p of patterns) {
353
+ if (isDangerousPath(p)) {
354
+ stats.permissionsRequested++
355
+ globalStats.permissionsRequested++
356
+ console.warn(`[destructive-check] PERMISSION REQUIRED: Dangerous file operation detected`)
357
+ console.warn(` Path: ${p}`)
358
+
359
+ // Ask for permission for dangerous file operations
360
+ output.status = "ask"
361
+ return
362
+ }
363
+ }
364
+ }
365
+ },
366
+
367
+ // After tool execution - log results for destructive operations
368
+ async ["tool.execute.after"](
369
+ hookInput: { tool: string; sessionID: string; callID: string },
370
+ result: { title: string; output: string; metadata: Record<string, unknown> },
371
+ ): Promise<void> {
372
+ const tool = hookInput.tool.toLowerCase()
373
+
374
+ // Log completion of potentially dangerous operations
375
+ if (tool === "bash" || tool === "shell") {
376
+ const output = result.output || ""
377
+ // Check for error messages that might indicate dangerous operation attempted
378
+ if (
379
+ output.includes("Permission denied") ||
380
+ output.includes("Operation not permitted") ||
381
+ output.includes("cannot remove") ||
382
+ output.includes("rm: refusing")
383
+ ) {
384
+ console.log(`[destructive-check] Dangerous operation was blocked by system`)
385
+ }
386
+ }
387
+ },
388
+ }
389
+ }
390
+
391
+ export default destructiveCheck