clawhub-guard 1.0.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/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # 🛡️ clawhub-guard
2
+
3
+ **Pre-install security scanner for ClawHub skills.**
4
+ Scan before you install. Never trust blindly.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/clawhub-guard)](https://www.npmjs.com/package/clawhub-guard)
7
+ [![license](https://img.shields.io/npm/l/clawhub-guard)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## Why?
12
+
13
+ ClawHub hosts thousands of community skills — but not all are safe. In early 2026, the ClawHavoc campaign distributed 341 malicious skills through the registry, stealing credentials and installing malware.
14
+
15
+ **clawhub-guard** adds a mandatory security checkpoint before every install.
16
+
17
+ ## What It Does
18
+
19
+ ```
20
+ $ clawhub-guard install summarize
21
+
22
+ 📦 Installing summarize for pre-scan...
23
+ ✓ Installed summarize@2.1.0
24
+
25
+ 🔍 Running security scan on summarize...
26
+
27
+ ═══════════════════════════════════════════════════════
28
+ SECURITY SCAN REPORT — clawhub-guard
29
+ ═══════════════════════════════════════════════════════
30
+ Target: ~/.openclaw/workspace/skills/summarize
31
+ Score: 94/100
32
+ Threshold: 70/100
33
+ Verdict: ✅ PASS
34
+ Engines: 1/4 available
35
+ Findings: 2
36
+ ───────────────────────────────────────────────────────
37
+ ⚪ low: 2
38
+ ───────────────────────────────────────────────────────
39
+ • [low] tool-shadowing: Redirects from another tool
40
+ • [low] excessive-perms: Too many permissions declared
41
+ ═══════════════════════════════════════════════════════
42
+
43
+ ✅ Safe! summarize is ready to use.
44
+ ```
45
+
46
+ If the score is too low:
47
+
48
+ ```
49
+ ❌ BLOCKED — Uninstalling malicious-skill for safety.
50
+ 💡 Tip: Review the findings above. Use --force to install anyway.
51
+ ```
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ npm install -g clawhub-guard
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ```bash
62
+ # Install a ClawHub skill with automatic pre-scan
63
+ clawhub-guard install <skill-name>
64
+
65
+ # Scan an already-installed skill
66
+ clawhub-guard scan <skill-name>
67
+
68
+ # Scan a local skill directory
69
+ clawhub-guard scan --local ./my-skill/
70
+
71
+ # Custom risk threshold (default: 70)
72
+ clawhub-guard install <skill-name> --threshold 80
73
+
74
+ # Force install (skip security scan)
75
+ clawhub-guard install <skill-name> --force
76
+
77
+ # JSON output for automation
78
+ clawhub-guard scan <skill-name> --json
79
+ ```
80
+
81
+ ## Scoring
82
+
83
+ | Score | Verdict | Action |
84
+ |-------|---------|--------|
85
+ | 90–100 | ✅ PASS | Safe to install |
86
+ | 70–89 | ⚠️ WARN | Installed with warnings — review findings |
87
+ | 40–69 | ❌ BLOCK | Automatically uninstalled — review report |
88
+ | 0–39 | ❌ BLOCK | Automatically uninstalled — do not use |
89
+
90
+ ## Powered By
91
+
92
+ - **[AgentShield](https://www.npmjs.com/package/@elliotllliu/agent-shield)** — 30 security rules covering credential theft, backdoors, prompt injection, obfuscation, and more.
93
+
94
+ ## Recommended Workflow
95
+
96
+ ```bash
97
+ # 1. Always use clawhub-guard instead of raw openclaw skills install
98
+ clawhub-guard install <skill-name>
99
+
100
+ # 2. If blocked, review the scan report
101
+ clawhub-guard scan <skill-name> --json | less
102
+
103
+ # 3. Only force-install if you've manually reviewed the code
104
+ clawhub-guard install <skill-name> --force
105
+ ```
106
+
107
+ ## Security
108
+
109
+ This tool is itself a security product. It:
110
+ - Runs **locally only** — no telemetry, no external calls beyond agent-shield
111
+ - Does **not** modify your OpenClaw config
112
+ - Does **not** read your credentials or session data
113
+ - Automatically **uninstalls** skills that fail the security threshold
114
+
115
+ ## License
116
+
117
+ MIT © [taiwanape](https://github.com/taiwanape)
118
+
119
+ ---
120
+
121
+ > "Trust is earned in milliseconds, lost in microseconds, and clawed back never." — clawhub-guard
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // clawhub-guard — Pre-install security scanner for ClawHub skills
3
+ require('../src/cli.js');
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "clawhub-guard",
3
+ "version": "1.0.0",
4
+ "description": "Pre-install security scanner for ClawHub skills — scan before you install, never trust blindly.",
5
+ "main": "src/cli.js",
6
+ "bin": {
7
+ "clawhub-guard": "./bin/clawhub-guard.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node src/cli.js scan --help"
11
+ },
12
+ "keywords": [
13
+ "clawhub",
14
+ "openclaw",
15
+ "security",
16
+ "skill",
17
+ "scanner",
18
+ "vetter",
19
+ "agent-shield"
20
+ ],
21
+ "author": "taiwanape",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/taiwanape/clawhub-guard.git"
26
+ },
27
+ "dependencies": {
28
+ "@elliotllliu/agent-shield": "^0.16.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
33
+ }
package/src/cli.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // src/cli.js — Main CLI for clawhub-guard
3
+
4
+ const { scanLocal, getSkillInstallPath, isSkillInstalled } = require('./scan.js');
5
+ const { installSkill } = require('./install.js');
6
+
7
+ const args = process.argv.slice(2);
8
+ const command = args[0];
9
+
10
+ function usage() {
11
+ console.log(`
12
+ 🛡️ clawhub-guard — Pre-install security scanner for ClawHub skills
13
+
14
+ Usage:
15
+ clawhub-guard install <skill-name> Scan + install a ClawHub skill
16
+ clawhub-guard scan <skill-name> Scan an installed ClawHub skill
17
+ clawhub-guard scan --local <path> Scan a local skill directory
18
+ clawhub-guard --help Show this help
19
+
20
+ Options:
21
+ --threshold <0-100> Minimum score to pass (default: 70)
22
+ --force Skip security scan and install anyway
23
+ --json Output scan result as JSON
24
+
25
+ Examples:
26
+ clawhub-guard install summarize
27
+ clawhub-guard scan --local ./my-skill/
28
+ clawhub-guard install database-query --threshold 80
29
+ clawhub-guard scan skill-vetter --json
30
+ `);
31
+ }
32
+
33
+ if (!command || command === '--help' || command === '-h') {
34
+ usage();
35
+ process.exit(0);
36
+ }
37
+
38
+ // Parse options
39
+ const options = {
40
+ threshold: 70,
41
+ force: false,
42
+ json: false,
43
+ };
44
+ for (let i = 0; i < args.length; i++) {
45
+ if (args[i] === '--threshold' && args[i + 1]) {
46
+ options.threshold = parseInt(args[i + 1], 10);
47
+ i++;
48
+ }
49
+ if (args[i] === '--force') options.force = true;
50
+ if (args[i] === '--json') options.json = true;
51
+ if (args[i] === '--local') options.local = true;
52
+ }
53
+
54
+ switch (command) {
55
+ case 'install': {
56
+ const skillName = args[1];
57
+ if (!skillName) {
58
+ console.error('❌ Error: skill name required.\nUsage: clawhub-guard install <skill-name>');
59
+ process.exit(1);
60
+ }
61
+ const result = installSkill(skillName, options);
62
+ process.exit(result.success ? 0 : 1);
63
+ }
64
+
65
+ case 'scan': {
66
+ let target = args[1];
67
+ if (!target && !options.local) {
68
+ console.error('❌ Error: skill name or --local <path> required.');
69
+ process.exit(1);
70
+ }
71
+
72
+ // --local mode: scan a directory
73
+ if (options.local) {
74
+ target = args.find((a, i) => args[i - 1] === '--local') || args[2];
75
+ if (!target) {
76
+ // If --local was passed as the target itself (e.g., scan --local ./path)
77
+ target = args[1];
78
+ }
79
+ }
80
+
81
+ const fs = require('node:fs');
82
+ const path = require('node:path');
83
+ const os = require('node:os');
84
+
85
+ let scanTarget;
86
+ if (options.local || target.includes('/') || target.includes('\\')) {
87
+ scanTarget = target;
88
+ } else {
89
+ scanTarget = getSkillInstallPath(target);
90
+ if (!isSkillInstalled(target)) {
91
+ // Try fuzzy match
92
+ const workspace = process.env.OPENCLAW_WORKSPACE_DIR ||
93
+ path.join(os.homedir(), '.openclaw', 'workspace');
94
+ const skillsDir = path.join(workspace, 'skills');
95
+ let installedList = [];
96
+ if (fs.existsSync(skillsDir)) {
97
+ installedList = fs.readdirSync(skillsDir, { withFileTypes: true })
98
+ .filter(d => d.isDirectory())
99
+ .map(d => d.name);
100
+ // Normalize names (strip hyphens, underscores) for fuzzy matching
101
+ const normalize = s => s.toLowerCase().replace(/[-_]/g, '');
102
+ const normTarget = normalize(target);
103
+ const match = installedList.find(e =>
104
+ normalize(e).includes(normTarget) || normTarget.includes(normalize(e))
105
+ );
106
+ if (match) {
107
+ scanTarget = getSkillInstallPath(match);
108
+ }
109
+ }
110
+ if (!scanTarget) {
111
+ console.error(`❌ Skill '${target}' not installed.`);
112
+ console.error(` Installed skills: ${installedList.join(', ') || 'none'}`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+ }
117
+
118
+ const result = scanLocal(scanTarget, options);
119
+ if (options.json) {
120
+ console.log(JSON.stringify(result, null, 2));
121
+ } else {
122
+ const { printScanReport } = require('./install.js');
123
+ printScanReport(result);
124
+ }
125
+ process.exit(result.passed ? 0 : 1);
126
+ }
127
+
128
+ default:
129
+ console.error(`❌ Unknown command: ${command}`);
130
+ usage();
131
+ process.exit(1);
132
+ }
package/src/install.js ADDED
@@ -0,0 +1,133 @@
1
+ // src/install.js — Install logic with pre-scan guard
2
+
3
+ const { execSync } = require('node:child_process');
4
+ const { scanLocal, isSkillInstalled } = require('./scan.js');
5
+
6
+ /**
7
+ * Install a ClawHub skill with pre-scan security check
8
+ * @param {string} skillName - Name of the ClawHub skill
9
+ * @param {object} options
10
+ * @param {number} options.threshold - Minimum security score (default 70)
11
+ * @param {boolean} options.force - Skip scan and install anyway
12
+ * @returns {object} Install result
13
+ */
14
+ function installSkill(skillName, options = {}) {
15
+ const threshold = options.threshold ?? 70;
16
+
17
+ // Step 0: Force mode — skip scan
18
+ if (options.force) {
19
+ return doInstall(skillName);
20
+ }
21
+
22
+ // Step 1: Can't pre-scan before download, so install first →
23
+ // scan the installed copy, then warn
24
+ console.log(`\n📦 Installing ${skillName} for pre-scan...\n`);
25
+
26
+ let installResult = doInstall(skillName);
27
+ if (!installResult.success) {
28
+ return installResult;
29
+ }
30
+
31
+ // Step 2: Scan the just-installed skill
32
+ console.log(`\n🔍 Running security scan on ${skillName}...\n`);
33
+
34
+ const scanResult = scanLocal(installResult.installPath, { threshold });
35
+
36
+ // Step 3: Print report
37
+ printScanReport(scanResult);
38
+
39
+ // Step 4: If unsafe, offer to uninstall
40
+ if (!scanResult.passed) {
41
+ console.log(`\n⚠️ RISK THRESHOLD NOT MET (${scanResult.score}/100, need ${threshold})`);
42
+ console.log(` Verdict: ${scanResult.verdict}`);
43
+
44
+ if (scanResult.verdict === 'BLOCK') {
45
+ console.log(`\n❌ BLOCKED — Uninstalling ${skillName} for safety.\n`);
46
+ try {
47
+ execSync(`openclaw.cmd skills uninstall ${skillName}`, {
48
+ encoding: 'utf-8',
49
+ stdio: 'pipe'
50
+ });
51
+ } catch {
52
+ // Manual cleanup
53
+ const fs = require('node:fs');
54
+ const path = require('node:path');
55
+ const skillPath = installResult.installPath;
56
+ if (fs.existsSync(skillPath)) {
57
+ fs.rmSync(skillPath, { recursive: true, force: true });
58
+ }
59
+ }
60
+ console.log(`💡 Tip: Review the findings above. Use --force to install anyway.\n`);
61
+ return { success: false, blocked: true, scanResult, installResult };
62
+ }
63
+
64
+ if (scanResult.verdict === 'WARN') {
65
+ console.log(`⚠️ Installed with warnings — review the findings above.\n`);
66
+ }
67
+ }
68
+
69
+ return { success: true, blocked: false, scanResult, installResult };
70
+ }
71
+
72
+ function doInstall(skillName) {
73
+ const path = require('node:path');
74
+ const fs = require('node:fs');
75
+ const os = require('node:os');
76
+
77
+ try {
78
+ const output = execSync(`openclaw.cmd skills install ${skillName}`, {
79
+ encoding: 'utf-8',
80
+ timeout: 60000,
81
+ stdio: ['pipe', 'pipe', 'pipe'],
82
+ });
83
+ console.log(output);
84
+
85
+ // Determine install path
86
+ const workspace = process.env.OPENCLAW_WORKSPACE_DIR ||
87
+ path.join(os.homedir(), '.openclaw', 'workspace');
88
+ const installPath = path.join(workspace, 'skills', skillName);
89
+
90
+ return { success: true, installPath, output };
91
+ } catch (err) {
92
+ const errMsg = err.stderr || err.stdout || err.message || 'Unknown error';
93
+ console.error(`❌ Install failed: ${errMsg}`);
94
+ return { success: false, error: errMsg };
95
+ }
96
+ }
97
+
98
+ function printScanReport(result) {
99
+ console.log(`\n${'═'.repeat(55)}`);
100
+ console.log(` SECURITY SCAN REPORT — clawhub-guard`);
101
+ console.log(`${'═'.repeat(55)}`);
102
+ console.log(` Target: ${result.target || 'N/A'}`);
103
+ console.log(` Score: ${result.score}/100`);
104
+ console.log(` Threshold: ${result.threshold}/100`);
105
+ console.log(` Verdict: ${result.passed ? '✅ PASS' : result.verdict === 'BLOCK' ? '❌ BLOCK' : '⚠️ WARN'}`);
106
+ console.log(` Engines: ${result.availableEngines}/${result.totalEngines} available`);
107
+ console.log(` Findings: ${(result.findings || []).length}`);
108
+ console.log(`${'─'.repeat(55)}`);
109
+
110
+ if (result.findings && result.findings.length > 0) {
111
+ const bySeverity = {};
112
+ for (const f of result.findings) {
113
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
114
+ }
115
+ for (const [sev, count] of Object.entries(bySeverity)) {
116
+ const icon = sev === 'critical' ? '🔴' : sev === 'high' ? '🟠' : sev === 'medium' ? '🟡' : '⚪';
117
+ console.log(` ${icon} ${sev}: ${count}`);
118
+ }
119
+ console.log(`${'─'.repeat(55)}`);
120
+ for (const f of (result.findings || []).slice(0, 5)) {
121
+ console.log(` • [${f.severity}] ${f.rule}: ${f.message}`);
122
+ }
123
+ if (result.findings.length > 5) {
124
+ console.log(` • ... and ${result.findings.length - 5} more`);
125
+ }
126
+ } else {
127
+ console.log(` ✅ No findings — clean!`);
128
+ }
129
+
130
+ console.log(`${'═'.repeat(55)}\n`);
131
+ }
132
+
133
+ module.exports = { installSkill, printScanReport };
package/src/scan.js ADDED
@@ -0,0 +1,104 @@
1
+ // src/scan.js — Security scanning engine using agent-shield
2
+
3
+ const { execSync } = require('node:child_process');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+
8
+ /**
9
+ * Scan a local directory for security issues using agent-shield
10
+ * @param {string} targetPath - Path to the skill directory
11
+ * @param {object} options
12
+ * @param {number} options.threshold - Minimum score to pass (0-100)
13
+ * @returns {object} Scan result with score, findings, and verdict
14
+ */
15
+ function scanLocal(targetPath, options = {}) {
16
+ const threshold = options.threshold ?? 70;
17
+
18
+ if (!fs.existsSync(targetPath)) {
19
+ return {
20
+ success: false,
21
+ error: `Target not found: ${targetPath}`,
22
+ score: 0,
23
+ verdict: 'ERROR',
24
+ };
25
+ }
26
+
27
+ let stdout;
28
+ try {
29
+ stdout = execSync(
30
+ `npx @elliotllliu/agent-shield scan "${targetPath}" --json`,
31
+ {
32
+ encoding: 'utf-8',
33
+ timeout: 60000,
34
+ stdio: ['pipe', 'pipe', 'pipe'],
35
+ env: { ...process.env },
36
+ }
37
+ );
38
+ } catch (err) {
39
+ // agent-shield exits non-zero if findings exist — still parse output
40
+ stdout = err.stdout || err.stderr || '';
41
+ }
42
+
43
+ // Try to extract JSON from agent-shield output (it may have noise)
44
+ let result;
45
+ try {
46
+ const jsonStart = stdout.indexOf('{');
47
+ if (jsonStart >= 0) {
48
+ result = JSON.parse(stdout.slice(jsonStart));
49
+ }
50
+ } catch {
51
+ // Fallback: parse manually
52
+ }
53
+
54
+ if (!result) {
55
+ return {
56
+ success: false,
57
+ error: 'Failed to parse agent-shield output',
58
+ rawOutput: stdout.slice(0, 500),
59
+ score: 0,
60
+ verdict: 'ERROR',
61
+ };
62
+ }
63
+
64
+ // Calculate score based on findings
65
+ const allFindings = result.allFindings || [];
66
+ const severityScores = { critical: 40, high: 25, medium: 10, low: 3 };
67
+ let penalty = 0;
68
+ for (const f of allFindings) {
69
+ penalty += severityScores[f.severity] || 5;
70
+ }
71
+ const score = Math.max(0, 100 - penalty);
72
+
73
+ return {
74
+ success: true,
75
+ score,
76
+ threshold,
77
+ passed: score >= threshold,
78
+ verdict: score >= threshold ? 'PASS' : score < 40 ? 'BLOCK' : 'WARN',
79
+ findings: allFindings,
80
+ engines: result.engines || [],
81
+ totalEngines: result.totalEngines || 0,
82
+ availableEngines: result.availableEngines || 0,
83
+ target: result.target || targetPath,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Determine ClawHub skill install directory
89
+ */
90
+ function getSkillInstallPath(skillName) {
91
+ const workspace = process.env.OPENCLAW_WORKSPACE_DIR ||
92
+ path.join(os.homedir(), '.openclaw', 'workspace');
93
+ return path.join(workspace, 'skills', skillName);
94
+ }
95
+
96
+ /**
97
+ * Check if a skill is already installed locally
98
+ */
99
+ function isSkillInstalled(skillName) {
100
+ const installPath = getSkillInstallPath(skillName);
101
+ return fs.existsSync(installPath);
102
+ }
103
+
104
+ module.exports = { scanLocal, getSkillInstallPath, isSkillInstalled };