destructive_command_guard 0.4.5

A Claude Code hook that blocks destructive commands before they execute
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
//! Disk patterns - protections against destructive disk operations.
//!
//! This includes patterns for:
//! - dd to block devices
//! - fdisk/parted operations
//! - mkfs (formatting)
//! - mount/umount operations
//! - mdadm RAID management
//! - btrfs filesystem operations
//! - dmsetup device-mapper operations
//! - nbd-client network block device
//! - LVM destructive commands (pvremove, vgremove, lvremove, etc.)

use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};

/// Create the Disk pack.
#[must_use]
pub fn create_pack() -> Pack {
    Pack {
        id: "system.disk".to_string(),
        name: "Disk Operations",
        description: "Protects against destructive disk operations like dd to devices, \
                      mkfs, partition table modifications, RAID management, \
                      btrfs/LVM/device-mapper operations, and network block devices",
        keywords: &[
            "dd",
            "fdisk",
            "mkfs",
            "parted",
            "mount",
            "wipefs",
            "/dev/",
            "mdadm",
            "btrfs",
            "dmsetup",
            "nbd-client",
            "pvremove",
            "vgremove",
            "lvremove",
            "vgreduce",
            "lvreduce",
            "lvresize",
            "pvmove",
        ],
        safe_patterns: create_safe_patterns(),
        destructive_patterns: create_destructive_patterns(),
        keyword_matcher: None,
        safe_regex_set: None,
        safe_regex_set_is_complete: false,
    }
}

fn create_safe_patterns() -> Vec<SafePattern> {
    vec![
        // dd to regular files is generally safe
        safe_pattern!("dd-file-out", r#"dd\s+.*of=['"]?[^/\s'"]+\."#),
        // dd to /dev/null|zero|full is safe (discard output). Accept optional
        // quotes so `dd of="/dev/null"` still short-circuits as safe.
        safe_pattern!(
            "dd-discard",
            r#"dd\s+.*of=['"]?/dev/(?:null|zero|full)['"]?(?:\s|$)"#
        ),
        // lsblk is safe (read-only)
        safe_pattern!("lsblk", r"\blsblk\b"),
        // fdisk -l (list) is safe
        safe_pattern!("fdisk-list", r"fdisk\s+-l"),
        // parted print is safe
        safe_pattern!("parted-print", r"parted\s+.*print"),
        // blkid is safe (read-only)
        safe_pattern!("blkid", r"\bblkid\b"),
        // df is safe
        safe_pattern!("df", r"\bdf\b"),
        // mount (without arguments, just list)
        safe_pattern!("mount-list", r"\bmount\s*$"),
        // --- mdadm safe patterns ---
        // mdadm --detail (read-only inspection)
        safe_pattern!("mdadm-detail", r"mdadm\s+--detail\b"),
        // mdadm --examine (read-only superblock inspection)
        safe_pattern!("mdadm-examine", r"mdadm\s+--examine\b"),
        // mdadm --query (read-only query)
        safe_pattern!("mdadm-query", r"mdadm\s+--query\b"),
        // mdadm -Q (short form of --query)
        safe_pattern!("mdadm-query-short", r"mdadm\s+-Q\b"),
        // mdadm --scan (scan for arrays, read-only)
        safe_pattern!("mdadm-scan", r"mdadm\s+--scan\b"),
        // --- btrfs safe patterns ---
        // btrfs subvolume list (read-only)
        safe_pattern!(
            "btrfs-subvolume-list",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+subvolume\s+list(?=\s|$)"
        ),
        // btrfs subvolume show (read-only)
        safe_pattern!(
            "btrfs-subvolume-show",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+subvolume\s+show(?=\s|$)"
        ),
        // btrfs filesystem show (read-only)
        safe_pattern!(
            "btrfs-filesystem-show",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+filesystem\s+show(?=\s|$)"
        ),
        // btrfs filesystem df (read-only)
        safe_pattern!(
            "btrfs-filesystem-df",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+filesystem\s+df(?=\s|$)"
        ),
        // btrfs filesystem usage (read-only)
        safe_pattern!(
            "btrfs-filesystem-usage",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+filesystem\s+usage(?=\s|$)"
        ),
        // btrfs device stats (read-only)
        safe_pattern!(
            "btrfs-device-stats",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+device\s+stats(?=\s|$)"
        ),
        // btrfs property get/list (read-only)
        safe_pattern!(
            "btrfs-property-get",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+property\s+(?:get|list)(?=\s|$)"
        ),
        // btrfs scrub status (read-only)
        safe_pattern!(
            "btrfs-scrub-status",
            r"btrfs\b(?:\s+--?\S+(?:\s+\S+)?)*\s+scrub\s+status(?=\s|$)"
        ),
        // --- dmsetup safe patterns ---
        // dmsetup ls (list devices)
        safe_pattern!(
            "dmsetup-ls",
            r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ls(?=\s|$)"
        ),
        // dmsetup status (show status)
        safe_pattern!(
            "dmsetup-status",
            r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+status(?=\s|$)"
        ),
        // dmsetup info (show info)
        safe_pattern!(
            "dmsetup-info",
            r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+info(?=\s|$)"
        ),
        // dmsetup table (show mapping table)
        safe_pattern!(
            "dmsetup-table",
            r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+table(?=\s|$)"
        ),
        // dmsetup deps (show dependencies)
        safe_pattern!(
            "dmsetup-deps",
            r"dmsetup\b(?:\s+--?\S+(?:\s+\S+)?)*\s+deps(?=\s|$)"
        ),
        // --- nbd-client safe patterns ---
        // nbd-client -l (list exports)
        safe_pattern!("nbd-client-list", r"nbd-client\s+-l\b"),
        // nbd-client -check (check connection)
        safe_pattern!("nbd-client-check", r"nbd-client\s+.*-check\b"),
        // --- LVM safe patterns (read-only) ---
        // lvs, vgs, pvs (list commands)
        safe_pattern!("lvm-list", r"\b(?:lvs|vgs|pvs)\b"),
        // lvdisplay, vgdisplay, pvdisplay (display commands)
        safe_pattern!("lvm-display", r"\b(?:lvdisplay|vgdisplay|pvdisplay)\b"),
        // lvscan, vgscan, pvscan (scan commands)
        safe_pattern!("lvm-scan", r"\b(?:lvscan|vgscan|pvscan)\b"),
    ]
}

fn create_destructive_patterns() -> Vec<DestructivePattern> {
    vec![
        // dd to block devices. Accept optional quotes around the device path
        // (`dd of="/dev/sda"` unquotes to `of=/dev/sda` before exec).
        destructive_pattern!(
            "dd-device",
            r#"dd\s+.*of=['"]?/dev/"#,
            "dd to a block device will OVERWRITE all data on that device. Extremely dangerous!"
        ),
        // dd with if=/dev/zero or if=/dev/urandom to devices
        destructive_pattern!(
            "dd-wipe",
            r#"dd\s+.*if=['"]?/dev/(?:zero|urandom|random).*of=['"]?/dev/"#,
            "dd from /dev/zero or /dev/urandom to a device will WIPE all data!"
        ),
        // fdisk (partition editing).
        // `['"]?` allows quoted variants like `fdisk "/dev/sda"` to match.
        destructive_pattern!(
            "fdisk-edit",
            r#"fdisk\s+['"]?/dev/(?!.*-l)"#,
            "fdisk can modify partition tables and cause data loss."
        ),
        // parted (except print)
        destructive_pattern!(
            "parted-modify",
            r#"parted\s+['"]?/dev/\S+\s+(?!print)"#,
            "parted can modify partition tables and cause data loss."
        ),
        // mkfs (format filesystem)
        destructive_pattern!(
            "mkfs",
            r"mkfs(?:\.[a-z0-9]+)?\s+",
            "mkfs formats a partition/device and ERASES all existing data."
        ),
        // wipefs
        destructive_pattern!(
            "wipefs",
            r"wipefs\s+",
            "wipefs removes filesystem signatures. Use with extreme caution."
        ),
        // mount with potentially dangerous options
        destructive_pattern!(
            "mount-bind-root",
            r#"mount\s+.*--bind\s+.*\s+['"]?/(?:$|[^a-z])"#,
            "mount --bind to root directory can have system-wide effects."
        ),
        // umount -f (force)
        destructive_pattern!(
            "umount-force",
            r"umount\s+.*-[a-z]*f",
            "umount -f force unmounts which may cause data loss if device is in use."
        ),
        // losetup can be dangerous
        destructive_pattern!(
            "losetup-device",
            r#"losetup\s+['"]?/dev/loop"#,
            "losetup modifies loop device associations. Verify before proceeding."
        ),
        // --- mdadm destructive patterns ---
        // mdadm --stop (stops a running RAID array)
        destructive_pattern!(
            "mdadm-stop",
            r"mdadm\s+(?:.*\s+)?(?:--stop|-S)\b",
            "mdadm --stop shuts down a RAID array. Data may become inaccessible."
        ),
        // mdadm --remove (removes a device from an array)
        destructive_pattern!(
            "mdadm-remove",
            r"mdadm\s+(?:.*\s+)?--remove\b",
            "mdadm --remove removes a drive from a RAID array. May cause data loss if redundancy is lost."
        ),
        // mdadm --fail (marks a device as failed)
        destructive_pattern!(
            "mdadm-fail",
            r"mdadm\s+(?:.*\s+)?(?:--fail|-f)\b",
            "mdadm --fail marks a device as failed. Use only for intentional drive replacement."
        ),
        // mdadm --zero-superblock (wipes RAID superblock)
        destructive_pattern!(
            "mdadm-zero-superblock",
            r"mdadm\s+(?:.*\s+)?--zero-superblock\b",
            "mdadm --zero-superblock PERMANENTLY erases RAID metadata. Array cannot be reassembled."
        ),
        // mdadm --create (creates a new array, can overwrite existing data)
        destructive_pattern!(
            "mdadm-create",
            r"mdadm\s+(?:.*\s+)?(?:--create|-C)\b",
            "mdadm --create initializes a new RAID array, ERASING existing data on member devices."
        ),
        // mdadm --grow with dangerous options
        destructive_pattern!(
            "mdadm-grow",
            r"mdadm\s+(?:.*\s+)?--grow\b",
            "mdadm --grow reshapes a RAID array. Interruption can cause data loss. Backup first."
        ),
        // --- btrfs destructive patterns ---
        // btrfs subvolume delete
        destructive_pattern!(
            "btrfs-subvolume-delete",
            r"btrfs\b.*?\s+subvolume\s+delete\b",
            "btrfs subvolume delete PERMANENTLY removes a subvolume and all its data."
        ),
        // btrfs device remove/delete
        destructive_pattern!(
            "btrfs-device-remove",
            r"btrfs\b.*?\s+device\s+(?:remove|delete)\b",
            "btrfs device remove redistributes data off a device. Interruption causes data loss."
        ),
        // btrfs device add (can be dangerous with wrong device)
        destructive_pattern!(
            "btrfs-device-add",
            r"btrfs\b.*?\s+device\s+add\b",
            "btrfs device add incorporates a device into the filesystem. Verify the device is correct."
        ),
        // btrfs balance start (can be very disruptive)
        destructive_pattern!(
            "btrfs-balance",
            r"btrfs\b.*?\s+balance\s+start\b",
            "btrfs balance redistributes data across devices. Can be slow and disruptive."
        ),
        // btrfs check --repair (dangerous, can corrupt filesystem)
        destructive_pattern!(
            "btrfs-check-repair",
            r"btrfs\b.*?\s+check\s+(?:.*\s+)?--repair\b",
            "btrfs check --repair is DANGEROUS and can cause data loss. Backup first!"
        ),
        // btrfs rescue (emergency operations)
        destructive_pattern!(
            "btrfs-rescue",
            r"btrfs\b.*?\s+rescue\b",
            "btrfs rescue operations modify filesystem metadata. Use only as last resort."
        ),
        // btrfs filesystem resize (can shrink)
        destructive_pattern!(
            "btrfs-filesystem-resize",
            r"btrfs\b.*?\s+filesystem\s+resize\b",
            "btrfs filesystem resize can shrink a filesystem. Data loss if size is too small."
        ),
        // --- dmsetup destructive patterns ---
        // dmsetup remove (removes a device-mapper device)
        destructive_pattern!(
            "dmsetup-remove",
            r"dmsetup\b.*?\s+remove\b",
            "dmsetup remove detaches a device-mapper device. May cause data loss if in use."
        ),
        // dmsetup remove_all (removes ALL device-mapper devices)
        destructive_pattern!(
            "dmsetup-remove-all",
            r"dmsetup\b.*?\s+remove_all\b",
            "dmsetup remove_all removes ALL device-mapper devices. Extremely dangerous!"
        ),
        // dmsetup wipe_table (replaces table with error target)
        destructive_pattern!(
            "dmsetup-wipe-table",
            r"dmsetup\b.*?\s+wipe_table\b",
            "dmsetup wipe_table replaces the device table, causing all I/O to fail."
        ),
        // dmsetup clear (clears the table)
        destructive_pattern!(
            "dmsetup-clear",
            r"dmsetup\b.*?\s+clear\b",
            "dmsetup clear removes the mapping table from a device."
        ),
        // dmsetup load (loads a new table)
        destructive_pattern!(
            "dmsetup-load",
            r"dmsetup\b.*?\s+load\b",
            "dmsetup load changes device mapping. Verify the new table is correct."
        ),
        // dmsetup create (creates a new device)
        destructive_pattern!(
            "dmsetup-create",
            r"dmsetup\b.*?\s+create\b",
            "dmsetup create sets up a new device-mapper device. Verify parameters carefully."
        ),
        // --- nbd-client destructive patterns ---
        // nbd-client -d (disconnect)
        destructive_pattern!(
            "nbd-client-disconnect",
            r"nbd-client\s+(?:.*\s+)?-d\b",
            "nbd-client -d disconnects a network block device. Data loss if not properly unmounted."
        ),
        // nbd-client connect (can overwrite existing data)
        destructive_pattern!(
            "nbd-client-connect",
            r#"nbd-client\s+\S+\s+\d+\s+['"]?/dev/nbd"#,
            "nbd-client connecting a device can expose or overwrite data. Verify server and device."
        ),
        // --- LVM destructive patterns ---
        // pvremove (removes physical volume)
        destructive_pattern!(
            "pvremove",
            r"\bpvremove\b",
            "pvremove ERASES LVM metadata from a physical volume. Data becomes inaccessible."
        ),
        // vgremove (removes volume group)
        destructive_pattern!(
            "vgremove",
            r"\bvgremove\b",
            "vgremove DELETES a volume group and all logical volumes within it."
        ),
        // lvremove (removes logical volume)
        destructive_pattern!(
            "lvremove",
            r"\blvremove\b",
            "lvremove PERMANENTLY deletes a logical volume and ALL its data."
        ),
        // vgreduce (removes PV from VG)
        destructive_pattern!(
            "vgreduce",
            r"\bvgreduce\b",
            "vgreduce removes a physical volume from a volume group. Data may be lost."
        ),
        // lvreduce (shrinks logical volume)
        destructive_pattern!(
            "lvreduce",
            r"\blvreduce\b",
            "lvreduce SHRINKS a logical volume. Data loss if filesystem isn't resized first!"
        ),
        // lvresize with shrink (can lose data)
        destructive_pattern!(
            "lvresize-shrink",
            r"lvresize\s+(?:.*\s+)?(?:-L\s*-|-l\s*-|--size\s+\S*-)",
            "lvresize with negative size SHRINKS the volume. Resize filesystem first or lose data!"
        ),
        // pvmove (moves data between PVs, interruptible = bad)
        destructive_pattern!(
            "pvmove",
            r"\bpvmove\b",
            "pvmove migrates data between physical volumes. Do NOT interrupt or data may be lost."
        ),
        // lvcreate with snapshot removal
        destructive_pattern!(
            "lvconvert-merge",
            r"lvconvert\s+(?:.*\s+)?--merge\b",
            "lvconvert --merge reverts LV to snapshot state, discarding changes since snapshot."
        ),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wipefs_is_reachable_via_keywords() {
        let pack = create_pack();
        assert!(
            pack.might_match("wipefs --all somefile.img"),
            "wipefs should be included in pack keywords to prevent false negatives"
        );
        let matched = pack
            .check("wipefs --all somefile.img")
            .expect("wipefs should be blocked by disk pack");
        assert_eq!(matched.name, Some("wipefs"));
    }

    #[test]
    fn keyword_absent_skips_pack() {
        let pack = create_pack();
        assert!(!pack.might_match("echo hello"));
        assert!(pack.check("echo hello").is_none());
    }

    #[test]
    fn dd_quote_bypass_is_closed() {
        // `dd of="/dev/sda"` unquotes to `dd of=/dev/sda` at exec time.
        // The destructive pattern must match both spellings. The earlier-listed
        // `dd-device` rule catches every `dd of=/dev/...` variant (including
        // the more-specific wipe cases), which is the correct, fail-safe
        // behavior.
        let pack = create_pack();
        let matched = pack
            .check("dd if=/dev/zero of=\"/dev/sda\" bs=1M")
            .expect("dd of=\"...\" must still block");
        assert_eq!(matched.name, Some("dd-device"));

        let matched = pack
            .check("dd of='/dev/sdb' if=something.img")
            .expect("dd of='...' must still block");
        assert_eq!(matched.name, Some("dd-device"));

        // /dev/null stays safe under quotes.
        assert!(
            pack.matches_safe("dd if=myfile of=\"/dev/null\""),
            "safe /dev/null discard must accept quoted path"
        );
    }

    #[test]
    fn btrfs_dmsetup_global_flags_do_not_bypass() {
        let pack = create_pack();
        // btrfs accepts --format, --verbose, --quiet before the subcommand.
        let matched = pack
            .check("btrfs --format json subvolume delete /mnt/foo")
            .expect("btrfs --format subvolume delete should still block");
        assert_eq!(matched.name, Some("btrfs-subvolume-delete"));

        let matched = pack
            .check("btrfs --verbose check --repair /dev/sda1")
            .expect("btrfs --verbose check --repair should still block");
        assert_eq!(matched.name, Some("btrfs-check-repair"));

        // dmsetup accepts -v, --noudevsync, --verifyudev before the subcommand.
        let matched = pack
            .check("dmsetup -v remove_all")
            .expect("dmsetup -v remove_all should still block");
        assert_eq!(matched.name, Some("dmsetup-remove-all"));

        let matched = pack
            .check("dmsetup --noudevsync remove my-dev")
            .expect("dmsetup with noudevsync should still block");
        assert_eq!(matched.name, Some("dmsetup-remove"));
    }
}