clawguard-cli 0.1.0 → 0.2.0

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs9 from "fs";
4
+ import fs11 from "fs";
5
5
  import { Command } from "commander";
6
6
  import chalk3 from "chalk";
7
7
 
@@ -65,7 +65,7 @@ function getInfoCount(result) {
65
65
 
66
66
  // src/reporter.ts
67
67
  import chalk from "chalk";
68
- var VERSION = "0.1.0";
68
+ var VERSION = "0.2.0";
69
69
  var SEVERITY_COLORS = {
70
70
  [Severity.CRITICAL]: chalk.red.bold,
71
71
  [Severity.HIGH]: chalk.yellow.bold,
@@ -177,16 +177,16 @@ function printJson(result) {
177
177
 
178
178
  // src/scanner.ts
179
179
  import crypto from "crypto";
180
- import fs8 from "fs";
181
- import path8 from "path";
182
- import os2 from "os";
183
- import JSON54 from "json5";
180
+ import fs10 from "fs";
181
+ import path11 from "path";
182
+ import os3 from "os";
183
+ import JSON55 from "json5";
184
184
  import chalk2 from "chalk";
185
185
 
186
186
  // src/checks/credentials.ts
187
- import fs from "fs";
188
- import path from "path";
189
- import JSON5 from "json5";
187
+ import fs2 from "fs";
188
+ import path2 from "path";
189
+ import JSON52 from "json5";
190
190
 
191
191
  // src/patterns.ts
192
192
  var API_KEY_PATTERNS = [
@@ -206,7 +206,8 @@ var API_KEY_PATTERNS = [
206
206
  ["OpenRouter API key", /sk-or-v1-[a-f0-9]{64}/],
207
207
  ["Google API key", /AIza[0-9A-Za-z_-]{35}/],
208
208
  ["Stripe Secret key", /sk_live_[a-zA-Z0-9]{20,}/],
209
- ["Generic Bearer token", /Bearer\s+[a-zA-Z0-9_.-]{20,}/]
209
+ ["Generic Bearer token", /Bearer\s+[a-zA-Z0-9_.-]{20,}/],
210
+ ["Brave Search API key", /BSA[a-zA-Z0-9]{20,}/]
210
211
  ];
211
212
  var ENV_VAR_PATTERN = /\$\{[A-Z_][A-Z0-9_]*\}/;
212
213
  var MALICIOUS_PATTERNS = [
@@ -258,6 +259,27 @@ var SUSPICIOUS_BINS = [
258
259
  "wireshark",
259
260
  "tshark"
260
261
  ];
262
+ var PROVIDER_ENV_MAP = {
263
+ anthropic: "ANTHROPIC_API_KEY",
264
+ openai: "OPENAI_API_KEY",
265
+ groq: "GROQ_API_KEY",
266
+ xai: "XAI_API_KEY",
267
+ openrouter: "OPENROUTER_API_KEY",
268
+ google: "GOOGLE_API_KEY",
269
+ mistral: "MISTRAL_API_KEY",
270
+ deepseek: "DEEPSEEK_API_KEY",
271
+ cohere: "COHERE_API_KEY",
272
+ together: "TOGETHER_API_KEY"
273
+ };
274
+ var PROVIDER_LIMITS = {
275
+ groq: { tpm: 6e3, rpm: 30, note: "Groq free tier: 6K TPM, 30 RPM" },
276
+ openrouter: {
277
+ tpm: 1e5,
278
+ rpm: 8,
279
+ note: "OpenRouter free tier: ~8 RPM"
280
+ }
281
+ };
282
+ var OPENCLAW_SYSTEM_PROMPT_TOKENS = 12600;
261
283
  var MEMORY_POISONING_PATTERNS = [
262
284
  [
263
285
  "Hidden instruction injection",
@@ -281,7 +303,12 @@ var MEMORY_POISONING_PATTERNS = [
281
303
  ]
282
304
  ];
283
305
 
284
- // src/checks/credentials.ts
306
+ // src/utils.ts
307
+ import { execFileSync } from "child_process";
308
+ import fs from "fs";
309
+ import os from "os";
310
+ import path from "path";
311
+ import JSON5 from "json5";
285
312
  function scanFileForKeys(filepath) {
286
313
  const hits = [];
287
314
  try {
@@ -293,7 +320,10 @@ function scanFileForKeys(filepath) {
293
320
  continue;
294
321
  }
295
322
  for (const [keyName, pattern] of API_KEY_PATTERNS) {
296
- const globalPattern = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
323
+ const globalPattern = new RegExp(
324
+ pattern.source,
325
+ pattern.flags + (pattern.flags.includes("g") ? "" : "g")
326
+ );
297
327
  let match;
298
328
  while ((match = globalPattern.exec(line)) !== null) {
299
329
  const matched = match[0];
@@ -306,27 +336,71 @@ function scanFileForKeys(filepath) {
306
336
  }
307
337
  return hits;
308
338
  }
339
+ function parseConfig(configPath) {
340
+ try {
341
+ const content = fs.readFileSync(configPath, { encoding: "utf-8" });
342
+ return JSON5.parse(content);
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ function findFilesRecursive(dir, match, mode = "extension") {
348
+ const results = [];
349
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
350
+ return results;
351
+ }
352
+ try {
353
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
354
+ for (const entry of entries) {
355
+ const fullPath = path.join(dir, entry.name);
356
+ if (entry.isDirectory()) {
357
+ results.push(...findFilesRecursive(fullPath, match, mode));
358
+ } else if (entry.isFile()) {
359
+ if (mode === "extension" && entry.name.endsWith(match)) {
360
+ results.push(fullPath);
361
+ } else if (mode === "exact" && entry.name === match) {
362
+ results.push(fullPath);
363
+ }
364
+ }
365
+ }
366
+ } catch {
367
+ }
368
+ return results;
369
+ }
370
+ function isDockerAvailable() {
371
+ try {
372
+ execFileSync("which", ["docker"], { stdio: "pipe" });
373
+ return true;
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
378
+ function getTotalMemoryGB() {
379
+ return os.totalmem() / (1024 * 1024 * 1024);
380
+ }
309
381
  function getRelativePath(filePath, basePath) {
310
382
  const parentDir = path.dirname(basePath);
311
383
  return path.relative(parentDir, filePath);
312
384
  }
385
+
386
+ // src/checks/credentials.ts
313
387
  function checkCredentials(openclawPath) {
314
388
  const findings = [];
315
389
  const configFiles = [
316
- path.join(openclawPath, "openclaw.json"),
317
- path.join(openclawPath, "credentials", "profiles.json")
390
+ path2.join(openclawPath, "openclaw.json"),
391
+ path2.join(openclawPath, "credentials", "profiles.json")
318
392
  ];
319
- const agentsDir = path.join(openclawPath, "agents");
320
- if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) {
393
+ const agentsDir = path2.join(openclawPath, "agents");
394
+ if (fs2.existsSync(agentsDir) && fs2.statSync(agentsDir).isDirectory()) {
321
395
  try {
322
- for (const agentEntry of fs.readdirSync(agentsDir)) {
323
- const authFile = path.join(
396
+ for (const agentEntry of fs2.readdirSync(agentsDir)) {
397
+ const authFile = path2.join(
324
398
  agentsDir,
325
399
  agentEntry,
326
400
  "agent",
327
401
  "auth-profiles.json"
328
402
  );
329
- if (fs.existsSync(authFile)) {
403
+ if (fs2.existsSync(authFile)) {
330
404
  configFiles.push(authFile);
331
405
  }
332
406
  }
@@ -334,17 +408,17 @@ function checkCredentials(openclawPath) {
334
408
  }
335
409
  }
336
410
  const envFiles = [
337
- path.join(openclawPath, ".env"),
338
- path.join(openclawPath, "workspace", ".env")
411
+ path2.join(openclawPath, ".env"),
412
+ path2.join(openclawPath, "workspace", ".env")
339
413
  ];
340
414
  for (const envFile of envFiles) {
341
- if (fs.existsSync(envFile)) {
415
+ if (fs2.existsSync(envFile)) {
342
416
  configFiles.push(envFile);
343
417
  }
344
418
  }
345
419
  const allHits = [];
346
420
  for (const filepath of configFiles) {
347
- if (fs.existsSync(filepath)) {
421
+ if (fs2.existsSync(filepath)) {
348
422
  const hits = scanFileForKeys(filepath);
349
423
  for (const { keyName, masked, lineNum } of hits) {
350
424
  const relPath = getRelativePath(filepath, openclawPath);
@@ -365,7 +439,7 @@ function checkCredentials(openclawPath) {
365
439
  );
366
440
  }
367
441
  const bakWithKeys = [];
368
- const bakFiles = findFilesRecursive(openclawPath, ".bak");
442
+ const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
369
443
  for (const bakFile of bakFiles) {
370
444
  const hits = scanFileForKeys(bakFile);
371
445
  if (hits.length > 0) {
@@ -384,11 +458,11 @@ function checkCredentials(openclawPath) {
384
458
  );
385
459
  }
386
460
  const transcriptHits = [];
387
- if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) {
388
- const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl");
461
+ if (fs2.existsSync(agentsDir) && fs2.statSync(agentsDir).isDirectory()) {
462
+ const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl", "extension");
389
463
  for (const jsonlFile of jsonlFiles) {
390
464
  try {
391
- const content = fs.readFileSync(jsonlFile, { encoding: "utf-8" });
465
+ const content = fs2.readFileSync(jsonlFile, { encoding: "utf-8" });
392
466
  const lines = content.split("\n");
393
467
  let found = false;
394
468
  for (let i = 0; i < Math.min(lines.length, 501); i++) {
@@ -419,11 +493,11 @@ function checkCredentials(openclawPath) {
419
493
  })
420
494
  );
421
495
  }
422
- const configFile = path.join(openclawPath, "openclaw.json");
423
- if (fs.existsSync(configFile)) {
496
+ const configFile = path2.join(openclawPath, "openclaw.json");
497
+ if (fs2.existsSync(configFile)) {
424
498
  try {
425
- const content = fs.readFileSync(configFile, { encoding: "utf-8" });
426
- const config = JSON5.parse(content);
499
+ const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
500
+ const config = JSON52.parse(content);
427
501
  const loggingConfig = config.logging ?? {};
428
502
  const redact = loggingConfig.redactSensitive;
429
503
  if (redact === void 0 || redact === null || redact === "off") {
@@ -442,31 +516,12 @@ function checkCredentials(openclawPath) {
442
516
  }
443
517
  return findings;
444
518
  }
445
- function findFilesRecursive(dir, ext) {
446
- const results = [];
447
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
448
- return results;
449
- }
450
- try {
451
- const entries = fs.readdirSync(dir, { withFileTypes: true });
452
- for (const entry of entries) {
453
- const fullPath = path.join(dir, entry.name);
454
- if (entry.isDirectory()) {
455
- results.push(...findFilesRecursive(fullPath, ext));
456
- } else if (entry.isFile() && entry.name.endsWith(ext)) {
457
- results.push(fullPath);
458
- }
459
- }
460
- } catch {
461
- }
462
- return results;
463
- }
464
519
 
465
520
  // src/checks/gateway.ts
466
- import fs2 from "fs";
521
+ import fs3 from "fs";
467
522
  import net from "net";
468
- import path2 from "path";
469
- import JSON52 from "json5";
523
+ import path3 from "path";
524
+ import JSON53 from "json5";
470
525
  function checkPortExposed(port) {
471
526
  return new Promise((resolve) => {
472
527
  const socket = new net.Socket();
@@ -492,8 +547,8 @@ function checkPortExposed(port) {
492
547
  }
493
548
  async function checkGateway(openclawPath) {
494
549
  const findings = [];
495
- const configFile = path2.join(openclawPath, "openclaw.json");
496
- if (!fs2.existsSync(configFile)) {
550
+ const configFile = path3.join(openclawPath, "openclaw.json");
551
+ if (!fs3.existsSync(configFile)) {
497
552
  findings.push(
498
553
  createFinding({
499
554
  severity: Severity.INFO,
@@ -506,8 +561,8 @@ async function checkGateway(openclawPath) {
506
561
  }
507
562
  let config;
508
563
  try {
509
- const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
510
- config = JSON52.parse(content);
564
+ const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
565
+ config = JSON53.parse(content);
511
566
  } catch {
512
567
  findings.push(
513
568
  createFinding({
@@ -576,32 +631,83 @@ async function checkGateway(openclawPath) {
576
631
  })
577
632
  );
578
633
  }
634
+ const ramGB = getTotalMemoryGB();
635
+ const nodeOptions = process.env.NODE_OPTIONS ?? "";
636
+ if (ramGB < 2) {
637
+ if (!nodeOptions.includes("max-old-space-size")) {
638
+ findings.push(
639
+ createFinding({
640
+ severity: Severity.HIGH,
641
+ title: `System has only ${ramGB.toFixed(1)}GB RAM without NODE_OPTIONS set`,
642
+ details: [
643
+ `Total RAM: ${ramGB.toFixed(1)}GB`,
644
+ "OpenClaw may be killed by OOM without memory limits",
645
+ "NODE_OPTIONS environment variable is not set"
646
+ ],
647
+ fix: 'Set NODE_OPTIONS="--max-old-space-size=512" in your service file or shell profile',
648
+ category: "gateway"
649
+ })
650
+ );
651
+ }
652
+ }
653
+ if (process.platform === "linux") {
654
+ const serviceFiles = [
655
+ "/etc/systemd/system/openclaw.service",
656
+ "/etc/systemd/system/openclaw-gateway.service"
657
+ ];
658
+ for (const serviceFile of serviceFiles) {
659
+ if (!fs3.existsSync(serviceFile)) continue;
660
+ try {
661
+ const serviceContent = fs3.readFileSync(serviceFile, { encoding: "utf-8" });
662
+ if (ramGB < 2 && !serviceContent.includes("NODE_OPTIONS")) {
663
+ findings.push(
664
+ createFinding({
665
+ severity: Severity.HIGH,
666
+ title: `Systemd service missing NODE_OPTIONS (${path3.basename(serviceFile)})`,
667
+ details: [
668
+ `Low RAM system (${ramGB.toFixed(1)}GB) but service has no NODE_OPTIONS`,
669
+ "Add Environment=NODE_OPTIONS=--max-old-space-size=512 to [Service] section"
670
+ ],
671
+ fix: `Edit ${serviceFile} and add NODE_OPTIONS, then systemctl daemon-reload`,
672
+ category: "gateway"
673
+ })
674
+ );
675
+ }
676
+ if (serviceContent.includes("Restart=always") && !serviceContent.includes("RestartSec")) {
677
+ findings.push(
678
+ createFinding({
679
+ severity: Severity.MEDIUM,
680
+ title: `Systemd service has Restart=always without RestartSec (${path3.basename(serviceFile)})`,
681
+ details: [
682
+ "Service will restart immediately on crash, potentially causing a restart storm",
683
+ "Add RestartSec=5 to [Service] section to prevent rapid restarts"
684
+ ],
685
+ fix: `Add RestartSec=5 to ${serviceFile} [Service] section`,
686
+ category: "gateway"
687
+ })
688
+ );
689
+ }
690
+ } catch {
691
+ }
692
+ }
693
+ }
579
694
  return findings;
580
695
  }
581
696
 
582
697
  // src/checks/sandbox.ts
583
- import { execFileSync } from "child_process";
584
- import fs3 from "fs";
585
- import path3 from "path";
586
- import JSON53 from "json5";
587
- function isDockerAvailable() {
588
- try {
589
- execFileSync("which", ["docker"], { stdio: "pipe" });
590
- return true;
591
- } catch {
592
- return false;
593
- }
594
- }
698
+ import fs4 from "fs";
699
+ import path4 from "path";
700
+ import JSON54 from "json5";
595
701
  function checkSandbox(openclawPath) {
596
702
  const findings = [];
597
- const configFile = path3.join(openclawPath, "openclaw.json");
598
- if (!fs3.existsSync(configFile)) {
703
+ const configFile = path4.join(openclawPath, "openclaw.json");
704
+ if (!fs4.existsSync(configFile)) {
599
705
  return findings;
600
706
  }
601
707
  let config;
602
708
  try {
603
- const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
604
- config = JSON53.parse(content);
709
+ const content = fs4.readFileSync(configFile, { encoding: "utf-8" });
710
+ config = JSON54.parse(content);
605
711
  } catch {
606
712
  return findings;
607
713
  }
@@ -670,7 +776,22 @@ function checkSandbox(openclawPath) {
670
776
  );
671
777
  }
672
778
  const execSecurity = execConfig.security;
673
- if (execSecurity !== "allowlist") {
779
+ const validExecSecurity = ["deny", "allowlist", "full"];
780
+ if (execSecurity && !validExecSecurity.includes(execSecurity)) {
781
+ findings.push(
782
+ createFinding({
783
+ severity: Severity.HIGH,
784
+ title: `Invalid tools.exec.security value: "${execSecurity}"`,
785
+ details: [
786
+ `tools.exec.security = "${execSecurity}"`,
787
+ `Valid values: ${validExecSecurity.join(", ")}`,
788
+ "Invalid value may cause unpredictable behavior"
789
+ ],
790
+ fix: 'Set tools.exec.security to "deny", "allowlist", or "full" in openclaw.json',
791
+ category: "sandbox"
792
+ })
793
+ );
794
+ } else if (execSecurity !== "allowlist") {
674
795
  findings.push(
675
796
  createFinding({
676
797
  severity: Severity.MEDIUM,
@@ -684,6 +805,26 @@ function checkSandbox(openclawPath) {
684
805
  })
685
806
  );
686
807
  }
808
+ const commands = config.commands ?? {};
809
+ const nativeSkills = commands.nativeSkills;
810
+ if (nativeSkills !== void 0 && nativeSkills !== null) {
811
+ const validNativeSkills = [false, "auto", "off"];
812
+ if (!validNativeSkills.includes(nativeSkills)) {
813
+ findings.push(
814
+ createFinding({
815
+ severity: Severity.MEDIUM,
816
+ title: `Invalid commands.nativeSkills value: "${String(nativeSkills)}"`,
817
+ details: [
818
+ `commands.nativeSkills = "${String(nativeSkills)}"`,
819
+ 'Valid values: false, "auto", "off"',
820
+ "Invalid value may cause skill execution issues"
821
+ ],
822
+ fix: 'Set commands.nativeSkills to false, "auto", or "off" in openclaw.json',
823
+ category: "sandbox"
824
+ })
825
+ );
826
+ }
827
+ }
687
828
  if (dockerAvailable) {
688
829
  findings.push(
689
830
  createFinding({
@@ -700,48 +841,44 @@ function checkSandbox(openclawPath) {
700
841
  }
701
842
 
702
843
  // src/checks/permissions.ts
703
- import fs4 from "fs";
704
- import path4 from "path";
844
+ import fs5 from "fs";
845
+ import path5 from "path";
705
846
  function getPermissionOctal(filepath) {
706
- const stats = fs4.statSync(filepath);
847
+ const stats = fs5.statSync(filepath);
707
848
  return "0o" + (stats.mode & 511).toString(8);
708
849
  }
709
850
  function isWorldReadable(filepath) {
710
- const stats = fs4.statSync(filepath);
851
+ const stats = fs5.statSync(filepath);
711
852
  return (stats.mode & 4) !== 0;
712
853
  }
713
854
  function isGroupReadable(filepath) {
714
- const stats = fs4.statSync(filepath);
855
+ const stats = fs5.statSync(filepath);
715
856
  return (stats.mode & 32) !== 0;
716
857
  }
717
- function getRelativePath2(filePath, basePath) {
718
- const parentDir = path4.dirname(basePath);
719
- return path4.relative(parentDir, filePath);
720
- }
721
858
  function checkPermissions(openclawPath) {
722
859
  const findings = [];
723
860
  const tooOpen = [];
724
- if (fs4.existsSync(openclawPath)) {
861
+ if (fs5.existsSync(openclawPath)) {
725
862
  const perms = getPermissionOctal(openclawPath);
726
863
  if (isWorldReadable(openclawPath) || isGroupReadable(openclawPath)) {
727
864
  tooOpen.push(`${openclawPath} is ${perms} (should be 0o700)`);
728
865
  }
729
866
  }
730
867
  const sensitiveFiles = [
731
- path4.join(openclawPath, "openclaw.json"),
732
- path4.join(openclawPath, ".env"),
733
- path4.join(openclawPath, "credentials", "profiles.json")
868
+ path5.join(openclawPath, "openclaw.json"),
869
+ path5.join(openclawPath, ".env"),
870
+ path5.join(openclawPath, "credentials", "profiles.json")
734
871
  ];
735
- const agentsDir = path4.join(openclawPath, "agents");
736
- if (fs4.existsSync(agentsDir) && fs4.statSync(agentsDir).isDirectory()) {
737
- const authFiles = findFilesRecursive2(agentsDir, "auth-profiles.json");
872
+ const agentsDir = path5.join(openclawPath, "agents");
873
+ if (fs5.existsSync(agentsDir) && fs5.statSync(agentsDir).isDirectory()) {
874
+ const authFiles = findFilesRecursive(agentsDir, "auth-profiles.json", "exact");
738
875
  sensitiveFiles.push(...authFiles);
739
876
  }
740
877
  for (const filepath of sensitiveFiles) {
741
- if (fs4.existsSync(filepath)) {
878
+ if (fs5.existsSync(filepath)) {
742
879
  const perms = getPermissionOctal(filepath);
743
880
  if (isWorldReadable(filepath) || isGroupReadable(filepath)) {
744
- const relPath = getRelativePath2(filepath, openclawPath);
881
+ const relPath = getRelativePath(filepath, openclawPath);
745
882
  tooOpen.push(`${relPath} is ${perms} (should be 0o600)`);
746
883
  }
747
884
  }
@@ -759,31 +896,12 @@ function checkPermissions(openclawPath) {
759
896
  }
760
897
  return findings;
761
898
  }
762
- function findFilesRecursive2(dir, filename) {
763
- const results = [];
764
- if (!fs4.existsSync(dir) || !fs4.statSync(dir).isDirectory()) {
765
- return results;
766
- }
767
- try {
768
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
769
- for (const entry of entries) {
770
- const fullPath = path4.join(dir, entry.name);
771
- if (entry.isDirectory()) {
772
- results.push(...findFilesRecursive2(fullPath, filename));
773
- } else if (entry.isFile() && entry.name === filename) {
774
- results.push(fullPath);
775
- }
776
- }
777
- } catch {
778
- }
779
- return results;
780
- }
781
899
 
782
900
  // src/checks/version.ts
783
901
  import { execFileSync as execFileSync2 } from "child_process";
784
- import fs5 from "fs";
785
- import path5 from "path";
786
- import os from "os";
902
+ import fs6 from "fs";
903
+ import path6 from "path";
904
+ import os2 from "os";
787
905
  var CVES = [
788
906
  {
789
907
  id: "CVE-2026-25253",
@@ -870,8 +988,8 @@ function checkVersion(_openclawPath) {
870
988
  }
871
989
  } else {
872
990
  const pkgPaths = [
873
- path5.join(
874
- os.homedir(),
991
+ path6.join(
992
+ os2.homedir(),
875
993
  ".bun",
876
994
  "install",
877
995
  "global",
@@ -883,9 +1001,9 @@ function checkVersion(_openclawPath) {
883
1001
  ];
884
1002
  let found = false;
885
1003
  for (const pkgPath of pkgPaths) {
886
- if (fs5.existsSync(pkgPath)) {
1004
+ if (fs6.existsSync(pkgPath)) {
887
1005
  try {
888
- const content = fs5.readFileSync(pkgPath, { encoding: "utf-8" });
1006
+ const content = fs6.readFileSync(pkgPath, { encoding: "utf-8" });
889
1007
  const pkg = JSON.parse(content);
890
1008
  ocVersion = pkg.version ?? "unknown";
891
1009
  findings.push(
@@ -943,11 +1061,11 @@ function checkVersion(_openclawPath) {
943
1061
  }
944
1062
 
945
1063
  // src/checks/skills.ts
946
- import fs6 from "fs";
947
- import path6 from "path";
1064
+ import fs7 from "fs";
1065
+ import path7 from "path";
948
1066
  import yaml from "js-yaml";
949
1067
  function parseSkillMd(skillPath) {
950
- const content = fs6.readFileSync(skillPath, { encoding: "utf-8" });
1068
+ const content = fs7.readFileSync(skillPath, { encoding: "utf-8" });
951
1069
  let frontmatter = {};
952
1070
  let body = content;
953
1071
  if (content.startsWith("---")) {
@@ -1044,18 +1162,18 @@ function checkSkillMalicious(name, body) {
1044
1162
  function checkSkills(openclawPath) {
1045
1163
  const findings = [];
1046
1164
  const skillDirs = [
1047
- path6.join(openclawPath, "workspace", "skills"),
1048
- path6.join(openclawPath, "skills")
1165
+ path7.join(openclawPath, "workspace", "skills"),
1166
+ path7.join(openclawPath, "skills")
1049
1167
  ];
1050
1168
  let totalSkills = 0;
1051
1169
  let flaggedSkills = 0;
1052
1170
  for (const skillsRoot of skillDirs) {
1053
- if (!fs6.existsSync(skillsRoot) || !fs6.statSync(skillsRoot).isDirectory()) {
1171
+ if (!fs7.existsSync(skillsRoot) || !fs7.statSync(skillsRoot).isDirectory()) {
1054
1172
  continue;
1055
1173
  }
1056
1174
  let entries;
1057
1175
  try {
1058
- entries = fs6.readdirSync(skillsRoot, { withFileTypes: true });
1176
+ entries = fs7.readdirSync(skillsRoot, { withFileTypes: true });
1059
1177
  } catch {
1060
1178
  continue;
1061
1179
  }
@@ -1063,8 +1181,8 @@ function checkSkills(openclawPath) {
1063
1181
  if (!entry.isDirectory()) {
1064
1182
  continue;
1065
1183
  }
1066
- const skillMdPath = path6.join(skillsRoot, entry.name, "SKILL.md");
1067
- if (!fs6.existsSync(skillMdPath)) {
1184
+ const skillMdPath = path7.join(skillsRoot, entry.name, "SKILL.md");
1185
+ if (!fs7.existsSync(skillMdPath)) {
1068
1186
  continue;
1069
1187
  }
1070
1188
  totalSkills++;
@@ -1079,7 +1197,7 @@ function checkSkills(openclawPath) {
1079
1197
  details: [
1080
1198
  "This publisher name is known to be associated with malicious skills"
1081
1199
  ],
1082
- fix: `REMOVE IMMEDIATELY: rm -rf ${path6.join(skillsRoot, entry.name)}`,
1200
+ fix: `REMOVE IMMEDIATELY: rm -rf ${path7.join(skillsRoot, entry.name)}`,
1083
1201
  category: "skills"
1084
1202
  })
1085
1203
  );
@@ -1119,23 +1237,23 @@ function checkSkills(openclawPath) {
1119
1237
  }
1120
1238
 
1121
1239
  // src/checks/memory.ts
1122
- import fs7 from "fs";
1123
- import path7 from "path";
1240
+ import fs8 from "fs";
1241
+ import path8 from "path";
1124
1242
  function checkMemory(openclawPath) {
1125
1243
  const findings = [];
1126
- const workspace = path7.join(openclawPath, "workspace");
1127
- if (!fs7.existsSync(workspace) || !fs7.statSync(workspace).isDirectory()) {
1244
+ const workspace = path8.join(openclawPath, "workspace");
1245
+ if (!fs8.existsSync(workspace) || !fs8.statSync(workspace).isDirectory()) {
1128
1246
  return findings;
1129
1247
  }
1130
1248
  const identityFiles = [
1131
- path7.join(workspace, "SOUL.md"),
1132
- path7.join(workspace, "IDENTITY.md")
1249
+ path8.join(workspace, "SOUL.md"),
1250
+ path8.join(workspace, "IDENTITY.md")
1133
1251
  ];
1134
1252
  for (const filepath of identityFiles) {
1135
- if (!fs7.existsSync(filepath)) {
1253
+ if (!fs8.existsSync(filepath)) {
1136
1254
  continue;
1137
1255
  }
1138
- const content = fs7.readFileSync(filepath, { encoding: "utf-8" });
1256
+ const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
1139
1257
  const detected = [];
1140
1258
  for (const [patternName, pattern] of MEMORY_POISONING_PATTERNS) {
1141
1259
  const matches = content.match(pattern);
@@ -1148,7 +1266,7 @@ function checkMemory(openclawPath) {
1148
1266
  findings.push(
1149
1267
  createFinding({
1150
1268
  severity: Severity.HIGH,
1151
- title: `Potential memory poisoning in ${path7.basename(filepath)}`,
1269
+ title: `Potential memory poisoning in ${path8.basename(filepath)}`,
1152
1270
  details: detected,
1153
1271
  fix: `Review ${filepath} for injected instructions and restore from backup`,
1154
1272
  category: "memory"
@@ -1156,14 +1274,14 @@ function checkMemory(openclawPath) {
1156
1274
  );
1157
1275
  }
1158
1276
  }
1159
- const memoryFiles = [path7.join(workspace, "MEMORY.md")];
1160
- const memoryDir = path7.join(workspace, "memory");
1161
- if (fs7.existsSync(memoryDir) && fs7.statSync(memoryDir).isDirectory()) {
1277
+ const memoryFiles = [path8.join(workspace, "MEMORY.md")];
1278
+ const memoryDir = path8.join(workspace, "memory");
1279
+ if (fs8.existsSync(memoryDir) && fs8.statSync(memoryDir).isDirectory()) {
1162
1280
  try {
1163
- const entries = fs7.readdirSync(memoryDir);
1281
+ const entries = fs8.readdirSync(memoryDir);
1164
1282
  for (const entry of entries) {
1165
1283
  if (entry.endsWith(".md")) {
1166
- memoryFiles.push(path7.join(memoryDir, entry));
1284
+ memoryFiles.push(path8.join(memoryDir, entry));
1167
1285
  }
1168
1286
  }
1169
1287
  } catch {
@@ -1171,13 +1289,13 @@ function checkMemory(openclawPath) {
1171
1289
  }
1172
1290
  const leakedIn = [];
1173
1291
  for (const filepath of memoryFiles) {
1174
- if (!fs7.existsSync(filepath)) {
1292
+ if (!fs8.existsSync(filepath)) {
1175
1293
  continue;
1176
1294
  }
1177
- const content = fs7.readFileSync(filepath, { encoding: "utf-8" });
1295
+ const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
1178
1296
  for (const [keyName, pattern] of API_KEY_PATTERNS) {
1179
1297
  if (pattern.test(content)) {
1180
- leakedIn.push(`${path7.basename(filepath)} contains ${keyName}`);
1298
+ leakedIn.push(`${path8.basename(filepath)} contains ${keyName}`);
1181
1299
  break;
1182
1300
  }
1183
1301
  }
@@ -1196,6 +1314,220 @@ function checkMemory(openclawPath) {
1196
1314
  return findings;
1197
1315
  }
1198
1316
 
1317
+ // src/checks/agents.ts
1318
+ import fs9 from "fs";
1319
+ import path9 from "path";
1320
+ function checkAgents(openclawPath) {
1321
+ const findings = [];
1322
+ const agentsDir = path9.join(openclawPath, "agents");
1323
+ if (!fs9.existsSync(agentsDir) || !fs9.statSync(agentsDir).isDirectory()) {
1324
+ return findings;
1325
+ }
1326
+ const globalConfig = parseConfig(
1327
+ path9.join(openclawPath, "openclaw.json")
1328
+ );
1329
+ const globalModel = getGlobalModel(globalConfig);
1330
+ let agentCount = 0;
1331
+ let issueCount = 0;
1332
+ let entries;
1333
+ try {
1334
+ entries = fs9.readdirSync(agentsDir, { withFileTypes: true });
1335
+ } catch {
1336
+ return findings;
1337
+ }
1338
+ for (const entry of entries) {
1339
+ if (!entry.isDirectory()) continue;
1340
+ const modelsJsonPath = path9.join(
1341
+ agentsDir,
1342
+ entry.name,
1343
+ "agent",
1344
+ "models.json"
1345
+ );
1346
+ if (!fs9.existsSync(modelsJsonPath)) continue;
1347
+ agentCount++;
1348
+ const agentName = entry.name;
1349
+ const keyHits = scanFileForKeys(modelsJsonPath);
1350
+ if (keyHits.length > 0) {
1351
+ issueCount++;
1352
+ const details = keyHits.slice(0, 5).map(
1353
+ (h) => `Line ${h.lineNum}: ${h.keyName} (${h.masked})`
1354
+ );
1355
+ findings.push(
1356
+ createFinding({
1357
+ severity: Severity.CRITICAL,
1358
+ title: `Agent '${agentName}' has plaintext API keys in models.json`,
1359
+ details,
1360
+ fix: `Replace raw keys with env var refs: "apiKey": "\${${PROVIDER_ENV_MAP.anthropic}}"`,
1361
+ category: "agents"
1362
+ })
1363
+ );
1364
+ }
1365
+ try {
1366
+ const content = fs9.readFileSync(modelsJsonPath, {
1367
+ encoding: "utf-8"
1368
+ });
1369
+ const hasEnvRefs = ENV_VAR_PATTERN.test(content);
1370
+ if (keyHits.length > 0 && !hasEnvRefs) {
1371
+ findings.push(
1372
+ createFinding({
1373
+ severity: Severity.HIGH,
1374
+ title: `Agent '${agentName}' uses raw keys instead of env var references`,
1375
+ details: [
1376
+ "models.json contains no ${ENV_VAR} references",
1377
+ "All API keys should use environment variable substitution"
1378
+ ],
1379
+ fix: 'Run "clawguard migrate-env" to auto-migrate keys to .env',
1380
+ category: "agents"
1381
+ })
1382
+ );
1383
+ issueCount++;
1384
+ }
1385
+ } catch {
1386
+ }
1387
+ if (globalModel) {
1388
+ const agentModel = getAgentModel(modelsJsonPath);
1389
+ if (agentModel && agentModel !== globalModel) {
1390
+ findings.push(
1391
+ createFinding({
1392
+ severity: Severity.MEDIUM,
1393
+ title: `Agent '${agentName}' model differs from global config`,
1394
+ details: [
1395
+ `Global model: ${globalModel}`,
1396
+ `Agent model: ${agentModel}`,
1397
+ "This agent overrides the global model setting (config shadow)"
1398
+ ],
1399
+ fix: "Verify this is intentional. Remove agent-level model to use global setting.",
1400
+ category: "agents"
1401
+ })
1402
+ );
1403
+ issueCount++;
1404
+ }
1405
+ }
1406
+ }
1407
+ if (agentCount > 0 && issueCount === 0) {
1408
+ findings.push(
1409
+ createFinding({
1410
+ severity: Severity.INFO,
1411
+ title: `${agentCount} agent config(s) scanned - no issues found`,
1412
+ category: "agents"
1413
+ })
1414
+ );
1415
+ }
1416
+ return findings;
1417
+ }
1418
+ function getGlobalModel(config) {
1419
+ if (!config) return null;
1420
+ try {
1421
+ const models = config.models;
1422
+ if (!models) return null;
1423
+ const defaultModel = models.default;
1424
+ if (defaultModel) return defaultModel;
1425
+ const providers = models.providers;
1426
+ if (providers) {
1427
+ for (const provider of Object.values(providers)) {
1428
+ if (provider.model) return String(provider.model);
1429
+ }
1430
+ }
1431
+ } catch {
1432
+ }
1433
+ return null;
1434
+ }
1435
+ function getAgentModel(modelsJsonPath) {
1436
+ try {
1437
+ const content = fs9.readFileSync(modelsJsonPath, { encoding: "utf-8" });
1438
+ const data = JSON.parse(content);
1439
+ if (data.model) return String(data.model);
1440
+ if (data.default) return String(data.default);
1441
+ const providers = data.providers;
1442
+ if (providers) {
1443
+ for (const provider of Object.values(providers)) {
1444
+ if (provider.model) return String(provider.model);
1445
+ }
1446
+ }
1447
+ } catch {
1448
+ }
1449
+ return null;
1450
+ }
1451
+
1452
+ // src/checks/providers.ts
1453
+ import path10 from "path";
1454
+ function checkProviders(openclawPath) {
1455
+ const findings = [];
1456
+ const config = parseConfig(path10.join(openclawPath, "openclaw.json"));
1457
+ if (!config) return findings;
1458
+ const models = config.models ?? {};
1459
+ const providers = models.providers ?? {};
1460
+ const providerSummary = [];
1461
+ for (const [providerName, providerConfig] of Object.entries(providers)) {
1462
+ const lowerName = providerName.toLowerCase();
1463
+ providerSummary.push(
1464
+ `${providerName}: model=${String(providerConfig.model ?? "default")}`
1465
+ );
1466
+ const limits = PROVIDER_LIMITS[lowerName];
1467
+ if (limits && limits.tpm < OPENCLAW_SYSTEM_PROMPT_TOKENS) {
1468
+ findings.push(
1469
+ createFinding({
1470
+ severity: Severity.HIGH,
1471
+ title: `Provider '${providerName}' TPM too low for system prompt`,
1472
+ details: [
1473
+ `${limits.note}`,
1474
+ `OpenClaw system prompt requires ~${OPENCLAW_SYSTEM_PROMPT_TOKENS} tokens`,
1475
+ `Provider free tier allows only ${limits.tpm} TPM`,
1476
+ "System prompt alone exceeds the per-minute token limit"
1477
+ ],
1478
+ fix: `Upgrade to a paid ${providerName} plan or switch to a provider with higher limits`,
1479
+ category: "providers"
1480
+ })
1481
+ );
1482
+ }
1483
+ if (limits && limits.rpm < 10) {
1484
+ findings.push(
1485
+ createFinding({
1486
+ severity: Severity.MEDIUM,
1487
+ title: `Provider '${providerName}' has very low rate limits (${limits.rpm} RPM)`,
1488
+ details: [
1489
+ `${limits.note}`,
1490
+ "Low RPM causes frequent rate limit errors during conversations"
1491
+ ],
1492
+ fix: `Consider upgrading ${providerName} plan or using a different provider`,
1493
+ category: "providers"
1494
+ })
1495
+ );
1496
+ }
1497
+ }
1498
+ const tools = config.tools ?? {};
1499
+ const webSearch = tools.webSearch ?? tools.web_search ?? {};
1500
+ const webSearchEnabled = webSearch.enabled !== false && Object.keys(webSearch).length > 0;
1501
+ if (webSearchEnabled) {
1502
+ const apiKey = webSearch.apiKey ?? webSearch.api_key;
1503
+ if (!apiKey) {
1504
+ findings.push(
1505
+ createFinding({
1506
+ severity: Severity.MEDIUM,
1507
+ title: "Web search tool has no API key configured",
1508
+ details: [
1509
+ "tools.webSearch.apiKey is not set",
1510
+ "Web search will fail or use a very limited fallback"
1511
+ ],
1512
+ fix: "Set tools.webSearch.apiKey to your Brave Search API key (get one at brave.com/search/api)",
1513
+ category: "providers"
1514
+ })
1515
+ );
1516
+ }
1517
+ }
1518
+ if (providerSummary.length > 0) {
1519
+ findings.push(
1520
+ createFinding({
1521
+ severity: Severity.INFO,
1522
+ title: `${providerSummary.length} LLM provider(s) configured`,
1523
+ details: providerSummary,
1524
+ category: "providers"
1525
+ })
1526
+ );
1527
+ }
1528
+ return findings;
1529
+ }
1530
+
1199
1531
  // src/scanner.ts
1200
1532
  var CHECK_REGISTRY = {
1201
1533
  credentials: checkCredentials,
@@ -1203,14 +1535,16 @@ var CHECK_REGISTRY = {
1203
1535
  sandbox: checkSandbox,
1204
1536
  permissions: checkPermissions,
1205
1537
  skills: checkSkills,
1206
- memory: checkMemory
1538
+ memory: checkMemory,
1539
+ agents: checkAgents,
1540
+ providers: checkProviders
1207
1541
  };
1208
1542
  function detectOpenclawPath() {
1209
1543
  const candidates = [
1210
- path8.join(os2.homedir(), ".openclaw"),
1211
- path8.join(os2.homedir(), ".clawdbot"),
1544
+ path11.join(os3.homedir(), ".openclaw"),
1545
+ path11.join(os3.homedir(), ".clawdbot"),
1212
1546
  // Legacy name
1213
- path8.join(os2.homedir(), ".moltbot")
1547
+ path11.join(os3.homedir(), ".moltbot")
1214
1548
  // Legacy name
1215
1549
  ];
1216
1550
  const envPath = process.env.OPENCLAW_HOME;
@@ -1218,7 +1552,7 @@ function detectOpenclawPath() {
1218
1552
  candidates.unshift(envPath);
1219
1553
  }
1220
1554
  for (const candidate of candidates) {
1221
- if (fs8.existsSync(candidate) && fs8.statSync(candidate).isDirectory()) {
1555
+ if (fs10.existsSync(candidate) && fs10.statSync(candidate).isDirectory()) {
1222
1556
  return candidate;
1223
1557
  }
1224
1558
  }
@@ -1256,36 +1590,36 @@ async function runScan(openclawPath, checks) {
1256
1590
  }
1257
1591
  function runFix(openclawPath) {
1258
1592
  const actions = [];
1259
- if (fs8.existsSync(openclawPath)) {
1260
- const currentMode = fs8.statSync(openclawPath).mode & 511;
1593
+ if (fs10.existsSync(openclawPath)) {
1594
+ const currentMode = fs10.statSync(openclawPath).mode & 511;
1261
1595
  const current = "0o" + currentMode.toString(8);
1262
1596
  if (current !== "0o700") {
1263
- fs8.chmodSync(openclawPath, 448);
1597
+ fs10.chmodSync(openclawPath, 448);
1264
1598
  actions.push(`Fixed ${openclawPath} permissions: ${current} -> 0o700`);
1265
1599
  }
1266
1600
  }
1267
- const configFile = path8.join(openclawPath, "openclaw.json");
1268
- if (fs8.existsSync(configFile)) {
1269
- const currentMode = fs8.statSync(configFile).mode & 511;
1601
+ const configFile = path11.join(openclawPath, "openclaw.json");
1602
+ if (fs10.existsSync(configFile)) {
1603
+ const currentMode = fs10.statSync(configFile).mode & 511;
1270
1604
  const current = "0o" + currentMode.toString(8);
1271
1605
  if (current !== "0o600") {
1272
- fs8.chmodSync(configFile, 384);
1606
+ fs10.chmodSync(configFile, 384);
1273
1607
  actions.push(
1274
- `Fixed ${path8.basename(configFile)} permissions: ${current} -> 0o600`
1608
+ `Fixed ${path11.basename(configFile)} permissions: ${current} -> 0o600`
1275
1609
  );
1276
1610
  }
1277
1611
  }
1278
- const credsDir = path8.join(openclawPath, "credentials");
1279
- if (fs8.existsSync(credsDir) && fs8.statSync(credsDir).isDirectory()) {
1612
+ const credsDir = path11.join(openclawPath, "credentials");
1613
+ if (fs10.existsSync(credsDir) && fs10.statSync(credsDir).isDirectory()) {
1280
1614
  try {
1281
- const entries = fs8.readdirSync(credsDir, { withFileTypes: true });
1615
+ const entries = fs10.readdirSync(credsDir, { withFileTypes: true });
1282
1616
  for (const entry of entries) {
1283
1617
  if (entry.isFile()) {
1284
- const filePath = path8.join(credsDir, entry.name);
1285
- const currentMode = fs8.statSync(filePath).mode & 511;
1618
+ const filePath = path11.join(credsDir, entry.name);
1619
+ const currentMode = fs10.statSync(filePath).mode & 511;
1286
1620
  const current = "0o" + currentMode.toString(8);
1287
1621
  if (current !== "0o600") {
1288
- fs8.chmodSync(filePath, 384);
1622
+ fs10.chmodSync(filePath, 384);
1289
1623
  actions.push(
1290
1624
  `Fixed ${entry.name} permissions: ${current} -> 0o600`
1291
1625
  );
@@ -1295,38 +1629,45 @@ function runFix(openclawPath) {
1295
1629
  } catch {
1296
1630
  }
1297
1631
  }
1298
- if (fs8.existsSync(configFile)) {
1632
+ if (fs10.existsSync(configFile)) {
1299
1633
  try {
1300
- const content = fs8.readFileSync(configFile, { encoding: "utf-8" });
1301
- const config = JSON54.parse(content);
1634
+ const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
1635
+ const config = JSON55.parse(content);
1302
1636
  let modified = false;
1303
- if (!config.agents) config.agents = {};
1304
- const agents = config.agents;
1305
- if (!agents.defaults) agents.defaults = {};
1306
- const defaults = agents.defaults;
1307
- if (!defaults.sandbox) defaults.sandbox = {};
1308
- const sandbox = defaults.sandbox;
1309
- if (sandbox.mode === "off" || !sandbox.mode) {
1310
- sandbox.mode = "all";
1311
- sandbox.scope = "session";
1312
- modified = true;
1313
- actions.push('Enabled sandbox: agents.defaults.sandbox.mode = "all"');
1314
- }
1315
- if (!sandbox.docker) sandbox.docker = {};
1316
- const docker = sandbox.docker;
1317
- if (docker.network !== "none") {
1318
- docker.network = "none";
1319
- modified = true;
1320
- actions.push('Set sandbox.docker.network = "none"');
1321
- }
1322
- if (!config.tools) config.tools = {};
1323
- const tools = config.tools;
1324
- if (!tools.exec) tools.exec = {};
1325
- const execConfig = tools.exec;
1326
- if (execConfig.host === "gateway") {
1327
- execConfig.host = "sandbox";
1328
- modified = true;
1329
- actions.push('Set tools.exec.host = "sandbox"');
1637
+ const dockerInstalled = isDockerAvailable();
1638
+ if (dockerInstalled) {
1639
+ if (!config.agents) config.agents = {};
1640
+ const agents = config.agents;
1641
+ if (!agents.defaults) agents.defaults = {};
1642
+ const defaults = agents.defaults;
1643
+ if (!defaults.sandbox) defaults.sandbox = {};
1644
+ const sandbox = defaults.sandbox;
1645
+ if (sandbox.mode === "off" || !sandbox.mode) {
1646
+ sandbox.mode = "all";
1647
+ sandbox.scope = "session";
1648
+ modified = true;
1649
+ actions.push('Enabled sandbox: agents.defaults.sandbox.mode = "all"');
1650
+ }
1651
+ if (!sandbox.docker) sandbox.docker = {};
1652
+ const docker = sandbox.docker;
1653
+ if (docker.network !== "none") {
1654
+ docker.network = "none";
1655
+ modified = true;
1656
+ actions.push('Set sandbox.docker.network = "none"');
1657
+ }
1658
+ if (!config.tools) config.tools = {};
1659
+ const tools = config.tools;
1660
+ if (!tools.exec) tools.exec = {};
1661
+ const execConfig = tools.exec;
1662
+ if (execConfig.host === "gateway") {
1663
+ execConfig.host = "sandbox";
1664
+ modified = true;
1665
+ actions.push('Set tools.exec.host = "sandbox"');
1666
+ }
1667
+ } else {
1668
+ actions.push(
1669
+ chalk2.yellow("Sandbox changes skipped: Docker not installed. Install Docker to enable sandbox isolation.")
1670
+ );
1330
1671
  }
1331
1672
  if (!config.logging) config.logging = {};
1332
1673
  const loggingConfig = config.logging;
@@ -1349,42 +1690,138 @@ function runFix(openclawPath) {
1349
1690
  );
1350
1691
  }
1351
1692
  if (modified) {
1352
- fs8.writeFileSync(configFile, JSON.stringify(config, null, 2));
1693
+ fs10.writeFileSync(configFile, JSON.stringify(config, null, 2));
1694
+ }
1695
+ const ramGB = getTotalMemoryGB();
1696
+ if (ramGB < 2) {
1697
+ actions.push(
1698
+ chalk2.yellow(`Warning: System has only ${ramGB.toFixed(1)}GB RAM. Set NODE_OPTIONS="--max-old-space-size=512" to prevent OOM kills.`)
1699
+ );
1700
+ }
1701
+ const hits = scanFileForKeys(configFile);
1702
+ if (hits.length > 0) {
1703
+ actions.push(
1704
+ `Tip: Run ${chalk2.bold("clawguard migrate-env")} to move ${hits.length} plaintext key(s) to .env`
1705
+ );
1353
1706
  }
1354
1707
  } catch (e) {
1355
1708
  const message = e instanceof Error ? e.message : String(e);
1356
1709
  actions.push(`Could not modify config: ${message}`);
1357
1710
  }
1358
1711
  }
1359
- const bakFiles = findFilesRecursive3(openclawPath, ".bak");
1712
+ const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
1360
1713
  for (const bakFile of bakFiles) {
1361
- fs8.unlinkSync(bakFile);
1362
- actions.push(`Deleted backup file: ${path8.basename(bakFile)}`);
1714
+ fs10.unlinkSync(bakFile);
1715
+ actions.push(`Deleted backup file: ${path11.basename(bakFile)}`);
1363
1716
  }
1364
1717
  return actions;
1365
1718
  }
1366
- function findFilesRecursive3(dir, ext) {
1367
- const results = [];
1368
- if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
1369
- return results;
1719
+ function runMigrateEnv(openclawPath, dryRun = false) {
1720
+ const result = {
1721
+ migrations: [],
1722
+ envFileCreated: false,
1723
+ configsUpdated: []
1724
+ };
1725
+ const configFiles = [];
1726
+ const mainConfig = path11.join(openclawPath, "openclaw.json");
1727
+ if (fs10.existsSync(mainConfig)) {
1728
+ configFiles.push(mainConfig);
1729
+ }
1730
+ const agentsDir = path11.join(openclawPath, "agents");
1731
+ const modelsFiles = findFilesRecursive(agentsDir, "models.json", "exact");
1732
+ configFiles.push(...modelsFiles);
1733
+ const keyToEnvVar = /* @__PURE__ */ new Map();
1734
+ for (const configFile of configFiles) {
1735
+ try {
1736
+ const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
1737
+ const lines = content.split("\n");
1738
+ for (const line of lines) {
1739
+ if (/\$\{[A-Z_][A-Z0-9_]*\}/.test(line)) continue;
1740
+ for (const [keyName, pattern] of API_KEY_PATTERNS) {
1741
+ const globalPattern = new RegExp(
1742
+ pattern.source,
1743
+ pattern.flags + (pattern.flags.includes("g") ? "" : "g")
1744
+ );
1745
+ let match;
1746
+ while ((match = globalPattern.exec(line)) !== null) {
1747
+ const rawKey = match[0];
1748
+ if (keyToEnvVar.has(rawKey)) continue;
1749
+ const envVar = guessEnvVarName(keyName, line);
1750
+ keyToEnvVar.set(rawKey, envVar);
1751
+ result.migrations.push({
1752
+ file: path11.relative(openclawPath, configFile),
1753
+ key: rawKey.slice(0, 8) + "..." + rawKey.slice(-4),
1754
+ envVar
1755
+ });
1756
+ }
1757
+ }
1758
+ }
1759
+ } catch {
1760
+ }
1370
1761
  }
1371
- try {
1372
- const entries = fs8.readdirSync(dir, { withFileTypes: true });
1373
- for (const entry of entries) {
1374
- const fullPath = path8.join(dir, entry.name);
1375
- if (entry.isDirectory()) {
1376
- results.push(...findFilesRecursive3(fullPath, ext));
1377
- } else if (entry.isFile() && entry.name.endsWith(ext)) {
1378
- results.push(fullPath);
1762
+ if (result.migrations.length === 0) {
1763
+ return result;
1764
+ }
1765
+ if (dryRun) {
1766
+ return result;
1767
+ }
1768
+ const envFilePath = path11.join(openclawPath, ".env");
1769
+ const existingEnv = fs10.existsSync(envFilePath) ? fs10.readFileSync(envFilePath, { encoding: "utf-8" }) : "";
1770
+ const existingKeys = new Set(
1771
+ existingEnv.split("\n").filter((l) => l.includes("=")).map((l) => l.split("=")[0].trim())
1772
+ );
1773
+ const newEnvLines = [];
1774
+ for (const [rawKey, envVar] of keyToEnvVar) {
1775
+ if (!existingKeys.has(envVar)) {
1776
+ newEnvLines.push(`${envVar}=${rawKey}`);
1777
+ }
1778
+ }
1779
+ if (newEnvLines.length > 0) {
1780
+ const separator = existingEnv.endsWith("\n") || existingEnv === "" ? "" : "\n";
1781
+ fs10.appendFileSync(envFilePath, separator + newEnvLines.join("\n") + "\n");
1782
+ result.envFileCreated = true;
1783
+ try {
1784
+ fs10.chmodSync(envFilePath, 384);
1785
+ } catch {
1786
+ }
1787
+ }
1788
+ for (const configFile of configFiles) {
1789
+ try {
1790
+ let content = fs10.readFileSync(configFile, { encoding: "utf-8" });
1791
+ let modified = false;
1792
+ for (const [rawKey, envVar] of keyToEnvVar) {
1793
+ if (content.includes(rawKey)) {
1794
+ content = content.split(rawKey).join(`\${${envVar}}`);
1795
+ modified = true;
1796
+ }
1379
1797
  }
1798
+ if (modified) {
1799
+ fs10.writeFileSync(configFile, content);
1800
+ result.configsUpdated.push(
1801
+ path11.relative(openclawPath, configFile)
1802
+ );
1803
+ }
1804
+ } catch {
1380
1805
  }
1381
- } catch {
1382
1806
  }
1383
- return results;
1807
+ return result;
1808
+ }
1809
+ function guessEnvVarName(keyName, contextLine) {
1810
+ const lower = keyName.toLowerCase();
1811
+ for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
1812
+ if (lower.includes(provider)) return envVar;
1813
+ }
1814
+ if (contextLine.includes("telegram")) return "TELEGRAM_BOT_TOKEN";
1815
+ if (contextLine.includes("discord")) return "DISCORD_BOT_TOKEN";
1816
+ if (contextLine.includes("slack")) return "SLACK_BOT_TOKEN";
1817
+ if (contextLine.includes("brave") || contextLine.includes("webSearch"))
1818
+ return "BRAVE_SEARCH_API_KEY";
1819
+ if (contextLine.includes("stripe")) return "STRIPE_SECRET_KEY";
1820
+ return keyName.toUpperCase().replace(/[\s/]/g, "_").replace(/[^A-Z0-9_]/g, "");
1384
1821
  }
1385
1822
 
1386
1823
  // src/index.ts
1387
- var VERSION2 = "0.1.0";
1824
+ var VERSION2 = "0.2.0";
1388
1825
  var program = new Command();
1389
1826
  program.name("clawguard").description("Security scanner for OpenClaw AI agent installations").version(VERSION2);
1390
1827
  program.command("scan").description("Scan your OpenClaw installation for security issues").option("-p, --path <path>", "Path to OpenClaw directory").option("-f, --format <format>", "Output format: rich or json", "rich").option(
@@ -1399,7 +1836,7 @@ program.command("scan").description("Scan your OpenClaw installation for securit
1399
1836
  console.log("Use --path to specify the directory.");
1400
1837
  process.exit(1);
1401
1838
  }
1402
- if (!fs9.existsSync(openclawPath)) {
1839
+ if (!fs11.existsSync(openclawPath)) {
1403
1840
  console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
1404
1841
  process.exit(1);
1405
1842
  }
@@ -1455,6 +1892,65 @@ ${actions.length} issue(s) fixed.`));
1455
1892
  console.log(chalk3.green("No auto-fixable issues found.\n"));
1456
1893
  }
1457
1894
  });
1895
+ program.command("migrate-env").description(
1896
+ "Migrate plaintext API keys from config files to .env with ${REF} substitution"
1897
+ ).option("-p, --path <path>", "Path to OpenClaw directory").option("--dry-run", "Show what would be migrated without making changes").action((options) => {
1898
+ const openclawPath = options.path ?? detectOpenclawPath();
1899
+ if (!openclawPath) {
1900
+ console.log(chalk3.red("Could not find OpenClaw installation."));
1901
+ process.exit(1);
1902
+ }
1903
+ if (!fs11.existsSync(openclawPath)) {
1904
+ console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
1905
+ process.exit(1);
1906
+ }
1907
+ const dryRun = options.dryRun ?? false;
1908
+ if (dryRun) {
1909
+ console.log(chalk3.cyan("\n-- DRY RUN (no files will be modified) --\n"));
1910
+ }
1911
+ const result = runMigrateEnv(openclawPath, dryRun);
1912
+ if (result.migrations.length === 0) {
1913
+ console.log(chalk3.green("No plaintext API keys found to migrate."));
1914
+ return;
1915
+ }
1916
+ console.log(
1917
+ chalk3.bold(`
1918
+ Found ${result.migrations.length} key(s) to migrate:
1919
+ `)
1920
+ );
1921
+ for (const m of result.migrations) {
1922
+ console.log(
1923
+ ` ${chalk3.cyan(m.envVar)} <- ${m.key} (${chalk3.dim(m.file)})`
1924
+ );
1925
+ }
1926
+ if (dryRun) {
1927
+ console.log(
1928
+ chalk3.cyan("\nRun without --dry-run to apply these changes.")
1929
+ );
1930
+ } else {
1931
+ console.log();
1932
+ if (result.envFileCreated) {
1933
+ console.log(
1934
+ chalk3.green(" Created/updated .env file (chmod 600)")
1935
+ );
1936
+ }
1937
+ for (const f of result.configsUpdated) {
1938
+ console.log(
1939
+ chalk3.green(` Updated ${f} with \${ENV_VAR} references`)
1940
+ );
1941
+ }
1942
+ console.log(
1943
+ chalk3.green(
1944
+ `
1945
+ ${result.migrations.length} key(s) migrated successfully.`
1946
+ )
1947
+ );
1948
+ console.log(
1949
+ `Run ${chalk3.bold("clawguard scan")} to verify.
1950
+ `
1951
+ );
1952
+ }
1953
+ });
1458
1954
  program.command("version").description("Show ClawGuard version").action(() => {
1459
1955
  console.log(`ClawGuard v${VERSION2}`);
1460
1956
  });