clawguard-cli 0.1.0 → 0.2.1

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,45 +336,80 @@ 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
  }
333
407
  } catch {
334
408
  }
335
409
  }
336
- const envFiles = [
337
- path.join(openclawPath, ".env"),
338
- path.join(openclawPath, "workspace", ".env")
339
- ];
340
- for (const envFile of envFiles) {
341
- if (fs.existsSync(envFile)) {
342
- configFiles.push(envFile);
343
- }
344
- }
345
410
  const allHits = [];
346
411
  for (const filepath of configFiles) {
347
- if (fs.existsSync(filepath)) {
412
+ if (fs2.existsSync(filepath)) {
348
413
  const hits = scanFileForKeys(filepath);
349
414
  for (const { keyName, masked, lineNum } of hits) {
350
415
  const relPath = getRelativePath(filepath, openclawPath);
@@ -356,16 +421,16 @@ function checkCredentials(openclawPath) {
356
421
  findings.push(
357
422
  createFinding({
358
423
  severity: Severity.CRITICAL,
359
- title: `${allHits.length} API key(s) stored in plaintext`,
424
+ title: `${allHits.length} API key(s) stored in plaintext config files`,
360
425
  details: allHits.slice(0, 10),
361
426
  // Show max 10
362
- fix: 'Use environment variables: "apiKey": "${ANTHROPIC_API_KEY}" instead of raw strings',
427
+ fix: 'Move keys to .env and use env var refs: "apiKey": "${ANTHROPIC_API_KEY}"',
363
428
  category: "credentials"
364
429
  })
365
430
  );
366
431
  }
367
432
  const bakWithKeys = [];
368
- const bakFiles = findFilesRecursive(openclawPath, ".bak");
433
+ const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
369
434
  for (const bakFile of bakFiles) {
370
435
  const hits = scanFileForKeys(bakFile);
371
436
  if (hits.length > 0) {
@@ -384,11 +449,11 @@ function checkCredentials(openclawPath) {
384
449
  );
385
450
  }
386
451
  const transcriptHits = [];
387
- if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) {
388
- const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl");
452
+ if (fs2.existsSync(agentsDir) && fs2.statSync(agentsDir).isDirectory()) {
453
+ const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl", "extension");
389
454
  for (const jsonlFile of jsonlFiles) {
390
455
  try {
391
- const content = fs.readFileSync(jsonlFile, { encoding: "utf-8" });
456
+ const content = fs2.readFileSync(jsonlFile, { encoding: "utf-8" });
392
457
  const lines = content.split("\n");
393
458
  let found = false;
394
459
  for (let i = 0; i < Math.min(lines.length, 501); i++) {
@@ -419,11 +484,11 @@ function checkCredentials(openclawPath) {
419
484
  })
420
485
  );
421
486
  }
422
- const configFile = path.join(openclawPath, "openclaw.json");
423
- if (fs.existsSync(configFile)) {
487
+ const configFile = path2.join(openclawPath, "openclaw.json");
488
+ if (fs2.existsSync(configFile)) {
424
489
  try {
425
- const content = fs.readFileSync(configFile, { encoding: "utf-8" });
426
- const config = JSON5.parse(content);
490
+ const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
491
+ const config = JSON52.parse(content);
427
492
  const loggingConfig = config.logging ?? {};
428
493
  const redact = loggingConfig.redactSensitive;
429
494
  if (redact === void 0 || redact === null || redact === "off") {
@@ -442,58 +507,41 @@ function checkCredentials(openclawPath) {
442
507
  }
443
508
  return findings;
444
509
  }
445
- function findFilesRecursive(dir, ext) {
446
- const results = [];
447
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
448
- return results;
510
+
511
+ // src/checks/gateway.ts
512
+ import { execSync } from "child_process";
513
+ import fs3 from "fs";
514
+ import path3 from "path";
515
+ import JSON53 from "json5";
516
+ function checkPortExposed(port) {
517
+ if (process.platform !== "linux") {
518
+ return false;
449
519
  }
450
520
  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);
521
+ const output = execSync(`ss -tlnp`, { encoding: "utf-8", timeout: 5e3 });
522
+ const lines = output.split("\n");
523
+ for (const line of lines) {
524
+ const columns = line.trim().split(/\s+/);
525
+ if (columns.length < 5) continue;
526
+ const localAddr = columns[3];
527
+ const lastColon = localAddr.lastIndexOf(":");
528
+ if (lastColon === -1) continue;
529
+ const portStr = localAddr.substring(lastColon + 1);
530
+ if (parseInt(portStr, 10) !== port) continue;
531
+ const host = localAddr.substring(0, lastColon);
532
+ if (host === "0.0.0.0" || host === "*" || host === "[::]" || host === "::") {
533
+ return true;
458
534
  }
459
535
  }
536
+ return false;
460
537
  } catch {
538
+ return false;
461
539
  }
462
- return results;
463
- }
464
-
465
- // src/checks/gateway.ts
466
- import fs2 from "fs";
467
- import net from "net";
468
- import path2 from "path";
469
- import JSON52 from "json5";
470
- function checkPortExposed(port) {
471
- return new Promise((resolve) => {
472
- const socket = new net.Socket();
473
- socket.setTimeout(1e3);
474
- socket.on("connect", () => {
475
- socket.destroy();
476
- resolve(true);
477
- });
478
- socket.on("timeout", () => {
479
- socket.destroy();
480
- resolve(false);
481
- });
482
- socket.on("error", () => {
483
- socket.destroy();
484
- resolve(false);
485
- });
486
- try {
487
- socket.connect(port, "0.0.0.0");
488
- } catch {
489
- resolve(false);
490
- }
491
- });
492
540
  }
493
- async function checkGateway(openclawPath) {
541
+ function checkGateway(openclawPath) {
494
542
  const findings = [];
495
- const configFile = path2.join(openclawPath, "openclaw.json");
496
- if (!fs2.existsSync(configFile)) {
543
+ const configFile = path3.join(openclawPath, "openclaw.json");
544
+ if (!fs3.existsSync(configFile)) {
497
545
  findings.push(
498
546
  createFinding({
499
547
  severity: Severity.INFO,
@@ -506,8 +554,8 @@ async function checkGateway(openclawPath) {
506
554
  }
507
555
  let config;
508
556
  try {
509
- const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
510
- config = JSON52.parse(content);
557
+ const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
558
+ config = JSON53.parse(content);
511
559
  } catch {
512
560
  findings.push(
513
561
  createFinding({
@@ -561,7 +609,7 @@ async function checkGateway(openclawPath) {
561
609
  );
562
610
  }
563
611
  const port = gateway.port ?? 18789;
564
- const portExposed = await checkPortExposed(port);
612
+ const portExposed = checkPortExposed(port);
565
613
  if (portExposed) {
566
614
  findings.push(
567
615
  createFinding({
@@ -576,32 +624,83 @@ async function checkGateway(openclawPath) {
576
624
  })
577
625
  );
578
626
  }
627
+ const ramGB = getTotalMemoryGB();
628
+ const nodeOptions = process.env.NODE_OPTIONS ?? "";
629
+ if (ramGB < 2) {
630
+ if (!nodeOptions.includes("max-old-space-size")) {
631
+ findings.push(
632
+ createFinding({
633
+ severity: Severity.HIGH,
634
+ title: `System has only ${ramGB.toFixed(1)}GB RAM without NODE_OPTIONS set`,
635
+ details: [
636
+ `Total RAM: ${ramGB.toFixed(1)}GB`,
637
+ "OpenClaw may be killed by OOM without memory limits",
638
+ "NODE_OPTIONS environment variable is not set"
639
+ ],
640
+ fix: 'Set NODE_OPTIONS="--max-old-space-size=512" in your service file or shell profile',
641
+ category: "gateway"
642
+ })
643
+ );
644
+ }
645
+ }
646
+ if (process.platform === "linux") {
647
+ const serviceFiles = [
648
+ "/etc/systemd/system/openclaw.service",
649
+ "/etc/systemd/system/openclaw-gateway.service"
650
+ ];
651
+ for (const serviceFile of serviceFiles) {
652
+ if (!fs3.existsSync(serviceFile)) continue;
653
+ try {
654
+ const serviceContent = fs3.readFileSync(serviceFile, { encoding: "utf-8" });
655
+ if (ramGB < 2 && !serviceContent.includes("NODE_OPTIONS")) {
656
+ findings.push(
657
+ createFinding({
658
+ severity: Severity.HIGH,
659
+ title: `Systemd service missing NODE_OPTIONS (${path3.basename(serviceFile)})`,
660
+ details: [
661
+ `Low RAM system (${ramGB.toFixed(1)}GB) but service has no NODE_OPTIONS`,
662
+ "Add Environment=NODE_OPTIONS=--max-old-space-size=512 to [Service] section"
663
+ ],
664
+ fix: `Edit ${serviceFile} and add NODE_OPTIONS, then systemctl daemon-reload`,
665
+ category: "gateway"
666
+ })
667
+ );
668
+ }
669
+ if (serviceContent.includes("Restart=always") && !serviceContent.includes("RestartSec")) {
670
+ findings.push(
671
+ createFinding({
672
+ severity: Severity.MEDIUM,
673
+ title: `Systemd service has Restart=always without RestartSec (${path3.basename(serviceFile)})`,
674
+ details: [
675
+ "Service will restart immediately on crash, potentially causing a restart storm",
676
+ "Add RestartSec=5 to [Service] section to prevent rapid restarts"
677
+ ],
678
+ fix: `Add RestartSec=5 to ${serviceFile} [Service] section`,
679
+ category: "gateway"
680
+ })
681
+ );
682
+ }
683
+ } catch {
684
+ }
685
+ }
686
+ }
579
687
  return findings;
580
688
  }
581
689
 
582
690
  // 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
- }
691
+ import fs4 from "fs";
692
+ import path4 from "path";
693
+ import JSON54 from "json5";
595
694
  function checkSandbox(openclawPath) {
596
695
  const findings = [];
597
- const configFile = path3.join(openclawPath, "openclaw.json");
598
- if (!fs3.existsSync(configFile)) {
696
+ const configFile = path4.join(openclawPath, "openclaw.json");
697
+ if (!fs4.existsSync(configFile)) {
599
698
  return findings;
600
699
  }
601
700
  let config;
602
701
  try {
603
- const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
604
- config = JSON53.parse(content);
702
+ const content = fs4.readFileSync(configFile, { encoding: "utf-8" });
703
+ config = JSON54.parse(content);
605
704
  } catch {
606
705
  return findings;
607
706
  }
@@ -670,7 +769,22 @@ function checkSandbox(openclawPath) {
670
769
  );
671
770
  }
672
771
  const execSecurity = execConfig.security;
673
- if (execSecurity !== "allowlist") {
772
+ const validExecSecurity = ["deny", "allowlist", "full"];
773
+ if (execSecurity && !validExecSecurity.includes(execSecurity)) {
774
+ findings.push(
775
+ createFinding({
776
+ severity: Severity.HIGH,
777
+ title: `Invalid tools.exec.security value: "${execSecurity}"`,
778
+ details: [
779
+ `tools.exec.security = "${execSecurity}"`,
780
+ `Valid values: ${validExecSecurity.join(", ")}`,
781
+ "Invalid value may cause unpredictable behavior"
782
+ ],
783
+ fix: 'Set tools.exec.security to "deny", "allowlist", or "full" in openclaw.json',
784
+ category: "sandbox"
785
+ })
786
+ );
787
+ } else if (execSecurity !== "allowlist") {
674
788
  findings.push(
675
789
  createFinding({
676
790
  severity: Severity.MEDIUM,
@@ -684,6 +798,26 @@ function checkSandbox(openclawPath) {
684
798
  })
685
799
  );
686
800
  }
801
+ const commands = config.commands ?? {};
802
+ const nativeSkills = commands.nativeSkills;
803
+ if (nativeSkills !== void 0 && nativeSkills !== null) {
804
+ const validNativeSkills = [false, "auto", "off"];
805
+ if (!validNativeSkills.includes(nativeSkills)) {
806
+ findings.push(
807
+ createFinding({
808
+ severity: Severity.MEDIUM,
809
+ title: `Invalid commands.nativeSkills value: "${String(nativeSkills)}"`,
810
+ details: [
811
+ `commands.nativeSkills = "${String(nativeSkills)}"`,
812
+ 'Valid values: false, "auto", "off"',
813
+ "Invalid value may cause skill execution issues"
814
+ ],
815
+ fix: 'Set commands.nativeSkills to false, "auto", or "off" in openclaw.json',
816
+ category: "sandbox"
817
+ })
818
+ );
819
+ }
820
+ }
687
821
  if (dockerAvailable) {
688
822
  findings.push(
689
823
  createFinding({
@@ -700,48 +834,44 @@ function checkSandbox(openclawPath) {
700
834
  }
701
835
 
702
836
  // src/checks/permissions.ts
703
- import fs4 from "fs";
704
- import path4 from "path";
837
+ import fs5 from "fs";
838
+ import path5 from "path";
705
839
  function getPermissionOctal(filepath) {
706
- const stats = fs4.statSync(filepath);
840
+ const stats = fs5.statSync(filepath);
707
841
  return "0o" + (stats.mode & 511).toString(8);
708
842
  }
709
843
  function isWorldReadable(filepath) {
710
- const stats = fs4.statSync(filepath);
844
+ const stats = fs5.statSync(filepath);
711
845
  return (stats.mode & 4) !== 0;
712
846
  }
713
847
  function isGroupReadable(filepath) {
714
- const stats = fs4.statSync(filepath);
848
+ const stats = fs5.statSync(filepath);
715
849
  return (stats.mode & 32) !== 0;
716
850
  }
717
- function getRelativePath2(filePath, basePath) {
718
- const parentDir = path4.dirname(basePath);
719
- return path4.relative(parentDir, filePath);
720
- }
721
851
  function checkPermissions(openclawPath) {
722
852
  const findings = [];
723
853
  const tooOpen = [];
724
- if (fs4.existsSync(openclawPath)) {
854
+ if (fs5.existsSync(openclawPath)) {
725
855
  const perms = getPermissionOctal(openclawPath);
726
856
  if (isWorldReadable(openclawPath) || isGroupReadable(openclawPath)) {
727
857
  tooOpen.push(`${openclawPath} is ${perms} (should be 0o700)`);
728
858
  }
729
859
  }
730
860
  const sensitiveFiles = [
731
- path4.join(openclawPath, "openclaw.json"),
732
- path4.join(openclawPath, ".env"),
733
- path4.join(openclawPath, "credentials", "profiles.json")
861
+ path5.join(openclawPath, "openclaw.json"),
862
+ path5.join(openclawPath, ".env"),
863
+ path5.join(openclawPath, "credentials", "profiles.json")
734
864
  ];
735
- const agentsDir = path4.join(openclawPath, "agents");
736
- if (fs4.existsSync(agentsDir) && fs4.statSync(agentsDir).isDirectory()) {
737
- const authFiles = findFilesRecursive2(agentsDir, "auth-profiles.json");
865
+ const agentsDir = path5.join(openclawPath, "agents");
866
+ if (fs5.existsSync(agentsDir) && fs5.statSync(agentsDir).isDirectory()) {
867
+ const authFiles = findFilesRecursive(agentsDir, "auth-profiles.json", "exact");
738
868
  sensitiveFiles.push(...authFiles);
739
869
  }
740
870
  for (const filepath of sensitiveFiles) {
741
- if (fs4.existsSync(filepath)) {
871
+ if (fs5.existsSync(filepath)) {
742
872
  const perms = getPermissionOctal(filepath);
743
873
  if (isWorldReadable(filepath) || isGroupReadable(filepath)) {
744
- const relPath = getRelativePath2(filepath, openclawPath);
874
+ const relPath = getRelativePath(filepath, openclawPath);
745
875
  tooOpen.push(`${relPath} is ${perms} (should be 0o600)`);
746
876
  }
747
877
  }
@@ -759,31 +889,12 @@ function checkPermissions(openclawPath) {
759
889
  }
760
890
  return findings;
761
891
  }
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
892
 
782
893
  // src/checks/version.ts
783
894
  import { execFileSync as execFileSync2 } from "child_process";
784
- import fs5 from "fs";
785
- import path5 from "path";
786
- import os from "os";
895
+ import fs6 from "fs";
896
+ import path6 from "path";
897
+ import os2 from "os";
787
898
  var CVES = [
788
899
  {
789
900
  id: "CVE-2026-25253",
@@ -870,8 +981,8 @@ function checkVersion(_openclawPath) {
870
981
  }
871
982
  } else {
872
983
  const pkgPaths = [
873
- path5.join(
874
- os.homedir(),
984
+ path6.join(
985
+ os2.homedir(),
875
986
  ".bun",
876
987
  "install",
877
988
  "global",
@@ -883,9 +994,9 @@ function checkVersion(_openclawPath) {
883
994
  ];
884
995
  let found = false;
885
996
  for (const pkgPath of pkgPaths) {
886
- if (fs5.existsSync(pkgPath)) {
997
+ if (fs6.existsSync(pkgPath)) {
887
998
  try {
888
- const content = fs5.readFileSync(pkgPath, { encoding: "utf-8" });
999
+ const content = fs6.readFileSync(pkgPath, { encoding: "utf-8" });
889
1000
  const pkg = JSON.parse(content);
890
1001
  ocVersion = pkg.version ?? "unknown";
891
1002
  findings.push(
@@ -943,11 +1054,11 @@ function checkVersion(_openclawPath) {
943
1054
  }
944
1055
 
945
1056
  // src/checks/skills.ts
946
- import fs6 from "fs";
947
- import path6 from "path";
1057
+ import fs7 from "fs";
1058
+ import path7 from "path";
948
1059
  import yaml from "js-yaml";
949
1060
  function parseSkillMd(skillPath) {
950
- const content = fs6.readFileSync(skillPath, { encoding: "utf-8" });
1061
+ const content = fs7.readFileSync(skillPath, { encoding: "utf-8" });
951
1062
  let frontmatter = {};
952
1063
  let body = content;
953
1064
  if (content.startsWith("---")) {
@@ -1044,18 +1155,18 @@ function checkSkillMalicious(name, body) {
1044
1155
  function checkSkills(openclawPath) {
1045
1156
  const findings = [];
1046
1157
  const skillDirs = [
1047
- path6.join(openclawPath, "workspace", "skills"),
1048
- path6.join(openclawPath, "skills")
1158
+ path7.join(openclawPath, "workspace", "skills"),
1159
+ path7.join(openclawPath, "skills")
1049
1160
  ];
1050
1161
  let totalSkills = 0;
1051
1162
  let flaggedSkills = 0;
1052
1163
  for (const skillsRoot of skillDirs) {
1053
- if (!fs6.existsSync(skillsRoot) || !fs6.statSync(skillsRoot).isDirectory()) {
1164
+ if (!fs7.existsSync(skillsRoot) || !fs7.statSync(skillsRoot).isDirectory()) {
1054
1165
  continue;
1055
1166
  }
1056
1167
  let entries;
1057
1168
  try {
1058
- entries = fs6.readdirSync(skillsRoot, { withFileTypes: true });
1169
+ entries = fs7.readdirSync(skillsRoot, { withFileTypes: true });
1059
1170
  } catch {
1060
1171
  continue;
1061
1172
  }
@@ -1063,8 +1174,8 @@ function checkSkills(openclawPath) {
1063
1174
  if (!entry.isDirectory()) {
1064
1175
  continue;
1065
1176
  }
1066
- const skillMdPath = path6.join(skillsRoot, entry.name, "SKILL.md");
1067
- if (!fs6.existsSync(skillMdPath)) {
1177
+ const skillMdPath = path7.join(skillsRoot, entry.name, "SKILL.md");
1178
+ if (!fs7.existsSync(skillMdPath)) {
1068
1179
  continue;
1069
1180
  }
1070
1181
  totalSkills++;
@@ -1079,7 +1190,7 @@ function checkSkills(openclawPath) {
1079
1190
  details: [
1080
1191
  "This publisher name is known to be associated with malicious skills"
1081
1192
  ],
1082
- fix: `REMOVE IMMEDIATELY: rm -rf ${path6.join(skillsRoot, entry.name)}`,
1193
+ fix: `REMOVE IMMEDIATELY: rm -rf ${path7.join(skillsRoot, entry.name)}`,
1083
1194
  category: "skills"
1084
1195
  })
1085
1196
  );
@@ -1119,23 +1230,23 @@ function checkSkills(openclawPath) {
1119
1230
  }
1120
1231
 
1121
1232
  // src/checks/memory.ts
1122
- import fs7 from "fs";
1123
- import path7 from "path";
1233
+ import fs8 from "fs";
1234
+ import path8 from "path";
1124
1235
  function checkMemory(openclawPath) {
1125
1236
  const findings = [];
1126
- const workspace = path7.join(openclawPath, "workspace");
1127
- if (!fs7.existsSync(workspace) || !fs7.statSync(workspace).isDirectory()) {
1237
+ const workspace = path8.join(openclawPath, "workspace");
1238
+ if (!fs8.existsSync(workspace) || !fs8.statSync(workspace).isDirectory()) {
1128
1239
  return findings;
1129
1240
  }
1130
1241
  const identityFiles = [
1131
- path7.join(workspace, "SOUL.md"),
1132
- path7.join(workspace, "IDENTITY.md")
1242
+ path8.join(workspace, "SOUL.md"),
1243
+ path8.join(workspace, "IDENTITY.md")
1133
1244
  ];
1134
1245
  for (const filepath of identityFiles) {
1135
- if (!fs7.existsSync(filepath)) {
1246
+ if (!fs8.existsSync(filepath)) {
1136
1247
  continue;
1137
1248
  }
1138
- const content = fs7.readFileSync(filepath, { encoding: "utf-8" });
1249
+ const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
1139
1250
  const detected = [];
1140
1251
  for (const [patternName, pattern] of MEMORY_POISONING_PATTERNS) {
1141
1252
  const matches = content.match(pattern);
@@ -1148,7 +1259,7 @@ function checkMemory(openclawPath) {
1148
1259
  findings.push(
1149
1260
  createFinding({
1150
1261
  severity: Severity.HIGH,
1151
- title: `Potential memory poisoning in ${path7.basename(filepath)}`,
1262
+ title: `Potential memory poisoning in ${path8.basename(filepath)}`,
1152
1263
  details: detected,
1153
1264
  fix: `Review ${filepath} for injected instructions and restore from backup`,
1154
1265
  category: "memory"
@@ -1156,14 +1267,14 @@ function checkMemory(openclawPath) {
1156
1267
  );
1157
1268
  }
1158
1269
  }
1159
- const memoryFiles = [path7.join(workspace, "MEMORY.md")];
1160
- const memoryDir = path7.join(workspace, "memory");
1161
- if (fs7.existsSync(memoryDir) && fs7.statSync(memoryDir).isDirectory()) {
1270
+ const memoryFiles = [path8.join(workspace, "MEMORY.md")];
1271
+ const memoryDir = path8.join(workspace, "memory");
1272
+ if (fs8.existsSync(memoryDir) && fs8.statSync(memoryDir).isDirectory()) {
1162
1273
  try {
1163
- const entries = fs7.readdirSync(memoryDir);
1274
+ const entries = fs8.readdirSync(memoryDir);
1164
1275
  for (const entry of entries) {
1165
1276
  if (entry.endsWith(".md")) {
1166
- memoryFiles.push(path7.join(memoryDir, entry));
1277
+ memoryFiles.push(path8.join(memoryDir, entry));
1167
1278
  }
1168
1279
  }
1169
1280
  } catch {
@@ -1171,13 +1282,13 @@ function checkMemory(openclawPath) {
1171
1282
  }
1172
1283
  const leakedIn = [];
1173
1284
  for (const filepath of memoryFiles) {
1174
- if (!fs7.existsSync(filepath)) {
1285
+ if (!fs8.existsSync(filepath)) {
1175
1286
  continue;
1176
1287
  }
1177
- const content = fs7.readFileSync(filepath, { encoding: "utf-8" });
1288
+ const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
1178
1289
  for (const [keyName, pattern] of API_KEY_PATTERNS) {
1179
1290
  if (pattern.test(content)) {
1180
- leakedIn.push(`${path7.basename(filepath)} contains ${keyName}`);
1291
+ leakedIn.push(`${path8.basename(filepath)} contains ${keyName}`);
1181
1292
  break;
1182
1293
  }
1183
1294
  }
@@ -1196,6 +1307,257 @@ function checkMemory(openclawPath) {
1196
1307
  return findings;
1197
1308
  }
1198
1309
 
1310
+ // src/checks/agents.ts
1311
+ import fs9 from "fs";
1312
+ import path9 from "path";
1313
+ function checkAgents(openclawPath) {
1314
+ const findings = [];
1315
+ const agentsDir = path9.join(openclawPath, "agents");
1316
+ if (!fs9.existsSync(agentsDir) || !fs9.statSync(agentsDir).isDirectory()) {
1317
+ return findings;
1318
+ }
1319
+ const globalConfig = parseConfig(
1320
+ path9.join(openclawPath, "openclaw.json")
1321
+ );
1322
+ const globalModel = getGlobalModel(globalConfig);
1323
+ let agentCount = 0;
1324
+ let issueCount = 0;
1325
+ let entries;
1326
+ try {
1327
+ entries = fs9.readdirSync(agentsDir, { withFileTypes: true });
1328
+ } catch {
1329
+ return findings;
1330
+ }
1331
+ for (const entry of entries) {
1332
+ if (!entry.isDirectory()) continue;
1333
+ const modelsJsonPath = path9.join(
1334
+ agentsDir,
1335
+ entry.name,
1336
+ "agent",
1337
+ "models.json"
1338
+ );
1339
+ if (!fs9.existsSync(modelsJsonPath)) continue;
1340
+ agentCount++;
1341
+ const agentName = entry.name;
1342
+ const keyHits = scanFileForKeys(modelsJsonPath);
1343
+ const openclawUsesEnvRefs = keyHits.length > 0 ? checkOpenclawModelsUseEnvRefs(openclawPath) : false;
1344
+ if (keyHits.length > 0) {
1345
+ if (openclawUsesEnvRefs) {
1346
+ findings.push(
1347
+ createFinding({
1348
+ severity: Severity.INFO,
1349
+ title: `Agent '${agentName}': models.json contains resolved keys (auto-generated)`,
1350
+ details: [
1351
+ "models.json contains resolved API keys, but openclaw.json uses ${ENV_VAR} references",
1352
+ "OpenClaw regenerates models.json from openclaw.json on every gateway startup",
1353
+ "This is expected behavior \u2014 no action needed"
1354
+ ],
1355
+ category: "agents"
1356
+ })
1357
+ );
1358
+ } else {
1359
+ issueCount++;
1360
+ const details = keyHits.slice(0, 5).map(
1361
+ (h) => `Line ${h.lineNum}: ${h.keyName} (${h.masked})`
1362
+ );
1363
+ findings.push(
1364
+ createFinding({
1365
+ severity: Severity.CRITICAL,
1366
+ title: `Agent '${agentName}' has plaintext API keys in models.json`,
1367
+ details,
1368
+ fix: `Replace raw keys with env var refs: "apiKey": "\${${PROVIDER_ENV_MAP.anthropic}}"`,
1369
+ category: "agents"
1370
+ })
1371
+ );
1372
+ }
1373
+ }
1374
+ if (!openclawUsesEnvRefs) {
1375
+ try {
1376
+ const content = fs9.readFileSync(modelsJsonPath, {
1377
+ encoding: "utf-8"
1378
+ });
1379
+ const hasEnvRefs = ENV_VAR_PATTERN.test(content);
1380
+ if (keyHits.length > 0 && !hasEnvRefs) {
1381
+ findings.push(
1382
+ createFinding({
1383
+ severity: Severity.HIGH,
1384
+ title: `Agent '${agentName}' uses raw keys instead of env var references`,
1385
+ details: [
1386
+ "models.json contains no ${ENV_VAR} references",
1387
+ "All API keys should use environment variable substitution"
1388
+ ],
1389
+ fix: 'Run "clawguard migrate-env" to auto-migrate keys to .env',
1390
+ category: "agents"
1391
+ })
1392
+ );
1393
+ issueCount++;
1394
+ }
1395
+ } catch {
1396
+ }
1397
+ }
1398
+ if (globalModel) {
1399
+ const agentModel = getAgentModel(modelsJsonPath);
1400
+ if (agentModel && agentModel !== globalModel) {
1401
+ findings.push(
1402
+ createFinding({
1403
+ severity: Severity.MEDIUM,
1404
+ title: `Agent '${agentName}' model differs from global config`,
1405
+ details: [
1406
+ `Global model: ${globalModel}`,
1407
+ `Agent model: ${agentModel}`,
1408
+ "This agent overrides the global model setting (config shadow)"
1409
+ ],
1410
+ fix: "Verify this is intentional. Remove agent-level model to use global setting.",
1411
+ category: "agents"
1412
+ })
1413
+ );
1414
+ issueCount++;
1415
+ }
1416
+ }
1417
+ }
1418
+ if (agentCount > 0 && issueCount === 0) {
1419
+ findings.push(
1420
+ createFinding({
1421
+ severity: Severity.INFO,
1422
+ title: `${agentCount} agent config(s) scanned - no issues found`,
1423
+ category: "agents"
1424
+ })
1425
+ );
1426
+ }
1427
+ return findings;
1428
+ }
1429
+ function getGlobalModel(config) {
1430
+ if (!config) return null;
1431
+ try {
1432
+ const models = config.models;
1433
+ if (!models) return null;
1434
+ const defaultModel = models.default;
1435
+ if (defaultModel) return defaultModel;
1436
+ const providers = models.providers;
1437
+ if (providers) {
1438
+ for (const provider of Object.values(providers)) {
1439
+ if (provider.model) return String(provider.model);
1440
+ }
1441
+ }
1442
+ } catch {
1443
+ }
1444
+ return null;
1445
+ }
1446
+ function checkOpenclawModelsUseEnvRefs(openclawPath) {
1447
+ try {
1448
+ const configPath = path9.join(openclawPath, "openclaw.json");
1449
+ const content = fs9.readFileSync(configPath, { encoding: "utf-8" });
1450
+ const config = JSON.parse(content);
1451
+ const models = config.models;
1452
+ if (!models) return false;
1453
+ const providers = models.providers;
1454
+ if (!providers) return false;
1455
+ for (const provider of Object.values(providers)) {
1456
+ const apiKey = String(provider.apiKey ?? provider.key ?? "");
1457
+ if (ENV_VAR_PATTERN.test(apiKey)) {
1458
+ return true;
1459
+ }
1460
+ }
1461
+ } catch {
1462
+ }
1463
+ return false;
1464
+ }
1465
+ function getAgentModel(modelsJsonPath) {
1466
+ try {
1467
+ const content = fs9.readFileSync(modelsJsonPath, { encoding: "utf-8" });
1468
+ const data = JSON.parse(content);
1469
+ if (data.model) return String(data.model);
1470
+ if (data.default) return String(data.default);
1471
+ const providers = data.providers;
1472
+ if (providers) {
1473
+ for (const provider of Object.values(providers)) {
1474
+ if (provider.model) return String(provider.model);
1475
+ }
1476
+ }
1477
+ } catch {
1478
+ }
1479
+ return null;
1480
+ }
1481
+
1482
+ // src/checks/providers.ts
1483
+ import path10 from "path";
1484
+ function checkProviders(openclawPath) {
1485
+ const findings = [];
1486
+ const config = parseConfig(path10.join(openclawPath, "openclaw.json"));
1487
+ if (!config) return findings;
1488
+ const models = config.models ?? {};
1489
+ const providers = models.providers ?? {};
1490
+ const providerSummary = [];
1491
+ for (const [providerName, providerConfig] of Object.entries(providers)) {
1492
+ const lowerName = providerName.toLowerCase();
1493
+ providerSummary.push(
1494
+ `${providerName}: model=${String(providerConfig.model ?? "default")}`
1495
+ );
1496
+ const limits = PROVIDER_LIMITS[lowerName];
1497
+ if (limits && limits.tpm < OPENCLAW_SYSTEM_PROMPT_TOKENS) {
1498
+ findings.push(
1499
+ createFinding({
1500
+ severity: Severity.HIGH,
1501
+ title: `Provider '${providerName}' TPM too low for system prompt`,
1502
+ details: [
1503
+ `${limits.note}`,
1504
+ `OpenClaw system prompt requires ~${OPENCLAW_SYSTEM_PROMPT_TOKENS} tokens`,
1505
+ `Provider free tier allows only ${limits.tpm} TPM`,
1506
+ "System prompt alone exceeds the per-minute token limit"
1507
+ ],
1508
+ fix: `Upgrade to a paid ${providerName} plan or switch to a provider with higher limits`,
1509
+ category: "providers"
1510
+ })
1511
+ );
1512
+ }
1513
+ if (limits && limits.rpm < 10) {
1514
+ findings.push(
1515
+ createFinding({
1516
+ severity: Severity.MEDIUM,
1517
+ title: `Provider '${providerName}' has very low rate limits (${limits.rpm} RPM)`,
1518
+ details: [
1519
+ `${limits.note}`,
1520
+ "Low RPM causes frequent rate limit errors during conversations"
1521
+ ],
1522
+ fix: `Consider upgrading ${providerName} plan or using a different provider`,
1523
+ category: "providers"
1524
+ })
1525
+ );
1526
+ }
1527
+ }
1528
+ const tools = config.tools ?? {};
1529
+ const webSearch = tools.webSearch ?? tools.web_search ?? {};
1530
+ const webSearchEnabled = webSearch.enabled !== false && Object.keys(webSearch).length > 0;
1531
+ if (webSearchEnabled) {
1532
+ const apiKey = webSearch.apiKey ?? webSearch.api_key;
1533
+ if (!apiKey) {
1534
+ findings.push(
1535
+ createFinding({
1536
+ severity: Severity.MEDIUM,
1537
+ title: "Web search tool has no API key configured",
1538
+ details: [
1539
+ "tools.webSearch.apiKey is not set",
1540
+ "Web search will fail or use a very limited fallback"
1541
+ ],
1542
+ fix: "Set tools.webSearch.apiKey to your Brave Search API key (get one at brave.com/search/api)",
1543
+ category: "providers"
1544
+ })
1545
+ );
1546
+ }
1547
+ }
1548
+ if (providerSummary.length > 0) {
1549
+ findings.push(
1550
+ createFinding({
1551
+ severity: Severity.INFO,
1552
+ title: `${providerSummary.length} LLM provider(s) configured`,
1553
+ details: providerSummary,
1554
+ category: "providers"
1555
+ })
1556
+ );
1557
+ }
1558
+ return findings;
1559
+ }
1560
+
1199
1561
  // src/scanner.ts
1200
1562
  var CHECK_REGISTRY = {
1201
1563
  credentials: checkCredentials,
@@ -1203,14 +1565,16 @@ var CHECK_REGISTRY = {
1203
1565
  sandbox: checkSandbox,
1204
1566
  permissions: checkPermissions,
1205
1567
  skills: checkSkills,
1206
- memory: checkMemory
1568
+ memory: checkMemory,
1569
+ agents: checkAgents,
1570
+ providers: checkProviders
1207
1571
  };
1208
1572
  function detectOpenclawPath() {
1209
1573
  const candidates = [
1210
- path8.join(os2.homedir(), ".openclaw"),
1211
- path8.join(os2.homedir(), ".clawdbot"),
1574
+ path11.join(os3.homedir(), ".openclaw"),
1575
+ path11.join(os3.homedir(), ".clawdbot"),
1212
1576
  // Legacy name
1213
- path8.join(os2.homedir(), ".moltbot")
1577
+ path11.join(os3.homedir(), ".moltbot")
1214
1578
  // Legacy name
1215
1579
  ];
1216
1580
  const envPath = process.env.OPENCLAW_HOME;
@@ -1218,7 +1582,7 @@ function detectOpenclawPath() {
1218
1582
  candidates.unshift(envPath);
1219
1583
  }
1220
1584
  for (const candidate of candidates) {
1221
- if (fs8.existsSync(candidate) && fs8.statSync(candidate).isDirectory()) {
1585
+ if (fs10.existsSync(candidate) && fs10.statSync(candidate).isDirectory()) {
1222
1586
  return candidate;
1223
1587
  }
1224
1588
  }
@@ -1256,36 +1620,36 @@ async function runScan(openclawPath, checks) {
1256
1620
  }
1257
1621
  function runFix(openclawPath) {
1258
1622
  const actions = [];
1259
- if (fs8.existsSync(openclawPath)) {
1260
- const currentMode = fs8.statSync(openclawPath).mode & 511;
1623
+ if (fs10.existsSync(openclawPath)) {
1624
+ const currentMode = fs10.statSync(openclawPath).mode & 511;
1261
1625
  const current = "0o" + currentMode.toString(8);
1262
1626
  if (current !== "0o700") {
1263
- fs8.chmodSync(openclawPath, 448);
1627
+ fs10.chmodSync(openclawPath, 448);
1264
1628
  actions.push(`Fixed ${openclawPath} permissions: ${current} -> 0o700`);
1265
1629
  }
1266
1630
  }
1267
- const configFile = path8.join(openclawPath, "openclaw.json");
1268
- if (fs8.existsSync(configFile)) {
1269
- const currentMode = fs8.statSync(configFile).mode & 511;
1631
+ const configFile = path11.join(openclawPath, "openclaw.json");
1632
+ if (fs10.existsSync(configFile)) {
1633
+ const currentMode = fs10.statSync(configFile).mode & 511;
1270
1634
  const current = "0o" + currentMode.toString(8);
1271
1635
  if (current !== "0o600") {
1272
- fs8.chmodSync(configFile, 384);
1636
+ fs10.chmodSync(configFile, 384);
1273
1637
  actions.push(
1274
- `Fixed ${path8.basename(configFile)} permissions: ${current} -> 0o600`
1638
+ `Fixed ${path11.basename(configFile)} permissions: ${current} -> 0o600`
1275
1639
  );
1276
1640
  }
1277
1641
  }
1278
- const credsDir = path8.join(openclawPath, "credentials");
1279
- if (fs8.existsSync(credsDir) && fs8.statSync(credsDir).isDirectory()) {
1642
+ const credsDir = path11.join(openclawPath, "credentials");
1643
+ if (fs10.existsSync(credsDir) && fs10.statSync(credsDir).isDirectory()) {
1280
1644
  try {
1281
- const entries = fs8.readdirSync(credsDir, { withFileTypes: true });
1645
+ const entries = fs10.readdirSync(credsDir, { withFileTypes: true });
1282
1646
  for (const entry of entries) {
1283
1647
  if (entry.isFile()) {
1284
- const filePath = path8.join(credsDir, entry.name);
1285
- const currentMode = fs8.statSync(filePath).mode & 511;
1648
+ const filePath = path11.join(credsDir, entry.name);
1649
+ const currentMode = fs10.statSync(filePath).mode & 511;
1286
1650
  const current = "0o" + currentMode.toString(8);
1287
1651
  if (current !== "0o600") {
1288
- fs8.chmodSync(filePath, 384);
1652
+ fs10.chmodSync(filePath, 384);
1289
1653
  actions.push(
1290
1654
  `Fixed ${entry.name} permissions: ${current} -> 0o600`
1291
1655
  );
@@ -1295,38 +1659,45 @@ function runFix(openclawPath) {
1295
1659
  } catch {
1296
1660
  }
1297
1661
  }
1298
- if (fs8.existsSync(configFile)) {
1662
+ if (fs10.existsSync(configFile)) {
1299
1663
  try {
1300
- const content = fs8.readFileSync(configFile, { encoding: "utf-8" });
1301
- const config = JSON54.parse(content);
1664
+ const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
1665
+ const config = JSON55.parse(content);
1302
1666
  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"');
1667
+ const dockerInstalled = isDockerAvailable();
1668
+ if (dockerInstalled) {
1669
+ if (!config.agents) config.agents = {};
1670
+ const agents = config.agents;
1671
+ if (!agents.defaults) agents.defaults = {};
1672
+ const defaults = agents.defaults;
1673
+ if (!defaults.sandbox) defaults.sandbox = {};
1674
+ const sandbox = defaults.sandbox;
1675
+ if (sandbox.mode === "off" || !sandbox.mode) {
1676
+ sandbox.mode = "all";
1677
+ sandbox.scope = "session";
1678
+ modified = true;
1679
+ actions.push('Enabled sandbox: agents.defaults.sandbox.mode = "all"');
1680
+ }
1681
+ if (!sandbox.docker) sandbox.docker = {};
1682
+ const docker = sandbox.docker;
1683
+ if (docker.network !== "none") {
1684
+ docker.network = "none";
1685
+ modified = true;
1686
+ actions.push('Set sandbox.docker.network = "none"');
1687
+ }
1688
+ if (!config.tools) config.tools = {};
1689
+ const tools = config.tools;
1690
+ if (!tools.exec) tools.exec = {};
1691
+ const execConfig = tools.exec;
1692
+ if (execConfig.host === "gateway") {
1693
+ execConfig.host = "sandbox";
1694
+ modified = true;
1695
+ actions.push('Set tools.exec.host = "sandbox"');
1696
+ }
1697
+ } else {
1698
+ actions.push(
1699
+ chalk2.yellow("Sandbox changes skipped: Docker not installed. Install Docker to enable sandbox isolation.")
1700
+ );
1330
1701
  }
1331
1702
  if (!config.logging) config.logging = {};
1332
1703
  const loggingConfig = config.logging;
@@ -1349,42 +1720,138 @@ function runFix(openclawPath) {
1349
1720
  );
1350
1721
  }
1351
1722
  if (modified) {
1352
- fs8.writeFileSync(configFile, JSON.stringify(config, null, 2));
1723
+ fs10.writeFileSync(configFile, JSON.stringify(config, null, 2));
1724
+ }
1725
+ const ramGB = getTotalMemoryGB();
1726
+ if (ramGB < 2) {
1727
+ actions.push(
1728
+ chalk2.yellow(`Warning: System has only ${ramGB.toFixed(1)}GB RAM. Set NODE_OPTIONS="--max-old-space-size=512" to prevent OOM kills.`)
1729
+ );
1730
+ }
1731
+ const hits = scanFileForKeys(configFile);
1732
+ if (hits.length > 0) {
1733
+ actions.push(
1734
+ `Tip: Run ${chalk2.bold("clawguard migrate-env")} to move ${hits.length} plaintext key(s) to .env`
1735
+ );
1353
1736
  }
1354
1737
  } catch (e) {
1355
1738
  const message = e instanceof Error ? e.message : String(e);
1356
1739
  actions.push(`Could not modify config: ${message}`);
1357
1740
  }
1358
1741
  }
1359
- const bakFiles = findFilesRecursive3(openclawPath, ".bak");
1742
+ const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
1360
1743
  for (const bakFile of bakFiles) {
1361
- fs8.unlinkSync(bakFile);
1362
- actions.push(`Deleted backup file: ${path8.basename(bakFile)}`);
1744
+ fs10.unlinkSync(bakFile);
1745
+ actions.push(`Deleted backup file: ${path11.basename(bakFile)}`);
1363
1746
  }
1364
1747
  return actions;
1365
1748
  }
1366
- function findFilesRecursive3(dir, ext) {
1367
- const results = [];
1368
- if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
1369
- return results;
1749
+ function runMigrateEnv(openclawPath, dryRun = false) {
1750
+ const result = {
1751
+ migrations: [],
1752
+ envFileCreated: false,
1753
+ configsUpdated: []
1754
+ };
1755
+ const configFiles = [];
1756
+ const mainConfig = path11.join(openclawPath, "openclaw.json");
1757
+ if (fs10.existsSync(mainConfig)) {
1758
+ configFiles.push(mainConfig);
1759
+ }
1760
+ const agentsDir = path11.join(openclawPath, "agents");
1761
+ const modelsFiles = findFilesRecursive(agentsDir, "models.json", "exact");
1762
+ configFiles.push(...modelsFiles);
1763
+ const keyToEnvVar = /* @__PURE__ */ new Map();
1764
+ for (const configFile of configFiles) {
1765
+ try {
1766
+ const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
1767
+ const lines = content.split("\n");
1768
+ for (const line of lines) {
1769
+ if (/\$\{[A-Z_][A-Z0-9_]*\}/.test(line)) continue;
1770
+ for (const [keyName, pattern] of API_KEY_PATTERNS) {
1771
+ const globalPattern = new RegExp(
1772
+ pattern.source,
1773
+ pattern.flags + (pattern.flags.includes("g") ? "" : "g")
1774
+ );
1775
+ let match;
1776
+ while ((match = globalPattern.exec(line)) !== null) {
1777
+ const rawKey = match[0];
1778
+ if (keyToEnvVar.has(rawKey)) continue;
1779
+ const envVar = guessEnvVarName(keyName, line);
1780
+ keyToEnvVar.set(rawKey, envVar);
1781
+ result.migrations.push({
1782
+ file: path11.relative(openclawPath, configFile),
1783
+ key: rawKey.slice(0, 8) + "..." + rawKey.slice(-4),
1784
+ envVar
1785
+ });
1786
+ }
1787
+ }
1788
+ }
1789
+ } catch {
1790
+ }
1370
1791
  }
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);
1792
+ if (result.migrations.length === 0) {
1793
+ return result;
1794
+ }
1795
+ if (dryRun) {
1796
+ return result;
1797
+ }
1798
+ const envFilePath = path11.join(openclawPath, ".env");
1799
+ const existingEnv = fs10.existsSync(envFilePath) ? fs10.readFileSync(envFilePath, { encoding: "utf-8" }) : "";
1800
+ const existingKeys = new Set(
1801
+ existingEnv.split("\n").filter((l) => l.includes("=")).map((l) => l.split("=")[0].trim())
1802
+ );
1803
+ const newEnvLines = [];
1804
+ for (const [rawKey, envVar] of keyToEnvVar) {
1805
+ if (!existingKeys.has(envVar)) {
1806
+ newEnvLines.push(`${envVar}=${rawKey}`);
1807
+ }
1808
+ }
1809
+ if (newEnvLines.length > 0) {
1810
+ const separator = existingEnv.endsWith("\n") || existingEnv === "" ? "" : "\n";
1811
+ fs10.appendFileSync(envFilePath, separator + newEnvLines.join("\n") + "\n");
1812
+ result.envFileCreated = true;
1813
+ try {
1814
+ fs10.chmodSync(envFilePath, 384);
1815
+ } catch {
1816
+ }
1817
+ }
1818
+ for (const configFile of configFiles) {
1819
+ try {
1820
+ let content = fs10.readFileSync(configFile, { encoding: "utf-8" });
1821
+ let modified = false;
1822
+ for (const [rawKey, envVar] of keyToEnvVar) {
1823
+ if (content.includes(rawKey)) {
1824
+ content = content.split(rawKey).join(`\${${envVar}}`);
1825
+ modified = true;
1826
+ }
1827
+ }
1828
+ if (modified) {
1829
+ fs10.writeFileSync(configFile, content);
1830
+ result.configsUpdated.push(
1831
+ path11.relative(openclawPath, configFile)
1832
+ );
1379
1833
  }
1834
+ } catch {
1380
1835
  }
1381
- } catch {
1382
1836
  }
1383
- return results;
1837
+ return result;
1838
+ }
1839
+ function guessEnvVarName(keyName, contextLine) {
1840
+ const lower = keyName.toLowerCase();
1841
+ for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
1842
+ if (lower.includes(provider)) return envVar;
1843
+ }
1844
+ if (contextLine.includes("telegram")) return "TELEGRAM_BOT_TOKEN";
1845
+ if (contextLine.includes("discord")) return "DISCORD_BOT_TOKEN";
1846
+ if (contextLine.includes("slack")) return "SLACK_BOT_TOKEN";
1847
+ if (contextLine.includes("brave") || contextLine.includes("webSearch"))
1848
+ return "BRAVE_SEARCH_API_KEY";
1849
+ if (contextLine.includes("stripe")) return "STRIPE_SECRET_KEY";
1850
+ return keyName.toUpperCase().replace(/[\s/]/g, "_").replace(/[^A-Z0-9_]/g, "");
1384
1851
  }
1385
1852
 
1386
1853
  // src/index.ts
1387
- var VERSION2 = "0.1.0";
1854
+ var VERSION2 = "0.2.0";
1388
1855
  var program = new Command();
1389
1856
  program.name("clawguard").description("Security scanner for OpenClaw AI agent installations").version(VERSION2);
1390
1857
  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 +1866,7 @@ program.command("scan").description("Scan your OpenClaw installation for securit
1399
1866
  console.log("Use --path to specify the directory.");
1400
1867
  process.exit(1);
1401
1868
  }
1402
- if (!fs9.existsSync(openclawPath)) {
1869
+ if (!fs11.existsSync(openclawPath)) {
1403
1870
  console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
1404
1871
  process.exit(1);
1405
1872
  }
@@ -1455,6 +1922,65 @@ ${actions.length} issue(s) fixed.`));
1455
1922
  console.log(chalk3.green("No auto-fixable issues found.\n"));
1456
1923
  }
1457
1924
  });
1925
+ program.command("migrate-env").description(
1926
+ "Migrate plaintext API keys from config files to .env with ${REF} substitution"
1927
+ ).option("-p, --path <path>", "Path to OpenClaw directory").option("--dry-run", "Show what would be migrated without making changes").action((options) => {
1928
+ const openclawPath = options.path ?? detectOpenclawPath();
1929
+ if (!openclawPath) {
1930
+ console.log(chalk3.red("Could not find OpenClaw installation."));
1931
+ process.exit(1);
1932
+ }
1933
+ if (!fs11.existsSync(openclawPath)) {
1934
+ console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
1935
+ process.exit(1);
1936
+ }
1937
+ const dryRun = options.dryRun ?? false;
1938
+ if (dryRun) {
1939
+ console.log(chalk3.cyan("\n-- DRY RUN (no files will be modified) --\n"));
1940
+ }
1941
+ const result = runMigrateEnv(openclawPath, dryRun);
1942
+ if (result.migrations.length === 0) {
1943
+ console.log(chalk3.green("No plaintext API keys found to migrate."));
1944
+ return;
1945
+ }
1946
+ console.log(
1947
+ chalk3.bold(`
1948
+ Found ${result.migrations.length} key(s) to migrate:
1949
+ `)
1950
+ );
1951
+ for (const m of result.migrations) {
1952
+ console.log(
1953
+ ` ${chalk3.cyan(m.envVar)} <- ${m.key} (${chalk3.dim(m.file)})`
1954
+ );
1955
+ }
1956
+ if (dryRun) {
1957
+ console.log(
1958
+ chalk3.cyan("\nRun without --dry-run to apply these changes.")
1959
+ );
1960
+ } else {
1961
+ console.log();
1962
+ if (result.envFileCreated) {
1963
+ console.log(
1964
+ chalk3.green(" Created/updated .env file (chmod 600)")
1965
+ );
1966
+ }
1967
+ for (const f of result.configsUpdated) {
1968
+ console.log(
1969
+ chalk3.green(` Updated ${f} with \${ENV_VAR} references`)
1970
+ );
1971
+ }
1972
+ console.log(
1973
+ chalk3.green(
1974
+ `
1975
+ ${result.migrations.length} key(s) migrated successfully.`
1976
+ )
1977
+ );
1978
+ console.log(
1979
+ `Run ${chalk3.bold("clawguard scan")} to verify.
1980
+ `
1981
+ );
1982
+ }
1983
+ });
1458
1984
  program.command("version").description("Show ClawGuard version").action(() => {
1459
1985
  console.log(`ClawGuard v${VERSION2}`);
1460
1986
  });