@node9/proxy 1.0.13 → 1.0.14

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/cli.mjs CHANGED
@@ -6,9 +6,9 @@ import { Command } from "commander";
6
6
  // src/core.ts
7
7
  import chalk2 from "chalk";
8
8
  import { confirm } from "@inquirer/prompts";
9
- import fs from "fs";
10
- import path3 from "path";
11
- import os from "os";
9
+ import fs2 from "fs";
10
+ import path4 from "path";
11
+ import os2 from "os";
12
12
  import pm from "picomatch";
13
13
  import { parse } from "sh-syntax";
14
14
 
@@ -338,25 +338,26 @@ import { z } from "zod";
338
338
  var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
339
339
  message: "Value must not contain literal newline characters (use \\n instead)"
340
340
  });
341
- var validRegex = noNewlines.refine(
342
- (s) => {
343
- try {
344
- new RegExp(s);
345
- return true;
346
- } catch {
347
- return false;
348
- }
349
- },
350
- { message: "Value must be a valid regular expression" }
351
- );
352
341
  var SmartConditionSchema = z.object({
353
342
  field: z.string().min(1, "Condition field must not be empty"),
354
- op: z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
355
- errorMap: () => ({
356
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
357
- })
358
- }),
359
- value: validRegex.optional(),
343
+ op: z.enum(
344
+ [
345
+ "matches",
346
+ "notMatches",
347
+ "contains",
348
+ "notContains",
349
+ "exists",
350
+ "notExists",
351
+ "matchesGlob",
352
+ "notMatchesGlob"
353
+ ],
354
+ {
355
+ errorMap: () => ({
356
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
357
+ })
358
+ }
359
+ ),
360
+ value: z.string().optional(),
360
361
  flags: z.string().optional()
361
362
  });
362
363
  var SmartRuleSchema = z.object({
@@ -369,11 +370,6 @@ var SmartRuleSchema = z.object({
369
370
  }),
370
371
  reason: z.string().optional()
371
372
  });
372
- var PolicyRuleSchema = z.object({
373
- action: z.string().min(1),
374
- allowPaths: z.array(z.string()).optional(),
375
- blockPaths: z.array(z.string()).optional()
376
- });
377
373
  var ConfigFileSchema = z.object({
378
374
  version: z.string().optional(),
379
375
  settings: z.object({
@@ -398,12 +394,15 @@ var ConfigFileSchema = z.object({
398
394
  dangerousWords: z.array(noNewlines).optional(),
399
395
  ignoredTools: z.array(z.string()).optional(),
400
396
  toolInspection: z.record(z.string()).optional(),
401
- rules: z.array(PolicyRuleSchema).optional(),
402
397
  smartRules: z.array(SmartRuleSchema).optional(),
403
398
  snapshot: z.object({
404
399
  tools: z.array(z.string()).optional(),
405
400
  onlyPaths: z.array(z.string()).optional(),
406
401
  ignorePaths: z.array(z.string()).optional()
402
+ }).optional(),
403
+ dlp: z.object({
404
+ enabled: z.boolean().optional(),
405
+ scanIgnoredTools: z.boolean().optional()
407
406
  }).optional()
408
407
  }).optional(),
409
408
  environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
@@ -425,8 +424,8 @@ function sanitizeConfig(raw) {
425
424
  }
426
425
  }
427
426
  const lines = result.error.issues.map((issue) => {
428
- const path8 = issue.path.length > 0 ? issue.path.join(".") : "root";
429
- return ` \u2022 ${path8}: ${issue.message}`;
427
+ const path9 = issue.path.length > 0 ? issue.path.join(".") : "root";
428
+ return ` \u2022 ${path9}: ${issue.message}`;
430
429
  });
431
430
  return {
432
431
  sanitized,
@@ -435,18 +434,301 @@ ${lines.join("\n")}`
435
434
  };
436
435
  }
437
436
 
437
+ // src/shields.ts
438
+ import fs from "fs";
439
+ import path3 from "path";
440
+ import os from "os";
441
+ import crypto from "crypto";
442
+ var SHIELDS = {
443
+ postgres: {
444
+ name: "postgres",
445
+ description: "Protects PostgreSQL databases from destructive AI operations",
446
+ aliases: ["pg", "postgresql"],
447
+ smartRules: [
448
+ {
449
+ name: "shield:postgres:block-drop-table",
450
+ tool: "*",
451
+ conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
452
+ verdict: "block",
453
+ reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
454
+ },
455
+ {
456
+ name: "shield:postgres:block-truncate",
457
+ tool: "*",
458
+ conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
459
+ verdict: "block",
460
+ reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
461
+ },
462
+ {
463
+ name: "shield:postgres:block-drop-column",
464
+ tool: "*",
465
+ conditions: [
466
+ { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
467
+ ],
468
+ verdict: "block",
469
+ reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
470
+ },
471
+ {
472
+ name: "shield:postgres:review-grant-revoke",
473
+ tool: "*",
474
+ conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
475
+ verdict: "review",
476
+ reason: "Permission changes require human approval (Postgres shield)"
477
+ }
478
+ ],
479
+ dangerousWords: ["dropdb", "pg_dropcluster"]
480
+ },
481
+ github: {
482
+ name: "github",
483
+ description: "Protects GitHub repositories from destructive AI operations",
484
+ aliases: ["git"],
485
+ smartRules: [
486
+ {
487
+ // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
488
+ // This rule adds coverage for `git push --delete` which the built-in does not match.
489
+ name: "shield:github:review-delete-branch-remote",
490
+ tool: "bash",
491
+ conditions: [
492
+ {
493
+ field: "command",
494
+ op: "matches",
495
+ value: "git\\s+push\\s+.*--delete",
496
+ flags: "i"
497
+ }
498
+ ],
499
+ verdict: "review",
500
+ reason: "Remote branch deletion requires human approval (GitHub shield)"
501
+ },
502
+ {
503
+ name: "shield:github:block-delete-repo",
504
+ tool: "*",
505
+ conditions: [
506
+ { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
507
+ ],
508
+ verdict: "block",
509
+ reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
510
+ }
511
+ ],
512
+ dangerousWords: []
513
+ },
514
+ aws: {
515
+ name: "aws",
516
+ description: "Protects AWS infrastructure from destructive AI operations",
517
+ aliases: ["amazon"],
518
+ smartRules: [
519
+ {
520
+ name: "shield:aws:block-delete-s3-bucket",
521
+ tool: "*",
522
+ conditions: [
523
+ {
524
+ field: "command",
525
+ op: "matches",
526
+ value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
527
+ flags: "i"
528
+ }
529
+ ],
530
+ verdict: "block",
531
+ reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
532
+ },
533
+ {
534
+ name: "shield:aws:review-iam-changes",
535
+ tool: "*",
536
+ conditions: [
537
+ {
538
+ field: "command",
539
+ op: "matches",
540
+ value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
541
+ flags: "i"
542
+ }
543
+ ],
544
+ verdict: "review",
545
+ reason: "IAM changes require human approval (AWS shield)"
546
+ },
547
+ {
548
+ name: "shield:aws:block-ec2-terminate",
549
+ tool: "*",
550
+ conditions: [
551
+ {
552
+ field: "command",
553
+ op: "matches",
554
+ value: "aws\\s+ec2\\s+terminate-instances",
555
+ flags: "i"
556
+ }
557
+ ],
558
+ verdict: "block",
559
+ reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
560
+ },
561
+ {
562
+ name: "shield:aws:review-rds-delete",
563
+ tool: "*",
564
+ conditions: [
565
+ { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
566
+ ],
567
+ verdict: "review",
568
+ reason: "RDS deletion requires human approval (AWS shield)"
569
+ }
570
+ ],
571
+ dangerousWords: []
572
+ },
573
+ filesystem: {
574
+ name: "filesystem",
575
+ description: "Protects the local filesystem from dangerous AI operations",
576
+ aliases: ["fs"],
577
+ smartRules: [
578
+ {
579
+ name: "shield:filesystem:review-chmod-777",
580
+ tool: "bash",
581
+ conditions: [
582
+ { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
583
+ ],
584
+ verdict: "review",
585
+ reason: "chmod 777 requires human approval (filesystem shield)"
586
+ },
587
+ {
588
+ name: "shield:filesystem:review-write-etc",
589
+ tool: "bash",
590
+ conditions: [
591
+ {
592
+ field: "command",
593
+ // Narrow to write-indicative operations to avoid approval fatigue on reads.
594
+ // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
595
+ op: "matches",
596
+ value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
597
+ }
598
+ ],
599
+ verdict: "review",
600
+ reason: "Writing to /etc requires human approval (filesystem shield)"
601
+ }
602
+ ],
603
+ // dd removed: too common as a legitimate tool (disk imaging, file ops).
604
+ // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
605
+ // wipefs retained: rarely legitimate in an agent context and not in built-ins.
606
+ dangerousWords: ["wipefs"]
607
+ }
608
+ };
609
+ function resolveShieldName(input) {
610
+ const lower = input.toLowerCase();
611
+ if (SHIELDS[lower]) return lower;
612
+ for (const [name, def] of Object.entries(SHIELDS)) {
613
+ if (def.aliases.includes(lower)) return name;
614
+ }
615
+ return null;
616
+ }
617
+ function getShield(name) {
618
+ const resolved = resolveShieldName(name);
619
+ return resolved ? SHIELDS[resolved] : null;
620
+ }
621
+ function listShields() {
622
+ return Object.values(SHIELDS);
623
+ }
624
+ var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
625
+ function readActiveShields() {
626
+ try {
627
+ const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
628
+ if (!raw.trim()) return [];
629
+ const parsed = JSON.parse(raw);
630
+ if (Array.isArray(parsed.active)) {
631
+ return parsed.active.filter(
632
+ (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
633
+ );
634
+ }
635
+ } catch (err) {
636
+ if (err.code !== "ENOENT") {
637
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
638
+ `);
639
+ }
640
+ }
641
+ return [];
642
+ }
643
+ function writeActiveShields(active) {
644
+ fs.mkdirSync(path3.dirname(SHIELDS_STATE_FILE), { recursive: true });
645
+ const tmp = `${SHIELDS_STATE_FILE}.${crypto.randomBytes(6).toString("hex")}.tmp`;
646
+ fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
647
+ fs.renameSync(tmp, SHIELDS_STATE_FILE);
648
+ }
649
+
650
+ // src/dlp.ts
651
+ var DLP_PATTERNS = [
652
+ { name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
653
+ { name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
654
+ { name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
655
+ { name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
656
+ { name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
657
+ {
658
+ name: "Private Key (PEM)",
659
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
660
+ severity: "block"
661
+ },
662
+ { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
663
+ ];
664
+ function maskSecret(raw, pattern) {
665
+ const match = raw.match(pattern);
666
+ if (!match) return "****";
667
+ const secret = match[0];
668
+ if (secret.length < 8) return "****";
669
+ const prefix = secret.slice(0, 4);
670
+ const suffix = secret.slice(-4);
671
+ const stars = "*".repeat(Math.min(secret.length - 8, 12));
672
+ return `${prefix}${stars}${suffix}`;
673
+ }
674
+ var MAX_DEPTH = 5;
675
+ var MAX_STRING_BYTES = 1e5;
676
+ var MAX_JSON_PARSE_BYTES = 1e4;
677
+ function scanArgs(args, depth = 0, fieldPath = "args") {
678
+ if (depth > MAX_DEPTH || args === null || args === void 0) return null;
679
+ if (Array.isArray(args)) {
680
+ for (let i = 0; i < args.length; i++) {
681
+ const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
682
+ if (match) return match;
683
+ }
684
+ return null;
685
+ }
686
+ if (typeof args === "object") {
687
+ for (const [key, value] of Object.entries(args)) {
688
+ const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
689
+ if (match) return match;
690
+ }
691
+ return null;
692
+ }
693
+ if (typeof args === "string") {
694
+ const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
695
+ for (const pattern of DLP_PATTERNS) {
696
+ if (pattern.regex.test(text)) {
697
+ return {
698
+ patternName: pattern.name,
699
+ fieldPath,
700
+ redactedSample: maskSecret(text, pattern.regex),
701
+ severity: pattern.severity
702
+ };
703
+ }
704
+ }
705
+ if (text.length < MAX_JSON_PARSE_BYTES) {
706
+ const trimmed = text.trim();
707
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
708
+ try {
709
+ const parsed = JSON.parse(text);
710
+ const inner = scanArgs(parsed, depth + 1, fieldPath);
711
+ if (inner) return inner;
712
+ } catch {
713
+ }
714
+ }
715
+ }
716
+ }
717
+ return null;
718
+ }
719
+
438
720
  // src/core.ts
439
- var PAUSED_FILE = path3.join(os.homedir(), ".node9", "PAUSED");
440
- var TRUST_FILE = path3.join(os.homedir(), ".node9", "trust.json");
441
- var LOCAL_AUDIT_LOG = path3.join(os.homedir(), ".node9", "audit.log");
442
- var HOOK_DEBUG_LOG = path3.join(os.homedir(), ".node9", "hook-debug.log");
721
+ var PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
722
+ var TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
723
+ var LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
724
+ var HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
443
725
  function checkPause() {
444
726
  try {
445
- if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
446
- const state = JSON.parse(fs.readFileSync(PAUSED_FILE, "utf-8"));
727
+ if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
728
+ const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
447
729
  if (state.expiry > 0 && Date.now() >= state.expiry) {
448
730
  try {
449
- fs.unlinkSync(PAUSED_FILE);
731
+ fs2.unlinkSync(PAUSED_FILE);
450
732
  } catch {
451
733
  }
452
734
  return { paused: false };
@@ -457,11 +739,11 @@ function checkPause() {
457
739
  }
458
740
  }
459
741
  function atomicWriteSync(filePath, data, options) {
460
- const dir = path3.dirname(filePath);
461
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
462
- const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
463
- fs.writeFileSync(tmpPath, data, options);
464
- fs.renameSync(tmpPath, filePath);
742
+ const dir = path4.dirname(filePath);
743
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
744
+ const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
745
+ fs2.writeFileSync(tmpPath, data, options);
746
+ fs2.renameSync(tmpPath, filePath);
465
747
  }
466
748
  function pauseNode9(durationMs, durationStr) {
467
749
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -469,18 +751,18 @@ function pauseNode9(durationMs, durationStr) {
469
751
  }
470
752
  function resumeNode9() {
471
753
  try {
472
- if (fs.existsSync(PAUSED_FILE)) fs.unlinkSync(PAUSED_FILE);
754
+ if (fs2.existsSync(PAUSED_FILE)) fs2.unlinkSync(PAUSED_FILE);
473
755
  } catch {
474
756
  }
475
757
  }
476
758
  function getActiveTrustSession(toolName) {
477
759
  try {
478
- if (!fs.existsSync(TRUST_FILE)) return false;
479
- const trust = JSON.parse(fs.readFileSync(TRUST_FILE, "utf-8"));
760
+ if (!fs2.existsSync(TRUST_FILE)) return false;
761
+ const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
480
762
  const now = Date.now();
481
763
  const active = trust.entries.filter((e) => e.expiry > now);
482
764
  if (active.length !== trust.entries.length) {
483
- fs.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
765
+ fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
484
766
  }
485
767
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
486
768
  } catch {
@@ -491,8 +773,8 @@ function writeTrustSession(toolName, durationMs) {
491
773
  try {
492
774
  let trust = { entries: [] };
493
775
  try {
494
- if (fs.existsSync(TRUST_FILE)) {
495
- trust = JSON.parse(fs.readFileSync(TRUST_FILE, "utf-8"));
776
+ if (fs2.existsSync(TRUST_FILE)) {
777
+ trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
496
778
  }
497
779
  } catch {
498
780
  }
@@ -508,9 +790,9 @@ function writeTrustSession(toolName, durationMs) {
508
790
  }
509
791
  function appendToLog(logPath, entry) {
510
792
  try {
511
- const dir = path3.dirname(logPath);
512
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
513
- fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
793
+ const dir = path4.dirname(logPath);
794
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
795
+ fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
514
796
  } catch {
515
797
  }
516
798
  }
@@ -522,7 +804,7 @@ function appendHookDebug(toolName, args, meta) {
522
804
  args: safeArgs,
523
805
  agent: meta?.agent,
524
806
  mcpServer: meta?.mcpServer,
525
- hostname: os.hostname(),
807
+ hostname: os2.hostname(),
526
808
  cwd: process.cwd()
527
809
  });
528
810
  }
@@ -536,7 +818,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
536
818
  checkedBy,
537
819
  agent: meta?.agent,
538
820
  mcpServer: meta?.mcpServer,
539
- hostname: os.hostname()
821
+ hostname: os2.hostname()
540
822
  });
541
823
  }
542
824
  function tokenize(toolName) {
@@ -552,9 +834,9 @@ function matchesPattern(text, patterns) {
552
834
  const withoutDotSlash = text.replace(/^\.\//, "");
553
835
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
554
836
  }
555
- function getNestedValue(obj, path8) {
837
+ function getNestedValue(obj, path9) {
556
838
  if (!obj || typeof obj !== "object") return null;
557
- return path8.split(".").reduce((prev, curr) => prev?.[curr], obj);
839
+ return path9.split(".").reduce((prev, curr) => prev?.[curr], obj);
558
840
  }
559
841
  function shouldSnapshot(toolName, args, config) {
560
842
  if (!config.settings.enableUndo) return false;
@@ -599,6 +881,10 @@ function evaluateSmartConditions(args, rule) {
599
881
  return true;
600
882
  }
601
883
  }
884
+ case "matchesGlob":
885
+ return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
886
+ case "notMatchesGlob":
887
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
602
888
  default:
603
889
  return false;
604
890
  }
@@ -762,25 +1048,27 @@ var DEFAULT_CONFIG = {
762
1048
  onlyPaths: [],
763
1049
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
764
1050
  },
765
- rules: [
766
- // Only use the legacy rules format for simple path-based rm control.
767
- // All other command-level enforcement lives in smartRules below.
768
- {
769
- action: "rm",
770
- allowPaths: [
771
- "**/node_modules/**",
772
- "dist/**",
773
- "build/**",
774
- ".next/**",
775
- "coverage/**",
776
- ".cache/**",
777
- "tmp/**",
778
- "temp/**",
779
- ".DS_Store"
780
- ]
781
- }
782
- ],
783
1051
  smartRules: [
1052
+ // ── rm safety (critical — always evaluated first) ──────────────────────
1053
+ {
1054
+ name: "block-rm-rf-home",
1055
+ tool: "bash",
1056
+ conditionMode: "all",
1057
+ conditions: [
1058
+ {
1059
+ field: "command",
1060
+ op: "matches",
1061
+ value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
1062
+ },
1063
+ {
1064
+ field: "command",
1065
+ op: "matches",
1066
+ value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
1067
+ }
1068
+ ],
1069
+ verdict: "block",
1070
+ reason: "Recursive delete of home directory is irreversible"
1071
+ },
784
1072
  // ── SQL safety ────────────────────────────────────────────────────────
785
1073
  {
786
1074
  name: "no-delete-without-where",
@@ -871,19 +1159,45 @@ var DEFAULT_CONFIG = {
871
1159
  verdict: "block",
872
1160
  reason: "Piping remote script into a shell is a supply-chain attack vector"
873
1161
  }
874
- ]
1162
+ ],
1163
+ dlp: { enabled: true, scanIgnoredTools: true }
875
1164
  },
876
1165
  environments: {}
877
1166
  };
1167
+ var ADVISORY_SMART_RULES = [
1168
+ {
1169
+ name: "allow-rm-safe-paths",
1170
+ tool: "*",
1171
+ conditionMode: "all",
1172
+ conditions: [
1173
+ { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
1174
+ {
1175
+ field: "command",
1176
+ op: "matches",
1177
+ // Matches known-safe build artifact paths in the command.
1178
+ value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
1179
+ }
1180
+ ],
1181
+ verdict: "allow",
1182
+ reason: "Deleting a known-safe build artifact path"
1183
+ },
1184
+ {
1185
+ name: "review-rm",
1186
+ tool: "*",
1187
+ conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
1188
+ verdict: "review",
1189
+ reason: "rm can permanently delete files \u2014 confirm the target path"
1190
+ }
1191
+ ];
878
1192
  var cachedConfig = null;
879
1193
  function _resetConfigCache() {
880
1194
  cachedConfig = null;
881
1195
  }
882
1196
  function getGlobalSettings() {
883
1197
  try {
884
- const globalConfigPath = path3.join(os.homedir(), ".node9", "config.json");
885
- if (fs.existsSync(globalConfigPath)) {
886
- const parsed = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
1198
+ const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
1199
+ if (fs2.existsSync(globalConfigPath)) {
1200
+ const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
887
1201
  const settings = parsed.settings || {};
888
1202
  return {
889
1203
  mode: settings.mode || "standard",
@@ -905,9 +1219,9 @@ function getGlobalSettings() {
905
1219
  }
906
1220
  function getInternalToken() {
907
1221
  try {
908
- const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
909
- if (!fs.existsSync(pidFile)) return null;
910
- const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1222
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1223
+ if (!fs2.existsSync(pidFile)) return null;
1224
+ const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
911
1225
  process.kill(data.pid, 0);
912
1226
  return data.internalToken ?? null;
913
1227
  } catch {
@@ -922,7 +1236,8 @@ async function evaluatePolicy(toolName, args, agent) {
922
1236
  (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
923
1237
  );
924
1238
  if (matchedRule) {
925
- if (matchedRule.verdict === "allow") return { decision: "allow" };
1239
+ if (matchedRule.verdict === "allow")
1240
+ return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
926
1241
  return {
927
1242
  decision: matchedRule.verdict,
928
1243
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -933,13 +1248,11 @@ async function evaluatePolicy(toolName, args, agent) {
933
1248
  }
934
1249
  }
935
1250
  let allTokens = [];
936
- let actionTokens = [];
937
1251
  let pathTokens = [];
938
1252
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
939
1253
  if (shellCommand) {
940
1254
  const analyzed = await analyzeShellCommand(shellCommand);
941
1255
  allTokens = analyzed.allTokens;
942
- actionTokens = analyzed.actions;
943
1256
  pathTokens = analyzed.paths;
944
1257
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
945
1258
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
@@ -947,11 +1260,9 @@ async function evaluatePolicy(toolName, args, agent) {
947
1260
  }
948
1261
  if (isSqlTool(toolName, config.policy.toolInspection)) {
949
1262
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
950
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
951
1263
  }
952
1264
  } else {
953
1265
  allTokens = tokenize(toolName);
954
- actionTokens = [toolName];
955
1266
  if (args && typeof args === "object") {
956
1267
  const flattenedArgs = JSON.stringify(args).toLowerCase();
957
1268
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
@@ -974,29 +1285,6 @@ async function evaluatePolicy(toolName, args, agent) {
974
1285
  const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
975
1286
  if (allInSandbox) return { decision: "allow" };
976
1287
  }
977
- for (const action of actionTokens) {
978
- const rule = config.policy.rules.find(
979
- (r) => r.action === action || matchesPattern(action, r.action)
980
- );
981
- if (rule) {
982
- if (pathTokens.length > 0) {
983
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
984
- if (anyBlocked)
985
- return {
986
- decision: "review",
987
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
988
- tier: 5
989
- };
990
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
991
- if (allAllowed) return { decision: "allow" };
992
- }
993
- return {
994
- decision: "review",
995
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
996
- tier: 5
997
- };
998
- }
999
- }
1000
1288
  let matchedDangerousWord;
1001
1289
  const isDangerous = allTokens.some(
1002
1290
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1048,9 +1336,9 @@ async function evaluatePolicy(toolName, args, agent) {
1048
1336
  }
1049
1337
  async function explainPolicy(toolName, args) {
1050
1338
  const steps = [];
1051
- const globalPath = path3.join(os.homedir(), ".node9", "config.json");
1052
- const projectPath = path3.join(process.cwd(), "node9.config.json");
1053
- const credsPath = path3.join(os.homedir(), ".node9", "credentials.json");
1339
+ const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
1340
+ const projectPath = path4.join(process.cwd(), "node9.config.json");
1341
+ const credsPath = path4.join(os2.homedir(), ".node9", "credentials.json");
1054
1342
  const waterfall = [
1055
1343
  {
1056
1344
  tier: 1,
@@ -1061,19 +1349,19 @@ async function explainPolicy(toolName, args) {
1061
1349
  {
1062
1350
  tier: 2,
1063
1351
  label: "Cloud policy",
1064
- status: fs.existsSync(credsPath) ? "active" : "missing",
1065
- note: fs.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1352
+ status: fs2.existsSync(credsPath) ? "active" : "missing",
1353
+ note: fs2.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1066
1354
  },
1067
1355
  {
1068
1356
  tier: 3,
1069
1357
  label: "Project config",
1070
- status: fs.existsSync(projectPath) ? "active" : "missing",
1358
+ status: fs2.existsSync(projectPath) ? "active" : "missing",
1071
1359
  path: projectPath
1072
1360
  },
1073
1361
  {
1074
1362
  tier: 4,
1075
1363
  label: "Global config",
1076
- status: fs.existsSync(globalPath) ? "active" : "missing",
1364
+ status: fs2.existsSync(globalPath) ? "active" : "missing",
1077
1365
  path: globalPath
1078
1366
  },
1079
1367
  {
@@ -1084,7 +1372,28 @@ async function explainPolicy(toolName, args) {
1084
1372
  }
1085
1373
  ];
1086
1374
  const config = getConfig();
1087
- if (matchesPattern(toolName, config.policy.ignoredTools)) {
1375
+ const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
1376
+ if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
1377
+ const dlpMatch = args !== void 0 ? scanArgs(args) : null;
1378
+ if (dlpMatch) {
1379
+ steps.push({
1380
+ name: "DLP Content Scanner",
1381
+ outcome: dlpMatch.severity === "block" ? "block" : "review",
1382
+ detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
1383
+ isFinal: dlpMatch.severity === "block"
1384
+ });
1385
+ if (dlpMatch.severity === "block") {
1386
+ return { tool: toolName, args, waterfall, steps, decision: "block" };
1387
+ }
1388
+ } else {
1389
+ steps.push({
1390
+ name: "DLP Content Scanner",
1391
+ outcome: "checked",
1392
+ detail: "No sensitive credentials detected in args"
1393
+ });
1394
+ }
1395
+ }
1396
+ if (wouldBeIgnored) {
1088
1397
  steps.push({
1089
1398
  name: "Ignored tools",
1090
1399
  outcome: "allow",
@@ -1137,13 +1446,11 @@ async function explainPolicy(toolName, args) {
1137
1446
  steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
1138
1447
  }
1139
1448
  let allTokens = [];
1140
- let actionTokens = [];
1141
1449
  let pathTokens = [];
1142
1450
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
1143
1451
  if (shellCommand) {
1144
1452
  const analyzed = await analyzeShellCommand(shellCommand);
1145
1453
  allTokens = analyzed.allTokens;
1146
- actionTokens = analyzed.actions;
1147
1454
  pathTokens = analyzed.paths;
1148
1455
  const patterns = Object.keys(config.policy.toolInspection);
1149
1456
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -1177,7 +1484,6 @@ async function explainPolicy(toolName, args) {
1177
1484
  });
1178
1485
  if (isSqlTool(toolName, config.policy.toolInspection)) {
1179
1486
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1180
- actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
1181
1487
  steps.push({
1182
1488
  name: "SQL token stripping",
1183
1489
  outcome: "checked",
@@ -1186,7 +1492,6 @@ async function explainPolicy(toolName, args) {
1186
1492
  }
1187
1493
  } else {
1188
1494
  allTokens = tokenize(toolName);
1189
- actionTokens = [toolName];
1190
1495
  let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
1191
1496
  if (args && typeof args === "object") {
1192
1497
  const flattenedArgs = JSON.stringify(args).toLowerCase();
@@ -1227,65 +1532,6 @@ async function explainPolicy(toolName, args) {
1227
1532
  detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
1228
1533
  });
1229
1534
  }
1230
- let ruleMatched = false;
1231
- for (const action of actionTokens) {
1232
- const rule = config.policy.rules.find(
1233
- (r) => r.action === action || matchesPattern(action, r.action)
1234
- );
1235
- if (rule) {
1236
- ruleMatched = true;
1237
- if (pathTokens.length > 0) {
1238
- const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
1239
- if (anyBlocked) {
1240
- steps.push({
1241
- name: "Policy rules",
1242
- outcome: "review",
1243
- detail: `Rule "${rule.action}" matched + path is in blockPaths`,
1244
- isFinal: true
1245
- });
1246
- return {
1247
- tool: toolName,
1248
- args,
1249
- waterfall,
1250
- steps,
1251
- decision: "review",
1252
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
1253
- };
1254
- }
1255
- const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
1256
- if (allAllowed) {
1257
- steps.push({
1258
- name: "Policy rules",
1259
- outcome: "allow",
1260
- detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
1261
- isFinal: true
1262
- });
1263
- return { tool: toolName, args, waterfall, steps, decision: "allow" };
1264
- }
1265
- }
1266
- steps.push({
1267
- name: "Policy rules",
1268
- outcome: "review",
1269
- detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
1270
- isFinal: true
1271
- });
1272
- return {
1273
- tool: toolName,
1274
- args,
1275
- waterfall,
1276
- steps,
1277
- decision: "review",
1278
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
1279
- };
1280
- }
1281
- }
1282
- if (!ruleMatched) {
1283
- steps.push({
1284
- name: "Policy rules",
1285
- outcome: "skip",
1286
- detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
1287
- });
1288
- }
1289
1535
  let matchedDangerousWord;
1290
1536
  const isDangerous = uniqueTokens.some(
1291
1537
  (token) => config.policy.dangerousWords.some((word) => {
@@ -1354,9 +1600,9 @@ var DAEMON_PORT = 7391;
1354
1600
  var DAEMON_HOST = "127.0.0.1";
1355
1601
  function isDaemonRunning() {
1356
1602
  try {
1357
- const pidFile = path3.join(os.homedir(), ".node9", "daemon.pid");
1358
- if (!fs.existsSync(pidFile)) return false;
1359
- const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
1603
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1604
+ if (!fs2.existsSync(pidFile)) return false;
1605
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1360
1606
  if (port !== DAEMON_PORT) return false;
1361
1607
  process.kill(pid, 0);
1362
1608
  return true;
@@ -1366,9 +1612,9 @@ function isDaemonRunning() {
1366
1612
  }
1367
1613
  function getPersistentDecision(toolName) {
1368
1614
  try {
1369
- const file = path3.join(os.homedir(), ".node9", "decisions.json");
1370
- if (!fs.existsSync(file)) return null;
1371
- const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
1615
+ const file = path4.join(os2.homedir(), ".node9", "decisions.json");
1616
+ if (!fs2.existsSync(file)) return null;
1617
+ const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
1372
1618
  const d = decisions[toolName];
1373
1619
  if (d === "allow" || d === "deny") return d;
1374
1620
  } catch {
@@ -1467,6 +1713,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1467
1713
  let policyMatchedField;
1468
1714
  let policyMatchedWord;
1469
1715
  let riskMetadata;
1716
+ if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
1717
+ const dlpMatch = scanArgs(args);
1718
+ if (dlpMatch) {
1719
+ const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
1720
+ if (dlpMatch.severity === "block") {
1721
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
1722
+ return {
1723
+ approved: false,
1724
+ reason: dlpReason,
1725
+ blockedBy: "local-config",
1726
+ blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1727
+ };
1728
+ }
1729
+ explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1730
+ }
1731
+ }
1470
1732
  if (config.settings.mode === "audit") {
1471
1733
  if (!isIgnoredTool(toolName)) {
1472
1734
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -1706,7 +1968,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1706
1968
  racePromises.push(
1707
1969
  (async () => {
1708
1970
  try {
1709
- console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1971
+ if (explainableLabel.includes("DLP")) {
1972
+ console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
1973
+ console.log(
1974
+ chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
1975
+ );
1976
+ } else {
1977
+ console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
1978
+ }
1710
1979
  console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
1711
1980
  console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
1712
1981
  if (isRemoteLocked) {
@@ -1811,8 +2080,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1811
2080
  }
1812
2081
  function getConfig() {
1813
2082
  if (cachedConfig) return cachedConfig;
1814
- const globalPath = path3.join(os.homedir(), ".node9", "config.json");
1815
- const projectPath = path3.join(process.cwd(), "node9.config.json");
2083
+ const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
2084
+ const projectPath = path4.join(process.cwd(), "node9.config.json");
1816
2085
  const globalConfig = tryLoadConfig(globalPath);
1817
2086
  const projectConfig = tryLoadConfig(projectPath);
1818
2087
  const mergedSettings = {
@@ -1824,13 +2093,13 @@ function getConfig() {
1824
2093
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
1825
2094
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1826
2095
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1827
- rules: [...DEFAULT_CONFIG.policy.rules],
1828
2096
  smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1829
2097
  snapshot: {
1830
2098
  tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1831
2099
  onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1832
2100
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1833
- }
2101
+ },
2102
+ dlp: { ...DEFAULT_CONFIG.policy.dlp }
1834
2103
  };
1835
2104
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
1836
2105
  const applyLayer = (source) => {
@@ -1850,7 +2119,6 @@ function getConfig() {
1850
2119
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
1851
2120
  if (p.toolInspection)
1852
2121
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1853
- if (p.rules) mergedPolicy.rules.push(...p.rules);
1854
2122
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1855
2123
  if (p.snapshot) {
1856
2124
  const s2 = p.snapshot;
@@ -1858,6 +2126,11 @@ function getConfig() {
1858
2126
  if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1859
2127
  if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1860
2128
  }
2129
+ if (p.dlp) {
2130
+ const d = p.dlp;
2131
+ if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
2132
+ if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
2133
+ }
1861
2134
  const envs = source.environments || {};
1862
2135
  for (const [envName, envConfig] of Object.entries(envs)) {
1863
2136
  if (envConfig && typeof envConfig === "object") {
@@ -1872,6 +2145,19 @@ function getConfig() {
1872
2145
  };
1873
2146
  applyLayer(globalConfig);
1874
2147
  applyLayer(projectConfig);
2148
+ for (const shieldName of readActiveShields()) {
2149
+ const shield = getShield(shieldName);
2150
+ if (!shield) continue;
2151
+ const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2152
+ for (const rule of shield.smartRules) {
2153
+ if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2154
+ }
2155
+ for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
2156
+ }
2157
+ const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
2158
+ for (const rule of ADVISORY_SMART_RULES) {
2159
+ if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
2160
+ }
1875
2161
  if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
1876
2162
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1877
2163
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
@@ -1887,10 +2173,10 @@ function getConfig() {
1887
2173
  return cachedConfig;
1888
2174
  }
1889
2175
  function tryLoadConfig(filePath) {
1890
- if (!fs.existsSync(filePath)) return null;
2176
+ if (!fs2.existsSync(filePath)) return null;
1891
2177
  let raw;
1892
2178
  try {
1893
- raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2179
+ raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1894
2180
  } catch (err) {
1895
2181
  const msg = err instanceof Error ? err.message : String(err);
1896
2182
  process.stderr.write(
@@ -1952,9 +2238,9 @@ function getCredentials() {
1952
2238
  };
1953
2239
  }
1954
2240
  try {
1955
- const credPath = path3.join(os.homedir(), ".node9", "credentials.json");
1956
- if (fs.existsSync(credPath)) {
1957
- const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
2241
+ const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
2242
+ if (fs2.existsSync(credPath)) {
2243
+ const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
1958
2244
  const profileName = process.env.NODE9_PROFILE || "default";
1959
2245
  const profile = creds[profileName];
1960
2246
  if (profile?.apiKey) {
@@ -1985,9 +2271,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1985
2271
  context: {
1986
2272
  agent: meta?.agent,
1987
2273
  mcpServer: meta?.mcpServer,
1988
- hostname: os.hostname(),
2274
+ hostname: os2.hostname(),
1989
2275
  cwd: process.cwd(),
1990
- platform: os.platform()
2276
+ platform: os2.platform()
1991
2277
  }
1992
2278
  }),
1993
2279
  signal: AbortSignal.timeout(5e3)
@@ -2008,9 +2294,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2008
2294
  context: {
2009
2295
  agent: meta?.agent,
2010
2296
  mcpServer: meta?.mcpServer,
2011
- hostname: os.hostname(),
2297
+ hostname: os2.hostname(),
2012
2298
  cwd: process.cwd(),
2013
- platform: os.platform()
2299
+ platform: os2.platform()
2014
2300
  },
2015
2301
  ...riskMetadata && { riskMetadata }
2016
2302
  }),
@@ -2069,9 +2355,9 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2069
2355
  }
2070
2356
 
2071
2357
  // src/setup.ts
2072
- import fs2 from "fs";
2073
- import path4 from "path";
2074
- import os2 from "os";
2358
+ import fs3 from "fs";
2359
+ import path5 from "path";
2360
+ import os3 from "os";
2075
2361
  import chalk3 from "chalk";
2076
2362
  import { confirm as confirm2 } from "@inquirer/prompts";
2077
2363
  function printDaemonTip() {
@@ -2087,22 +2373,22 @@ function fullPathCommand(subcommand) {
2087
2373
  }
2088
2374
  function readJson(filePath) {
2089
2375
  try {
2090
- if (fs2.existsSync(filePath)) {
2091
- return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
2376
+ if (fs3.existsSync(filePath)) {
2377
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
2092
2378
  }
2093
2379
  } catch {
2094
2380
  }
2095
2381
  return null;
2096
2382
  }
2097
2383
  function writeJson(filePath, data) {
2098
- const dir = path4.dirname(filePath);
2099
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
2100
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2384
+ const dir = path5.dirname(filePath);
2385
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2386
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
2101
2387
  }
2102
2388
  async function setupClaude() {
2103
- const homeDir2 = os2.homedir();
2104
- const mcpPath = path4.join(homeDir2, ".claude.json");
2105
- const hooksPath = path4.join(homeDir2, ".claude", "settings.json");
2389
+ const homeDir2 = os3.homedir();
2390
+ const mcpPath = path5.join(homeDir2, ".claude.json");
2391
+ const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
2106
2392
  const claudeConfig = readJson(mcpPath) ?? {};
2107
2393
  const settings = readJson(hooksPath) ?? {};
2108
2394
  const servers = claudeConfig.mcpServers ?? {};
@@ -2176,8 +2462,8 @@ async function setupClaude() {
2176
2462
  }
2177
2463
  }
2178
2464
  async function setupGemini() {
2179
- const homeDir2 = os2.homedir();
2180
- const settingsPath = path4.join(homeDir2, ".gemini", "settings.json");
2465
+ const homeDir2 = os3.homedir();
2466
+ const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
2181
2467
  const settings = readJson(settingsPath) ?? {};
2182
2468
  const servers = settings.mcpServers ?? {};
2183
2469
  let anythingChanged = false;
@@ -2259,36 +2545,11 @@ async function setupGemini() {
2259
2545
  }
2260
2546
  }
2261
2547
  async function setupCursor() {
2262
- const homeDir2 = os2.homedir();
2263
- const mcpPath = path4.join(homeDir2, ".cursor", "mcp.json");
2264
- const hooksPath = path4.join(homeDir2, ".cursor", "hooks.json");
2548
+ const homeDir2 = os3.homedir();
2549
+ const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
2265
2550
  const mcpConfig = readJson(mcpPath) ?? {};
2266
- const hooksFile = readJson(hooksPath) ?? { version: 1 };
2267
2551
  const servers = mcpConfig.mcpServers ?? {};
2268
2552
  let anythingChanged = false;
2269
- if (!hooksFile.hooks) hooksFile.hooks = {};
2270
- const hasPreHook = hooksFile.hooks.preToolUse?.some(
2271
- (h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
2272
- );
2273
- if (!hasPreHook) {
2274
- if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
2275
- hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
2276
- console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
2277
- anythingChanged = true;
2278
- }
2279
- const hasPostHook = hooksFile.hooks.postToolUse?.some(
2280
- (h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
2281
- );
2282
- if (!hasPostHook) {
2283
- if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
2284
- hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
2285
- console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
2286
- anythingChanged = true;
2287
- }
2288
- if (anythingChanged) {
2289
- writeJson(hooksPath, hooksFile);
2290
- console.log("");
2291
- }
2292
2553
  const serversToWrap = [];
2293
2554
  for (const [name, server] of Object.entries(servers)) {
2294
2555
  if (!server.command || server.command === "node9") continue;
@@ -2317,13 +2578,23 @@ async function setupCursor() {
2317
2578
  }
2318
2579
  console.log("");
2319
2580
  }
2581
+ console.log(
2582
+ chalk3.yellow(
2583
+ " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
2584
+ )
2585
+ );
2586
+ console.log("");
2320
2587
  if (!anythingChanged && serversToWrap.length === 0) {
2321
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
2588
+ console.log(
2589
+ chalk3.blue(
2590
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
2591
+ )
2592
+ );
2322
2593
  printDaemonTip();
2323
2594
  return;
2324
2595
  }
2325
2596
  if (anythingChanged) {
2326
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
2597
+ console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
2327
2598
  console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
2328
2599
  printDaemonTip();
2329
2600
  }
@@ -3373,34 +3644,34 @@ var UI_HTML_TEMPLATE = ui_default;
3373
3644
 
3374
3645
  // src/daemon/index.ts
3375
3646
  import http from "http";
3376
- import fs3 from "fs";
3377
- import path5 from "path";
3378
- import os3 from "os";
3647
+ import fs4 from "fs";
3648
+ import path6 from "path";
3649
+ import os4 from "os";
3379
3650
  import { spawn as spawn2 } from "child_process";
3380
3651
  import { randomUUID } from "crypto";
3381
3652
  import chalk4 from "chalk";
3382
3653
  var DAEMON_PORT2 = 7391;
3383
3654
  var DAEMON_HOST2 = "127.0.0.1";
3384
- var homeDir = os3.homedir();
3385
- var DAEMON_PID_FILE = path5.join(homeDir, ".node9", "daemon.pid");
3386
- var DECISIONS_FILE = path5.join(homeDir, ".node9", "decisions.json");
3387
- var GLOBAL_CONFIG_FILE = path5.join(homeDir, ".node9", "config.json");
3388
- var CREDENTIALS_FILE = path5.join(homeDir, ".node9", "credentials.json");
3389
- var AUDIT_LOG_FILE = path5.join(homeDir, ".node9", "audit.log");
3390
- var TRUST_FILE2 = path5.join(homeDir, ".node9", "trust.json");
3655
+ var homeDir = os4.homedir();
3656
+ var DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
3657
+ var DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
3658
+ var GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
3659
+ var CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
3660
+ var AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
3661
+ var TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
3391
3662
  function atomicWriteSync2(filePath, data, options) {
3392
- const dir = path5.dirname(filePath);
3393
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
3663
+ const dir = path6.dirname(filePath);
3664
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3394
3665
  const tmpPath = `${filePath}.${randomUUID()}.tmp`;
3395
- fs3.writeFileSync(tmpPath, data, options);
3396
- fs3.renameSync(tmpPath, filePath);
3666
+ fs4.writeFileSync(tmpPath, data, options);
3667
+ fs4.renameSync(tmpPath, filePath);
3397
3668
  }
3398
3669
  function writeTrustEntry(toolName, durationMs) {
3399
3670
  try {
3400
3671
  let trust = { entries: [] };
3401
3672
  try {
3402
- if (fs3.existsSync(TRUST_FILE2))
3403
- trust = JSON.parse(fs3.readFileSync(TRUST_FILE2, "utf-8"));
3673
+ if (fs4.existsSync(TRUST_FILE2))
3674
+ trust = JSON.parse(fs4.readFileSync(TRUST_FILE2, "utf-8"));
3404
3675
  } catch {
3405
3676
  }
3406
3677
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -3433,16 +3704,16 @@ function appendAuditLog(data) {
3433
3704
  decision: data.decision,
3434
3705
  source: "daemon"
3435
3706
  };
3436
- const dir = path5.dirname(AUDIT_LOG_FILE);
3437
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
3438
- fs3.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3707
+ const dir = path6.dirname(AUDIT_LOG_FILE);
3708
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3709
+ fs4.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
3439
3710
  } catch {
3440
3711
  }
3441
3712
  }
3442
3713
  function getAuditHistory(limit = 20) {
3443
3714
  try {
3444
- if (!fs3.existsSync(AUDIT_LOG_FILE)) return [];
3445
- const lines = fs3.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3715
+ if (!fs4.existsSync(AUDIT_LOG_FILE)) return [];
3716
+ const lines = fs4.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
3446
3717
  if (lines.length === 1 && lines[0] === "") return [];
3447
3718
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
3448
3719
  } catch {
@@ -3452,7 +3723,7 @@ function getAuditHistory(limit = 20) {
3452
3723
  var AUTO_DENY_MS = 12e4;
3453
3724
  function getOrgName() {
3454
3725
  try {
3455
- if (fs3.existsSync(CREDENTIALS_FILE)) {
3726
+ if (fs4.existsSync(CREDENTIALS_FILE)) {
3456
3727
  return "Node9 Cloud";
3457
3728
  }
3458
3729
  } catch {
@@ -3461,13 +3732,13 @@ function getOrgName() {
3461
3732
  }
3462
3733
  var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
3463
3734
  function hasStoredSlackKey() {
3464
- return fs3.existsSync(CREDENTIALS_FILE);
3735
+ return fs4.existsSync(CREDENTIALS_FILE);
3465
3736
  }
3466
3737
  function writeGlobalSetting(key, value) {
3467
3738
  let config = {};
3468
3739
  try {
3469
- if (fs3.existsSync(GLOBAL_CONFIG_FILE)) {
3470
- config = JSON.parse(fs3.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3740
+ if (fs4.existsSync(GLOBAL_CONFIG_FILE)) {
3741
+ config = JSON.parse(fs4.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
3471
3742
  }
3472
3743
  } catch {
3473
3744
  }
@@ -3491,7 +3762,7 @@ function abandonPending() {
3491
3762
  });
3492
3763
  if (autoStarted) {
3493
3764
  try {
3494
- fs3.unlinkSync(DAEMON_PID_FILE);
3765
+ fs4.unlinkSync(DAEMON_PID_FILE);
3495
3766
  } catch {
3496
3767
  }
3497
3768
  setTimeout(() => {
@@ -3529,8 +3800,8 @@ function readBody(req) {
3529
3800
  }
3530
3801
  function readPersistentDecisions() {
3531
3802
  try {
3532
- if (fs3.existsSync(DECISIONS_FILE)) {
3533
- return JSON.parse(fs3.readFileSync(DECISIONS_FILE, "utf-8"));
3803
+ if (fs4.existsSync(DECISIONS_FILE)) {
3804
+ return JSON.parse(fs4.readFileSync(DECISIONS_FILE, "utf-8"));
3534
3805
  }
3535
3806
  } catch {
3536
3807
  }
@@ -3557,7 +3828,7 @@ function startDaemon() {
3557
3828
  idleTimer = setTimeout(() => {
3558
3829
  if (autoStarted) {
3559
3830
  try {
3560
- fs3.unlinkSync(DAEMON_PID_FILE);
3831
+ fs4.unlinkSync(DAEMON_PID_FILE);
3561
3832
  } catch {
3562
3833
  }
3563
3834
  }
@@ -3875,14 +4146,14 @@ data: ${JSON.stringify(readPersistentDecisions())}
3875
4146
  server.on("error", (e) => {
3876
4147
  if (e.code === "EADDRINUSE") {
3877
4148
  try {
3878
- if (fs3.existsSync(DAEMON_PID_FILE)) {
3879
- const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
4149
+ if (fs4.existsSync(DAEMON_PID_FILE)) {
4150
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
3880
4151
  process.kill(pid, 0);
3881
4152
  return process.exit(0);
3882
4153
  }
3883
4154
  } catch {
3884
4155
  try {
3885
- fs3.unlinkSync(DAEMON_PID_FILE);
4156
+ fs4.unlinkSync(DAEMON_PID_FILE);
3886
4157
  } catch {
3887
4158
  }
3888
4159
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
@@ -3902,25 +4173,25 @@ data: ${JSON.stringify(readPersistentDecisions())}
3902
4173
  });
3903
4174
  }
3904
4175
  function stopDaemon() {
3905
- if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4176
+ if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
3906
4177
  try {
3907
- const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
4178
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
3908
4179
  process.kill(pid, "SIGTERM");
3909
4180
  console.log(chalk4.green("\u2705 Stopped."));
3910
4181
  } catch {
3911
4182
  console.log(chalk4.gray("Cleaned up stale PID file."));
3912
4183
  } finally {
3913
4184
  try {
3914
- fs3.unlinkSync(DAEMON_PID_FILE);
4185
+ fs4.unlinkSync(DAEMON_PID_FILE);
3915
4186
  } catch {
3916
4187
  }
3917
4188
  }
3918
4189
  }
3919
4190
  function daemonStatus() {
3920
- if (!fs3.existsSync(DAEMON_PID_FILE))
4191
+ if (!fs4.existsSync(DAEMON_PID_FILE))
3921
4192
  return console.log(chalk4.yellow("Node9 daemon: not running"));
3922
4193
  try {
3923
- const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
4194
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
3924
4195
  process.kill(pid, 0);
3925
4196
  console.log(chalk4.green("Node9 daemon: running"));
3926
4197
  } catch {
@@ -3934,30 +4205,30 @@ import { parseCommandString } from "execa";
3934
4205
  import { execa } from "execa";
3935
4206
  import chalk5 from "chalk";
3936
4207
  import readline from "readline";
3937
- import fs5 from "fs";
3938
- import path7 from "path";
3939
- import os5 from "os";
4208
+ import fs6 from "fs";
4209
+ import path8 from "path";
4210
+ import os6 from "os";
3940
4211
 
3941
4212
  // src/undo.ts
3942
4213
  import { spawnSync } from "child_process";
3943
- import fs4 from "fs";
3944
- import path6 from "path";
3945
- import os4 from "os";
3946
- var SNAPSHOT_STACK_PATH = path6.join(os4.homedir(), ".node9", "snapshots.json");
3947
- var UNDO_LATEST_PATH = path6.join(os4.homedir(), ".node9", "undo_latest.txt");
4214
+ import fs5 from "fs";
4215
+ import path7 from "path";
4216
+ import os5 from "os";
4217
+ var SNAPSHOT_STACK_PATH = path7.join(os5.homedir(), ".node9", "snapshots.json");
4218
+ var UNDO_LATEST_PATH = path7.join(os5.homedir(), ".node9", "undo_latest.txt");
3948
4219
  var MAX_SNAPSHOTS = 10;
3949
4220
  function readStack() {
3950
4221
  try {
3951
- if (fs4.existsSync(SNAPSHOT_STACK_PATH))
3952
- return JSON.parse(fs4.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
4222
+ if (fs5.existsSync(SNAPSHOT_STACK_PATH))
4223
+ return JSON.parse(fs5.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
3953
4224
  } catch {
3954
4225
  }
3955
4226
  return [];
3956
4227
  }
3957
4228
  function writeStack(stack) {
3958
- const dir = path6.dirname(SNAPSHOT_STACK_PATH);
3959
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
3960
- fs4.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
4229
+ const dir = path7.dirname(SNAPSHOT_STACK_PATH);
4230
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
4231
+ fs5.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
3961
4232
  }
3962
4233
  function buildArgsSummary(tool, args) {
3963
4234
  if (!args || typeof args !== "object") return "";
@@ -3973,13 +4244,13 @@ function buildArgsSummary(tool, args) {
3973
4244
  async function createShadowSnapshot(tool = "unknown", args = {}) {
3974
4245
  try {
3975
4246
  const cwd = process.cwd();
3976
- if (!fs4.existsSync(path6.join(cwd, ".git"))) return null;
3977
- const tempIndex = path6.join(cwd, ".git", `node9_index_${Date.now()}`);
4247
+ if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
4248
+ const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
3978
4249
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
3979
4250
  spawnSync("git", ["add", "-A"], { env });
3980
4251
  const treeRes = spawnSync("git", ["write-tree"], { env });
3981
4252
  const treeHash = treeRes.stdout.toString().trim();
3982
- if (fs4.existsSync(tempIndex)) fs4.unlinkSync(tempIndex);
4253
+ if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
3983
4254
  if (!treeHash || treeRes.status !== 0) return null;
3984
4255
  const commitRes = spawnSync("git", [
3985
4256
  "commit-tree",
@@ -4000,7 +4271,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
4000
4271
  stack.push(entry);
4001
4272
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
4002
4273
  writeStack(stack);
4003
- fs4.writeFileSync(UNDO_LATEST_PATH, commitHash);
4274
+ fs5.writeFileSync(UNDO_LATEST_PATH, commitHash);
4004
4275
  return commitHash;
4005
4276
  } catch (err) {
4006
4277
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
@@ -4038,9 +4309,9 @@ function applyUndo(hash, cwd) {
4038
4309
  const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4039
4310
  const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
4040
4311
  for (const file of [...tracked, ...untracked]) {
4041
- const fullPath = path6.join(dir, file);
4042
- if (!snapshotFiles.has(file) && fs4.existsSync(fullPath)) {
4043
- fs4.unlinkSync(fullPath);
4312
+ const fullPath = path7.join(dir, file);
4313
+ if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
4314
+ fs5.unlinkSync(fullPath);
4044
4315
  }
4045
4316
  }
4046
4317
  return true;
@@ -4052,7 +4323,7 @@ function applyUndo(hash, cwd) {
4052
4323
  // src/cli.ts
4053
4324
  import { confirm as confirm3 } from "@inquirer/prompts";
4054
4325
  var { version } = JSON.parse(
4055
- fs5.readFileSync(path7.join(__dirname, "../package.json"), "utf-8")
4326
+ fs6.readFileSync(path8.join(__dirname, "../package.json"), "utf-8")
4056
4327
  );
4057
4328
  function parseDuration(str) {
4058
4329
  const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
@@ -4084,6 +4355,15 @@ INSTRUCTIONS:
4084
4355
  - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
4085
4356
  }
4086
4357
  const label = blockedByLabel.toLowerCase();
4358
+ if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
4359
+ return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
4360
+ CRITICAL INSTRUCTION: Do NOT retry this action.
4361
+ REQUIRED ACTIONS:
4362
+ 1. Remove the hardcoded credential from your command or code.
4363
+ 2. Use an environment variable or a dedicated secrets manager instead.
4364
+ 3. Treat the leaked credential as compromised and rotate it immediately.
4365
+ Do NOT attempt to bypass this check or pass the credential through another tool.`;
4366
+ }
4087
4367
  if (label.includes("sql safety") && label.includes("delete without where")) {
4088
4368
  return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
4089
4369
  INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
@@ -4245,14 +4525,14 @@ async function runProxy(targetCommand) {
4245
4525
  }
4246
4526
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
4247
4527
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
4248
- const credPath = path7.join(os5.homedir(), ".node9", "credentials.json");
4249
- if (!fs5.existsSync(path7.dirname(credPath)))
4250
- fs5.mkdirSync(path7.dirname(credPath), { recursive: true });
4528
+ const credPath = path8.join(os6.homedir(), ".node9", "credentials.json");
4529
+ if (!fs6.existsSync(path8.dirname(credPath)))
4530
+ fs6.mkdirSync(path8.dirname(credPath), { recursive: true });
4251
4531
  const profileName = options.profile || "default";
4252
4532
  let existingCreds = {};
4253
4533
  try {
4254
- if (fs5.existsSync(credPath)) {
4255
- const raw = JSON.parse(fs5.readFileSync(credPath, "utf-8"));
4534
+ if (fs6.existsSync(credPath)) {
4535
+ const raw = JSON.parse(fs6.readFileSync(credPath, "utf-8"));
4256
4536
  if (raw.apiKey) {
4257
4537
  existingCreds = {
4258
4538
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -4264,13 +4544,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4264
4544
  } catch {
4265
4545
  }
4266
4546
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
4267
- fs5.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4547
+ fs6.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
4268
4548
  if (profileName === "default") {
4269
- const configPath = path7.join(os5.homedir(), ".node9", "config.json");
4549
+ const configPath = path8.join(os6.homedir(), ".node9", "config.json");
4270
4550
  let config = {};
4271
4551
  try {
4272
- if (fs5.existsSync(configPath))
4273
- config = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
4552
+ if (fs6.existsSync(configPath))
4553
+ config = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
4274
4554
  } catch {
4275
4555
  }
4276
4556
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -4285,9 +4565,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
4285
4565
  approvers.cloud = false;
4286
4566
  }
4287
4567
  s.approvers = approvers;
4288
- if (!fs5.existsSync(path7.dirname(configPath)))
4289
- fs5.mkdirSync(path7.dirname(configPath), { recursive: true });
4290
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4568
+ if (!fs6.existsSync(path8.dirname(configPath)))
4569
+ fs6.mkdirSync(path8.dirname(configPath), { recursive: true });
4570
+ fs6.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4291
4571
  }
4292
4572
  if (options.profile && profileName !== "default") {
4293
4573
  console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
@@ -4326,7 +4606,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
4326
4606
  process.exit(1);
4327
4607
  });
4328
4608
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
4329
- const homeDir2 = os5.homedir();
4609
+ const homeDir2 = os6.homedir();
4330
4610
  let failures = 0;
4331
4611
  function pass(msg) {
4332
4612
  console.log(chalk5.green(" \u2705 ") + msg);
@@ -4372,10 +4652,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4372
4652
  );
4373
4653
  }
4374
4654
  section("Configuration");
4375
- const globalConfigPath = path7.join(homeDir2, ".node9", "config.json");
4376
- if (fs5.existsSync(globalConfigPath)) {
4655
+ const globalConfigPath = path8.join(homeDir2, ".node9", "config.json");
4656
+ if (fs6.existsSync(globalConfigPath)) {
4377
4657
  try {
4378
- JSON.parse(fs5.readFileSync(globalConfigPath, "utf-8"));
4658
+ JSON.parse(fs6.readFileSync(globalConfigPath, "utf-8"));
4379
4659
  pass("~/.node9/config.json found and valid");
4380
4660
  } catch {
4381
4661
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -4383,17 +4663,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
4383
4663
  } else {
4384
4664
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
4385
4665
  }
4386
- const projectConfigPath = path7.join(process.cwd(), "node9.config.json");
4387
- if (fs5.existsSync(projectConfigPath)) {
4666
+ const projectConfigPath = path8.join(process.cwd(), "node9.config.json");
4667
+ if (fs6.existsSync(projectConfigPath)) {
4388
4668
  try {
4389
- JSON.parse(fs5.readFileSync(projectConfigPath, "utf-8"));
4669
+ JSON.parse(fs6.readFileSync(projectConfigPath, "utf-8"));
4390
4670
  pass("node9.config.json found and valid (project)");
4391
4671
  } catch {
4392
4672
  fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
4393
4673
  }
4394
4674
  }
4395
- const credsPath = path7.join(homeDir2, ".node9", "credentials.json");
4396
- if (fs5.existsSync(credsPath)) {
4675
+ const credsPath = path8.join(homeDir2, ".node9", "credentials.json");
4676
+ if (fs6.existsSync(credsPath)) {
4397
4677
  pass("Cloud credentials found (~/.node9/credentials.json)");
4398
4678
  } else {
4399
4679
  warn(
@@ -4402,10 +4682,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4402
4682
  );
4403
4683
  }
4404
4684
  section("Agent Hooks");
4405
- const claudeSettingsPath = path7.join(homeDir2, ".claude", "settings.json");
4406
- if (fs5.existsSync(claudeSettingsPath)) {
4685
+ const claudeSettingsPath = path8.join(homeDir2, ".claude", "settings.json");
4686
+ if (fs6.existsSync(claudeSettingsPath)) {
4407
4687
  try {
4408
- const cs = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
4688
+ const cs = JSON.parse(fs6.readFileSync(claudeSettingsPath, "utf-8"));
4409
4689
  const hasHook = cs.hooks?.PreToolUse?.some(
4410
4690
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4411
4691
  );
@@ -4418,10 +4698,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4418
4698
  } else {
4419
4699
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
4420
4700
  }
4421
- const geminiSettingsPath = path7.join(homeDir2, ".gemini", "settings.json");
4422
- if (fs5.existsSync(geminiSettingsPath)) {
4701
+ const geminiSettingsPath = path8.join(homeDir2, ".gemini", "settings.json");
4702
+ if (fs6.existsSync(geminiSettingsPath)) {
4423
4703
  try {
4424
- const gs = JSON.parse(fs5.readFileSync(geminiSettingsPath, "utf-8"));
4704
+ const gs = JSON.parse(fs6.readFileSync(geminiSettingsPath, "utf-8"));
4425
4705
  const hasHook = gs.hooks?.BeforeTool?.some(
4426
4706
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
4427
4707
  );
@@ -4434,10 +4714,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
4434
4714
  } else {
4435
4715
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
4436
4716
  }
4437
- const cursorHooksPath = path7.join(homeDir2, ".cursor", "hooks.json");
4438
- if (fs5.existsSync(cursorHooksPath)) {
4717
+ const cursorHooksPath = path8.join(homeDir2, ".cursor", "hooks.json");
4718
+ if (fs6.existsSync(cursorHooksPath)) {
4439
4719
  try {
4440
- const cur = JSON.parse(fs5.readFileSync(cursorHooksPath, "utf-8"));
4720
+ const cur = JSON.parse(fs6.readFileSync(cursorHooksPath, "utf-8"));
4441
4721
  const hasHook = cur.hooks?.preToolUse?.some(
4442
4722
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
4443
4723
  );
@@ -4539,8 +4819,8 @@ program.command("explain").description(
4539
4819
  console.log("");
4540
4820
  });
4541
4821
  program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
4542
- const configPath = path7.join(os5.homedir(), ".node9", "config.json");
4543
- if (fs5.existsSync(configPath) && !options.force) {
4822
+ const configPath = path8.join(os6.homedir(), ".node9", "config.json");
4823
+ if (fs6.existsSync(configPath) && !options.force) {
4544
4824
  console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
4545
4825
  console.log(chalk5.gray(` Run with --force to overwrite.`));
4546
4826
  return;
@@ -4554,9 +4834,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
4554
4834
  mode: safeMode
4555
4835
  }
4556
4836
  };
4557
- const dir = path7.dirname(configPath);
4558
- if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
4559
- fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4837
+ const dir = path8.dirname(configPath);
4838
+ if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
4839
+ fs6.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
4560
4840
  console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
4561
4841
  console.log(chalk5.cyan(` Mode set to: ${safeMode}`));
4562
4842
  console.log(
@@ -4574,14 +4854,14 @@ function formatRelativeTime(timestamp) {
4574
4854
  return new Date(timestamp).toLocaleDateString();
4575
4855
  }
4576
4856
  program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
4577
- const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
4578
- if (!fs5.existsSync(logPath)) {
4857
+ const logPath = path8.join(os6.homedir(), ".node9", "audit.log");
4858
+ if (!fs6.existsSync(logPath)) {
4579
4859
  console.log(
4580
4860
  chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
4581
4861
  );
4582
4862
  return;
4583
4863
  }
4584
- const raw = fs5.readFileSync(logPath, "utf-8");
4864
+ const raw = fs6.readFileSync(logPath, "utf-8");
4585
4865
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
4586
4866
  let entries = lines.flatMap((line) => {
4587
4867
  try {
@@ -4664,13 +4944,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
4664
4944
  console.log("");
4665
4945
  const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
4666
4946
  console.log(` Mode: ${modeLabel}`);
4667
- const projectConfig = path7.join(process.cwd(), "node9.config.json");
4668
- const globalConfig = path7.join(os5.homedir(), ".node9", "config.json");
4947
+ const projectConfig = path8.join(process.cwd(), "node9.config.json");
4948
+ const globalConfig = path8.join(os6.homedir(), ".node9", "config.json");
4669
4949
  console.log(
4670
- ` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
4950
+ ` Local: ${fs6.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
4671
4951
  );
4672
4952
  console.log(
4673
- ` Global: ${fs5.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
4953
+ ` Global: ${fs6.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
4674
4954
  );
4675
4955
  if (mergedConfig.policy.sandboxPaths.length > 0) {
4676
4956
  console.log(
@@ -4733,9 +5013,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
4733
5013
  } catch (err) {
4734
5014
  const tempConfig = getConfig();
4735
5015
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
4736
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
5016
+ const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
4737
5017
  const errMsg = err instanceof Error ? err.message : String(err);
4738
- fs5.appendFileSync(
5018
+ fs6.appendFileSync(
4739
5019
  logPath,
4740
5020
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
4741
5021
  RAW: ${raw}
@@ -4753,10 +5033,10 @@ RAW: ${raw}
4753
5033
  }
4754
5034
  const config = getConfig();
4755
5035
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
4756
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
4757
- if (!fs5.existsSync(path7.dirname(logPath)))
4758
- fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
4759
- fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
5036
+ const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
5037
+ if (!fs6.existsSync(path8.dirname(logPath)))
5038
+ fs6.mkdirSync(path8.dirname(logPath), { recursive: true });
5039
+ fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
4760
5040
  `);
4761
5041
  }
4762
5042
  const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
@@ -4767,8 +5047,14 @@ RAW: ${raw}
4767
5047
  const sendBlock = (msg, result2) => {
4768
5048
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
4769
5049
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
4770
- console.error(chalk5.red(`
5050
+ if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
5051
+ console.error(chalk5.bgRed.white.bold(`
5052
+ \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
5053
+ console.error(chalk5.red.bold(` A sensitive secret was found in the tool arguments!`));
5054
+ } else {
5055
+ console.error(chalk5.red(`
4771
5056
  \u{1F6D1} Node9 blocked "${toolName}"`));
5057
+ }
4772
5058
  console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
4773
5059
  if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
4774
5060
  console.error("");
@@ -4827,9 +5113,9 @@ RAW: ${raw}
4827
5113
  });
4828
5114
  } catch (err) {
4829
5115
  if (process.env.NODE9_DEBUG === "1") {
4830
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
5116
+ const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
4831
5117
  const errMsg = err instanceof Error ? err.message : String(err);
4832
- fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
5118
+ fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
4833
5119
  `);
4834
5120
  }
4835
5121
  process.exit(0);
@@ -4874,10 +5160,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
4874
5160
  decision: "allowed",
4875
5161
  source: "post-hook"
4876
5162
  };
4877
- const logPath = path7.join(os5.homedir(), ".node9", "audit.log");
4878
- if (!fs5.existsSync(path7.dirname(logPath)))
4879
- fs5.mkdirSync(path7.dirname(logPath), { recursive: true });
4880
- fs5.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5163
+ const logPath = path8.join(os6.homedir(), ".node9", "audit.log");
5164
+ if (!fs6.existsSync(path8.dirname(logPath)))
5165
+ fs6.mkdirSync(path8.dirname(logPath), { recursive: true });
5166
+ fs6.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4881
5167
  const config = getConfig();
4882
5168
  if (shouldSnapshot(tool, {}, config)) {
4883
5169
  await createShadowSnapshot();
@@ -5059,13 +5345,103 @@ program.command("undo").description(
5059
5345
  console.log(chalk5.gray("\nCancelled.\n"));
5060
5346
  }
5061
5347
  });
5348
+ var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
5349
+ shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
5350
+ const name = resolveShieldName(service);
5351
+ if (!name) {
5352
+ console.error(chalk5.red(`
5353
+ \u274C Unknown shield: "${service}"
5354
+ `));
5355
+ console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
5356
+ `);
5357
+ process.exit(1);
5358
+ }
5359
+ const shield = getShield(name);
5360
+ const active = readActiveShields();
5361
+ if (active.includes(name)) {
5362
+ console.log(chalk5.yellow(`
5363
+ \u2139\uFE0F Shield "${name}" is already active.
5364
+ `));
5365
+ return;
5366
+ }
5367
+ writeActiveShields([...active, name]);
5368
+ console.log(chalk5.green(`
5369
+ \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
5370
+ console.log(chalk5.gray(` ${shield.smartRules.length} smart rules now active.`));
5371
+ if (shield.dangerousWords.length > 0)
5372
+ console.log(chalk5.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
5373
+ if (name === "filesystem") {
5374
+ console.log(
5375
+ chalk5.yellow(
5376
+ `
5377
+ \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
5378
+ Tools like unlink, find -delete, or language-level file ops are not intercepted.`
5379
+ )
5380
+ );
5381
+ }
5382
+ console.log("");
5383
+ });
5384
+ shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
5385
+ const name = resolveShieldName(service);
5386
+ if (!name) {
5387
+ console.error(chalk5.red(`
5388
+ \u274C Unknown shield: "${service}"
5389
+ `));
5390
+ console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
5391
+ `);
5392
+ process.exit(1);
5393
+ }
5394
+ const active = readActiveShields();
5395
+ if (!active.includes(name)) {
5396
+ console.log(chalk5.yellow(`
5397
+ \u2139\uFE0F Shield "${name}" is not active.
5398
+ `));
5399
+ return;
5400
+ }
5401
+ writeActiveShields(active.filter((s) => s !== name));
5402
+ console.log(chalk5.green(`
5403
+ \u{1F6E1}\uFE0F Shield "${name}" disabled.
5404
+ `));
5405
+ });
5406
+ shieldCmd.command("list").description("Show all available shields").action(() => {
5407
+ const active = new Set(readActiveShields());
5408
+ console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
5409
+ for (const shield of listShields()) {
5410
+ const status = active.has(shield.name) ? chalk5.green("\u25CF enabled") : chalk5.gray("\u25CB disabled");
5411
+ console.log(` ${status} ${chalk5.cyan(shield.name.padEnd(12))} ${shield.description}`);
5412
+ if (shield.aliases.length > 0)
5413
+ console.log(chalk5.gray(` aliases: ${shield.aliases.join(", ")}`));
5414
+ }
5415
+ console.log("");
5416
+ });
5417
+ shieldCmd.command("status").description("Show which shields are currently active").action(() => {
5418
+ const active = readActiveShields();
5419
+ if (active.length === 0) {
5420
+ console.log(chalk5.yellow("\n\u2139\uFE0F No shields are active.\n"));
5421
+ console.log(`Run ${chalk5.cyan("node9 shield list")} to see available shields.
5422
+ `);
5423
+ return;
5424
+ }
5425
+ console.log(chalk5.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
5426
+ for (const name of active) {
5427
+ const shield = getShield(name);
5428
+ if (!shield) continue;
5429
+ console.log(` ${chalk5.green("\u25CF")} ${chalk5.cyan(name)}`);
5430
+ console.log(
5431
+ chalk5.gray(
5432
+ ` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
5433
+ )
5434
+ );
5435
+ }
5436
+ console.log("");
5437
+ });
5062
5438
  process.on("unhandledRejection", (reason) => {
5063
5439
  const isCheckHook = process.argv[2] === "check";
5064
5440
  if (isCheckHook) {
5065
5441
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
5066
- const logPath = path7.join(os5.homedir(), ".node9", "hook-debug.log");
5442
+ const logPath = path8.join(os6.homedir(), ".node9", "hook-debug.log");
5067
5443
  const msg = reason instanceof Error ? reason.message : String(reason);
5068
- fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5444
+ fs6.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
5069
5445
  `);
5070
5446
  }
5071
5447
  process.exit(0);