ship-safe 6.0.0 → 6.1.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.
@@ -25,6 +25,8 @@ export { RAGSecurityAgent } from './rag-security-agent.js';
25
25
  export { PIIComplianceAgent } from './pii-compliance-agent.js';
26
26
  export { VibeCodingAgent } from './vibe-coding-agent.js';
27
27
  export { ExceptionHandlerAgent } from './exception-handler-agent.js';
28
+ export { AgentConfigScanner } from './agent-config-scanner.js';
29
+ export { ABOMGenerator } from './abom-generator.js';
28
30
  export { VerifierAgent } from './verifier-agent.js';
29
31
  export { DeepAnalyzer } from './deep-analyzer.js';
30
32
  export { ScoringEngine, GRADES, CATEGORIES } from './scoring-engine.js';
@@ -33,7 +35,7 @@ export { PolicyEngine } from './policy-engine.js';
33
35
  export { HTMLReporter } from './html-reporter.js';
34
36
 
35
37
  /**
36
- * Create a fully configured orchestrator with all 15 scanning agents.
38
+ * Create a fully configured orchestrator with all 16 scanning agents.
37
39
  * (VerifierAgent and DeepAnalyzer run as post-processors, not in the agent pool.)
38
40
  */
39
41
  import { Orchestrator as OrchestratorClass } from './orchestrator.js';
@@ -54,6 +56,7 @@ import { RAGSecurityAgent as RAGSecurityAgentClass } from './rag-security-agent.
54
56
  import { PIIComplianceAgent as PIIComplianceAgentClass } from './pii-compliance-agent.js';
55
57
  import { VibeCodingAgent as VibeCodingAgentClass } from './vibe-coding-agent.js';
56
58
  import { ExceptionHandlerAgent as ExceptionHandlerAgentClass } from './exception-handler-agent.js';
59
+ import { AgentConfigScanner as AgentConfigScannerClass } from './agent-config-scanner.js';
57
60
 
58
61
  export function buildOrchestrator() {
59
62
  const orchestrator = new OrchestratorClass();
@@ -75,6 +78,7 @@ export function buildOrchestrator() {
75
78
  new PIIComplianceAgentClass(),
76
79
  new VibeCodingAgentClass(),
77
80
  new ExceptionHandlerAgentClass(),
81
+ new AgentConfigScannerClass(),
78
82
  ]);
79
83
  return orchestrator;
80
84
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
+ import { getComplianceSummary } from '../utils/compliance-map.js';
14
15
 
15
16
  // =============================================================================
16
17
  // SCORING CONFIGURATION
@@ -47,6 +48,7 @@ const FALLBACK_CATEGORY_MAP = {
47
48
  'rag': 'llm',
48
49
  'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
49
50
  'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
51
+ 'agent-config': 'llm', // Agent config security → AI/LLM category
50
52
  'recon': null, // skip recon findings
51
53
  };
52
54
 
@@ -136,12 +138,21 @@ export class ScoringEngine {
136
138
  const score = Math.max(0, 100 - totalDeduction);
137
139
  const grade = GRADES.find(g => score >= g.min);
138
140
 
141
+ // ── Compliance mapping ─────────────────────────────────────────────────
142
+ let compliance;
143
+ try {
144
+ compliance = getComplianceSummary(findings);
145
+ } catch {
146
+ compliance = null;
147
+ }
148
+
139
149
  return {
140
150
  score,
141
151
  grade,
142
152
  categories: categoryResults,
143
153
  totalFindings: findings.length,
144
154
  totalDepVulns: depVulns.length,
155
+ compliance,
145
156
  };
146
157
  }
147
158
 
@@ -40,6 +40,11 @@ import { ciCommand } from '../commands/ci.js';
40
40
  import { diffCommand } from '../commands/diff.js';
41
41
  import { vibeCheckCommand } from '../commands/vibe-check.js';
42
42
  import { benchmarkCommand } from '../commands/benchmark.js';
43
+ import { openclawCommand } from '../commands/openclaw.js';
44
+ import { scanSkillCommand } from '../commands/scan-skill.js';
45
+ import { abomCommand } from '../commands/abom.js';
46
+ import { updateIntelCommand } from '../commands/update-intel.js';
47
+ import { ABOMGenerator } from '../agents/abom-generator.js';
43
48
  import { PolicyEngine } from '../agents/policy-engine.js';
44
49
  import { SBOMGenerator } from '../agents/sbom-generator.js';
45
50
 
@@ -110,6 +115,7 @@ program
110
115
  .option('--gitignore', 'Only copy .gitignore')
111
116
  .option('--headers', 'Only copy security headers config')
112
117
  .option('--agents', 'Only add security rules to AI agent instruction files (CLAUDE.md, .cursor/rules/, .windsurfrules, copilot-instructions.md)')
118
+ .option('--openclaw', 'Generate a hardened openclaw.json template')
113
119
  .action(initCommand);
114
120
 
115
121
  // -----------------------------------------------------------------------------
@@ -128,6 +134,7 @@ program
128
134
  .command('guard [action]')
129
135
  .description('Install a git hook to block pushes if secrets are found')
130
136
  .option('--pre-commit', 'Install as pre-commit hook instead of pre-push')
137
+ .option('--generate-hooks', 'Generate defensive Claude Code hooks (.claude/settings.json)')
131
138
  .action(guardCommand);
132
139
 
133
140
  // -----------------------------------------------------------------------------
@@ -192,7 +199,7 @@ program
192
199
  // -----------------------------------------------------------------------------
193
200
  program
194
201
  .command('audit [path]')
195
- .description('Full security audit: secrets + 17 agents + deps + score + deep analysis + remediation plan')
202
+ .description('Full security audit: secrets + 18 agents + deps + score + deep analysis + remediation plan')
196
203
  .option('--json', 'Output results as JSON')
197
204
  .option('--sarif', 'Output results in SARIF format')
198
205
  .option('--csv', 'Output results as CSV')
@@ -230,7 +237,7 @@ program
230
237
  // -----------------------------------------------------------------------------
231
238
  program
232
239
  .command('red-team [path]')
233
- .description('Multi-agent security audit: 17 agents scan for 80+ attack classes')
240
+ .description('Multi-agent security audit: 18 agents scan for 80+ attack classes')
234
241
  .option('--agents <list>', 'Comma-separated list of agents to run')
235
242
  .option('--json', 'Output results as JSON')
236
243
  .option('--sarif', 'Output results in SARIF format')
@@ -252,6 +259,7 @@ program
252
259
  .command('watch [path]')
253
260
  .description('Continuous monitoring: watch files for security issues in real-time')
254
261
  .option('--poll', 'Use polling mode (for network drives)')
262
+ .option('--configs', 'Watch only agent config files (openclaw.json, .cursorrules, mcp.json, etc.)')
255
263
  .action(watchCommand);
256
264
 
257
265
  // -----------------------------------------------------------------------------
@@ -327,6 +335,47 @@ program
327
335
  .option('--json', 'Output results as JSON')
328
336
  .action(benchmarkCommand);
329
337
 
338
+ // -----------------------------------------------------------------------------
339
+ // OPENCLAW COMMAND
340
+ // -----------------------------------------------------------------------------
341
+ program
342
+ .command('openclaw [path]')
343
+ .description('OpenClaw security scan: agent configs, MCP servers, skills, hooks')
344
+ .option('--fix', 'Auto-harden OpenClaw and agent configurations')
345
+ .option('--preflight', 'Exit non-zero on critical findings (for CI)')
346
+ .option('--red-team', 'Simulate adversarial attacks against agent configs')
347
+ .option('--json', 'Output results as JSON')
348
+ .action(openclawCommand);
349
+
350
+ // -----------------------------------------------------------------------------
351
+ // SCAN-SKILL COMMAND
352
+ // -----------------------------------------------------------------------------
353
+ program
354
+ .command('scan-skill [target]')
355
+ .description('Analyze an AI agent skill for security issues before installing it')
356
+ .option('--all', 'Scan all skills defined in openclaw.json')
357
+ .option('--json', 'Output results as JSON')
358
+ .action(scanSkillCommand);
359
+
360
+ // -----------------------------------------------------------------------------
361
+ // ABOM COMMAND
362
+ // -----------------------------------------------------------------------------
363
+ program
364
+ .command('abom [path]')
365
+ .description('Generate Agent Bill of Materials (CycloneDX ABOM) — MCP servers, skills, configs, LLM providers')
366
+ .option('-o, --output <file>', 'Output file path', 'abom.json')
367
+ .option('--json', 'Output to stdout as JSON')
368
+ .action(abomCommand);
369
+
370
+ // -----------------------------------------------------------------------------
371
+ // UPDATE-INTEL COMMAND
372
+ // -----------------------------------------------------------------------------
373
+ program
374
+ .command('update-intel')
375
+ .description('Update threat intelligence feed (malicious skill hashes, compromised MCP servers)')
376
+ .option('--url <url>', 'Custom feed URL')
377
+ .action(updateIntelCommand);
378
+
330
379
  // -----------------------------------------------------------------------------
331
380
  // DOCTOR COMMAND
332
381
  // -----------------------------------------------------------------------------
@@ -344,15 +393,19 @@ if (process.argv.length === 2) {
344
393
  console.log(banner);
345
394
  console.log(chalk.yellow('\nQuick start:\n'));
346
395
  console.log(chalk.cyan.bold(' v6.0 — Full Security Audit'));
347
- console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 17 agents + deps + remediation'));
396
+ console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 18 agents + deps + remediation'));
348
397
  console.log(chalk.white(' npx ship-safe audit . --deep') + chalk.gray('# LLM-powered taint analysis (Anthropic/Ollama)'));
349
- console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 17-agent red team scan (80+ attack classes)'));
398
+ console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 18-agent red team scan (80+ attack classes)'));
350
399
  console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
351
400
  console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
352
401
  console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
353
402
  console.log(chalk.white(' npx ship-safe diff ') + chalk.gray('# Scan only changed files (fast pre-commit)'));
354
403
  console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
404
+ console.log(chalk.white(' npx ship-safe openclaw . ') + chalk.gray('# OpenClaw & agent config security scan'));
405
+ console.log(chalk.white(' npx ship-safe scan-skill <u>') + chalk.gray('# Vet a skill before installing'));
406
+ console.log(chalk.white(' npx ship-safe abom . ') + chalk.gray('# Agent Bill of Materials (CycloneDX)'));
355
407
  console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM (CRA-ready)'));
408
+ console.log(chalk.white(' npx ship-safe update-intel ') + chalk.gray('# Update threat intelligence feed'));
356
409
  console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
357
410
  console.log(chalk.white(' npx ship-safe doctor ') + chalk.gray('# Check environment and configuration'));
358
411
  console.log();
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ABOM Command
3
+ * =============
4
+ *
5
+ * Generate an Agent Bill of Materials in CycloneDX format.
6
+ * Lists all AI agent components: MCP servers, skills, configs, LLM providers.
7
+ *
8
+ * USAGE:
9
+ * ship-safe abom [path] Generate ABOM
10
+ * ship-safe abom . -o agent-bom.json Custom output path
11
+ */
12
+
13
+ import path from 'path';
14
+ import chalk from 'chalk';
15
+ import { ABOMGenerator } from '../agents/abom-generator.js';
16
+ import * as output from '../utils/output.js';
17
+
18
+ export async function abomCommand(targetPath = '.', options = {}) {
19
+ const absolutePath = path.resolve(targetPath);
20
+ const outputFile = options.output || 'abom.json';
21
+
22
+ console.log();
23
+ output.header('Ship Safe — Agent Bill of Materials');
24
+ console.log();
25
+
26
+ const generator = new ABOMGenerator();
27
+ const bom = generator.generate(absolutePath);
28
+
29
+ if (options.json) {
30
+ console.log(JSON.stringify(bom, null, 2));
31
+ return;
32
+ }
33
+
34
+ generator.generateToFile(absolutePath, outputFile);
35
+
36
+ const agentComponents = bom.components.filter(c => c.properties?.some(p => p.name?.startsWith('agent:')));
37
+ const mcpServers = agentComponents.filter(c => c.properties?.some(p => p.value === 'mcp-server'));
38
+ const skills = agentComponents.filter(c => c.properties?.some(p => p.value === 'openclaw-skill'));
39
+ const configs = agentComponents.filter(c => c.properties?.some(p => p.value === 'agent-rules' || p.value === 'agent-config'));
40
+ const providers = agentComponents.filter(c => c.properties?.some(p => p.value === 'llm-provider'));
41
+
42
+ console.log(chalk.gray(` Project: ${bom.metadata.component.name}`));
43
+ console.log();
44
+ console.log(` ${chalk.cyan('MCP Servers')}: ${mcpServers.length}`);
45
+ console.log(` ${chalk.cyan('OpenClaw Skills')}: ${skills.length}`);
46
+ console.log(` ${chalk.cyan('Agent Configs')}: ${configs.length}`);
47
+ console.log(` ${chalk.cyan('LLM Providers')}: ${providers.length}`);
48
+ console.log(` ${chalk.cyan('Total Components')}: ${bom.components.length}`);
49
+ console.log();
50
+
51
+ if (mcpServers.length > 0) {
52
+ console.log(chalk.white.bold(' MCP Servers:'));
53
+ for (const s of mcpServers) {
54
+ const cmd = s.properties?.find(p => p.name === 'agent:command')?.value || 'N/A';
55
+ console.log(chalk.gray(` · ${s.name} (${cmd})`));
56
+ }
57
+ console.log();
58
+ }
59
+
60
+ if (skills.length > 0) {
61
+ console.log(chalk.white.bold(' OpenClaw Skills:'));
62
+ for (const s of skills) {
63
+ const verified = s.properties?.find(p => p.name === 'agent:verified')?.value;
64
+ const icon = verified === 'true' ? chalk.green('✔') : chalk.yellow('?');
65
+ console.log(chalk.gray(` ${icon} ${s.name}`));
66
+ }
67
+ console.log();
68
+ }
69
+
70
+ console.log(chalk.green(` ✔ ABOM saved to ${outputFile}`));
71
+ console.log(chalk.gray(` Format: CycloneDX ${bom.specVersion}`));
72
+ console.log();
73
+ }
@@ -391,6 +391,7 @@ export async function auditCommand(targetPath = '.', options = {}) {
391
391
  reporter.generateFullReport(scoreResult, filteredFindings, depVulns, recon, remediationPlan, absolutePath, htmlPath);
392
392
  console.log();
393
393
  console.log(chalk.cyan(` Full report: ${chalk.white.bold(htmlPath)}`));
394
+ console.log(chalk.gray(` Dashboard: `) + chalk.cyan('https://shipsafecli.com/app'));
394
395
 
395
396
  // PDF export
396
397
  if (options.pdf) {
@@ -662,6 +663,7 @@ function outputJSON(scoreResult, findings, depVulns, recon, agentResults, remedi
662
663
  recon,
663
664
  agents: agentResults,
664
665
  };
666
+ if (scoreResult.compliance) output.compliance = scoreResult.compliance;
665
667
  if (suppressions) output.suppressions = suppressions;
666
668
  if (history && history.length >= 2) {
667
669
  const prev = history[history.length - 2];
@@ -311,7 +311,7 @@ function postPRComment(scoreResult, findings, depVulns, rootPath, duration) {
311
311
  body += '> No security issues found — looking good! 🎉\n\n';
312
312
  }
313
313
 
314
- body += `<sub>Generated by <a href="https://shipsafecli.com">Ship Safe</a></sub>`;
314
+ body += `\n---\n<sub>Generated by <a href="https://shipsafecli.com">Ship Safe</a> · <a href="https://shipsafecli.com/app">View full report in dashboard</a></sub>`;
315
315
 
316
316
  // Post comment via gh CLI
317
317
  execFileSync('gh', ['pr', 'comment', prNumber, '--body', body], { // ship-safe-ignore — execFileSync, not MCP
@@ -136,6 +136,10 @@ export async function guardCommand(action, options = {}) {
136
136
  process.exit(1);
137
137
  }
138
138
 
139
+ if (options.generateHooks) {
140
+ return generateClaudeHooks(cwd);
141
+ }
142
+
139
143
  if (action === 'remove') {
140
144
  return removeHooks(gitDir, cwd);
141
145
  }
@@ -278,6 +282,101 @@ function removeHooks(gitDir, cwd) {
278
282
  }
279
283
  }
280
284
 
285
+ // =============================================================================
286
+ // CLAUDE CODE DEFENSIVE HOOKS
287
+ // =============================================================================
288
+
289
+ function generateClaudeHooks(cwd) {
290
+ output.header('Generating Defensive Claude Code Hooks');
291
+
292
+ const claudeDir = path.join(cwd, '.claude');
293
+ const settingsPath = path.join(claudeDir, 'settings.json');
294
+
295
+ // Defensive hooks that block common attack patterns
296
+ const preToolCmd = [
297
+ 'node -e "',
298
+ 'const c=process.argv[1]||String();',
299
+ 'const bad=[/curl.*[|].*(?:bash|sh|node|python)/i,/wget.*[|].*(?:bash|sh)/i,',
300
+ '/rm\\s+-rf\\s+\\//,/webhook[.]site|requestbin|ngrok[.]io|pipedream/i,',
301
+ '/base64.*-d.*[|].*(?:bash|sh)/i];',
302
+ 'const m=bad.find(r=>r.test(c));',
303
+ 'if(m){console.error(String.fromCharCode(10060)+String.fromCharCode(32)+c.slice(0,80));process.exit(1)}',
304
+ '" "$INPUT"',
305
+ ].join('');
306
+
307
+ const postToolCmd = [
308
+ 'node -e "',
309
+ 'const f=process.argv[1]||String();',
310
+ 'if(/[.]env$|[.]env[.]/.test(f)){console.log(String.fromCharCode(9888)+String.fromCharCode(32)+f)}',
311
+ '" "$INPUT"',
312
+ ].join('');
313
+
314
+ const defensiveHooks = {
315
+ hooks: {
316
+ PreToolUse: [
317
+ {
318
+ matcher: 'Bash',
319
+ command: preToolCmd,
320
+ description: 'Ship Safe: Block dangerous command patterns (curl|bash, rm -rf /, exfil domains)',
321
+ },
322
+ ],
323
+ PostToolUse: [
324
+ {
325
+ matcher: 'Write',
326
+ command: postToolCmd,
327
+ description: 'Ship Safe: Alert when .env files are modified',
328
+ },
329
+ ],
330
+ },
331
+ };
332
+
333
+ // Merge with existing settings
334
+ let existing = {};
335
+ if (fs.existsSync(settingsPath)) {
336
+ try {
337
+ existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
338
+ } catch { /* fresh start */ }
339
+ }
340
+
341
+ // Merge hooks — don't overwrite existing hooks, append
342
+ if (!existing.hooks) existing.hooks = {};
343
+ if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
344
+ if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
345
+
346
+ // Check if ship-safe hooks already present
347
+ const hasPreHook = existing.hooks.PreToolUse.some(h => h.description?.includes('Ship Safe'));
348
+ const hasPostHook = existing.hooks.PostToolUse.some(h => h.description?.includes('Ship Safe'));
349
+
350
+ if (hasPreHook && hasPostHook) {
351
+ output.warning('Ship Safe hooks already installed in .claude/settings.json');
352
+ return;
353
+ }
354
+
355
+ if (!hasPreHook) {
356
+ existing.hooks.PreToolUse.push(...defensiveHooks.hooks.PreToolUse);
357
+ }
358
+ if (!hasPostHook) {
359
+ existing.hooks.PostToolUse.push(...defensiveHooks.hooks.PostToolUse);
360
+ }
361
+
362
+ // Write
363
+ if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
364
+ fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
365
+
366
+ output.success('Defensive hooks installed in .claude/settings.json');
367
+ console.log();
368
+ console.log(chalk.gray(' Hooks installed:'));
369
+ console.log(chalk.gray(' PreToolUse → Block curl|bash, rm -rf /, exfil domains'));
370
+ console.log(chalk.gray(' PostToolUse → Alert on .env file modifications'));
371
+ console.log();
372
+ console.log(chalk.gray(' These hooks protect against:'));
373
+ console.log(chalk.gray(' • Remote code execution via piped downloads'));
374
+ console.log(chalk.gray(' • Data exfiltration to webhook.site/ngrok/requestbin'));
375
+ console.log(chalk.gray(' • Destructive filesystem operations'));
376
+ console.log(chalk.gray(' • Unauthorized .env modifications'));
377
+ console.log();
378
+ }
379
+
281
380
  // =============================================================================
282
381
  // UTILITIES
283
382
  // =============================================================================
@@ -50,6 +50,11 @@ export async function initCommand(options = {}) {
50
50
  // Determine which files to copy.
51
51
  // If a specific flag is set, only run that category.
52
52
  // With no flags, run everything.
53
+ // Handle --openclaw flag separately
54
+ if (options.openclaw) {
55
+ return handleOpenClawInit(targetDir, options.force, results);
56
+ }
57
+
53
58
  const hasSpecificFlag = options.gitignore || options.headers || options.agents;
54
59
  const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
55
60
  const copyHeaders = hasSpecificFlag ? !!options.headers : true;
@@ -296,6 +301,59 @@ async function handleAgentFiles(targetDir, force, results) {
296
301
  }
297
302
  }
298
303
 
304
+ // =============================================================================
305
+ // OPENCLAW HARDENED CONFIG
306
+ // =============================================================================
307
+
308
+ const HARDENED_OPENCLAW = `{
309
+ "// SECURITY": "Generated by ship-safe init --openclaw — hardened defaults",
310
+
311
+ "// host": "Bind to localhost only — never 0.0.0.0 (CVE-2026-25253 ClawJacked)",
312
+ "host": "127.0.0.1",
313
+ "port": 3100,
314
+
315
+ "// auth": "Always require authentication — prevents unauthorized agent takeover",
316
+ "auth": {
317
+ "type": "apiKey",
318
+ "key": "\${OPENCLAW_API_KEY}"
319
+ },
320
+
321
+ "// url": "Use wss:// for all non-localhost connections (encrypted WebSocket)",
322
+ "url": "wss://localhost:3100",
323
+
324
+ "// safeBins": "Allowlist of binaries the agent can execute — block everything else",
325
+ "safeBins": ["node", "git", "npx", "npm"],
326
+
327
+ "// skills": "Only add verified skills from trusted sources — ClawHavoc had 1,184 malicious skills",
328
+ "skills": [],
329
+
330
+ "// logging": "Enable audit logging for security monitoring",
331
+ "logging": {
332
+ "level": "info",
333
+ "auditLog": true
334
+ }
335
+ }
336
+ `;
337
+
338
+ async function handleOpenClawInit(targetDir, force, results) {
339
+ const targetPath = path.join(targetDir, 'openclaw.json');
340
+
341
+ if (fs.existsSync(targetPath) && !force) {
342
+ results.skipped.push('openclaw.json (already exists, use -f to overwrite)');
343
+ } else {
344
+ fs.writeFileSync(targetPath, HARDENED_OPENCLAW.trim() + '\n');
345
+ results.copied.push('openclaw.json (hardened template)');
346
+ }
347
+
348
+ printSummary(results);
349
+
350
+ console.log(chalk.cyan('Important:'));
351
+ console.log(chalk.white(' 1.') + ' Set the OPENCLAW_API_KEY environment variable');
352
+ console.log(chalk.white(' 2.') + ' Only add verified skills from trusted sources');
353
+ console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe openclaw .') + ' to verify security');
354
+ console.log();
355
+ }
356
+
299
357
  // =============================================================================
300
358
  // SUMMARY
301
359
  // =============================================================================