clawarmor 2.0.0-alpha.3 โ†’ 2.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 CHANGED
@@ -1,163 +1,67 @@
1
- <div align="center">
1
+ # ClawArmor
2
2
 
3
- # ๐Ÿ›ก ClawArmor
4
-
5
- **The security auditor for OpenClaw agents.**
6
-
7
- Checks your config. Probes your live gateway. Scans your skills.
8
- Runs in 30 seconds. Finds what config-only tools miss. Free forever.
3
+ Security armor for OpenClaw agents โ€” audit, scan, monitor.
9
4
 
10
5
  [![npm version](https://img.shields.io/npm/v/clawarmor?color=3fb950&label=npm&style=flat-square)](https://www.npmjs.com/package/clawarmor)
11
6
  [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
- [![node](https://img.shields.io/badge/node-%3E%3D18-green?style=flat-square)](package.json)
13
-
14
- ```bash
15
- npm install -g clawarmor && clawarmor audit
16
- ```
17
-
18
- </div>
19
-
20
- ---
21
-
22
- ```
23
- โ„น Reads: ~/.openclaw/openclaw.json + file permissions only
24
- Network: registry.npmjs.org (version check) + 127.0.0.1:18789 (live probes)
25
- Sends nothing. Source: github.com/pinzasai/clawarmor
26
-
27
- โ”€โ”€ LIVE GATEWAY PROBES (connecting to 127.0.0.1) โ”€โ”€
28
- โœ“ Gateway running on port 18789
29
- โœ“ Not reachable on network interfaces (probed live)
30
- โœ“ Authentication required (WebSocket probe confirmed)
31
- โœ“ /health endpoint does not leak sensitive data
32
- โœ“ CORS not open to arbitrary origins
33
-
34
- Security Score: 100/100 โ”ƒ Grade: A
35
- โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 100%
36
-
37
- Verdict: Your instance is secure. No issues found.
38
-
39
- โ”€โ”€ PASSED (30 checks) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
40
- โœ“ Gateway bound to loopback only
41
- โœ“ Auth token is strong
42
- โœ“ Agent sandbox mode: "non-main" (sessions isolated)
43
- โœ“ Browser SSRF to private networks blocked
44
- โœ“ All channel allowFrom settings are restricted
45
- ... 25 more
46
- ```
47
-
48
- ---
7
+ [![zero deps](https://img.shields.io/badge/deps-zero-green?style=flat-square)](package.json)
49
8
 
50
- ## Why ClawArmor
9
+ ## What it does
51
10
 
52
- Every other OpenClaw security tool reads your config file and tells you if things look right on paper.
11
+ - Audits your OpenClaw config and live gateway with 30+ checks โ€” scored 0โ€“100
12
+ - Scans every installed skill file for malicious code and prompt injection patterns
13
+ - Guards every install: intercepts `openclaw clawhub install`, pre-scans before activation
53
14
 
54
- **ClawArmor also connects locally to your running gateway and verifies live behavior.**
55
-
56
- Config says `bind: loopback`. Is your gateway *actually* unreachable on LAN? Config says auth is enabled. Does the live WebSocket endpoint *actually* reject unauthenticated connections? A misconfigured nginx in front can make your config lie. Live probes can't be faked.
57
-
58
- > All probes connect from your machine to `127.0.0.1` (and your local network interfaces). Nothing leaves your machine.
59
-
60
- ---
61
-
62
- ## Five commands
15
+ ## Quick start
63
16
 
64
17
  ```bash
65
- clawarmor audit # 30 checks + 5 live gateway probes. Score 0-100. Plain-English verdict.
66
- clawarmor scan # Scan every skill file (.js .sh .py .ts SKILL.md) for malicious code.
67
- clawarmor fix # Auto-apply safe fixes. --dry-run to preview, --apply to execute.
68
- clawarmor verify # Re-run only previously-failed checks. Exit 0 if all fixed (CI-friendly).
69
- clawarmor trend # ASCII chart of your security score over time.
18
+ npm install -g clawarmor
19
+ clawarmor protect --install
20
+ clawarmor audit
70
21
  ```
71
22
 
72
- ---
73
-
74
- ## What it checks
75
-
76
- ### Live gateway probes (behavioral โ€” not just config reads)
23
+ ## Commands
77
24
 
78
- | Probe | What it checks |
25
+ | Command | Description |
79
26
  |---|---|
80
- | Port reachability | TCP-connects to gateway on every non-loopback interface |
81
- | Auth enforcement | WebSocket handshake without token โ€” does server reject it? |
82
- | Health endpoint | GET /health โ€” does response contain config data or secrets? |
83
- | CORS headers | OPTIONS with `Origin: https://evil.example.com` |
84
-
85
- These probes are **read-only and non-destructive**. They observe โ€” they don't modify anything.
86
-
87
- ### Config audit (30 checks)
88
-
89
- Gateway bind ยท auth mode ยท token strength ยท dangerous flags ยท mDNS exposure ยท real-IP fallback ยท trusted proxy config ยท file permissions (`~/.openclaw/`, `openclaw.json`, `agent-accounts.json`, `credentials/`) ยท channel allowFrom policies ยท wildcard detection ยท group policies ยท elevated tools ยท exec sandbox ยท tool restrictions (filesystem scope, apply_patch scope) ยท browser SSRF policy ยท plugin allowlist ยท log redaction ยท version currency ยท webhook security ยท multi-user trust model
90
-
91
- ### Skill supply chain scan
92
-
93
- Scans **all files** in every installed skill โ€” `.js`, `.ts`, `.sh`, `.py`, `.rb` and `SKILL.md`. Not just markdown.
94
-
95
- **Code patterns:** `eval()`, `new Function()`, `child_process`, credential file reads, pipe-to-shell, known exfil domains, large base64 blobs, dynamic `require()`
96
-
97
- **SKILL.md instruction patterns:** credential read instructions, system prompt overrides, exfiltration instructions, deception instructions, hardcoded IP fetches
98
-
99
- > **Honest limitation:** The scanner catches unsophisticated threats and common patterns. Obfuscated code (string concatenation, encoded payloads) can bypass static analysis. Treat a clean scan as a good signal, not a guarantee.
100
-
101
- ---
102
-
103
- ## What it protects against
104
-
105
- | Threat | Covered | Notes |
27
+ | `audit` | Score your OpenClaw config (0โ€“100), live gateway probes, plain-English verdict |
28
+ | `scan` | Scan all installed skill files for malicious code and SKILL.md instructions |
29
+ | `prescan <skill>` | Pre-scan a skill before installing โ€” blocks on CRITICAL findings |
30
+ | `protect --install` | Install guard hook, shell intercept (zsh/bash/fish), and watch daemon |
31
+ | `protect --uninstall` | Remove all ClawArmor protection components |
32
+ | `protect --status` | Show current protection state |
33
+ | `watch` | Monitor config and skill changes in real time |
34
+ | `watch --daemon` | Start the watcher as a background daemon |
35
+ | `harden` | Interactive hardening wizard (--dry-run, --auto) |
36
+ | `status` | One-screen security posture dashboard |
37
+ | `log` | View the audit event log |
38
+ | `digest` | Show weekly security digest |
39
+ | `verify` | Re-run only previously-failed checks (CI-friendly, exit 0 = all fixed) |
40
+ | `trend` | ASCII chart of your security score over time |
41
+ | `compare` | Compare coverage vs openclaw security audit |
42
+ | `fix` | Auto-apply safe fixes (--dry-run to preview, --apply to run) |
43
+
44
+ ## What it catches
45
+
46
+ | Threat | Description | Coverage |
106
47
  |---|---|---|
107
- | T-ACCESS-003: Token/config exposure | โœ… | File permission checks + config hardening |
108
- | T-PERSIST-001: Malicious skill supply chain | โœ… | All skill files scanned, not just SKILL.md |
109
- | T-EXEC-001/002: Prompt injection | โŒ | Runtime policy layer โ€” use [SupraWall](https://suprawall.io) |
110
- | T-EXFIL-001: Data exfiltration | โŒ | Runtime policy layer โ€” use SupraWall |
111
-
112
- ClawArmor hardens your configuration and detects supply chain threats. It does not provide runtime policy enforcement โ€” that's a different layer.
113
-
114
- ---
115
-
116
- ## Auto-fix
117
-
118
- ```bash
119
- clawarmor fix --dry-run # preview what would change
120
- clawarmor fix --apply # apply safe one-liner fixes + gateway restart instructions
121
- ```
122
-
123
- Sandbox isolation is enabled safely: if Docker is installed, `fix --apply` sets `sandbox.mode=non-main` + `workspaceAccess=rw` so your Telegram/group sessions keep workspace access.
124
-
125
- ---
126
-
127
- ## CI integration
128
-
129
- ```bash
130
- # Fail CI if security score drops
131
- clawarmor verify # exit 0 = all previously-failed checks now pass
132
- # exit 1 = still failing
133
- ```
134
-
135
- Score history persists in `~/.clawarmor/history.json`.
136
-
137
- ---
138
-
139
- ## Privacy & security
140
-
141
- - `audit`, `scan`, `fix`, `verify`, `trend` run **entirely locally**
142
- - One optional network call: `registry.npmjs.org` for version check (skippable with `--offline`)
143
- - Every run prints exactly what files it reads and what network calls it makes before executing
144
- - Nothing is sent anywhere
145
-
146
- **Found a vulnerability in ClawArmor itself?** Please email `pinzasrojas@proton.me` before public disclosure.
147
-
148
- ---
149
-
150
- ## Installation
151
-
152
- ```bash
153
- npm install -g clawarmor # requires Node.js 18+
154
- clawarmor audit
155
- ```
156
-
157
- Zero runtime npm dependencies. Node.js built-ins only (`net`, `http`, `os`, `fs`, `crypto`).
158
-
159
- ---
48
+ | Token/config exposure | File permission checks, config hardening | Full |
49
+ | Malicious skill supply chain | All skill files scanned โ€” not just SKILL.md | Full |
50
+ | Credential hygiene | Token age, rotation reminders, access scope | Full |
51
+ | Config drift | Baseline hashing, change detection on every startup | Full |
52
+ | Obfuscation | Base64 blobs, dynamic eval, encoded payloads | Partial |
53
+ | Prompt injection via SKILL.md | Instruction patterns, exfil, deception, system overrides | Full |
54
+ | Live gateway auth | WebSocket probe โ€” does server actually reject unauthenticated connections? | Full |
55
+ | CORS misconfiguration | OPTIONS probe with arbitrary origin | Full |
56
+ | Gateway exposure | TCP-connects to every non-loopback interface | Full |
57
+ | Runtime policy enforcement | Requires a runtime layer (SupraWall) | None |
58
+
59
+ ## Philosophy
60
+
61
+ ClawArmor runs entirely on your machine โ€” no telemetry, no cloud, no accounts.
62
+ It has zero npm runtime dependencies, using only Node.js built-ins.
63
+ Every run prints exactly what files it reads and what network calls it makes before executing anything.
160
64
 
161
65
  ## License
162
66
 
163
- MIT โ€” see [LICENSE](LICENSE)
67
+ MIT
package/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { paint } from './lib/output/colors.js';
5
5
 
6
- const VERSION = '2.0.0-alpha.3';
6
+ const VERSION = '2.0.0';
7
7
  const GATEWAY_PORT_DEFAULT = 18789;
8
8
 
9
9
  function isLocalhost(host) {
package/lib/prescan.js CHANGED
@@ -1,16 +1,17 @@
1
1
  // ClawArmor v2.0 โ€” Pre-scan a skill before installing
2
- // Downloads the npm package to a temp dir, scans it with the full
3
- // ClawArmor scanner, and exits 1 (blocks install) only on CRITICAL findings.
2
+ // Supports both npm packages and ClawHub skills.
3
+ // ClawHub skills are checked locally first; npm is used as fallback.
4
4
 
5
- import { mkdirSync, rmSync, readdirSync } from 'fs';
5
+ import { mkdirSync, rmSync, readdirSync, existsSync } from 'fs';
6
6
  import { join } from 'path';
7
- import { tmpdir } from 'os';
7
+ import { tmpdir, homedir } from 'os';
8
8
  import { execSync } from 'child_process';
9
9
  import { scanFile } from './scanner/file-scanner.js';
10
10
  import { scanSkillMdFiles } from './scanner/skill-md-scanner.js';
11
11
  import { paint, severityColor } from './output/colors.js';
12
12
  import { append } from './audit-log.js';
13
13
 
14
+ const HOME = homedir();
14
15
  const SEP = paint.dim('โ”€'.repeat(52));
15
16
 
16
17
  function getAllFiles(dir, files = []) {
@@ -29,91 +30,83 @@ function cleanupTmp(dir) {
29
30
  try { rmSync(dir, { recursive: true, force: true }); } catch { /* non-fatal */ }
30
31
  }
31
32
 
32
- export async function runPrescan(skillName) {
33
- console.log('');
34
- console.log(` ${paint.bold('ClawArmor Prescan')} โ€” ${paint.cyan(skillName)}`);
35
- console.log(` ${paint.dim('Fetching package from npm registry...')}`);
36
- console.log('');
33
+ // โ”€โ”€ ClawHub skill detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
37
34
 
38
- const tmpDir = join(tmpdir(), `clawarmor-prescan-${Date.now()}`);
39
- mkdirSync(tmpDir, { recursive: true });
35
+ function isNpmScoped(name) {
36
+ // Scoped npm package: @org/pkg
37
+ return name.startsWith('@') && name.includes('/');
38
+ }
40
39
 
41
- // โ”€โ”€ Step 1: Download via npm pack โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
42
- let tarball;
40
+ function looksLikeClawHubSkill(name) {
41
+ // Plain name, no scope, no slash โ€” could be a ClawHub skill; try local first
42
+ return !name.startsWith('@') && !name.includes('/');
43
+ }
44
+
45
+ // Returns the local install path for a ClawHub skill, or null if not found.
46
+ function findLocalClawHubSkill(name) {
47
+ // Path 1: ~/.openclaw/skills/<name>/
48
+ const userSkillsPath = join(HOME, '.openclaw', 'skills', name);
49
+ if (existsSync(userSkillsPath)) return userSkillsPath;
50
+
51
+ // Path 2: openclaw npm module's skills directory
52
+ // Try common global npm locations
53
+ const candidates = [
54
+ join(HOME, '.npm-global', 'lib', 'node_modules', 'openclaw', 'skills', name),
55
+ '/usr/local/lib/node_modules/openclaw/skills/' + name,
56
+ '/usr/lib/node_modules/openclaw/skills/' + name,
57
+ join(HOME, '.nvm', 'versions', 'node'), // nvm โ€” we check dirs below
58
+ ];
59
+
60
+ // Try to resolve openclaw via node resolution from this file's location
43
61
  try {
44
- execSync(`npm pack ${skillName}`, {
45
- cwd: tmpDir,
46
- timeout: 30000,
62
+ const result = execSync('node -e "console.log(require.resolve(\'openclaw/package.json\'))"', {
63
+ encoding: 'utf8',
64
+ timeout: 5000,
47
65
  stdio: ['ignore', 'pipe', 'ignore'],
48
- });
49
- const tarballs = readdirSync(tmpDir).filter(f => f.endsWith('.tgz'));
50
- if (!tarballs.length) throw new Error('npm pack produced no tarball');
51
- tarball = join(tmpDir, tarballs[0]);
52
- } catch {
53
- cleanupTmp(tmpDir);
54
- console.log(` ${paint.dim('โ„น')} Could not fetch skill for scanning`);
55
- console.log(` ${paint.dim('(package not found or network error โ€” install not blocked)')}`);
56
- console.log('');
57
- return 0;
58
- }
66
+ }).trim();
67
+ if (result) {
68
+ // result is like /path/to/node_modules/openclaw/package.json
69
+ const ocDir = result.replace(/[\\/]package\.json$/, '');
70
+ const skillPath = join(ocDir, 'skills', name);
71
+ if (existsSync(skillPath)) return skillPath;
72
+ }
73
+ } catch { /* openclaw may not be installed */ }
59
74
 
60
- // โ”€โ”€ Step 2: Extract tarball โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
61
- const extractDir = join(tmpDir, 'extracted');
62
- mkdirSync(extractDir, { recursive: true });
63
- try {
64
- execSync(`tar -xzf "${tarball}" -C "${extractDir}"`, {
65
- timeout: 15000,
66
- stdio: ['ignore', 'ignore', 'ignore'],
67
- });
68
- } catch {
69
- cleanupTmp(tmpDir);
70
- console.log(` ${paint.dim('โ„น')} Could not extract skill package โ€” install not blocked`);
71
- console.log('');
72
- return 0;
75
+ for (const candidate of candidates) {
76
+ if (existsSync(candidate)) return candidate;
73
77
  }
74
78
 
75
- // โ”€โ”€ Step 3: Collect all files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
76
- const allFiles = getAllFiles(extractDir);
77
- console.log(` ${paint.dim('Scanning')} ${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}...`);
78
- console.log('');
79
+ return null;
80
+ }
81
+
82
+ // โ”€โ”€ Scan a directory of files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
79
83
 
80
- // โ”€โ”€ Step 4: Run ClawArmor scanners โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
81
- // Not a built-in โ€” treat as third-party (isBuiltin = false)
84
+ function scanDirectory(dir) {
85
+ const allFiles = getAllFiles(dir);
82
86
  const codeFindings = allFiles.flatMap(f => scanFile(f, false));
83
87
  const mdResults = scanSkillMdFiles(allFiles, false);
84
88
  const mdFindings = mdResults.flatMap(r => r.findings);
85
- const allFindings = [...codeFindings, ...mdFindings];
89
+ return { allFiles, allFindings: [...codeFindings, ...mdFindings] };
90
+ }
91
+
92
+ // โ”€โ”€ Result printer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
86
93
 
94
+ function printResult(skillName, allFiles, allFindings) {
87
95
  const criticals = allFindings.filter(f => f.severity === 'CRITICAL');
88
96
  const highs = allFindings.filter(f => f.severity === 'HIGH');
89
97
  const mediums = allFindings.filter(f => f.severity === 'MEDIUM');
90
98
  const lows = allFindings.filter(f => f.severity === 'LOW' || f.severity === 'INFO');
99
+ const fileCount = allFiles.length;
91
100
 
92
- // โ”€โ”€ Cleanup (always) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
93
- cleanupTmp(tmpDir);
94
-
95
- // โ”€โ”€ Audit log โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
96
- append({
97
- cmd: 'prescan',
98
- trigger: 'prescan',
99
- score: null,
100
- delta: null,
101
- findings: allFindings.map(f => ({ id: f.patternId || f.id || '?', severity: f.severity })),
102
- blocked: criticals.length > 0,
103
- skill: skillName,
104
- });
105
-
106
- // โ”€โ”€ Output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
101
  if (!allFindings.length) {
108
- console.log(` ${paint.green('โœ“')} ClawArmor prescan: clean โ€” 0 findings`);
102
+ console.log(` ${paint.green('โœ“')} ${paint.bold(skillName)} ${paint.dim('โ€”')} clean ${paint.dim('(' + fileCount + ' file' + (fileCount !== 1 ? 's' : '') + ' scanned, 0 findings)')}`);
109
103
  console.log('');
110
104
  return 0;
111
105
  }
112
106
 
113
- // CRITICAL โ†’ print details, block install (exit 1)
114
107
  if (criticals.length) {
115
- console.log(SEP);
116
- console.log(` ${paint.red('โœ—')} ${paint.bold(`CRITICAL (${criticals.length}) โ€” install blocked`)}`);
108
+ console.log(` ${paint.red('โœ—')} ${paint.bold(skillName)} ${paint.dim('โ€”')} ${paint.red('BLOCKED')} ${paint.dim('(' + criticals.length + ' critical finding' + (criticals.length !== 1 ? 's' : '') + ')')}`);
109
+ console.log('');
117
110
  console.log(SEP);
118
111
  for (const f of criticals) {
119
112
  console.log('');
@@ -138,10 +131,10 @@ export async function runPrescan(skillName) {
138
131
  return 1;
139
132
  }
140
133
 
141
- // HIGH โ†’ warn, allow install
134
+ // HIGH โ†’ warn, allow
142
135
  if (highs.length) {
143
- console.log(SEP);
144
- console.log(` ${paint.yellow('โš ')} ${paint.bold(`HIGH (${highs.length}) โ€” review before using`)}`);
136
+ console.log(` ${paint.yellow('โš ')} ${paint.bold(skillName)} ${paint.dim('โ€”')} ${paint.yellow('review recommended')} ${paint.dim('(' + highs.length + ' high finding' + (highs.length !== 1 ? 's' : '') + ', ' + fileCount + ' files)')}`);
137
+ console.log('');
145
138
  console.log(SEP);
146
139
  for (const f of highs) {
147
140
  console.log('');
@@ -152,9 +145,10 @@ export async function runPrescan(skillName) {
152
145
  }
153
146
  }
154
147
  console.log('');
148
+ } else {
149
+ console.log(` ${paint.yellow('!')} ${paint.bold(skillName)} ${paint.dim('โ€”')} ${paint.dim(fileCount + ' files scanned, ' + (mediums.length + lows.length) + ' low/medium findings')}`);
155
150
  }
156
151
 
157
- // MEDIUM/LOW โ†’ summary line only
158
152
  if (mediums.length || lows.length) {
159
153
  const parts = [];
160
154
  if (mediums.length) parts.push(`${mediums.length} medium`);
@@ -165,3 +159,105 @@ export async function runPrescan(skillName) {
165
159
 
166
160
  return 0;
167
161
  }
162
+
163
+ // โ”€โ”€ Download via npm pack โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
164
+
165
+ async function scanViaNpm(skillName, tmpDir) {
166
+ let tarball;
167
+ try {
168
+ execSync(`npm pack ${skillName}`, {
169
+ cwd: tmpDir,
170
+ timeout: 30000,
171
+ stdio: ['ignore', 'pipe', 'ignore'],
172
+ });
173
+ const tarballs = readdirSync(tmpDir).filter(f => f.endsWith('.tgz'));
174
+ if (!tarballs.length) throw new Error('npm pack produced no tarball');
175
+ tarball = join(tmpDir, tarballs[0]);
176
+ } catch {
177
+ return null; // not found or network error
178
+ }
179
+
180
+ const extractDir = join(tmpDir, 'extracted');
181
+ mkdirSync(extractDir, { recursive: true });
182
+ try {
183
+ execSync(`tar -xzf "${tarball}" -C "${extractDir}"`, {
184
+ timeout: 15000,
185
+ stdio: ['ignore', 'ignore', 'ignore'],
186
+ });
187
+ } catch {
188
+ return null;
189
+ }
190
+
191
+ return extractDir;
192
+ }
193
+
194
+ // โ”€โ”€ Main export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
195
+
196
+ export async function runPrescan(skillName) {
197
+ console.log('');
198
+ console.log(` ${paint.bold('ClawArmor Prescan')} โ€” ${paint.cyan(skillName)}`);
199
+ console.log('');
200
+
201
+ let scanDir = null;
202
+ let usedTmp = null;
203
+ let source = 'npm';
204
+
205
+ // โ”€โ”€ Step 1: Check local ClawHub install (for plain skill names) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
206
+ if (looksLikeClawHubSkill(skillName)) {
207
+ const localPath = findLocalClawHubSkill(skillName);
208
+ if (localPath) {
209
+ console.log(` ${paint.dim('Found locally:')} ${paint.dim(localPath)}`);
210
+ console.log(` ${paint.dim('Scanning local files...')}`);
211
+ console.log('');
212
+ scanDir = localPath;
213
+ source = 'local';
214
+ } else {
215
+ console.log(` ${paint.dim('Not found locally โ€” fetching from npm registry...')}`);
216
+ console.log('');
217
+ }
218
+ } else {
219
+ console.log(` ${paint.dim('Fetching package from npm registry...')}`);
220
+ console.log('');
221
+ }
222
+
223
+ // โ”€โ”€ Step 2: Fallback to npm pack if not local โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
224
+ if (!scanDir) {
225
+ const tmpDir = join(tmpdir(), `clawarmor-prescan-${Date.now()}`);
226
+ mkdirSync(tmpDir, { recursive: true });
227
+ usedTmp = tmpDir;
228
+
229
+ const extractDir = await scanViaNpm(skillName, tmpDir);
230
+ if (!extractDir) {
231
+ cleanupTmp(tmpDir);
232
+ console.log(` ${paint.dim('โ„น')} Could not fetch skill for scanning`);
233
+ console.log(` ${paint.dim('(package not found or network error โ€” install not blocked)')}`);
234
+ console.log('');
235
+ return 0;
236
+ }
237
+ scanDir = extractDir;
238
+ }
239
+
240
+ // โ”€โ”€ Step 3: Scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
241
+ const { allFiles, allFindings } = scanDirectory(scanDir);
242
+ console.log(` ${paint.dim('Scanning')} ${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}...`);
243
+ console.log('');
244
+
245
+ // โ”€โ”€ Cleanup tmp (always, only if we created one) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
246
+ if (usedTmp) cleanupTmp(usedTmp);
247
+
248
+ // โ”€โ”€ Audit log โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
249
+ const criticals = allFindings.filter(f => f.severity === 'CRITICAL');
250
+ append({
251
+ cmd: 'prescan',
252
+ trigger: 'prescan',
253
+ score: null,
254
+ delta: null,
255
+ findings: allFindings.map(f => ({ id: f.patternId || f.id || '?', severity: f.severity })),
256
+ blocked: criticals.length > 0,
257
+ skill: skillName,
258
+ source,
259
+ });
260
+
261
+ // โ”€โ”€ Output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
262
+ return printResult(skillName, allFiles, allFindings);
263
+ }
package/lib/protect.js CHANGED
@@ -18,6 +18,9 @@ const HOOKS_DIR = join(OC_DIR, 'hooks');
18
18
  const GUARD_HOOK_DIR = join(HOOKS_DIR, 'clawarmor-guard');
19
19
  const CLI_PATH = new URL('../cli.js', import.meta.url).pathname;
20
20
 
21
+ const FISH_FUNCTIONS_DIR = join(HOME, '.config', 'fish', 'functions');
22
+ const FISH_FUNCTION_FILE = join(FISH_FUNCTIONS_DIR, 'openclaw.fish');
23
+
21
24
  const SHELL_FUNCTION = `
22
25
  # ClawArmor intercept โ€” added by: clawarmor protect --install
23
26
  # Wraps 'openclaw clawhub install' to scan skills before activation.
@@ -34,6 +37,17 @@ openclaw() {
34
37
  const SHELL_MARKER_START = '# ClawArmor intercept โ€” added by: clawarmor protect --install';
35
38
  const SHELL_MARKER_END = '# End ClawArmor intercept';
36
39
 
40
+ const FISH_FUNCTION = `# ClawArmor intercept โ€” added by: clawarmor protect --install
41
+ function openclaw
42
+ if test (count $argv) -ge 3 -a "$argv[1]" = clawhub -a "$argv[2]" = install
43
+ echo "๐Ÿ›ก ClawArmor: scanning $argv[3] before install..."
44
+ clawarmor prescan $argv[3]; or begin; echo "โŒ Blocked."; return 1; end
45
+ end
46
+ command openclaw $argv
47
+ end
48
+ `;
49
+ const FISH_MARKER = '# ClawArmor intercept โ€” added by: clawarmor protect --install';
50
+
37
51
  const HOOK_MD = `---
38
52
  name: clawarmor-guard
39
53
  description: Runs a silent security audit on gateway startup and alerts on regressions
@@ -148,6 +162,39 @@ export default async function handler(event) {
148
162
  }
149
163
  `;
150
164
 
165
+ // โ”€โ”€ Fish shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
166
+
167
+ function fishConfigExists() {
168
+ return existsSync(join(HOME, '.config', 'fish', 'config.fish'));
169
+ }
170
+
171
+ function fishFunctionPresent() {
172
+ if (!existsSync(FISH_FUNCTION_FILE)) return false;
173
+ try {
174
+ return readFileSync(FISH_FUNCTION_FILE, 'utf8').includes(FISH_MARKER);
175
+ } catch { return false; }
176
+ }
177
+
178
+ function injectFishFunction() {
179
+ if (!fishConfigExists()) return false;
180
+ if (fishFunctionPresent()) return true;
181
+ try {
182
+ mkdirSync(FISH_FUNCTIONS_DIR, { recursive: true });
183
+ writeFileSync(FISH_FUNCTION_FILE, FISH_FUNCTION, 'utf8');
184
+ return true;
185
+ } catch { return false; }
186
+ }
187
+
188
+ function removeFishFunction() {
189
+ if (!existsSync(FISH_FUNCTION_FILE)) return false;
190
+ try {
191
+ const content = readFileSync(FISH_FUNCTION_FILE, 'utf8');
192
+ if (!content.includes(FISH_MARKER)) return false;
193
+ rmSync(FISH_FUNCTION_FILE, { force: true });
194
+ return true;
195
+ } catch { return false; }
196
+ }
197
+
151
198
  // โ”€โ”€ Install โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
152
199
 
153
200
  function writeHookFiles() {
@@ -251,10 +298,14 @@ export async function runProtect(flags = {}) {
251
298
  shellPath = shellPath ? shellPath + ', ~/.bashrc' : '~/.bashrc';
252
299
  shellInstalled = true;
253
300
  }
301
+ if (injectFishFunction()) {
302
+ shellPath = shellPath ? shellPath + ', ~/.config/fish' : '~/.config/fish';
303
+ shellInstalled = true;
304
+ }
254
305
  if (shellInstalled) {
255
306
  console.log(` โœ“ Shell intercept added (${shellPath})`);
256
307
  } else {
257
- console.log(` ! No ~/.zshrc or ~/.bashrc found โ€” shell intercept skipped`);
308
+ console.log(` ! No ~/.zshrc, ~/.bashrc, or fish config found โ€” shell intercept skipped`);
258
309
  }
259
310
 
260
311
  // 4. Weekly digest cron
@@ -296,6 +347,10 @@ export async function runProtect(flags = {}) {
296
347
  console.log(` โœ“ Shell intercept removed from ~/.bashrc`);
297
348
  shellRemoved = true;
298
349
  }
350
+ if (removeFishFunction()) {
351
+ console.log(` โœ“ Shell intercept removed from ~/.config/fish/functions/openclaw.fish`);
352
+ shellRemoved = true;
353
+ }
299
354
  if (!shellRemoved) {
300
355
  console.log(` - No shell intercept found to remove`);
301
356
  }
@@ -322,14 +377,15 @@ export async function runProtect(flags = {}) {
322
377
  // Shell function
323
378
  const inZsh = shellFunctionPresent(zshrc);
324
379
  const inBash = shellFunctionPresent(bashrc);
325
- if (inZsh || inBash) {
326
- const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc'].filter(Boolean).join(', ');
380
+ const inFish = fishFunctionPresent();
381
+ if (inZsh || inBash || inFish) {
382
+ const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc', inFish && '~/.config/fish'].filter(Boolean).join(', ');
327
383
  console.log(` Shell intercept โœ“ active (${where})`);
328
384
  } else {
329
385
  console.log(` Shell intercept โœ— not installed`);
330
386
  }
331
387
 
332
- const allActive = hookOk && daemon.running && (inZsh || inBash);
388
+ const allActive = hookOk && daemon.running && (inZsh || inBash || inFish);
333
389
  console.log('');
334
390
  if (allActive) {
335
391
  console.log(` Full protection active.`);
package/lib/status.js CHANGED
@@ -16,9 +16,11 @@ const HISTORY_FILE = join(CLAWARMOR_DIR, 'history.json');
16
16
  const AUDIT_LOG = join(CLAWARMOR_DIR, 'audit.log');
17
17
  const ZSHRC = join(HOME, '.zshrc');
18
18
  const BASHRC = join(HOME, '.bashrc');
19
+ const FISH_FUNCTION_FILE = join(HOME, '.config', 'fish', 'functions', 'openclaw.fish');
19
20
  const SHELL_MARKER = '# ClawArmor intercept โ€” added by: clawarmor protect --install';
20
21
  const CRON_JOBS_FILE = join(OC_DIR, 'cron', 'jobs.json');
21
- const VERSION = '2.0.0-alpha.3';
22
+ const HOOKS_DIR = join(OC_DIR, 'hooks', 'clawarmor-guard');
23
+ const VERSION = '2.0.0';
22
24
 
23
25
  const SEP = paint.dim('โ”€'.repeat(52));
24
26
 
@@ -54,13 +56,19 @@ function trendArrow(delta) {
54
56
  function intercept() {
55
57
  const inZsh = existsSync(ZSHRC) && readFileSync(ZSHRC, 'utf8').includes(SHELL_MARKER);
56
58
  const inBash = existsSync(BASHRC) && readFileSync(BASHRC, 'utf8').includes(SHELL_MARKER);
57
- if (inZsh || inBash) {
58
- const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc'].filter(Boolean).join(', ');
59
+ const inFish = existsSync(FISH_FUNCTION_FILE) && readFileSync(FISH_FUNCTION_FILE, 'utf8').includes(SHELL_MARKER);
60
+ if (inZsh || inBash || inFish) {
61
+ const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc', inFish && '~/.config/fish'].filter(Boolean).join(', ');
59
62
  return { active: true, where };
60
63
  }
61
64
  return { active: false, where: null };
62
65
  }
63
66
 
67
+ function hookFilesExist() {
68
+ return existsSync(join(HOOKS_DIR, 'HOOK.md')) &&
69
+ existsSync(join(HOOKS_DIR, 'handler.js'));
70
+ }
71
+
64
72
  function parseAuditLog() {
65
73
  if (!existsSync(AUDIT_LOG)) return { count: 0, lastEntry: null };
66
74
  try {
@@ -91,7 +99,6 @@ function credentialSummary() {
91
99
  if (!existsSync(credFile)) return { count: 0, oldestDays: null };
92
100
  try {
93
101
  const data = JSON.parse(readFileSync(credFile, 'utf8'));
94
- // Count token entries โ€” format varies; try common patterns
95
102
  let tokens = [];
96
103
  if (Array.isArray(data)) tokens = data;
97
104
  else if (data.accounts) tokens = Object.values(data.accounts);
@@ -99,7 +106,6 @@ function credentialSummary() {
99
106
 
100
107
  const count = tokens.length;
101
108
 
102
- // Try to find oldest by looking for date fields
103
109
  let oldest = null;
104
110
  for (const tok of tokens) {
105
111
  if (tok && typeof tok === 'object') {
@@ -120,15 +126,13 @@ function configBaselineStatus() {
120
126
  if (!existsSync(baselineFile)) return { status: 'unknown' };
121
127
  try {
122
128
  const baseline = JSON.parse(readFileSync(baselineFile, 'utf8'));
123
- // Simple presence check โ€” full integrity is handled by lib/integrity.js
124
129
  return { status: 'baseline', at: baseline.at || null };
125
130
  } catch { return { status: 'unknown' }; }
126
131
  }
127
132
 
128
133
  function nextDigestDate() {
129
- // Next Sunday at 9am
130
134
  const now = new Date();
131
- const dayOfWeek = now.getDay(); // 0=Sun
135
+ const dayOfWeek = now.getDay();
132
136
  const daysUntilSunday = dayOfWeek === 0 ? 7 : 7 - dayOfWeek;
133
137
  const next = new Date(now);
134
138
  next.setDate(now.getDate() + daysUntilSunday);
@@ -146,6 +150,15 @@ function digestInstalled() {
146
150
  return jobs.some(j => j.id === 'clawarmor-weekly-digest');
147
151
  }
148
152
 
153
+ // โ”€โ”€ Grade color (A+/A=green, B=yellow, C=orange/yellow, D/F=red) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
154
+
155
+ function gradeStatusColor(grade) {
156
+ if (grade === 'A+' || grade === 'A') return paint.green;
157
+ if (grade === 'B') return paint.yellow;
158
+ if (grade === 'C') return paint.yellow; // no orange in ANSI; yellow is closest
159
+ return paint.red; // D, F
160
+ }
161
+
149
162
  // โ”€โ”€ Main export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
150
163
 
151
164
  export async function runStatus() {
@@ -158,12 +171,10 @@ export async function runStatus() {
158
171
  const history = readJson(HISTORY_FILE) || [];
159
172
  const latestHistoryForPosture = history.length ? history[history.length - 1] : null;
160
173
 
161
- // Prefer last-score.json (written by watch/guard hook), fall back to history.json
162
174
  let score = lastScore?.score ?? latestHistoryForPosture?.score ?? null;
163
175
  let grade = lastScore?.grade ?? latestHistoryForPosture?.grade ?? null;
164
176
  let scoreTs = lastScore?.timestamp ?? latestHistoryForPosture?.timestamp ?? null;
165
177
 
166
- // Trend: compare to ~7 days ago from history
167
178
  let weekDelta = null;
168
179
  if (history.length >= 2) {
169
180
  const weekAgo = Date.now() - 7 * 86_400_000;
@@ -173,13 +184,13 @@ export async function runStatus() {
173
184
  }
174
185
  }
175
186
 
176
- // Score line
177
187
  if (score != null) {
178
188
  const grade2 = grade || scoreToGrade(score);
179
189
  const colorFn = scoreColor(score);
190
+ const gradeFn = gradeStatusColor(grade2);
180
191
  const arrow = trendArrow(weekDelta);
181
192
  const weekNote = weekDelta != null ? paint.dim(' vs last week') : '';
182
- console.log(` ${paint.dim('Posture')} ${gradeColor(grade2)} ${colorFn(score + '/100')} ${arrow}${weekNote}`);
193
+ console.log(` ${paint.dim('Posture')} ${gradeFn(grade2)} ${colorFn(score + '/100')} ${arrow}${weekNote}`);
183
194
  } else {
184
195
  console.log(` ${paint.dim('Posture')} ${paint.dim('No audit data โ€” run: clawarmor audit')}`);
185
196
  }
@@ -193,9 +204,12 @@ export async function runStatus() {
193
204
 
194
205
  // โ”€โ”€ Watcher โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
195
206
  const daemon = watchDaemonStatus();
196
- const watchStr = daemon.running
197
- ? `${paint.green('โ—')} ${paint.bold('running')} ${paint.dim('(PID ' + daemon.pid + ')')}`
198
- : `${paint.dim('โ—‹')} ${paint.dim('stopped')}`;
207
+ let watchStr;
208
+ if (daemon.running) {
209
+ watchStr = `${paint.green('โ—')} ${paint.bold('running')} ${paint.dim('(PID ' + daemon.pid + ')')}`;
210
+ } else {
211
+ watchStr = `${paint.red('โ—‹')} ${paint.red('stopped')} ${paint.dim('โ†’ run: clawarmor watch --daemon')}`;
212
+ }
199
213
  console.log(` ${paint.dim('Watcher')} ${watchStr}`);
200
214
 
201
215
  // โ”€โ”€ Shell intercept โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -245,6 +259,15 @@ export async function runStatus() {
245
259
  console.log(` ${paint.dim('Next digest')} ${paint.dim('not scheduled')} ${paint.dim('(run: clawarmor protect --install)')}`);
246
260
  }
247
261
 
262
+ // โ”€โ”€ Full protection footer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
263
+ const hookOk = hookFilesExist();
264
+ const fullProtection = hookOk && daemon.running && icp.active;
265
+ console.log('');
266
+ if (fullProtection) {
267
+ console.log(` Full protection: ${paint.green('[โœ“ YES]')}`);
268
+ } else {
269
+ console.log(` Full protection: ${paint.red('[โœ— NO')} ${paint.dim('โ€” run clawarmor protect --install]')}`);
270
+ }
248
271
  console.log('');
249
272
  return 0;
250
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawarmor",
3
- "version": "2.0.0-alpha.3",
3
+ "version": "2.0.0",
4
4
  "description": "Security armor for OpenClaw agents โ€” audit, scan, monitor",
5
5
  "bin": {
6
6
  "clawarmor": "cli.js"