destructive_command_guard 0.5.6

An AI coding agent 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
//! `Split.io` Feature Flags pack - protections for destructive `Split.io` operations.
//!
//! Covers destructive operations for:
//! - split CLI (`split splits delete`, `split environments delete`, etc.)
//! - `Split.io` API (DELETE requests to `api.split.io`)

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

/// Create the `Split.io` Feature Flags pack.
#[must_use]
pub fn create_pack() -> Pack {
    Pack {
        id: "featureflags.split".to_string(),
        name: "Split.io",
        description: "Protects against destructive Split.io CLI and API operations.",
        keywords: &["split", "api.split.io"],
        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![
        // split CLI - list/get operations
        safe_pattern!(
            "split-splits-list",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+splits\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "split-splits-get",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+splits\s+get(?=\s|$)"
        ),
        safe_pattern!(
            "split-splits-create",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+splits\s+create(?=\s|$)"
        ),
        safe_pattern!(
            "split-splits-update",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+splits\s+update(?=\s|$)"
        ),
        safe_pattern!(
            "split-environments-list",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+environments\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "split-environments-get",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+environments\s+get(?=\s|$)"
        ),
        safe_pattern!(
            "split-environments-create",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+environments\s+create(?=\s|$)"
        ),
        safe_pattern!(
            "split-segments-list",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+segments\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "split-segments-get",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+segments\s+get(?=\s|$)"
        ),
        safe_pattern!(
            "split-segments-create",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+segments\s+create(?=\s|$)"
        ),
        safe_pattern!(
            "split-traffic-types-list",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+traffic-types\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "split-traffic-types-get",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+traffic-types\s+get(?=\s|$)"
        ),
        safe_pattern!(
            "split-workspaces-list",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+workspaces\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "split-workspaces-get",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+workspaces\s+get(?=\s|$)"
        ),
        // Help and version commands
        safe_pattern!(
            "split-help",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:--help|-h|help)(?=\s|$)"
        ),
        safe_pattern!(
            "split-version",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:--version|version)(?=\s|$)"
        ),
        // API - GET requests
        safe_pattern!(
            "split-api-get",
            r"(?i)^(?!(?=.*(?:-X\s*|--request(?:=|\s+))DELETE\b)(?=.*api\.split\.io))curl\s+.*(?:-X\s*|--request(?:=|\s+))GET\b.*api\.split\.io"
        ),
    ]
}

fn create_destructive_patterns() -> Vec<DestructivePattern> {
    vec![
        // split CLI - delete operations
        destructive_pattern!(
            "split-splits-delete",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+splits\s+delete\b",
            "split splits delete permanently removes a split definition. This cannot be undone.",
            Critical,
            "Deleting a split permanently removes the feature flag definition and all its \
             targeting rules. SDKs will return control treatment for this split. Historical \
             data and metrics are preserved but the split cannot be recovered.\n\n\
             Safer alternatives:\n\
             - split splits kill: Stop traffic without deleting\n\
             - Archive the split in the UI\n\
             - Export split configuration first"
        ),
        destructive_pattern!(
            "split-splits-kill",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+splits\s+kill\b",
            "split splits kill terminates a split, stopping all traffic to treatments.",
            High,
            "Killing a split immediately stops all treatment assignment and returns the \
             default treatment to all users. Unlike delete, the split can be reactivated, \
             but all users will see behavior change immediately.\n\n\
             Safer alternatives:\n\
             - Gradually ramp down traffic percentages first\n\
             - Verify the default treatment behavior\n\
             - Communicate the change to stakeholders"
        ),
        destructive_pattern!(
            "split-environments-delete",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+environments\s+delete\b",
            "split environments delete removes an environment and all its configurations.",
            Critical,
            "Deleting an environment removes all split configurations, targeting rules, \
             and API keys for that environment. Applications using this environment will \
             receive default treatments for all splits.\n\n\
             Safer alternatives:\n\
             - Export environment configuration\n\
             - Rotate API keys before deletion\n\
             - Kill all splits in the environment first"
        ),
        destructive_pattern!(
            "split-segments-delete",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+segments\s+delete\b",
            "split segments delete removes a segment and its targeting rules.",
            High,
            "Deleting a segment removes user grouping definitions. Splits targeting this \
             segment will lose that targeting rule, changing which users receive which \
             treatments.\n\n\
             Safer alternatives:\n\
             - Check which splits use this segment\n\
             - Update split targeting before deletion\n\
             - Export segment membership"
        ),
        destructive_pattern!(
            "split-traffic-types-delete",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+traffic-types\s+delete\b",
            "split traffic-types delete removes a traffic type. This affects all splits using it.",
            Critical,
            "Deleting a traffic type affects ALL splits configured for that traffic type. \
             SDKs sending this traffic type will no longer match any splits, returning \
             control treatment for all evaluations.\n\n\
             Safer alternatives:\n\
             - Review all splits using this traffic type\n\
             - Migrate splits to a different traffic type\n\
             - Ensure no SDKs are sending this traffic type"
        ),
        destructive_pattern!(
            "split-workspaces-delete",
            r"split(?:\s+--?\S+(?:\s+\S+)?)*\s+workspaces\s+delete\b",
            "split workspaces delete removes a workspace and all its resources.",
            Critical,
            "Deleting a workspace removes ALL splits, segments, environments, and API keys \
             within it. This is the most destructive operation and affects all applications \
             using any resource in this workspace.\n\n\
             Safer alternatives:\n\
             - Export complete workspace configuration\n\
             - Migrate critical splits to another workspace\n\
             - Contact Split.io support for assistance"
        ),
        // API - DELETE requests
        destructive_pattern!(
            "split-api-delete-splits",
            r"(?i)\bcurl\b(?=.*(?:-X\s*|--request(?:=|\s+))DELETE\b)(?=.*api\.split\.io/.*/splits/).*",
            "DELETE request to Split.io API removes split definitions.",
            Critical,
            "API DELETE calls to splits permanently remove feature flags without CLI \
             confirmation. All targeting rules and treatments are lost immediately.\n\n\
             Safer alternatives:\n\
             - Use the Split CLI for confirmation prompts\n\
             - GET the split configuration first\n\
             - Use the Split UI for visibility into impact"
        ),
        destructive_pattern!(
            "split-api-delete-environments",
            r"(?i)\bcurl\b(?=.*(?:-X\s*|--request(?:=|\s+))DELETE\b)(?=.*api\.split\.io/.*/environments/).*",
            "DELETE request to Split.io API removes environments.",
            Critical,
            "API DELETE calls to environments invalidate all API keys and remove all \
             split configurations for that environment. Applications will lose all \
             feature flag evaluations.\n\n\
             Safer alternatives:\n\
             - Use the Split CLI for better confirmation\n\
             - Export environment configuration first\n\
             - Rotate API keys before deletion"
        ),
        destructive_pattern!(
            "split-api-delete-segments",
            r"(?i)\bcurl\b(?=.*(?:-X\s*|--request(?:=|\s+))DELETE\b)(?=.*api\.split\.io/.*/segments/).*",
            "DELETE request to Split.io API removes segments.",
            High,
            "API DELETE calls to segments remove user groupings. Splits using this \
             segment will lose targeting rules, changing treatment assignment for \
             affected users.\n\n\
             Safer alternatives:\n\
             - Check segment dependencies first\n\
             - Update split targeting before deletion\n\
             - Export segment membership data"
        ),
        destructive_pattern!(
            "split-api-delete-generic",
            r"(?i)\bcurl\b(?=.*(?:-X\s*|--request(?:=|\s+))DELETE\b)(?=.*api\.split\.io).*",
            "DELETE request to Split.io API can remove resources.",
            High,
            "Generic DELETE requests to the Split.io API can remove various resources. \
             Review the specific endpoint to understand what will be deleted.\n\n\
             Safer alternatives:\n\
             - Verify the exact resource being deleted\n\
             - Use the Split CLI or UI for better visibility\n\
             - GET the resource first to confirm"
        ),
    ]
}

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

    #[test]
    fn test_pack_creation() {
        let pack = create_pack();
        assert_eq!(pack.id, "featureflags.split");
        assert_eq!(pack.name, "Split.io");
        assert!(!pack.description.is_empty());
        assert!(pack.keywords.contains(&"split"));
        assert!(pack.keywords.contains(&"api.split.io"));

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

    #[test]
    fn allows_safe_commands() {
        let pack = create_pack();
        // split CLI - list/get operations
        assert_safe_pattern_matches(&pack, "split splits list");
        assert_safe_pattern_matches(&pack, "split splits list --workspace my-workspace");
        assert_safe_pattern_matches(&pack, "split splits get my-split");
        assert_safe_pattern_matches(&pack, "split splits create --name new-split");
        assert_safe_pattern_matches(&pack, "split splits update my-split --name renamed");
        assert_safe_pattern_matches(&pack, "split environments list");
        assert_safe_pattern_matches(&pack, "split environments get production");
        assert_safe_pattern_matches(&pack, "split environments create --name staging");
        assert_safe_pattern_matches(&pack, "split segments list");
        assert_safe_pattern_matches(&pack, "split segments get beta-users");
        assert_safe_pattern_matches(&pack, "split traffic-types list");
        assert_safe_pattern_matches(&pack, "split traffic-types get user");
        assert_safe_pattern_matches(&pack, "split workspaces list");
        assert_safe_pattern_matches(&pack, "split workspaces get my-workspace");
        // Help commands
        assert_safe_pattern_matches(&pack, "split --help");
        assert_safe_pattern_matches(&pack, "split help");
        assert_safe_pattern_matches(&pack, "split --version");
        // API - GET requests
        assert_safe_pattern_matches(
            &pack,
            "curl -X GET https://api.split.io/internal/api/v2/splits",
        );
    }

    #[test]
    fn blocks_splits_delete() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "split splits delete my-split --workspace my-workspace",
            "split-splits-delete",
        );
    }

    #[test]
    fn blocks_splits_kill() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "split splits kill my-split --workspace my-workspace",
            "split-splits-kill",
        );
    }

    #[test]
    fn blocks_environments_delete() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "split environments delete staging --workspace my-workspace",
            "split-environments-delete",
        );
    }

    #[test]
    fn blocks_segments_delete() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "split segments delete beta-users",
            "split-segments-delete",
        );
    }

    #[test]
    fn blocks_traffic_types_delete() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "split traffic-types delete user",
            "split-traffic-types-delete",
        );
    }

    #[test]
    fn blocks_workspaces_delete() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "split workspaces delete my-workspace",
            "split-workspaces-delete",
        );
    }

    #[test]
    fn blocks_api_delete_splits() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE https://api.split.io/internal/api/v2/splits/my-split",
            "split-api-delete-splits",
        );
    }

    #[test]
    fn blocks_api_delete_environments() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE https://api.split.io/internal/api/v2/environments/staging",
            "split-api-delete-environments",
        );
    }

    #[test]
    fn blocks_api_delete_segments() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE https://api.split.io/internal/api/v2/segments/beta-users",
            "split-api-delete-segments",
        );
    }

    #[test]
    fn curl_get_safe_pattern_does_not_mask_destructive_api_methods() {
        let pack = create_pack();
        let command = "curl -X GET https://api.split.io/internal/api/v2/splits \
            -X DELETE https://api.split.io/internal/api/v2/splits/my-split";

        assert_no_safe_match(&pack, command);
        assert_blocks_with_pattern(&pack, command, "split-api-delete-splits");

        assert_blocks_with_pattern(
            &pack,
            "curl https://api.split.io/internal/api/v2/environments/staging -XDELETE",
            "split-api-delete-environments",
        );
    }

    #[test]
    fn allows_non_split_commands() {
        let pack = create_pack();
        assert_allows(&pack, "echo split");
        assert_allows(&pack, "cat split.log");
    }

    #[test]
    fn global_flags_do_not_bypass() {
        let pack = create_pack();
        // split CLI accepts --apikey, --workspace, --format global flags.
        assert_blocks_with_pattern(
            &pack,
            "split --apikey abc123 splits delete my-split",
            "split-splits-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "split --workspace ws --apikey abc workspaces delete ws2",
            "split-workspaces-delete",
        );
        assert!(
            pack.check("split --apikey abc123 splits list").is_none(),
            "safe list with global flag should remain safe"
        );
    }
}