@node9/proxy 1.7.1 → 1.8.3

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.
package/dist/index.mjs CHANGED
@@ -250,251 +250,73 @@ ${lines.join("\n")}`
250
250
  import fs2 from "fs";
251
251
  import path2 from "path";
252
252
  import os2 from "os";
253
- var SHIELDS = {
254
- postgres: {
255
- name: "postgres",
256
- description: "Protects PostgreSQL databases from destructive AI operations",
257
- aliases: ["pg", "postgresql"],
258
- smartRules: [
259
- {
260
- name: "shield:postgres:block-drop-table",
261
- tool: "*",
262
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
263
- verdict: "block",
264
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
265
- },
266
- {
267
- name: "shield:postgres:block-truncate",
268
- tool: "*",
269
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
270
- verdict: "block",
271
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
272
- },
273
- {
274
- name: "shield:postgres:block-drop-column",
275
- tool: "*",
276
- conditions: [
277
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
278
- ],
279
- verdict: "block",
280
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
281
- },
282
- {
283
- name: "shield:postgres:review-grant-revoke",
284
- tool: "*",
285
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
286
- verdict: "review",
287
- reason: "Permission changes require human approval (Postgres shield)"
288
- }
289
- ],
290
- dangerousWords: ["dropdb", "pg_dropcluster"]
291
- },
292
- github: {
293
- name: "github",
294
- description: "Protects GitHub repositories from destructive AI operations",
295
- aliases: ["git"],
296
- smartRules: [
297
- {
298
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
299
- // This rule adds coverage for `git push --delete` which the built-in does not match.
300
- name: "shield:github:review-delete-branch-remote",
301
- tool: "bash",
302
- conditions: [
303
- {
304
- field: "command",
305
- op: "matches",
306
- value: "git\\s+push\\s+.*--delete",
307
- flags: "i"
308
- }
309
- ],
310
- verdict: "review",
311
- reason: "Remote branch deletion requires human approval (GitHub shield)"
312
- },
313
- {
314
- name: "shield:github:block-delete-repo",
315
- tool: "*",
316
- conditions: [
317
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
318
- ],
319
- verdict: "block",
320
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
321
- }
322
- ],
323
- dangerousWords: []
324
- },
325
- aws: {
326
- name: "aws",
327
- description: "Protects AWS infrastructure from destructive AI operations",
328
- aliases: ["amazon"],
329
- smartRules: [
330
- {
331
- name: "shield:aws:block-delete-s3-bucket",
332
- tool: "*",
333
- conditions: [
334
- {
335
- field: "command",
336
- op: "matches",
337
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
338
- flags: "i"
339
- }
340
- ],
341
- verdict: "block",
342
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
343
- },
344
- {
345
- name: "shield:aws:review-iam-changes",
346
- tool: "*",
347
- conditions: [
348
- {
349
- field: "command",
350
- op: "matches",
351
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
352
- flags: "i"
353
- }
354
- ],
355
- verdict: "review",
356
- reason: "IAM changes require human approval (AWS shield)"
357
- },
358
- {
359
- name: "shield:aws:block-ec2-terminate",
360
- tool: "*",
361
- conditions: [
362
- {
363
- field: "command",
364
- op: "matches",
365
- value: "aws\\s+ec2\\s+terminate-instances",
366
- flags: "i"
367
- }
368
- ],
369
- verdict: "block",
370
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
371
- },
372
- {
373
- name: "shield:aws:review-rds-delete",
374
- tool: "*",
375
- conditions: [
376
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
377
- ],
378
- verdict: "review",
379
- reason: "RDS deletion requires human approval (AWS shield)"
380
- }
381
- ],
382
- dangerousWords: []
383
- },
384
- "bash-safe": {
385
- name: "bash-safe",
386
- description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
387
- aliases: ["bash", "shell"],
388
- smartRules: [
389
- {
390
- name: "shield:bash-safe:block-pipe-to-shell",
391
- tool: "bash",
392
- conditions: [
393
- {
394
- field: "command",
395
- op: "matches",
396
- value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
397
- flags: "i"
398
- }
399
- ],
400
- verdict: "block",
401
- reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
402
- },
403
- {
404
- name: "shield:bash-safe:block-obfuscated-exec",
405
- tool: "bash",
406
- conditions: [
407
- {
408
- field: "command",
409
- op: "matches",
410
- value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
411
- flags: "i"
412
- }
413
- ],
414
- verdict: "block",
415
- reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
416
- },
417
- {
418
- name: "shield:bash-safe:block-rm-root",
419
- tool: "bash",
420
- conditions: [
421
- {
422
- field: "command",
423
- op: "matches",
424
- value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
425
- flags: "i"
426
- }
427
- ],
428
- verdict: "block",
429
- reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
430
- },
431
- {
432
- name: "shield:bash-safe:block-disk-overwrite",
433
- tool: "bash",
434
- conditions: [
435
- {
436
- field: "command",
437
- op: "matches",
438
- value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
439
- flags: "i"
440
- }
441
- ],
442
- verdict: "block",
443
- reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
444
- },
445
- {
446
- name: "shield:bash-safe:review-eval",
447
- tool: "bash",
448
- conditions: [
449
- {
450
- field: "command",
451
- op: "matches",
452
- value: '\\beval\\s+[\\$`("]',
453
- flags: "i"
454
- }
455
- ],
456
- verdict: "review",
457
- reason: "eval of dynamic content requires human approval (bash-safe shield)"
458
- }
459
- ],
460
- dangerousWords: []
461
- },
462
- filesystem: {
463
- name: "filesystem",
464
- description: "Protects the local filesystem from dangerous AI operations",
465
- aliases: ["fs"],
466
- smartRules: [
467
- {
468
- name: "shield:filesystem:review-chmod-777",
469
- tool: "bash",
470
- conditions: [
471
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
472
- ],
473
- verdict: "review",
474
- reason: "chmod 777 requires human approval (filesystem shield)"
475
- },
476
- {
477
- name: "shield:filesystem:review-write-etc",
478
- tool: "bash",
479
- conditions: [
480
- {
481
- field: "command",
482
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
483
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
484
- op: "matches",
485
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
486
- }
487
- ],
488
- verdict: "review",
489
- reason: "Writing to /etc requires human approval (filesystem shield)"
490
- }
491
- ],
492
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
493
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
494
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
495
- dangerousWords: ["wipefs"]
253
+ var BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
254
+ var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
255
+ function validateShieldDefinition(raw, filePath) {
256
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
257
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
258
+ `);
259
+ return null;
496
260
  }
497
- };
261
+ const r = raw;
262
+ if (typeof r.name !== "string" || !r.name) {
263
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
264
+ `);
265
+ return null;
266
+ }
267
+ if (typeof r.description !== "string") {
268
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
269
+ `);
270
+ return null;
271
+ }
272
+ if (!Array.isArray(r.aliases)) {
273
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
274
+ `);
275
+ return null;
276
+ }
277
+ if (!Array.isArray(r.smartRules)) {
278
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
279
+ `);
280
+ return null;
281
+ }
282
+ if (!Array.isArray(r.dangerousWords)) {
283
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
284
+ `);
285
+ return null;
286
+ }
287
+ return r;
288
+ }
289
+ function loadShieldsFromDir(dir, label) {
290
+ const result = {};
291
+ let entries;
292
+ try {
293
+ entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
294
+ } catch (err) {
295
+ if (err.code !== "ENOENT") {
296
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err)}
297
+ `);
298
+ }
299
+ return result;
300
+ }
301
+ for (const file of entries) {
302
+ const filePath = path2.join(dir, file);
303
+ try {
304
+ const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
305
+ const shield = validateShieldDefinition(raw, filePath);
306
+ if (shield) result[shield.name] = shield;
307
+ } catch (err) {
308
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err)}
309
+ `);
310
+ }
311
+ }
312
+ return result;
313
+ }
314
+ function buildSHIELDS() {
315
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
316
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
317
+ return { ...builtins, ...userShields };
318
+ }
319
+ var SHIELDS = buildSHIELDS();
498
320
  function resolveShieldName(input) {
499
321
  const lower = input.toLowerCase();
500
322
  if (SHIELDS[lower]) return lower;
@@ -2118,7 +1940,7 @@ function isDaemonRunning() {
2118
1940
  return false;
2119
1941
  }
2120
1942
  }
2121
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
1943
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2122
1944
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2123
1945
  const ctrl = new AbortController();
2124
1946
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2139,7 +1961,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2139
1961
  ...cwd && { cwd },
2140
1962
  ...recoveryCommand && { recoveryCommand },
2141
1963
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2142
- ...viewOnly && { viewOnly: true }
1964
+ ...viewOnly && { viewOnly: true },
1965
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2143
1966
  }),
2144
1967
  signal: ctrl.signal
2145
1968
  });
@@ -2529,9 +2352,7 @@ end run`;
2529
2352
  "--text",
2530
2353
  pangoMessage,
2531
2354
  "--ok-label",
2532
- locked ? "Waiting..." : "Allow \u21B5",
2533
- "--timeout",
2534
- "300"
2355
+ locked ? "Waiting..." : "Allow \u21B5"
2535
2356
  ];
2536
2357
  if (!locked) {
2537
2358
  argsList.push("--cancel-label", "Block \u238B");
@@ -2807,6 +2628,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2807
2628
  let policyMatchedWord;
2808
2629
  let riskMetadata;
2809
2630
  let statefulRecoveryCommand;
2631
+ let localSmartRuleMatched = false;
2810
2632
  let taintWarning = null;
2811
2633
  if (isNetworkTool(toolName, args)) {
2812
2634
  const filePaths = extractFilePaths(toolName, args);
@@ -2950,6 +2772,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2950
2772
  explainableLabel = policyResult.blockedByLabel || "Local Config";
2951
2773
  policyMatchedField = policyResult.matchedField;
2952
2774
  policyMatchedWord = policyResult.matchedWord;
2775
+ if (policyResult.ruleName) localSmartRuleMatched = true;
2953
2776
  riskMetadata = computeRiskMetadata(
2954
2777
  args,
2955
2778
  policyResult.tier ?? 6,
@@ -2992,22 +2815,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2992
2815
  }
2993
2816
  let cloudRequestId = null;
2994
2817
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
2995
- if (cloudEnforced) {
2818
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
2996
2819
  try {
2997
2820
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
2998
2821
  if (!initResult.pending) {
2999
2822
  if (initResult.shadowMode) {
3000
2823
  return { approved: true, checkedBy: "cloud" };
3001
2824
  }
3002
- return {
3003
- approved: !!initResult.approved,
3004
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3005
- checkedBy: initResult.approved ? "cloud" : void 0,
3006
- blockedBy: initResult.approved ? void 0 : "team-policy",
3007
- blockedByLabel: "Organization Policy (SaaS)"
3008
- };
2825
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
2826
+ return {
2827
+ approved: !!initResult.approved,
2828
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
2829
+ checkedBy: initResult.approved ? "cloud" : void 0,
2830
+ blockedBy: initResult.approved ? void 0 : "team-policy",
2831
+ blockedByLabel: "Organization Policy (SaaS)"
2832
+ };
2833
+ }
2834
+ }
2835
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
2836
+ cloudRequestId = initResult.requestId || null;
3009
2837
  }
3010
- cloudRequestId = initResult.requestId || null;
3011
2838
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3012
2839
  } catch {
3013
2840
  }
@@ -3053,7 +2880,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3053
2880
  riskMetadata,
3054
2881
  options?.activityId,
3055
2882
  options?.cwd,
3056
- statefulRecoveryCommand
2883
+ statefulRecoveryCommand,
2884
+ void 0,
2885
+ void 0,
2886
+ localSmartRuleMatched || options?.localSmartRuleMatched
3057
2887
  );
3058
2888
  daemonEntryId = entry.id;
3059
2889
  daemonAllowCount = entry.allowCount;
@@ -3061,7 +2891,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3061
2891
  }
3062
2892
  }
3063
2893
  }
3064
- if (cloudEnforced && cloudRequestId) {
2894
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3065
2895
  racePromises.push(
3066
2896
  (async () => {
3067
2897
  try {
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "aws",
3
+ "description": "Protects AWS infrastructure from destructive AI operations",
4
+ "aliases": ["amazon"],
5
+ "smartRules": [
6
+ {
7
+ "name": "shield:aws:block-delete-s3-bucket",
8
+ "tool": "*",
9
+ "conditions": [
10
+ {
11
+ "field": "command",
12
+ "op": "matches",
13
+ "value": "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
14
+ "flags": "i"
15
+ }
16
+ ],
17
+ "verdict": "block",
18
+ "reason": "S3 bucket deletion is irreversible — blocked by AWS shield"
19
+ },
20
+ {
21
+ "name": "shield:aws:review-iam-changes",
22
+ "tool": "*",
23
+ "conditions": [
24
+ {
25
+ "field": "command",
26
+ "op": "matches",
27
+ "value": "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
28
+ "flags": "i"
29
+ }
30
+ ],
31
+ "verdict": "review",
32
+ "reason": "IAM changes require human approval (AWS shield)"
33
+ },
34
+ {
35
+ "name": "shield:aws:block-ec2-terminate",
36
+ "tool": "*",
37
+ "conditions": [
38
+ {
39
+ "field": "command",
40
+ "op": "matches",
41
+ "value": "aws\\s+ec2\\s+terminate-instances",
42
+ "flags": "i"
43
+ }
44
+ ],
45
+ "verdict": "block",
46
+ "reason": "EC2 instance termination is irreversible — blocked by AWS shield"
47
+ },
48
+ {
49
+ "name": "shield:aws:review-rds-delete",
50
+ "tool": "*",
51
+ "conditions": [
52
+ { "field": "command", "op": "matches", "value": "aws\\s+rds\\s+delete-", "flags": "i" }
53
+ ],
54
+ "verdict": "review",
55
+ "reason": "RDS deletion requires human approval (AWS shield)"
56
+ }
57
+ ],
58
+ "dangerousWords": []
59
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "bash-safe",
3
+ "description": "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
4
+ "aliases": ["bash", "shell"],
5
+ "smartRules": [
6
+ {
7
+ "name": "shield:bash-safe:block-pipe-to-shell",
8
+ "tool": "bash",
9
+ "conditions": [
10
+ {
11
+ "field": "command",
12
+ "op": "matches",
13
+ "value": "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
14
+ "flags": "i"
15
+ }
16
+ ],
17
+ "verdict": "block",
18
+ "reason": "Pipe-to-shell is a common supply-chain attack vector — blocked by bash-safe shield"
19
+ },
20
+ {
21
+ "name": "shield:bash-safe:block-obfuscated-exec",
22
+ "tool": "bash",
23
+ "conditions": [
24
+ {
25
+ "field": "command",
26
+ "op": "matches",
27
+ "value": "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
28
+ "flags": "i"
29
+ }
30
+ ],
31
+ "verdict": "block",
32
+ "reason": "Obfuscated execution via base64 decode — blocked by bash-safe shield"
33
+ },
34
+ {
35
+ "name": "shield:bash-safe:block-rm-root",
36
+ "tool": "bash",
37
+ "conditions": [
38
+ {
39
+ "field": "command",
40
+ "op": "matches",
41
+ "value": "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
42
+ "flags": "i"
43
+ }
44
+ ],
45
+ "verdict": "block",
46
+ "reason": "rm -rf of root or home directory is catastrophic — blocked by bash-safe shield"
47
+ },
48
+ {
49
+ "name": "shield:bash-safe:block-disk-overwrite",
50
+ "tool": "bash",
51
+ "conditions": [
52
+ {
53
+ "field": "command",
54
+ "op": "matches",
55
+ "value": "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
56
+ "flags": "i"
57
+ }
58
+ ],
59
+ "verdict": "block",
60
+ "reason": "Writing directly to a block device is irreversible — blocked by bash-safe shield"
61
+ },
62
+ {
63
+ "name": "shield:bash-safe:review-eval",
64
+ "tool": "bash",
65
+ "conditions": [
66
+ {
67
+ "field": "command",
68
+ "op": "matches",
69
+ "value": "\\beval\\s+[\\$`(\"]",
70
+ "flags": "i"
71
+ }
72
+ ],
73
+ "verdict": "review",
74
+ "reason": "eval of dynamic content requires human approval (bash-safe shield)"
75
+ }
76
+ ],
77
+ "dangerousWords": []
78
+ }
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "docker",
3
+ "description": "Protects Docker environments from destructive AI operations",
4
+ "aliases": [],
5
+ "smartRules": [
6
+ {
7
+ "name": "shield:docker:block-system-prune",
8
+ "tool": "*",
9
+ "conditions": [
10
+ {
11
+ "field": "command",
12
+ "op": "matches",
13
+ "value": "docker\\s+system\\s+prune",
14
+ "flags": "i"
15
+ }
16
+ ],
17
+ "verdict": "block",
18
+ "reason": "docker system prune removes all unused containers, images, and volumes — blocked by Docker shield"
19
+ },
20
+ {
21
+ "name": "shield:docker:block-volume-prune",
22
+ "tool": "*",
23
+ "conditions": [
24
+ {
25
+ "field": "command",
26
+ "op": "matches",
27
+ "value": "docker\\s+volume\\s+prune",
28
+ "flags": "i"
29
+ }
30
+ ],
31
+ "verdict": "block",
32
+ "reason": "docker volume prune destroys all unused volumes and their data — blocked by Docker shield"
33
+ },
34
+ {
35
+ "name": "shield:docker:block-rm-force",
36
+ "tool": "*",
37
+ "conditionMode": "all",
38
+ "conditions": [
39
+ {
40
+ "field": "command",
41
+ "op": "matches",
42
+ "value": "docker\\s+rm\\b",
43
+ "flags": "i"
44
+ },
45
+ {
46
+ "field": "command",
47
+ "op": "matches",
48
+ "value": "(^|\\s)(-f|--force)(\\s|$)",
49
+ "flags": "i"
50
+ }
51
+ ],
52
+ "verdict": "block",
53
+ "reason": "Force-removing running containers is destructive — blocked by Docker shield"
54
+ },
55
+ {
56
+ "name": "shield:docker:review-volume-rm",
57
+ "tool": "*",
58
+ "conditions": [
59
+ {
60
+ "field": "command",
61
+ "op": "matches",
62
+ "value": "docker\\s+volume\\s+rm\\s+",
63
+ "flags": "i"
64
+ }
65
+ ],
66
+ "verdict": "review",
67
+ "reason": "Volume removal deletes persistent data and requires human approval (Docker shield)"
68
+ },
69
+ {
70
+ "name": "shield:docker:review-stop-kill",
71
+ "tool": "*",
72
+ "conditions": [
73
+ {
74
+ "field": "command",
75
+ "op": "matches",
76
+ "value": "docker\\s+(stop|kill)\\s+",
77
+ "flags": "i"
78
+ }
79
+ ],
80
+ "verdict": "review",
81
+ "reason": "Stopping or killing containers requires human approval (Docker shield)"
82
+ },
83
+ {
84
+ "name": "shield:docker:review-image-rm",
85
+ "tool": "*",
86
+ "conditions": [
87
+ {
88
+ "field": "command",
89
+ "op": "matches",
90
+ "value": "docker\\s+image\\s+rm\\b",
91
+ "flags": "i"
92
+ }
93
+ ],
94
+ "verdict": "review",
95
+ "reason": "Image removal requires human approval (Docker shield)"
96
+ },
97
+ {
98
+ "name": "shield:docker:review-rmi-force",
99
+ "tool": "*",
100
+ "conditionMode": "all",
101
+ "conditions": [
102
+ {
103
+ "field": "command",
104
+ "op": "matches",
105
+ "value": "docker\\s+rmi\\b",
106
+ "flags": "i"
107
+ },
108
+ {
109
+ "field": "command",
110
+ "op": "matches",
111
+ "value": "(^|\\s)(-f|--force)(\\s|$)",
112
+ "flags": "i"
113
+ }
114
+ ],
115
+ "verdict": "review",
116
+ "reason": "Force image removal requires human approval (Docker shield)"
117
+ }
118
+ ],
119
+ "dangerousWords": []
120
+ }