clawarmor 2.0.0 → 2.1.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/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';
6
+ const VERSION = '2.1.0';
7
7
  const GATEWAY_PORT_DEFAULT = 18789;
8
8
 
9
9
  function isLocalhost(host) {
@@ -156,7 +156,7 @@ if (cmd === 'compare') {
156
156
 
157
157
  if (cmd === 'fix') {
158
158
  const { runFix } = await import('./lib/fix.js');
159
- const fixFlags = { apply: process.argv.includes('--apply'), dryRun: process.argv.includes('--dry-run') };
159
+ const fixFlags = { apply: process.argv.includes('--apply'), dryRun: process.argv.includes('--dry-run'), force: process.argv.includes('--force') };
160
160
  process.exit(await runFix(fixFlags));
161
161
  }
162
162
 
@@ -202,6 +202,7 @@ if (cmd === 'harden') {
202
202
  const hardenFlags = {
203
203
  dryRun: args.includes('--dry-run'),
204
204
  auto: args.includes('--auto'),
205
+ force: args.includes('--force'),
205
206
  };
206
207
  const { runHarden } = await import('./lib/harden.js');
207
208
  process.exit(await runHarden(hardenFlags));
Binary file
package/demo.cast ADDED
@@ -0,0 +1,680 @@
1
+ {"version": 2, "width": 110, "height": 35, "timestamp": 1772424511, "title": "ClawArmor v2.0 \u2014 Security Audit Demo", "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
2
+ [0.0, "o", "$ "]
3
+ [0.808, "o", "#"]
4
+ [0.838, "o", " "]
5
+ [0.868, "o", "S"]
6
+ [0.898, "o", "t"]
7
+ [0.928, "o", "e"]
8
+ [0.958, "o", "p"]
9
+ [0.988, "o", " "]
10
+ [1.018, "o", "1"]
11
+ [1.048, "o", ":"]
12
+ [1.078, "o", " "]
13
+ [1.108, "o", "C"]
14
+ [1.138, "o", "h"]
15
+ [1.168, "o", "e"]
16
+ [1.198, "o", "c"]
17
+ [1.228, "o", "k"]
18
+ [1.258, "o", " "]
19
+ [1.288, "o", "y"]
20
+ [1.318, "o", "o"]
21
+ [1.348, "o", "u"]
22
+ [1.378, "o", "r"]
23
+ [1.408, "o", " "]
24
+ [1.438, "o", "O"]
25
+ [1.468, "o", "p"]
26
+ [1.498, "o", "e"]
27
+ [1.528, "o", "n"]
28
+ [1.558, "o", "C"]
29
+ [1.588, "o", "l"]
30
+ [1.618, "o", "a"]
31
+ [1.648, "o", "w"]
32
+ [1.678, "o", " "]
33
+ [1.708, "o", "s"]
34
+ [1.738, "o", "e"]
35
+ [1.768, "o", "c"]
36
+ [1.798, "o", "u"]
37
+ [1.828, "o", "r"]
38
+ [1.858, "o", "i"]
39
+ [1.888, "o", "t"]
40
+ [1.918, "o", "y"]
41
+ [1.948, "o", " "]
42
+ [1.978, "o", "p"]
43
+ [2.008, "o", "o"]
44
+ [2.038, "o", "s"]
45
+ [2.068, "o", "t"]
46
+ [2.098, "o", "u"]
47
+ [2.128, "o", "r"]
48
+ [2.158, "o", "e"]
49
+ [2.188, "o", "\n"]
50
+ [2.288, "o", "$ "]
51
+ [2.796, "o", "c"]
52
+ [2.841, "o", "l"]
53
+ [2.886, "o", "a"]
54
+ [2.931, "o", "w"]
55
+ [2.976, "o", "a"]
56
+ [3.021, "o", "r"]
57
+ [3.066, "o", "m"]
58
+ [3.111, "o", "o"]
59
+ [3.156, "o", "r"]
60
+ [3.201, "o", " "]
61
+ [3.246, "o", "a"]
62
+ [3.291, "o", "u"]
63
+ [3.336, "o", "d"]
64
+ [3.381, "o", "i"]
65
+ [3.426, "o", "t"]
66
+ [3.471, "o", "\n"]
67
+ [3.671, "o", "\n"]
68
+ [3.683, "o", " \u2139 Config: local (~/.openclaw/openclaw.json)\n"]
69
+ [3.695, "o", " Probes: 127.0.0.1:18789 (local)\n"]
70
+ [3.707, "o", " Sends nothing. Source: github.com/pinzasai/clawarmor\n"]
71
+ [3.719, "o", "\n"]
72
+ [3.731, "o", "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n"]
73
+ [3.743, "o", "\u2551 ClawArmor Audit v2.0.0-alpha.1 \u2551\n"]
74
+ [3.755, "o", "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n"]
75
+ [3.767, "o", "\n"]
76
+ [3.779, "o", " Config: /Users/pinzas/.openclaw/openclaw.json\n"]
77
+ [3.791, "o", " Scanned: Mar 1, 2026, 8:08 PM\n"]
78
+ [3.803, "o", "\n"]
79
+ [3.815, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
80
+ [3.827, "o", " LIVE GATEWAY PROBES (connecting to 127.0.0.1:18789)\n"]
81
+ [3.839, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
82
+ [3.851, "o", " \u2713 Gateway running on port 18789\n"]
83
+ [3.863, "o", " \u2713 Not reachable on network interfaces (probed live)\n"]
84
+ [3.875, "o", " \u2713 Authentication required (WebSocket probe confirmed)\n"]
85
+ [3.887, "o", " \u2713 /health endpoint does not leak sensitive data\n"]
86
+ [3.899, "o", " \u2713 CORS not open to arbitrary origins\n"]
87
+ [3.911, "o", "\n"]
88
+ [3.923, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
89
+ [3.935, "o", " Security Score: 45/100 \u2503 Grade: D\n"]
90
+ [3.947, "o", " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 45%\n"]
91
+ [3.959, "o", "\n"]
92
+ [3.971, "o", " Verdict: Your instance has CRITICAL exposure. Fix immediately before using.\n"]
93
+ [3.983, "o", "\n"]
94
+ [3.995, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
95
+ [4.007, "o", " CRITICAL (1 finding)\n"]
96
+ [4.019, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
97
+ [4.031, "o", "\n"]
98
+ [4.043, "o", " \u2717 World-readable credential files in ~/.openclaw/ (3)\n"]
99
+ [4.055, "o", " The following files are readable by any user on the system:\n"]
100
+ [4.067, "o", " \u2022 .env (644)\n"]
101
+ [4.079, "o", " \u2022 google-auth-setup.py (644)\n"]
102
+ [4.091, "o", " \u2022 update-check.json (644)\n"]
103
+ [4.103, "o", " \n"]
104
+ [4.115, "o", " Any local process or user can read your API keys and tokens.\n"]
105
+ [4.127, "o", "\n"]
106
+ [4.139, "o", " Fix: Fix immediately:\n"]
107
+ [4.151, "o", " chmod 600 /Users/pinzas/.openclaw/.env\n"]
108
+ [4.163, "o", " chmod 600 /Users/pinzas/.openclaw/google-auth-setup.py\n"]
109
+ [4.175, "o", " chmod 600 /Users/pinzas/.openclaw/update-check.json\n"]
110
+ [4.187, "o", "\n"]
111
+ [4.199, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
112
+ [4.211, "o", " HIGH (2 findings)\n"]
113
+ [4.223, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
114
+ [4.235, "o", "\n"]
115
+ [4.247, "o", " \u2717 Exec approval disabled \u2014 all shell commands run without confirmation\n"]
116
+ [4.259, "o", " tools.exec.ask=\"off\" means every shell command the agent triggers\n"]
117
+ [4.271, "o", " runs immediately with zero user approval. Any prompt injection or malicious\n"]
118
+ [4.283, "o", " skill can execute arbitrary commands on your system without you seeing them.\n"]
119
+ [4.295, "o", " Attack: attacker injects \"run rm -rf ~/important\" \u2014 it executes silently.\n"]
120
+ [4.307, "o", "\n"]
121
+ [4.319, "o", " Fix: openclaw config set tools.exec.ask always\n"]
122
+ [4.331, "o", " # or, to allow a specific set without prompts:\n"]
123
+ [4.343, "o", " openctl config set tools.exec.ask on-miss\n"]
124
+ [4.355, "o", " openctl config set tools.exec.allowed '[\"git\",\"npm\",\"node\"]'\n"]
125
+ [4.367, "o", "\n"]
126
+ [4.379, "o", " \u2717 API key patterns found in ~/.openclaw/ JSON files (3 files)\n"]
127
+ [4.391, "o", " The following JSON files in ~/.openclaw/ contain patterns matching API keys or secrets:\n"]
128
+ [4.403, "o", " \u2022 agent-accounts.json\n"]
129
+ [4.415, "o", " \u2022 exec-approvals.json\n"]
130
+ [4.427, "o", " \u2022 openclaw.json\n"]
131
+ [4.439, "o", " \n"]
132
+ [4.451, "o", " Note: Only key name patterns are detected \u2014 actual values are never read or stored.\n"]
133
+ [4.463, "o", " Credentials in the wrong files may be at risk if file permissions are too open.\n"]
134
+ [4.475, "o", "\n"]
135
+ [4.487, "o", " Fix: Ensure all credential files use 0600 permissions:\n"]
136
+ [4.499, "o", " chmod 600 ~/.openclaw/*.json\n"]
137
+ [4.511, "o", " \n"]
138
+ [4.523, "o", " If credentials are in unexpected files, move them to agent-accounts.json.\n"]
139
+ [4.535, "o", "\n"]
140
+ [4.547, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
141
+ [4.559, "o", " PASSED (34 checks)\n"]
142
+ [4.571, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
143
+ [4.583, "o", " \u2713 Gateway bound to loopback only\n"]
144
+ [4.595, "o", " \u2713 Tailscale Funnel not enabled\n"]
145
+ [4.607, "o", " \u2713 Auth token is strong\n"]
146
+ [4.619, "o", " \u2713 No dangerous flags enabled\n"]
147
+ [4.631, "o", " \u2713 mDNS mode: \"minimal\" (not leaking sensitive data)\n"]
148
+ [4.643, "o", " \u2713 Real-IP fallback disabled\n"]
149
+ [4.655, "o", " \u2713 Gateway is loopback-only \u2014 trustedProxies not needed\n"]
150
+ [4.667, "o", " \u2713 Trust model appropriate for current channel configuration\n"]
151
+ [4.679, "o", " \u2713 ~/.openclaw/ is owner-only (700)\n"]
152
+ [4.691, "o", " \u2713 openclaw.json is owner-only (600)\n"]
153
+ [4.703, "o", " \u2713 agent-accounts.json is owner-only (600)\n"]
154
+ [4.715, "o", " \u2713 credentials/ directory is locked down\n"]
155
+ [4.727, "o", " \u2713 Session transcripts are private\n"]
156
+ [4.739, "o", " \u2713 Telegram DM policy: \"pairing\" (restricted)\n"]
157
+ [4.751, "o", " \u2713 All group policies use allowlist\n"]
158
+ [4.763, "o", " \u2713 No open groups with elevated tools (safe)\n"]
159
+ [4.775, "o", " \u2713 DM sessions are isolated per user\n"]
160
+ [4.787, "o", " \u2713 Agent sandbox mode: \"all\" (sessions isolated)\n"]
161
+ [4.799, "o", " \u2713 exec sandbox configuration is consistent\n"]
162
+ [4.811, "o", " \u2713 Thinking stream not leaking reasoning\n"]
163
+ [4.823, "o", " \u2713 Elevated tools not configured\n"]
164
+ [4.835, "o", " \u2713 Filesystem restricted to workspace\n"]
165
+ [4.847, "o", " \u2713 apply_patch restricted to workspace\n"]
166
+ [4.859, "o", " \u2713 Browser SSRF to private networks blocked\n"]
167
+ [4.871, "o", " \u2713 Plugin allowlist configured\n"]
168
+ [4.883, "o", " \u2713 Log redaction enabled\n"]
169
+ [4.895, "o", " \u2713 OpenClaw 2026.2.26 (up to date)\n"]
170
+ [4.907, "o", " \u2713 Webhooks cannot control session routing\n"]
171
+ [4.919, "o", " \u2713 No webhook token configured\n"]
172
+ [4.931, "o", " \u2713 All channel allowFrom settings are restricted\n"]
173
+ [4.943, "o", " \u2713 All credential date fields are within 90 days\n"]
174
+ [4.955, "o", " \u2713 All installed skills have explicit version pins\n"]
175
+ [4.967, "o", " \u2713 Workspace directory not found \u2014 git credential leak check skipped\n"]
176
+ [4.979, "o", " \u2713 ~/.openclaw/ directory permissions are secure (700)\n"]
177
+ [4.991, "o", "\n"]
178
+ [5.003, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
179
+ [5.015, "o", " 3 issues found. Fix above to improve score.\n"]
180
+ [5.027, "o", " Run clawarmor scan to check installed skills.\n"]
181
+ [5.039, "o", " Run clawarmor trend to see score history.\n"]
182
+ [5.051, "o", " Continuous monitoring: github.com/pinzasai/clawarmor\n"]
183
+ [5.063, "o", "\n"]
184
+ [5.075, "o", "\n"]
185
+ [5.087, "o", " ! Config changed since last clean audit\n"]
186
+ [5.099, "o", " Size: 4751 \u2192 5295 bytes (+544)\n"]
187
+ [5.111, "o", " Lines: 186 \u2192 197 (+11)\n"]
188
+ [5.123, "o", " Hash: fda00bbb0ac793f7 \u2192 d757fa979eb98f41\n"]
189
+ [5.135, "o", " Baseline set: 2026-03-01\n"]
190
+ [5.147, "o", " Run clawarmor audit --accept-changes to update baseline\n"]
191
+ [5.159, "o", ""]
192
+ [8.171, "o", "$ "]
193
+ [8.679, "o", "#"]
194
+ [8.709, "o", " "]
195
+ [8.739, "o", "S"]
196
+ [8.769, "o", "t"]
197
+ [8.799, "o", "e"]
198
+ [8.829, "o", "p"]
199
+ [8.859, "o", " "]
200
+ [8.889, "o", "2"]
201
+ [8.919, "o", ":"]
202
+ [8.949, "o", " "]
203
+ [8.979, "o", "S"]
204
+ [9.009, "o", "e"]
205
+ [9.039, "o", "e"]
206
+ [9.069, "o", " "]
207
+ [9.099, "o", "w"]
208
+ [9.129, "o", "h"]
209
+ [9.159, "o", "a"]
210
+ [9.189, "o", "t"]
211
+ [9.219, "o", " "]
212
+ [9.249, "o", "C"]
213
+ [9.279, "o", "l"]
214
+ [9.309, "o", "a"]
215
+ [9.339, "o", "w"]
216
+ [9.369, "o", "A"]
217
+ [9.399, "o", "r"]
218
+ [9.429, "o", "m"]
219
+ [9.459, "o", "o"]
220
+ [9.489, "o", "r"]
221
+ [9.519, "o", " "]
222
+ [9.549, "o", "c"]
223
+ [9.579, "o", "a"]
224
+ [9.609, "o", "n"]
225
+ [9.639, "o", " "]
226
+ [9.669, "o", "f"]
227
+ [9.699, "o", "i"]
228
+ [9.729, "o", "x"]
229
+ [9.759, "o", " "]
230
+ [9.789, "o", "a"]
231
+ [9.819, "o", "u"]
232
+ [9.849, "o", "t"]
233
+ [9.879, "o", "o"]
234
+ [9.909, "o", "m"]
235
+ [9.939, "o", "a"]
236
+ [9.969, "o", "t"]
237
+ [9.999, "o", "i"]
238
+ [10.029, "o", "c"]
239
+ [10.059, "o", "a"]
240
+ [10.089, "o", "l"]
241
+ [10.119, "o", "l"]
242
+ [10.149, "o", "y"]
243
+ [10.179, "o", "\n"]
244
+ [10.279, "o", "$ "]
245
+ [10.787, "o", "c"]
246
+ [10.832, "o", "l"]
247
+ [10.877, "o", "a"]
248
+ [10.922, "o", "w"]
249
+ [10.967, "o", "a"]
250
+ [11.012, "o", "r"]
251
+ [11.057, "o", "m"]
252
+ [11.102, "o", "o"]
253
+ [11.147, "o", "r"]
254
+ [11.192, "o", " "]
255
+ [11.237, "o", "h"]
256
+ [11.282, "o", "a"]
257
+ [11.327, "o", "r"]
258
+ [11.372, "o", "d"]
259
+ [11.417, "o", "e"]
260
+ [11.462, "o", "n"]
261
+ [11.507, "o", " "]
262
+ [11.552, "o", "-"]
263
+ [11.597, "o", "-"]
264
+ [11.642, "o", "d"]
265
+ [11.687, "o", "r"]
266
+ [11.732, "o", "y"]
267
+ [11.777, "o", "-"]
268
+ [11.822, "o", "r"]
269
+ [11.867, "o", "u"]
270
+ [11.912, "o", "n"]
271
+ [11.957, "o", "\n"]
272
+ [12.157, "o", "\n"]
273
+ [12.169, "o", " \u2139 Config: local (~/.openclaw/openclaw.json)\n"]
274
+ [12.181, "o", " Probes: 127.0.0.1:18789 (local)\n"]
275
+ [12.193, "o", " Sends nothing. Source: github.com/pinzasai/clawarmor\n"]
276
+ [12.205, "o", "\n"]
277
+ [12.217, "o", "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n"]
278
+ [12.229, "o", "\u2551 ClawArmor Harden v2.0 \u2551\n"]
279
+ [12.241, "o", "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n"]
280
+ [12.253, "o", "\n"]
281
+ [12.265, "o", " Dry run \u2014 showing what would be fixed (no changes applied):\n"]
282
+ [12.277, "o", "\n"]
283
+ [12.289, "o", " ! .env is readable by other users (permissions: 644)\n"]
284
+ [12.301, "o", " Fix: Set permissions to 600 (owner-only) on /Users/pinzas/.openclaw/.env\n"]
285
+ [12.313, "o", " Cmd: chmod 600 /Users/pinzas/.openclaw/.env\n"]
286
+ [12.325, "o", "\n"]
287
+ [12.337, "o", " ! google-auth-setup.py is readable by other users (permissions: 644)\n"]
288
+ [12.349, "o", " Fix: Set permissions to 600 (owner-only) on /Users/pinzas/.openclaw/google-auth-setup.py\n"]
289
+ [12.361, "o", " Cmd: chmod 600 /Users/pinzas/.openclaw/google-auth-setup.py\n"]
290
+ [12.373, "o", "\n"]
291
+ [12.385, "o", " ! update-check.json is readable by other users (permissions: 644)\n"]
292
+ [12.397, "o", " Fix: Set permissions to 600 (owner-only) on /Users/pinzas/.openclaw/update-check.json\n"]
293
+ [12.409, "o", " Cmd: chmod 600 /Users/pinzas/.openclaw/update-check.json\n"]
294
+ [12.421, "o", "\n"]
295
+ [12.433, "o", " ! exec.ask is off \u2014 shell commands run without user confirmation\n"]
296
+ [12.445, "o", " Fix: Enable exec.ask so shell commands require confirmation\n"]
297
+ [12.457, "o", " Cmd: openclaw config set exec.ask always\n"]
298
+ [12.469, "o", " Note: Restart gateway after applying: openclaw gateway restart\n"]
299
+ [12.481, "o", "\n"]
300
+ [12.493, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
301
+ [12.505, "o", " 4 fixes available.\n"]
302
+ [12.517, "o", " Run clawarmor harden to apply interactively.\n"]
303
+ [12.529, "o", " Run clawarmor harden --auto to apply all without prompts.\n"]
304
+ [12.541, "o", "\n"]
305
+ [12.553, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
306
+ [12.565, "o", " Manual follow-up required:\n"]
307
+ [12.577, "o", " \u2022 Rotate tokens older than 90 days (run: clawarmor log --tokens)\n"]
308
+ [12.589, "o", " \u2022 Review and rotate any compromised or exposed credentials\n"]
309
+ [12.601, "o", " \u2022 Enable agent sandbox isolation if Docker Desktop is available\n"]
310
+ [12.613, "o", "\n"]
311
+ [12.625, "o", ""]
312
+ [15.637, "o", "$ "]
313
+ [16.145, "o", "#"]
314
+ [16.175, "o", " "]
315
+ [16.205, "o", "S"]
316
+ [16.235, "o", "t"]
317
+ [16.265, "o", "e"]
318
+ [16.295, "o", "p"]
319
+ [16.325, "o", " "]
320
+ [16.355, "o", "3"]
321
+ [16.385, "o", ":"]
322
+ [16.415, "o", " "]
323
+ [16.445, "o", "F"]
324
+ [16.475, "o", "i"]
325
+ [16.505, "o", "x"]
326
+ [16.535, "o", " "]
327
+ [16.565, "o", "e"]
328
+ [16.595, "o", "v"]
329
+ [16.625, "o", "e"]
330
+ [16.655, "o", "r"]
331
+ [16.685, "o", "y"]
332
+ [16.715, "o", "t"]
333
+ [16.745, "o", "h"]
334
+ [16.775, "o", "i"]
335
+ [16.805, "o", "n"]
336
+ [16.835, "o", "g"]
337
+ [16.865, "o", " "]
338
+ [16.895, "o", "a"]
339
+ [16.925, "o", "u"]
340
+ [16.955, "o", "t"]
341
+ [16.985, "o", "o"]
342
+ [17.015, "o", "m"]
343
+ [17.045, "o", "a"]
344
+ [17.075, "o", "t"]
345
+ [17.105, "o", "i"]
346
+ [17.135, "o", "c"]
347
+ [17.165, "o", "a"]
348
+ [17.195, "o", "l"]
349
+ [17.225, "o", "l"]
350
+ [17.255, "o", "y"]
351
+ [17.285, "o", "\n"]
352
+ [17.385, "o", "$ "]
353
+ [17.893, "o", "c"]
354
+ [17.938, "o", "l"]
355
+ [17.983, "o", "a"]
356
+ [18.028, "o", "w"]
357
+ [18.073, "o", "a"]
358
+ [18.118, "o", "r"]
359
+ [18.163, "o", "m"]
360
+ [18.208, "o", "o"]
361
+ [18.253, "o", "r"]
362
+ [18.298, "o", " "]
363
+ [18.343, "o", "h"]
364
+ [18.388, "o", "a"]
365
+ [18.433, "o", "r"]
366
+ [18.478, "o", "d"]
367
+ [18.523, "o", "e"]
368
+ [18.568, "o", "n"]
369
+ [18.613, "o", " "]
370
+ [18.658, "o", "-"]
371
+ [18.703, "o", "-"]
372
+ [18.748, "o", "a"]
373
+ [18.793, "o", "u"]
374
+ [18.838, "o", "t"]
375
+ [18.883, "o", "o"]
376
+ [18.928, "o", "\n"]
377
+ [19.128, "o", "\n"]
378
+ [19.14, "o", " \u2139 Config: local (~/.openclaw/openclaw.json)\n"]
379
+ [19.152, "o", " Probes: 127.0.0.1:18789 (local)\n"]
380
+ [19.164, "o", " Sends nothing. Source: github.com/pinzasai/clawarmor\n"]
381
+ [19.176, "o", "\n"]
382
+ [19.188, "o", "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n"]
383
+ [19.2, "o", "\u2551 ClawArmor Harden v2.0 \u2551\n"]
384
+ [19.212, "o", "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n"]
385
+ [19.224, "o", "\n"]
386
+ [19.236, "o", " Auto mode \u2014 applying all safe fixes without confirmation\n"]
387
+ [19.248, "o", "\n"]
388
+ [19.26, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
389
+ [19.272, "o", " Problem: .env is readable by other users (permissions: 644)\n"]
390
+ [19.284, "o", " Fix: Set permissions to 600 (owner-only) on /Users/pinzas/.openclaw/.env\n"]
391
+ [19.296, "o", " Command: chmod 600 /Users/pinzas/.openclaw/.env\n"]
392
+ [19.308, "o", "\n"]
393
+ [19.32, "o", " \u2713 Fixed\n"]
394
+ [19.332, "o", "\n"]
395
+ [19.344, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
396
+ [19.356, "o", " Problem: google-auth-setup.py is readable by other users (permissions: 644)\n"]
397
+ [19.368, "o", " Fix: Set permissions to 600 (owner-only) on /Users/pinzas/.openclaw/google-auth-setup.py\n"]
398
+ [19.38, "o", " Command: chmod 600 /Users/pinzas/.openclaw/google-auth-setup.py\n"]
399
+ [19.392, "o", "\n"]
400
+ [19.404, "o", " \u2713 Fixed\n"]
401
+ [19.416, "o", "\n"]
402
+ [19.428, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
403
+ [19.44, "o", " Problem: update-check.json is readable by other users (permissions: 644)\n"]
404
+ [19.452, "o", " Fix: Set permissions to 600 (owner-only) on /Users/pinzas/.openclaw/update-check.json\n"]
405
+ [19.464, "o", " Command: chmod 600 /Users/pinzas/.openclaw/update-check.json\n"]
406
+ [19.476, "o", "\n"]
407
+ [19.488, "o", " \u2713 Fixed\n"]
408
+ [19.5, "o", "\n"]
409
+ [19.512, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
410
+ [19.524, "o", " Problem: exec.ask is off \u2014 shell commands run without user confirmation\n"]
411
+ [19.536, "o", " Fix: Enable exec.ask so shell commands require confirmation\n"]
412
+ [19.548, "o", " Command: openclaw config set exec.ask always\n"]
413
+ [19.56, "o", "\n"]
414
+ [19.572, "o", " \u2717 Failed: Command failed: openclaw config set exec.ask always\n"]
415
+ [19.584, "o", "\n"]
416
+ [19.596, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
417
+ [19.608, "o", "\n"]
418
+ [19.62, "o", " Applied: 3 Skipped: 0 Failed: 1\n"]
419
+ [19.632, "o", "\n"]
420
+ [19.644, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
421
+ [19.656, "o", " Manual follow-up required:\n"]
422
+ [19.668, "o", " \u2022 Rotate tokens older than 90 days (run: clawarmor log --tokens)\n"]
423
+ [19.68, "o", " \u2022 Review and rotate any compromised or exposed credentials\n"]
424
+ [19.692, "o", " \u2022 Enable agent sandbox isolation if Docker Desktop is available\n"]
425
+ [19.704, "o", "\n"]
426
+ [19.716, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
427
+ [19.728, "o", " Re-running audit to measure impact...\n"]
428
+ [19.74, "o", "\n"]
429
+ [19.752, "o", " Before: 45/100 Grade: D\n"]
430
+ [19.764, "o", " After: 70/100 Grade: C +25\n"]
431
+ [19.776, "o", "\n"]
432
+ [19.788, "o", ""]
433
+ [22.3, "o", "$ "]
434
+ [22.808, "o", "#"]
435
+ [22.838, "o", " "]
436
+ [22.868, "o", "S"]
437
+ [22.898, "o", "t"]
438
+ [22.928, "o", "e"]
439
+ [22.958, "o", "p"]
440
+ [22.988, "o", " "]
441
+ [23.018, "o", "4"]
442
+ [23.048, "o", ":"]
443
+ [23.078, "o", " "]
444
+ [23.108, "o", "V"]
445
+ [23.138, "o", "e"]
446
+ [23.168, "o", "r"]
447
+ [23.198, "o", "i"]
448
+ [23.228, "o", "f"]
449
+ [23.258, "o", "y"]
450
+ [23.288, "o", " "]
451
+ [23.318, "o", "\u2014"]
452
+ [23.348, "o", " "]
453
+ [23.378, "o", "r"]
454
+ [23.408, "o", "e"]
455
+ [23.438, "o", "-"]
456
+ [23.468, "o", "a"]
457
+ [23.498, "o", "u"]
458
+ [23.528, "o", "d"]
459
+ [23.558, "o", "i"]
460
+ [23.588, "o", "t"]
461
+ [23.618, "o", " "]
462
+ [23.648, "o", "a"]
463
+ [23.678, "o", "f"]
464
+ [23.708, "o", "t"]
465
+ [23.738, "o", "e"]
466
+ [23.768, "o", "r"]
467
+ [23.798, "o", " "]
468
+ [23.828, "o", "h"]
469
+ [23.858, "o", "a"]
470
+ [23.888, "o", "r"]
471
+ [23.918, "o", "d"]
472
+ [23.948, "o", "e"]
473
+ [23.978, "o", "n"]
474
+ [24.008, "o", "i"]
475
+ [24.038, "o", "n"]
476
+ [24.068, "o", "g"]
477
+ [24.098, "o", "\n"]
478
+ [24.198, "o", "$ "]
479
+ [24.706, "o", "c"]
480
+ [24.751, "o", "l"]
481
+ [24.796, "o", "a"]
482
+ [24.841, "o", "w"]
483
+ [24.886, "o", "a"]
484
+ [24.931, "o", "r"]
485
+ [24.976, "o", "m"]
486
+ [25.021, "o", "o"]
487
+ [25.066, "o", "r"]
488
+ [25.111, "o", " "]
489
+ [25.156, "o", "a"]
490
+ [25.201, "o", "u"]
491
+ [25.246, "o", "d"]
492
+ [25.291, "o", "i"]
493
+ [25.336, "o", "t"]
494
+ [25.381, "o", "\n"]
495
+ [25.581, "o", "\n"]
496
+ [25.593, "o", " \u2139 Config: local (~/.openclaw/openclaw.json)\n"]
497
+ [25.605, "o", " Probes: 127.0.0.1:18789 (local)\n"]
498
+ [25.617, "o", " Sends nothing. Source: github.com/pinzasai/clawarmor\n"]
499
+ [25.629, "o", "\n"]
500
+ [25.641, "o", "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n"]
501
+ [25.653, "o", "\u2551 ClawArmor Audit v2.0.0-alpha.1 \u2551\n"]
502
+ [25.665, "o", "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n"]
503
+ [25.677, "o", "\n"]
504
+ [25.689, "o", " Config: /Users/pinzas/.openclaw/openclaw.json\n"]
505
+ [25.701, "o", " Scanned: Mar 1, 2026, 8:08 PM\n"]
506
+ [25.713, "o", "\n"]
507
+ [25.725, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
508
+ [25.737, "o", " LIVE GATEWAY PROBES (connecting to 127.0.0.1:18789)\n"]
509
+ [25.749, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
510
+ [25.761, "o", " \u2713 Gateway running on port 18789\n"]
511
+ [25.773, "o", " \u2713 Not reachable on network interfaces (probed live)\n"]
512
+ [25.785, "o", " \u2713 Authentication required (WebSocket probe confirmed)\n"]
513
+ [25.797, "o", " \u2713 /health endpoint does not leak sensitive data\n"]
514
+ [25.809, "o", " \u2713 CORS not open to arbitrary origins\n"]
515
+ [25.821, "o", "\n"]
516
+ [25.833, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
517
+ [25.845, "o", " Security Score: 70/100 \u2503 Grade: C\n"]
518
+ [25.857, "o", " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591 70%\n"]
519
+ [25.869, "o", "\n"]
520
+ [25.881, "o", " Verdict: Your instance has HIGH-risk issues. Fix before going to production.\n"]
521
+ [25.893, "o", "\n"]
522
+ [25.905, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
523
+ [25.917, "o", " HIGH (2 findings)\n"]
524
+ [25.929, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
525
+ [25.941, "o", "\n"]
526
+ [25.953, "o", " \u2717 Exec approval disabled \u2014 all shell commands run without confirmation\n"]
527
+ [25.965, "o", " tools.exec.ask=\"off\" means every shell command the agent triggers\n"]
528
+ [25.977, "o", " runs immediately with zero user approval. Any prompt injection or malicious\n"]
529
+ [25.989, "o", " skill can execute arbitrary commands on your system without you seeing them.\n"]
530
+ [26.001, "o", " Attack: attacker injects \"run rm -rf ~/important\" \u2014 it executes silently.\n"]
531
+ [26.013, "o", "\n"]
532
+ [26.025, "o", " Fix: openclaw config set tools.exec.ask always\n"]
533
+ [26.037, "o", " # or, to allow a specific set without prompts:\n"]
534
+ [26.049, "o", " openctl config set tools.exec.ask on-miss\n"]
535
+ [26.061, "o", " openctl config set tools.exec.allowed '[\"git\",\"npm\",\"node\"]'\n"]
536
+ [26.073, "o", "\n"]
537
+ [26.085, "o", " \u2717 API key patterns found in ~/.openclaw/ JSON files (3 files)\n"]
538
+ [26.097, "o", " The following JSON files in ~/.openclaw/ contain patterns matching API keys or secrets:\n"]
539
+ [26.109, "o", " \u2022 agent-accounts.json\n"]
540
+ [26.121, "o", " \u2022 exec-approvals.json\n"]
541
+ [26.133, "o", " \u2022 openclaw.json\n"]
542
+ [26.145, "o", " \n"]
543
+ [26.157, "o", " Note: Only key name patterns are detected \u2014 actual values are never read or stored.\n"]
544
+ [26.169, "o", " Credentials in the wrong files may be at risk if file permissions are too open.\n"]
545
+ [26.181, "o", "\n"]
546
+ [26.193, "o", " Fix: Ensure all credential files use 0600 permissions:\n"]
547
+ [26.205, "o", " chmod 600 ~/.openclaw/*.json\n"]
548
+ [26.217, "o", " \n"]
549
+ [26.229, "o", " If credentials are in unexpected files, move them to agent-accounts.json.\n"]
550
+ [26.241, "o", "\n"]
551
+ [26.253, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
552
+ [26.265, "o", " PASSED (35 checks)\n"]
553
+ [26.277, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
554
+ [26.289, "o", " \u2713 Gateway bound to loopback only\n"]
555
+ [26.301, "o", " \u2713 Tailscale Funnel not enabled\n"]
556
+ [26.313, "o", " \u2713 Auth token is strong\n"]
557
+ [26.325, "o", " \u2713 No dangerous flags enabled\n"]
558
+ [26.337, "o", " \u2713 mDNS mode: \"minimal\" (not leaking sensitive data)\n"]
559
+ [26.349, "o", " \u2713 Real-IP fallback disabled\n"]
560
+ [26.361, "o", " \u2713 Gateway is loopback-only \u2014 trustedProxies not needed\n"]
561
+ [26.373, "o", " \u2713 Trust model appropriate for current channel configuration\n"]
562
+ [26.385, "o", " \u2713 ~/.openclaw/ is owner-only (700)\n"]
563
+ [26.397, "o", " \u2713 openclaw.json is owner-only (600)\n"]
564
+ [26.409, "o", " \u2713 agent-accounts.json is owner-only (600)\n"]
565
+ [26.421, "o", " \u2713 credentials/ directory is locked down\n"]
566
+ [26.433, "o", " \u2713 Session transcripts are private\n"]
567
+ [26.445, "o", " \u2713 Telegram DM policy: \"pairing\" (restricted)\n"]
568
+ [26.457, "o", " \u2713 All group policies use allowlist\n"]
569
+ [26.469, "o", " \u2713 No open groups with elevated tools (safe)\n"]
570
+ [26.481, "o", " \u2713 DM sessions are isolated per user\n"]
571
+ [26.493, "o", " \u2713 Agent sandbox mode: \"all\" (sessions isolated)\n"]
572
+ [26.505, "o", " \u2713 exec sandbox configuration is consistent\n"]
573
+ [26.517, "o", " \u2713 Thinking stream not leaking reasoning\n"]
574
+ [26.529, "o", " \u2713 Elevated tools not configured\n"]
575
+ [26.541, "o", " \u2713 Filesystem restricted to workspace\n"]
576
+ [26.553, "o", " \u2713 apply_patch restricted to workspace\n"]
577
+ [26.565, "o", " \u2713 Browser SSRF to private networks blocked\n"]
578
+ [26.577, "o", " \u2713 Plugin allowlist configured\n"]
579
+ [26.589, "o", " \u2713 Log redaction enabled\n"]
580
+ [26.601, "o", " \u2713 OpenClaw 2026.2.26 (up to date)\n"]
581
+ [26.613, "o", " \u2713 Webhooks cannot control session routing\n"]
582
+ [26.625, "o", " \u2713 No webhook token configured\n"]
583
+ [26.637, "o", " \u2713 All channel allowFrom settings are restricted\n"]
584
+ [26.649, "o", " \u2713 All credential date fields are within 90 days\n"]
585
+ [26.661, "o", " \u2713 All installed skills have explicit version pins\n"]
586
+ [26.673, "o", " \u2713 Workspace directory not found \u2014 git credential leak check skipped\n"]
587
+ [26.685, "o", " \u2713 ~/.openclaw/ directory permissions are secure (700)\n"]
588
+ [26.697, "o", " \u2713 Credential file permissions are secure (all \u2264 0600)\n"]
589
+ [26.709, "o", "\n"]
590
+ [26.721, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
591
+ [26.733, "o", " 2 issues found. Fix above to improve score.\n"]
592
+ [26.745, "o", " Run clawarmor scan to check installed skills.\n"]
593
+ [26.757, "o", " Run clawarmor trend to see score history.\n"]
594
+ [26.769, "o", " Continuous monitoring: github.com/pinzasai/clawarmor\n"]
595
+ [26.781, "o", "\n"]
596
+ [26.793, "o", "\n"]
597
+ [26.805, "o", " ! Config changed since last clean audit\n"]
598
+ [26.817, "o", " Size: 4751 \u2192 5295 bytes (+544)\n"]
599
+ [26.829, "o", " Lines: 186 \u2192 197 (+11)\n"]
600
+ [26.841, "o", " Hash: fda00bbb0ac793f7 \u2192 d757fa979eb98f41\n"]
601
+ [26.853, "o", " Baseline set: 2026-03-01\n"]
602
+ [26.865, "o", " Run clawarmor audit --accept-changes to update baseline\n"]
603
+ [26.877, "o", ""]
604
+ [29.889, "o", "$ "]
605
+ [30.397, "o", "#"]
606
+ [30.427, "o", " "]
607
+ [30.457, "o", "B"]
608
+ [30.487, "o", "o"]
609
+ [30.517, "o", "n"]
610
+ [30.547, "o", "u"]
611
+ [30.577, "o", "s"]
612
+ [30.607, "o", ":"]
613
+ [30.637, "o", " "]
614
+ [30.667, "o", "F"]
615
+ [30.697, "o", "u"]
616
+ [30.727, "o", "l"]
617
+ [30.757, "o", "l"]
618
+ [30.787, "o", " "]
619
+ [30.817, "o", "s"]
620
+ [30.847, "o", "e"]
621
+ [30.877, "o", "c"]
622
+ [30.907, "o", "u"]
623
+ [30.937, "o", "r"]
624
+ [30.967, "o", "i"]
625
+ [30.997, "o", "t"]
626
+ [31.027, "o", "y"]
627
+ [31.057, "o", " "]
628
+ [31.087, "o", "d"]
629
+ [31.117, "o", "a"]
630
+ [31.147, "o", "s"]
631
+ [31.177, "o", "h"]
632
+ [31.207, "o", "b"]
633
+ [31.237, "o", "o"]
634
+ [31.267, "o", "a"]
635
+ [31.297, "o", "r"]
636
+ [31.327, "o", "d"]
637
+ [31.357, "o", "\n"]
638
+ [31.457, "o", "$ "]
639
+ [31.965, "o", "c"]
640
+ [32.01, "o", "l"]
641
+ [32.055, "o", "a"]
642
+ [32.1, "o", "w"]
643
+ [32.145, "o", "a"]
644
+ [32.19, "o", "r"]
645
+ [32.235, "o", "m"]
646
+ [32.28, "o", "o"]
647
+ [32.325, "o", "r"]
648
+ [32.37, "o", " "]
649
+ [32.415, "o", "s"]
650
+ [32.46, "o", "t"]
651
+ [32.505, "o", "a"]
652
+ [32.55, "o", "t"]
653
+ [32.595, "o", "u"]
654
+ [32.64, "o", "s"]
655
+ [32.685, "o", "\n"]
656
+ [32.885, "o", "\n"]
657
+ [32.897, "o", " \u2139 Config: local (~/.openclaw/openclaw.json)\n"]
658
+ [32.909, "o", " Probes: 127.0.0.1:18789 (local)\n"]
659
+ [32.921, "o", " Sends nothing. Source: github.com/pinzasai/clawarmor\n"]
660
+ [32.933, "o", "\n"]
661
+ [32.945, "o", " ClawArmor v2.0.0 \u2014 Security Status\n"]
662
+ [32.957, "o", "\n"]
663
+ [32.969, "o", " Posture D 50/100 \u2014\n"]
664
+ [32.981, "o", " Last audit 0s ago (manual)\n"]
665
+ [32.993, "o", " Watcher \u25cf running (PID 16371)\n"]
666
+ [33.005, "o", " Intercept \u2713 active (~/.zshrc)\n"]
667
+ [33.017, "o", " Audit log 36 events (clawarmor log to view)\n"]
668
+ [33.029, "o", "\n"]
669
+ [33.041, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
670
+ [33.053, "o", " Skills 0 installed (clawarmor scan to check)\n"]
671
+ [33.065, "o", " Config No baseline yet \u2014 run: clawarmor audit\n"]
672
+ [33.077, "o", " Credentials 6 tokens, oldest: 1d \u2713\n"]
673
+ [33.089, "o", "\n"]
674
+ [33.101, "o", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"]
675
+ [33.113, "o", " Next digest not scheduled (run: clawarmor protect --install)\n"]
676
+ [33.125, "o", "\n"]
677
+ [33.137, "o", " Full protection: [\u2713 YES]\n"]
678
+ [33.149, "o", "\n"]
679
+ [33.161, "o", ""]
680
+ [36.173, "o", "$ "]
package/demo.gif ADDED
Binary file
package/lib/fix.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // clawarmor fix — auto-apply safe one-liner fixes
2
+ // Now with impact classification: safe / caution / breaking
2
3
  import { readFileSync } from 'fs';
3
4
  import { homedir } from 'os';
4
5
  import { join } from 'path';
@@ -9,57 +10,90 @@ import { loadConfig, get } from './config.js';
9
10
  const HISTORY_PATH = join(homedir(), '.clawarmor', 'history.json');
10
11
  const SEP = paint.dim('─'.repeat(52));
11
12
 
13
+ // Impact levels
14
+ const IMPACT = { SAFE: 'safe', CAUTION: 'caution', BREAKING: 'breaking' };
15
+
16
+ const IMPACT_BADGE = {
17
+ [IMPACT.SAFE]: () => paint.green('🟢 Safe'),
18
+ [IMPACT.CAUTION]: () => paint.yellow('🟡 Caution'),
19
+ [IMPACT.BREAKING]: () => paint.red('🔴 Breaking'),
20
+ };
21
+
12
22
  // Fixes that are safe to auto-apply (single config set command, no restart risk)
13
23
  const AUTO_FIXABLE = {
14
24
  'browser.ssrf': {
15
25
  cmd: 'openclaw config set browser.ssrfPolicy.dangerouslyAllowPrivateNetwork false',
16
26
  desc: 'Block browser SSRF to private networks',
17
27
  needsRestart: true,
28
+ impact: IMPACT.CAUTION,
29
+ impactDetail: 'Browser tool will no longer be able to access local/private network URLs.\n' +
30
+ ' If your agent browses internal dashboards or local services, those will be blocked.',
18
31
  },
19
32
  'discovery.mdns': {
20
33
  cmd: 'openclaw config set discovery.mdns.mode minimal',
21
34
  desc: 'Set mDNS to minimal mode',
22
35
  needsRestart: true,
36
+ impact: IMPACT.SAFE,
37
+ impactDetail: 'Only reduces network advertisement. No functionality change.',
23
38
  },
24
39
  'logging.redact': {
25
40
  cmd: 'openclaw config set logging.redactSensitive tools',
26
41
  desc: 'Enable log redaction',
27
42
  needsRestart: false,
43
+ impact: IMPACT.SAFE,
44
+ impactDetail: 'Redacts sensitive data from logs. No functionality change.',
28
45
  },
29
46
  'tools.fs.workspaceOnly': {
30
47
  cmd: 'openclaw config set tools.fs.workspaceOnly true',
31
48
  desc: 'Restrict filesystem to workspace',
32
49
  needsRestart: true,
50
+ impact: IMPACT.BREAKING,
51
+ impactDetail: 'Agent will ONLY be able to read/write files inside the workspace directory.\n' +
52
+ ' Access to home directory, system files, and other paths will be blocked.\n' +
53
+ ' Skills or workflows that read files outside workspace will break.',
33
54
  },
34
55
  'tools.applyPatch.workspaceOnly': {
35
56
  cmd: 'openclaw config set tools.exec.applyPatch.workspaceOnly true',
36
57
  desc: 'Restrict apply_patch to workspace',
37
58
  needsRestart: true,
59
+ impact: IMPACT.CAUTION,
60
+ impactDetail: 'Patches can only be applied to files in the workspace. Usually fine unless\n' +
61
+ ' your agent patches system files or configs outside the workspace.',
38
62
  },
39
63
  'fs.config.perms': {
40
64
  cmd: 'chmod 600 ~/.openclaw/openclaw.json',
41
65
  desc: 'Lock down config file permissions',
42
66
  needsRestart: false,
43
67
  shell: true,
68
+ impact: IMPACT.SAFE,
69
+ impactDetail: 'Only restricts other system users. Your agent runs as you.',
44
70
  },
45
71
  'fs.accounts.perms': {
46
72
  cmd: 'chmod 600 ~/.openclaw/agent-accounts.json',
47
73
  desc: 'Lock down credentials file permissions',
48
74
  needsRestart: false,
49
75
  shell: true,
76
+ impact: IMPACT.SAFE,
77
+ impactDetail: 'Only restricts other system users. Your agent runs as you.',
50
78
  },
51
79
  'agents.sandbox': {
52
- // Multi-step fix: enables sandbox WITH workspace access so Telegram sessions don't lose memory files
53
80
  cmd: "openclaw config set agents.defaults.sandbox.mode non-main && openclaw config set agents.defaults.sandbox.workspaceAccess rw && openclaw config set agents.defaults.sandbox.scope session",
54
81
  desc: 'Enable sandbox isolation (with workspace access preserved for Telegram/group sessions)',
55
82
  needsRestart: true,
56
83
  requiresDocker: true,
84
+ impact: IMPACT.BREAKING,
85
+ impactDetail: 'Non-main sessions will run inside Docker containers.\n' +
86
+ ' Requires Docker Desktop to be installed and running.\n' +
87
+ ' Telegram/group sessions will lose direct host access (shell commands,\n' +
88
+ ' file reads outside workspace). Workspace files remain accessible.',
57
89
  },
58
90
  'fs.dir.perms': {
59
91
  cmd: 'chmod 700 ~/.openclaw',
60
92
  desc: 'Lock down ~/.openclaw directory',
61
93
  needsRestart: false,
62
94
  shell: true,
95
+ impact: IMPACT.SAFE,
96
+ impactDetail: 'Only restricts other system users from listing the directory.',
63
97
  },
64
98
  };
65
99
 
@@ -71,7 +105,7 @@ function box(title) {
71
105
  }
72
106
 
73
107
  export async function runFix(flags = {}) {
74
- console.log(''); console.log(box('ClawArmor Fix v1.1.0')); console.log('');
108
+ console.log(''); console.log(box('ClawArmor Fix v2.1')); console.log('');
75
109
 
76
110
  // Load last audit failures
77
111
  let lastFailed = [];
@@ -89,8 +123,13 @@ export async function runFix(flags = {}) {
89
123
  const fixable = lastFailed.filter(id => AUTO_FIXABLE[id]);
90
124
  const manual = lastFailed.filter(id => !AUTO_FIXABLE[id]);
91
125
 
126
+ const safeFixes = fixable.filter(id => AUTO_FIXABLE[id].impact === IMPACT.SAFE);
127
+ const cautionFixes = fixable.filter(id => AUTO_FIXABLE[id].impact === IMPACT.CAUTION);
128
+ const breakingFixes = fixable.filter(id => AUTO_FIXABLE[id].impact === IMPACT.BREAKING);
129
+
92
130
  console.log(` ${paint.dim('Last audit had')} ${paint.bold(String(lastFailed.length))} ${paint.dim('failing checks.')}`);
93
- console.log(` ${paint.bold(String(fixable.length))} ${paint.dim('can be auto-fixed.')} ${paint.dim(String(manual.length))} ${paint.dim('require manual steps.')}`);
131
+ console.log(` ${paint.bold(String(fixable.length))} ${paint.dim('can be auto-fixed:')} ${paint.green(String(safeFixes.length) + ' safe')} ${paint.dim('·')} ${paint.yellow(String(cautionFixes.length) + ' caution')} ${paint.dim('·')} ${paint.red(String(breakingFixes.length) + ' breaking')}`);
132
+ console.log(` ${paint.dim(String(manual.length))} ${paint.dim('require manual steps.')}`);
94
133
 
95
134
  if (!fixable.length) {
96
135
  console.log('');
@@ -99,40 +138,60 @@ export async function runFix(flags = {}) {
99
138
  console.log(''); return 0;
100
139
  }
101
140
 
102
- // Load config for mainKey check (needed by both dry-run and apply paths)
141
+ // Load config for mainKey check
103
142
  const { config: cfg } = loadConfig();
104
143
  const mainKey = get(cfg, 'agents.mainKey', 'main');
105
144
 
106
145
  console.log('');
107
146
  if (flags.dryRun) {
108
147
  console.log(` ${paint.cyan('Dry run — would apply:')}`);
148
+ console.log('');
109
149
  for (const id of fixable) {
110
150
  const f = AUTO_FIXABLE[id];
151
+ const badge = IMPACT_BADGE[f.impact]();
111
152
  if (id === 'agents.sandbox' && mainKey !== 'main') {
112
153
  console.log(` ${paint.yellow('⚠')} ${paint.bold('Custom mainKey detected:')} agents.mainKey="${mainKey}"`);
113
154
  console.log(` ${paint.dim('Verify that sandbox.mode=non-main won\'t affect your main session.')}`);
114
155
  }
115
- console.log(` ${paint.dim('→')} ${f.desc}`);
116
- console.log(` ${paint.dim(f.cmd)}`);
117
- if (f.requiresDocker) console.log(` ${paint.yellow('⚠')} ${paint.dim('Requires Docker Desktop to be installed and running')}`);
118
- if (f.needsRestart) console.log(` ${paint.dim(' gateway restart required after applying')}`);
156
+ console.log(` ${badge} ${paint.dim('→')} ${f.desc}`);
157
+ console.log(` ${paint.dim('Impact:')} ${f.impactDetail}`);
158
+ console.log(` ${paint.dim(f.cmd)}`);
159
+ if (f.requiresDocker) console.log(` ${paint.yellow('⚠')} ${paint.dim('Requires Docker Desktop to be installed and running')}`);
160
+ if (f.needsRestart) console.log(` ${paint.dim('↻ gateway restart required after applying')}`);
161
+ console.log('');
162
+ }
163
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply safe + caution fixes.')}`);
164
+ if (breakingFixes.length) {
165
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply --force')} ${paint.dim('to apply ALL fixes (including breaking).')}`);
119
166
  }
120
- console.log('');
121
- console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply these fixes.')}`);
122
167
  console.log(''); return 0;
123
168
  }
124
169
 
125
170
  if (!flags.apply) {
126
- console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --dry-run')} ${paint.dim('to preview fixes.')}`);
127
- console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply them.')}`);
171
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --dry-run')} ${paint.dim('to preview fixes with impact analysis.')}`);
172
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply safe + caution fixes.')}`);
173
+ if (breakingFixes.length) {
174
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply --force')} ${paint.dim('to apply ALL fixes (including breaking).')}`);
175
+ }
128
176
  console.log(''); return 0;
129
177
  }
130
178
 
131
179
  // Apply fixes
132
- let applied = 0, failed = 0, needsRestart = false;
180
+ let applied = 0, failed = 0, skippedBreaking = 0, needsRestart = false;
133
181
  console.log(SEP);
134
182
  for (const id of fixable) {
135
183
  const f = AUTO_FIXABLE[id];
184
+ const badge = IMPACT_BADGE[f.impact]();
185
+
186
+ // Skip breaking unless --force
187
+ if (f.impact === IMPACT.BREAKING && !flags.force) {
188
+ console.log(` ${badge} ${f.desc}`);
189
+ console.log(` ${paint.dim('Impact:')} ${f.impactDetail}`);
190
+ console.log(` ${paint.red('⊘ Skipped')} ${paint.dim('(breaking — use --apply --force to include)')}`);
191
+ console.log('');
192
+ skippedBreaking++;
193
+ continue;
194
+ }
136
195
 
137
196
  // Warn about custom mainKey before sandbox fix
138
197
  if (id === 'agents.sandbox' && mainKey !== 'main') {
@@ -140,7 +199,7 @@ export async function runFix(flags = {}) {
140
199
  console.log(` ${paint.dim('Verify that sandbox.mode=non-main won\'t affect your main session.')}`);
141
200
  }
142
201
 
143
- process.stdout.write(` ${paint.dim('→')} ${f.desc}...`);
202
+ process.stdout.write(` ${badge} ${f.desc}...`);
144
203
  // Check Docker requirement
145
204
  if (f.requiresDocker) {
146
205
  const dockerCheck = spawnSync('docker', ['info'], { stdio: 'ignore' });
@@ -162,6 +221,9 @@ export async function runFix(flags = {}) {
162
221
  }
163
222
 
164
223
  console.log('');
224
+ if (skippedBreaking > 0) {
225
+ console.log(` ${paint.dim(`${skippedBreaking} breaking fix${skippedBreaking !== 1 ? 'es' : ''} skipped — use --apply --force to include`)}`);
226
+ }
165
227
  if (needsRestart) {
166
228
  console.log(` ${paint.yellow('!')} ${paint.bold('Restart required:')} ${paint.dim('openclaw gateway restart')}`);
167
229
  }
package/lib/harden.js CHANGED
@@ -2,7 +2,8 @@
2
2
  // Modes:
3
3
  // default: show each fix, prompt y/N before applying
4
4
  // --dry-run: show what WOULD be fixed, no writes
5
- // --auto: apply all safe fixes without confirmation (CI mode)
5
+ // --auto: apply all safe + caution fixes without confirmation (skips breaking)
6
+ // --auto --force: apply ALL fixes including breaking ones
6
7
 
7
8
  import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
8
9
  import { join } from 'path';
@@ -20,6 +21,29 @@ const HISTORY_FILE = join(CLAWARMOR_DIR, 'history.json');
20
21
  const CLI_PATH = new URL('../cli.js', import.meta.url).pathname;
21
22
  const SEP = paint.dim('─'.repeat(52));
22
23
 
24
+ // ── Impact levels ─────────────────────────────────────────────────────────────
25
+ // SAFE: No functionality impact. Pure security improvement.
26
+ // CAUTION: May change agent behavior. User should be aware.
27
+ // BREAKING: Will disable or restrict features currently in use.
28
+
29
+ const IMPACT = {
30
+ SAFE: 'safe',
31
+ CAUTION: 'caution',
32
+ BREAKING: 'breaking',
33
+ };
34
+
35
+ const IMPACT_BADGE = {
36
+ [IMPACT.SAFE]: (s) => paint.green(`🟢 Safe`),
37
+ [IMPACT.CAUTION]: (s) => paint.yellow(`🟡 Caution`),
38
+ [IMPACT.BREAKING]: (s) => paint.red(`🔴 Breaking`),
39
+ };
40
+
41
+ const IMPACT_LABEL = {
42
+ [IMPACT.SAFE]: 'No functionality impact',
43
+ [IMPACT.CAUTION]: 'May change agent behavior',
44
+ [IMPACT.BREAKING]: 'Will disable or restrict features you\'re actively using',
45
+ };
46
+
23
47
  function box(title) {
24
48
  const W = 52, pad = W - 2 - title.length, l = Math.floor(pad / 2), r = pad - l;
25
49
  return [
@@ -56,12 +80,23 @@ function buildFixes(config) {
56
80
  // Fix 1: world/group-readable credential files — chmod 600
57
81
  const badFiles = findWorldReadableCredFiles();
58
82
  for (const f of badFiles) {
83
+ // Classify: credential files (tokens, keys) are safe to lock down.
84
+ // Config files that other tools might read need caution.
85
+ const isSensitive = /\.(env|json|key|pem|token|secret)$/i.test(f.name) ||
86
+ f.name === 'agent-accounts.json' ||
87
+ f.name === 'openclaw.json';
88
+ const isScript = /\.(py|sh|js|ts)$/i.test(f.name);
89
+
59
90
  fixes.push({
60
91
  id: `cred.perms.${f.name}`,
61
92
  problem: `${f.name} is readable by other users (permissions: ${f.mode})`,
62
93
  action: `chmod 600 ${f.path}`,
63
94
  description: `Set permissions to 600 (owner-only) on ${f.path}`,
64
95
  type: 'shell',
96
+ impact: IMPACT.SAFE,
97
+ impactDetail: isScript
98
+ ? 'Only restricts other system users. Scripts will still run as you.'
99
+ : 'Only restricts other system users from reading this file. Your agent is unaffected.',
65
100
  manualNote: null,
66
101
  });
67
102
  }
@@ -75,6 +110,11 @@ function buildFixes(config) {
75
110
  action: 'openclaw config set gateway.host 127.0.0.1',
76
111
  description: 'Change gateway.host to 127.0.0.1 (loopback only)',
77
112
  type: 'openclaw',
113
+ impact: IMPACT.BREAKING,
114
+ impactDetail: 'Remote connections to the gateway will stop working.\n' +
115
+ ' If you access OpenClaw from other devices on your network (e.g. phone,\n' +
116
+ ' tablet, or another computer), those connections will be blocked.\n' +
117
+ ' Only localhost access will work after this change.',
78
118
  manualNote: 'Restart gateway after applying: openclaw gateway restart',
79
119
  });
80
120
  }
@@ -85,9 +125,14 @@ function buildFixes(config) {
85
125
  fixes.push({
86
126
  id: 'exec.ask.off',
87
127
  problem: 'exec.ask is off — shell commands run without user confirmation',
88
- action: 'openclaw config set exec.ask always',
89
- description: 'Enable exec.ask so shell commands require confirmation',
128
+ action: 'openclaw config set tools.exec.ask on-miss',
129
+ description: 'Enable exec approval for unrecognized commands',
90
130
  type: 'openclaw',
131
+ impact: IMPACT.BREAKING,
132
+ impactDetail: 'Your agent will need approval for shell commands not in the allowlist.\n' +
133
+ ' Autonomous workflows (cron jobs, background tasks, sub-agents) that run\n' +
134
+ ' shell commands may pause waiting for approval.\n' +
135
+ ' You\'ll need to approve commands via the web UI or CLI.',
91
136
  manualNote: 'Restart gateway after applying: openclaw gateway restart',
92
137
  });
93
138
  }
@@ -157,10 +202,24 @@ function getManualItems() {
157
202
  ];
158
203
  }
159
204
 
205
+ // ── Print impact summary ──────────────────────────────────────────────────────
206
+
207
+ function printImpactSummary(fixes) {
208
+ const counts = { [IMPACT.SAFE]: 0, [IMPACT.CAUTION]: 0, [IMPACT.BREAKING]: 0 };
209
+ for (const fix of fixes) counts[fix.impact]++;
210
+
211
+ const parts = [];
212
+ if (counts[IMPACT.SAFE]) parts.push(paint.green(`${counts[IMPACT.SAFE]} safe`));
213
+ if (counts[IMPACT.CAUTION]) parts.push(paint.yellow(`${counts[IMPACT.CAUTION]} caution`));
214
+ if (counts[IMPACT.BREAKING]) parts.push(paint.red(`${counts[IMPACT.BREAKING]} breaking`));
215
+
216
+ return parts.join(paint.dim(' · '));
217
+ }
218
+
160
219
  // ── Main export ───────────────────────────────────────────────────────────────
161
220
 
162
221
  export async function runHarden(flags = {}) {
163
- console.log(''); console.log(box('ClawArmor Harden v2.0')); console.log('');
222
+ console.log(''); console.log(box('ClawArmor Harden v2.1')); console.log('');
164
223
 
165
224
  const { config } = loadConfig();
166
225
  const fixes = buildFixes(config);
@@ -170,6 +229,11 @@ export async function runHarden(flags = {}) {
170
229
  const beforeScore = before?.score ?? null;
171
230
  const beforeGrade = before?.grade ?? null;
172
231
 
232
+ // Count by impact
233
+ const safeFixes = fixes.filter(f => f.impact === IMPACT.SAFE);
234
+ const cautionFixes = fixes.filter(f => f.impact === IMPACT.CAUTION);
235
+ const breakingFixes = fixes.filter(f => f.impact === IMPACT.BREAKING);
236
+
173
237
  // ── DRY RUN ────────────────────────────────────────────────────────────────
174
238
  if (flags.dryRun) {
175
239
  console.log(` ${paint.cyan('Dry run — showing what would be fixed (no changes applied):')}`);
@@ -183,17 +247,23 @@ export async function runHarden(flags = {}) {
183
247
  }
184
248
 
185
249
  for (const fix of fixes) {
250
+ const badge = IMPACT_BADGE[fix.impact]();
186
251
  console.log(` ${paint.yellow('!')} ${paint.bold(fix.problem)}`);
187
252
  console.log(` ${paint.dim('Fix:')} ${fix.description}`);
188
253
  console.log(` ${paint.dim('Cmd:')} ${fix.action}`);
254
+ console.log(` ${badge}${paint.dim(':')} ${fix.impactDetail}`);
189
255
  if (fix.manualNote) console.log(` ${paint.dim('Note:')} ${fix.manualNote}`);
190
256
  console.log('');
191
257
  }
192
258
 
193
259
  console.log(SEP);
194
- console.log(` ${fixes.length} fix${fixes.length !== 1 ? 'es' : ''} available.`);
260
+ console.log(` ${fixes.length} fix${fixes.length !== 1 ? 'es' : ''} available: ${printImpactSummary(fixes)}`);
261
+ console.log('');
195
262
  console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden')} ${paint.dim('to apply interactively.')}`);
196
- console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --auto')} ${paint.dim('to apply all without prompts.')}`);
263
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --auto')} ${paint.dim('to apply safe + caution fixes.')}`);
264
+ if (breakingFixes.length) {
265
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --auto --force')} ${paint.dim('to apply ALL fixes (including breaking).')}`);
266
+ }
197
267
  console.log('');
198
268
 
199
269
  const manualItems = getManualItems();
@@ -223,25 +293,52 @@ export async function runHarden(flags = {}) {
223
293
  return 0;
224
294
  }
225
295
 
226
- const modeLabel = flags.auto
227
- ? paint.cyan('Auto mode applying all safe fixes without confirmation')
228
- : paint.cyan('Interactive mode — review and apply fixes one by one');
229
- console.log(` ${modeLabel}`);
296
+ if (flags.auto) {
297
+ // --auto mode: apply safe + caution, SKIP breaking unless --force
298
+ const autoLabel = flags.force
299
+ ? paint.cyan('Auto mode (--force) — applying ALL fixes including breaking')
300
+ : paint.cyan('Auto mode — applying safe + caution fixes (skipping breaking)');
301
+ console.log(` ${autoLabel}`);
302
+ console.log(` ${paint.dim('Fixes found:')} ${printImpactSummary(fixes)}`);
303
+ } else {
304
+ console.log(` ${paint.cyan('Interactive mode — review and apply fixes one by one')}`);
305
+ console.log(` ${paint.dim('Fixes found:')} ${printImpactSummary(fixes)}`);
306
+ }
230
307
  console.log('');
231
308
 
232
- let applied = 0, skipped = 0, failed = 0;
309
+ let applied = 0, skipped = 0, failed = 0, skippedBreaking = 0;
233
310
  const restartNotes = [];
234
311
 
235
312
  for (const fix of fixes) {
313
+ const badge = IMPACT_BADGE[fix.impact]();
314
+
236
315
  console.log(SEP);
316
+ console.log(` ${badge}`);
237
317
  console.log(` ${paint.bold('Problem:')} ${fix.problem}`);
238
318
  console.log(` ${paint.dim('Fix:')} ${fix.description}`);
319
+ console.log(` ${paint.dim('Impact:')} ${fix.impactDetail}`);
239
320
  console.log(` ${paint.dim('Command:')} ${paint.dim(fix.action)}`);
240
321
  console.log('');
241
322
 
242
- let doApply = flags.auto;
323
+ let doApply;
243
324
 
244
- if (!flags.auto) {
325
+ if (flags.auto) {
326
+ // In auto mode: apply safe + caution, skip breaking unless --force
327
+ if (fix.impact === IMPACT.BREAKING && !flags.force) {
328
+ console.log(` ${paint.red('⊘ Skipped')} ${paint.dim('(breaking — use --auto --force to include)')}`);
329
+ skippedBreaking++;
330
+ skipped++;
331
+ console.log('');
332
+ continue;
333
+ }
334
+ doApply = true;
335
+ } else {
336
+ // Interactive mode: always ask, but warn about breaking
337
+ if (fix.impact === IMPACT.BREAKING) {
338
+ console.log(` ${paint.red('⚠ This fix will change how your agent works.')}`);
339
+ console.log(` ${paint.red(' Read the impact above carefully before applying.')}`);
340
+ console.log('');
341
+ }
245
342
  doApply = await askYN(` Apply this fix? [y/N] `);
246
343
  }
247
344
 
@@ -268,6 +365,10 @@ export async function runHarden(flags = {}) {
268
365
  console.log('');
269
366
  console.log(` Applied: ${paint.green(String(applied))} Skipped: ${paint.dim(String(skipped))} Failed: ${failed > 0 ? paint.red(String(failed)) : paint.dim('0')}`);
270
367
 
368
+ if (skippedBreaking > 0) {
369
+ console.log(` ${paint.dim(`(${skippedBreaking} breaking fix${skippedBreaking !== 1 ? 'es' : ''} skipped — use --auto --force to include)`)}`);
370
+ }
371
+
271
372
  // Restart notes
272
373
  if (restartNotes.length) {
273
374
  console.log('');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clawarmor",
3
- "version": "2.0.0",
4
- "description": "Security armor for OpenClaw agents audit, scan, monitor",
3
+ "version": "2.1.0",
4
+ "description": "Security armor for OpenClaw agents \u2014 audit, scan, monitor",
5
5
  "bin": {
6
6
  "clawarmor": "cli.js"
7
7
  },
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate a scripted asciinema .cast file for ClawArmor v2.0 demo.
4
+ Captures real command output with simulated typing for professional pacing.
5
+ """
6
+ import json
7
+ import subprocess
8
+ import time
9
+ import os
10
+
11
+ CAST_FILE = os.path.expanduser("~/clawarmor/demo.cast")
12
+ COLS = 110
13
+ ROWS = 35
14
+ PROMPT = "$ "
15
+
16
+ def run_cmd(cmd):
17
+ """Run a command and return its output."""
18
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True,
19
+ env={**os.environ, "FORCE_COLOR": "0", "NO_COLOR": "1", "TERM": "dumb"})
20
+ return result.stdout + result.stderr
21
+
22
+ def write_cast(events):
23
+ """Write asciicast v2 file."""
24
+ header = {
25
+ "version": 2,
26
+ "width": COLS,
27
+ "height": ROWS,
28
+ "timestamp": int(time.time()),
29
+ "title": "ClawArmor v2.0 — Security Audit Demo",
30
+ "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}
31
+ }
32
+ with open(CAST_FILE, 'w') as f:
33
+ f.write(json.dumps(header) + '\n')
34
+ for ts, etype, data in events:
35
+ f.write(json.dumps([round(ts, 6), etype, data]) + '\n')
36
+ print(f"Written to {CAST_FILE}")
37
+
38
+ def type_text(events, t, text, char_delay=0.045):
39
+ for ch in text:
40
+ events.append((t, "o", ch))
41
+ t += char_delay
42
+ return t
43
+
44
+ def output_text(events, t, text, line_delay=0.008):
45
+ lines = text.split('\n')
46
+ for i, line in enumerate(lines):
47
+ events.append((t, "o", line + ('\n' if i < len(lines) - 1 else '')))
48
+ t += line_delay
49
+ return t
50
+
51
+ def pause(t, seconds):
52
+ return t + seconds
53
+
54
+ def main():
55
+ events = []
56
+ t = 0.0
57
+
58
+ # Opening
59
+ t = output_text(events, t, PROMPT)
60
+ t = pause(t, 0.8)
61
+
62
+ # 1. Audit
63
+ t = type_text(events, t, "# Step 1: Check your OpenClaw security posture", 0.03)
64
+ events.append((t, "o", "\n")); t += 0.1
65
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
66
+ t = type_text(events, t, "clawarmor audit")
67
+ events.append((t, "o", "\n")); t += 0.2
68
+ output = run_cmd("clawarmor audit 2>&1")
69
+ t = output_text(events, t, output, 0.012)
70
+ t = pause(t, 3.0)
71
+
72
+ # 2. Harden dry-run
73
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
74
+ t = type_text(events, t, "# Step 2: See what ClawArmor can fix automatically", 0.03)
75
+ events.append((t, "o", "\n")); t += 0.1
76
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
77
+ t = type_text(events, t, "clawarmor harden --dry-run")
78
+ events.append((t, "o", "\n")); t += 0.2
79
+ output = run_cmd("clawarmor harden --dry-run 2>&1")
80
+ t = output_text(events, t, output, 0.012)
81
+ t = pause(t, 3.0)
82
+
83
+ # 3. Harden auto
84
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
85
+ t = type_text(events, t, "# Step 3: Fix everything automatically", 0.03)
86
+ events.append((t, "o", "\n")); t += 0.1
87
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
88
+ t = type_text(events, t, "clawarmor harden --auto")
89
+ events.append((t, "o", "\n")); t += 0.2
90
+ output = run_cmd("clawarmor harden --auto 2>&1")
91
+ t = output_text(events, t, output, 0.012)
92
+ t = pause(t, 2.5)
93
+
94
+ # 4. Re-audit
95
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
96
+ t = type_text(events, t, "# Step 4: Verify — re-audit after hardening", 0.03)
97
+ events.append((t, "o", "\n")); t += 0.1
98
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
99
+ t = type_text(events, t, "clawarmor audit")
100
+ events.append((t, "o", "\n")); t += 0.2
101
+ output = run_cmd("clawarmor audit 2>&1")
102
+ t = output_text(events, t, output, 0.012)
103
+ t = pause(t, 3.0)
104
+
105
+ # 5. Status
106
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
107
+ t = type_text(events, t, "# Bonus: Full security dashboard", 0.03)
108
+ events.append((t, "o", "\n")); t += 0.1
109
+ t = output_text(events, t, PROMPT); t = pause(t, 0.5)
110
+ t = type_text(events, t, "clawarmor status")
111
+ events.append((t, "o", "\n")); t += 0.2
112
+ output = run_cmd("clawarmor status 2>&1")
113
+ t = output_text(events, t, output, 0.012)
114
+ t = pause(t, 3.0)
115
+
116
+ # End
117
+ t = output_text(events, t, PROMPT)
118
+ t = pause(t, 1.0)
119
+
120
+ write_cast(events)
121
+ print(f"Total duration: {t:.1f}s")
122
+ print(f"Events: {len(events)}")
123
+
124
+ if __name__ == "__main__":
125
+ main()