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
//! AWS SES pack - protections for destructive AWS Simple Email Service operations.
//!
//! Covers destructive operations:
//! - Identity deletion (ses and sesv2)
//! - Template deletion
//! - Configuration set deletion
//! - Receipt rule deletion
//! - Contact list deletion

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

/// Create the AWS SES pack.
#[must_use]
pub fn create_pack() -> Pack {
    Pack {
        id: "email.ses".to_string(),
        name: "AWS SES",
        description: "Protects against destructive AWS Simple Email Service operations like \
                      identity deletion, template deletion, and configuration set removal.",
        keywords: &["ses", "sesv2"],
        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![
        // SES v1 read operations
        safe_pattern!(
            "ses-list-identities",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+list-identities(?=\s|$)"
        ),
        safe_pattern!(
            "ses-list-templates",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+list-templates(?=\s|$)"
        ),
        safe_pattern!(
            "ses-list-configuration-sets",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+list-configuration-sets\b"
        ),
        safe_pattern!(
            "ses-list-receipt-rules",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+list-receipt-rules\b"
        ),
        safe_pattern!(
            "ses-list-receipt-rule-sets",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+list-receipt-rule-sets\b"
        ),
        safe_pattern!(
            "ses-get-identity-verification-attributes",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+get-identity-verification-attributes\b"
        ),
        safe_pattern!(
            "ses-get-identity-dkim-attributes",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+get-identity-dkim-attributes\b"
        ),
        safe_pattern!(
            "ses-get-identity-notification-attributes",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+get-identity-notification-attributes\b"
        ),
        safe_pattern!(
            "ses-get-template",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+get-template(?=\s|$)"
        ),
        safe_pattern!(
            "ses-describe-configuration-set",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+describe-configuration-set\b"
        ),
        safe_pattern!(
            "ses-describe-receipt-rule",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+describe-receipt-rule\b"
        ),
        safe_pattern!(
            "ses-describe-receipt-rule-set",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+describe-receipt-rule-set\b"
        ),
        safe_pattern!(
            "ses-get-send-quota",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+get-send-quota(?=\s|$)"
        ),
        safe_pattern!(
            "ses-get-send-statistics",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+get-send-statistics\b"
        ),
        // SES v2 read operations
        safe_pattern!(
            "sesv2-list-email-identities",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+list-email-identities\b"
        ),
        safe_pattern!(
            "sesv2-list-email-templates",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+list-email-templates\b"
        ),
        safe_pattern!(
            "sesv2-list-configuration-sets",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+list-configuration-sets\b"
        ),
        safe_pattern!(
            "sesv2-list-contact-lists",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+list-contact-lists\b"
        ),
        safe_pattern!(
            "sesv2-list-dedicated-ip-pools",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+list-dedicated-ip-pools\b"
        ),
        safe_pattern!(
            "sesv2-get-email-identity",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+get-email-identity\b"
        ),
        safe_pattern!(
            "sesv2-get-email-template",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+get-email-template\b"
        ),
        safe_pattern!(
            "sesv2-get-configuration-set",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+get-configuration-set\b"
        ),
        safe_pattern!(
            "sesv2-get-contact-list",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+get-contact-list\b"
        ),
        safe_pattern!(
            "sesv2-get-dedicated-ip-pool",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+get-dedicated-ip-pool\b"
        ),
        safe_pattern!(
            "sesv2-get-account",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+get-account(?=\s|$)"
        ),
    ]
}

fn create_destructive_patterns() -> Vec<DestructivePattern> {
    vec![
        // SES v1 deletion operations
        destructive_pattern!(
            "ses-delete-identity",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+delete-identity\b",
            "aws ses delete-identity removes a verified email identity.",
            High,
            "Deleting a verified identity prevents sending from that address or domain. \
             Applications using this identity will fail to send emails. Re-verification \
             requires DNS changes and propagation time.\n\n\
             Safer alternatives:\n\
             - aws ses list-identities: Review identities first\n\
             - Check which applications use this identity\n\
             - Create new identity before deleting old one"
        ),
        destructive_pattern!(
            "ses-delete-template",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+delete-template\b",
            "aws ses delete-template removes an email template.",
            Medium,
            "Deleting a template breaks any applications that reference it. Emails using \
             this template will fail to send until the template is recreated.\n\n\
             Safer alternatives:\n\
             - aws ses get-template: Export template before deletion\n\
             - Verify no active campaigns use this template\n\
             - Create replacement template before deleting"
        ),
        destructive_pattern!(
            "ses-delete-configuration-set",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+delete-configuration-set\b",
            "aws ses delete-configuration-set removes a configuration set.",
            High,
            "Deleting a configuration set removes tracking and event destinations. \
             Applications using this set will lose metrics, bounce handling, and \
             complaint processing.\n\n\
             Safer alternatives:\n\
             - aws ses describe-configuration-set: Review configuration\n\
             - Migrate applications to a new configuration set first\n\
             - Document event destinations before deletion"
        ),
        destructive_pattern!(
            "ses-delete-receipt-rule-set",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+delete-receipt-rule-set\b",
            "aws ses delete-receipt-rule-set removes a receipt rule set.",
            Critical,
            "Deleting a receipt rule set stops all email receiving configured by that set. \
             Incoming emails may bounce or be lost. This affects all receipt rules in the set.\n\n\
             Safer alternatives:\n\
             - aws ses describe-receipt-rule-set: Review rules first\n\
             - Create replacement rule set before deletion\n\
             - Test with a non-active rule set"
        ),
        destructive_pattern!(
            "ses-delete-receipt-rule",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+ses\s+delete-receipt-rule(?:\s|$)",
            "aws ses delete-receipt-rule removes a receipt rule.",
            High,
            "Deleting a receipt rule changes how incoming emails are processed. Actions \
             like S3 storage, Lambda triggers, or SNS notifications will stop for \
             matching emails.\n\n\
             Safer alternatives:\n\
             - aws ses describe-receipt-rule: Review rule configuration\n\
             - Disable the rule before deleting\n\
             - Ensure no critical workflows depend on this rule"
        ),
        // SES v2 deletion operations
        destructive_pattern!(
            "sesv2-delete-email-identity",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+delete-email-identity\b",
            "aws sesv2 delete-email-identity removes a verified email identity.",
            High,
            "Deleting a verified identity prevents sending from that address or domain. \
             DKIM and SPF records become orphaned. Applications will fail until a new \
             identity is verified.\n\n\
             Safer alternatives:\n\
             - aws sesv2 get-email-identity: Review identity configuration\n\
             - Verify replacement identity before deletion\n\
             - Update applications to use new identity first"
        ),
        destructive_pattern!(
            "sesv2-delete-email-template",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+delete-email-template\b",
            "aws sesv2 delete-email-template removes an email template.",
            Medium,
            "Deleting a template breaks any send operations referencing it. Bulk email \
             sends and transactional emails using this template will fail.\n\n\
             Safer alternatives:\n\
             - aws sesv2 get-email-template: Export template content\n\
             - Check for active campaigns using this template\n\
             - Version templates rather than deleting"
        ),
        destructive_pattern!(
            "sesv2-delete-configuration-set",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+delete-configuration-set\b",
            "aws sesv2 delete-configuration-set removes a configuration set.",
            High,
            "Deleting a configuration set removes all event destinations, tracking options, \
             and delivery settings. Applications using this set lose visibility into \
             email delivery.\n\n\
             Safer alternatives:\n\
             - aws sesv2 get-configuration-set: Export configuration\n\
             - Migrate to new configuration set first\n\
             - Document all event destinations"
        ),
        destructive_pattern!(
            "sesv2-delete-contact-list",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+delete-contact-list\b",
            "aws sesv2 delete-contact-list removes a contact list.",
            High,
            "Deleting a contact list permanently removes all contacts and their preferences. \
             Subscription management and list-based sending will fail. This cannot be undone.\n\n\
             Safer alternatives:\n\
             - Export contact list data before deletion\n\
             - Check for active campaigns using this list\n\
             - Use list segmentation instead of deletion"
        ),
        destructive_pattern!(
            "sesv2-delete-dedicated-ip-pool",
            r"\baws\b(?:\s+--?\S+(?:\s+\S+)?)*\s+sesv2\s+delete-dedicated-ip-pool\b",
            "aws sesv2 delete-dedicated-ip-pool removes a dedicated IP pool.",
            Critical,
            "Deleting a dedicated IP pool releases the IPs back to the shared pool. \
             Email reputation built on these IPs is lost. Configuration sets using \
             this pool will fall back to shared IPs.\n\n\
             Safer alternatives:\n\
             - Migrate configuration sets to a new pool first\n\
             - Document IP addresses and reputation metrics\n\
             - Contact AWS support if IPs need to be preserved"
        ),
    ]
}

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

    #[test]
    fn test_pack_creation() {
        let pack = create_pack();
        assert_eq!(pack.id, "email.ses");
        assert_eq!(pack.name, "AWS SES");
        assert!(!pack.description.is_empty());
        assert!(pack.keywords.contains(&"ses"));
        assert!(pack.keywords.contains(&"sesv2"));

        assert_patterns_compile(&pack);
        assert_all_patterns_have_reasons(&pack);
        assert_unique_pattern_names(&pack);
    }

    #[test]
    fn allows_safe_commands() {
        let pack = create_pack();
        // SES v1 read operations
        assert_safe_pattern_matches(&pack, "aws ses list-identities");
        assert_safe_pattern_matches(&pack, "aws ses list-templates");
        assert_safe_pattern_matches(&pack, "aws ses list-configuration-sets");
        assert_safe_pattern_matches(
            &pack,
            "aws ses list-receipt-rules --rule-set-name MyRuleSet",
        );
        assert_safe_pattern_matches(&pack, "aws ses list-receipt-rule-sets");
        assert_safe_pattern_matches(
            &pack,
            "aws ses get-identity-verification-attributes --identities example.com",
        );
        assert_safe_pattern_matches(
            &pack,
            "aws ses get-identity-dkim-attributes --identities example.com",
        );
        assert_safe_pattern_matches(&pack, "aws ses get-template --template-name MyTemplate");
        assert_safe_pattern_matches(
            &pack,
            "aws ses describe-configuration-set --configuration-set-name MySet",
        );
        assert_safe_pattern_matches(&pack, "aws ses get-send-quota");
        assert_safe_pattern_matches(&pack, "aws ses get-send-statistics");
        // SES v2 read operations
        assert_safe_pattern_matches(&pack, "aws sesv2 list-email-identities");
        assert_safe_pattern_matches(&pack, "aws sesv2 list-email-templates");
        assert_safe_pattern_matches(&pack, "aws sesv2 list-configuration-sets");
        assert_safe_pattern_matches(&pack, "aws sesv2 list-contact-lists");
        assert_safe_pattern_matches(&pack, "aws sesv2 list-dedicated-ip-pools");
        assert_safe_pattern_matches(
            &pack,
            "aws sesv2 get-email-identity --email-identity example.com",
        );
        assert_safe_pattern_matches(
            &pack,
            "aws sesv2 get-email-template --template-name MyTemplate",
        );
        assert_safe_pattern_matches(
            &pack,
            "aws sesv2 get-configuration-set --configuration-set-name MySet",
        );
        assert_safe_pattern_matches(&pack, "aws sesv2 get-account");
    }

    #[test]
    fn blocks_destructive_commands() {
        let pack = create_pack();
        // SES v1 deletion operations
        assert_blocks_with_pattern(
            &pack,
            "aws ses delete-identity --identity example.com",
            "ses-delete-identity",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws ses delete-template --template-name MyTemplate",
            "ses-delete-template",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws ses delete-configuration-set --configuration-set-name MySet",
            "ses-delete-configuration-set",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws ses delete-receipt-rule --rule-set-name MyRuleSet --rule-name MyRule",
            "ses-delete-receipt-rule",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws ses delete-receipt-rule-set --rule-set-name MyRuleSet",
            "ses-delete-receipt-rule-set",
        );
        // SES v2 deletion operations
        assert_blocks_with_pattern(
            &pack,
            "aws sesv2 delete-email-identity --email-identity example.com",
            "sesv2-delete-email-identity",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws sesv2 delete-email-template --template-name MyTemplate",
            "sesv2-delete-email-template",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws sesv2 delete-configuration-set --configuration-set-name MySet",
            "sesv2-delete-configuration-set",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws sesv2 delete-contact-list --contact-list-name MyList",
            "sesv2-delete-contact-list",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws sesv2 delete-dedicated-ip-pool --pool-name MyPool",
            "sesv2-delete-dedicated-ip-pool",
        );
    }

    #[test]
    fn aws_global_flags_do_not_bypass() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "aws --profile prod ses delete-identity --identity example.com",
            "ses-delete-identity",
        );
        assert_blocks_with_pattern(
            &pack,
            "aws --region us-east-1 --profile prod sesv2 delete-contact-list --contact-list-name X",
            "sesv2-delete-contact-list",
        );
        assert!(
            pack.check("aws --profile prod ses list-identities")
                .is_none(),
            "safe read with global flag should remain safe"
        );
    }
}