shield-harness 0.3.0 → 0.4.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.
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ // policy-drift.js — Detect drift between permissions-spec.json deny rules and OpenShell policy YAML
3
+ // Part of Shield Harness: ensures policy files stay in sync with the canonical deny rules
4
+ "use strict";
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ // --- Dependencies (local modules) ---
10
+
11
+ const { loadPermissionsSpec } = require("./permissions-validator");
12
+ const { classifyRulesByDomain } = require("./tier-policy-gen");
13
+
14
+ // --- YAML Parsing (regex-based, no js-yaml dependency) ---
15
+
16
+ /**
17
+ * Extract a YAML list section by name.
18
+ * Expects predictable structure generated by tier-policy-gen.js:
19
+ * section_name:
20
+ * - item1
21
+ * - item2
22
+ *
23
+ * @param {string} content - Full YAML file content
24
+ * @param {string} sectionName - Name of the section (e.g., "deny_read")
25
+ * @returns {string[]} List of items found under the section
26
+ */
27
+ function extractYamlList(content, sectionName) {
28
+ // Match section header followed by indented list items
29
+ var pattern = new RegExp(
30
+ sectionName + ":\\n((?:[ \\t]+-[ \\t]+.+\\n)*)",
31
+ "m",
32
+ );
33
+ var match = content.match(pattern);
34
+ if (!match) return [];
35
+ return match[1]
36
+ .split("\n")
37
+ .filter(function (l) {
38
+ return l.trim().startsWith("- ");
39
+ })
40
+ .map(function (l) {
41
+ return l.trim().replace(/^-\s+/, "");
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Extract blocked network operations from YAML comment block.
47
+ * Expects format generated by tier-policy-gen.js:
48
+ * # Blocked network operations (from permissions-spec.json deny rules):
49
+ * # - curl *
50
+ * # - wget *
51
+ *
52
+ * @param {string} content - Full YAML file content
53
+ * @returns {string[]} List of blocked network commands
54
+ */
55
+ function extractBlockedNetworkComments(content) {
56
+ var pattern =
57
+ /# Blocked network operations[^\n]*:\n((?:#[ \t]+-[ \t]+.+\n)*)/m;
58
+ var match = content.match(pattern);
59
+ if (!match) return [];
60
+ return match[1]
61
+ .split("\n")
62
+ .filter(function (l) {
63
+ return l.trim().startsWith("#");
64
+ })
65
+ .map(function (l) {
66
+ return l.trim().replace(/^#\s+-\s+/, "");
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Parse all relevant sections from a policy YAML file.
72
+ * Returns structured data without requiring js-yaml.
73
+ *
74
+ * @param {string} content - Full YAML file content
75
+ * @returns {{
76
+ * denyRead: string[],
77
+ * denyWrite: string[],
78
+ * readWrite: string[],
79
+ * blockedNetwork: string[]
80
+ * }}
81
+ */
82
+ function parsePolicyYaml(content) {
83
+ return {
84
+ denyRead: extractYamlList(content, "deny_read"),
85
+ denyWrite: extractYamlList(content, "deny_write"),
86
+ readWrite: extractYamlList(content, "read_write"),
87
+ blockedNetwork: extractBlockedNetworkComments(content),
88
+ };
89
+ }
90
+
91
+ // --- Drift Detection ---
92
+
93
+ /**
94
+ * Check drift between permissions-spec deny rules and OpenShell policy YAML.
95
+ *
96
+ * Compares the classified deny rules from permissions-spec.json against
97
+ * the content of policy YAML files in the specified directory.
98
+ *
99
+ * Drift rules:
100
+ * 1. Missing filesystem deny (read): spec denyRead paths not in policy deny_read
101
+ * 2. Missing filesystem deny (write): spec denyWrite paths not in policy deny_write
102
+ * 3. Policy allows denied: paths in denyRead/denyWrite that also appear in read_write
103
+ * 4. Missing network deny: network commands not in blocked network comments
104
+ * 5. No policy files: skip (no drift reported)
105
+ * 6. No spec file: skip (no drift reported)
106
+ * 7. Parse errors: fail-safe (no drift, warning logged)
107
+ *
108
+ * @param {{ specPath: string, policyDir: string }} params
109
+ * @returns {{
110
+ * has_drift: boolean,
111
+ * warnings: string[],
112
+ * details: {
113
+ * missing_filesystem_deny: string[],
114
+ * missing_network_deny: string[],
115
+ * policy_allows_denied: string[]
116
+ * },
117
+ * checked_at: string
118
+ * }}
119
+ */
120
+ function checkPolicyDrift({ specPath, policyDir }) {
121
+ var emptyResult = {
122
+ has_drift: false,
123
+ warnings: [],
124
+ details: {
125
+ missing_filesystem_deny: [],
126
+ missing_network_deny: [],
127
+ policy_allows_denied: [],
128
+ },
129
+ checked_at: new Date().toISOString(),
130
+ };
131
+
132
+ // Rule 6: No spec file — skip silently
133
+ if (!specPath || !fs.existsSync(specPath)) {
134
+ return emptyResult;
135
+ }
136
+
137
+ // Rule 5: No policy directory or no .yaml files — skip silently
138
+ if (!policyDir || !fs.existsSync(policyDir)) {
139
+ return emptyResult;
140
+ }
141
+
142
+ var policyFiles;
143
+ try {
144
+ policyFiles = fs.readdirSync(policyDir).filter(function (f) {
145
+ return f.endsWith(".yaml") || f.endsWith(".yml");
146
+ });
147
+ } catch (e) {
148
+ return emptyResult;
149
+ }
150
+
151
+ if (policyFiles.length === 0) {
152
+ return emptyResult;
153
+ }
154
+
155
+ // Load and classify spec deny rules
156
+ var spec;
157
+ try {
158
+ spec = loadPermissionsSpec(specPath);
159
+ } catch (err) {
160
+ return {
161
+ has_drift: false,
162
+ warnings: ["parse_error: spec load failed \u2014 " + err.message],
163
+ details: {
164
+ missing_filesystem_deny: [],
165
+ missing_network_deny: [],
166
+ policy_allows_denied: [],
167
+ },
168
+ checked_at: new Date().toISOString(),
169
+ };
170
+ }
171
+
172
+ var classified;
173
+ try {
174
+ classified = classifyRulesByDomain(spec.deny);
175
+ } catch (err) {
176
+ return {
177
+ has_drift: false,
178
+ warnings: [
179
+ "parse_error: rule classification failed \u2014 " + err.message,
180
+ ],
181
+ details: {
182
+ missing_filesystem_deny: [],
183
+ missing_network_deny: [],
184
+ policy_allows_denied: [],
185
+ },
186
+ checked_at: new Date().toISOString(),
187
+ };
188
+ }
189
+
190
+ // Aggregate policy content from all YAML files
191
+ var aggregated = {
192
+ denyRead: new Set(),
193
+ denyWrite: new Set(),
194
+ readWrite: new Set(),
195
+ blockedNetwork: new Set(),
196
+ };
197
+
198
+ for (var i = 0; i < policyFiles.length; i++) {
199
+ var filePath = path.join(policyDir, policyFiles[i]);
200
+ var content;
201
+ try {
202
+ content = fs.readFileSync(filePath, "utf8");
203
+ } catch (err) {
204
+ // Rule 7: Parse error — fail-safe
205
+ return {
206
+ has_drift: false,
207
+ warnings: [
208
+ "parse_error: cannot read " + filePath + " \u2014 " + err.message,
209
+ ],
210
+ details: {
211
+ missing_filesystem_deny: [],
212
+ missing_network_deny: [],
213
+ policy_allows_denied: [],
214
+ },
215
+ checked_at: new Date().toISOString(),
216
+ };
217
+ }
218
+
219
+ var parsed;
220
+ try {
221
+ parsed = parsePolicyYaml(content);
222
+ } catch (err) {
223
+ return {
224
+ has_drift: false,
225
+ warnings: [
226
+ "parse_error: cannot parse " + filePath + " \u2014 " + err.message,
227
+ ],
228
+ details: {
229
+ missing_filesystem_deny: [],
230
+ missing_network_deny: [],
231
+ policy_allows_denied: [],
232
+ },
233
+ checked_at: new Date().toISOString(),
234
+ };
235
+ }
236
+
237
+ parsed.denyRead.forEach(function (p) {
238
+ aggregated.denyRead.add(p);
239
+ });
240
+ parsed.denyWrite.forEach(function (p) {
241
+ aggregated.denyWrite.add(p);
242
+ });
243
+ parsed.readWrite.forEach(function (p) {
244
+ aggregated.readWrite.add(p);
245
+ });
246
+ parsed.blockedNetwork.forEach(function (cmd) {
247
+ aggregated.blockedNetwork.add(cmd);
248
+ });
249
+ }
250
+
251
+ // --- Compare classified rules against aggregated policy ---
252
+
253
+ var warnings = [];
254
+ var missingFilesystemDeny = [];
255
+ var missingNetworkDeny = [];
256
+ var policyAllowsDenied = [];
257
+
258
+ // Rule 1: Missing filesystem deny (read)
259
+ for (var ri = 0; ri < classified.denyRead.length; ri++) {
260
+ var readPath = classified.denyRead[ri];
261
+ if (!aggregated.denyRead.has(readPath)) {
262
+ warnings.push("deny_read missing in policy: " + readPath);
263
+ missingFilesystemDeny.push(readPath);
264
+ }
265
+ }
266
+
267
+ // Rule 2: Missing filesystem deny (write)
268
+ for (var wi = 0; wi < classified.denyWrite.length; wi++) {
269
+ var writePath = classified.denyWrite[wi];
270
+ if (!aggregated.denyWrite.has(writePath)) {
271
+ warnings.push("deny_write missing in policy: " + writePath);
272
+ missingFilesystemDeny.push(writePath);
273
+ }
274
+ }
275
+
276
+ // Rule 3: Policy allows denied — conflict detection
277
+ var allDeniedPaths = new Set(
278
+ classified.denyRead.concat(classified.denyWrite),
279
+ );
280
+ aggregated.readWrite.forEach(function (rwPath) {
281
+ if (allDeniedPaths.has(rwPath)) {
282
+ warnings.push("conflict: read_write allows denied path: " + rwPath);
283
+ policyAllowsDenied.push(rwPath);
284
+ }
285
+ });
286
+
287
+ // Rule 4: Missing network deny
288
+ for (var ni = 0; ni < classified.network.length; ni++) {
289
+ var netCmd = classified.network[ni];
290
+ if (!aggregated.blockedNetwork.has(netCmd)) {
291
+ warnings.push(
292
+ "blocked network command missing in policy comments: " + netCmd,
293
+ );
294
+ missingNetworkDeny.push(netCmd);
295
+ }
296
+ }
297
+
298
+ var hasDrift =
299
+ missingFilesystemDeny.length > 0 ||
300
+ missingNetworkDeny.length > 0 ||
301
+ policyAllowsDenied.length > 0;
302
+
303
+ return {
304
+ has_drift: hasDrift,
305
+ warnings: warnings,
306
+ details: {
307
+ missing_filesystem_deny: missingFilesystemDeny,
308
+ missing_network_deny: missingNetworkDeny,
309
+ policy_allows_denied: policyAllowsDenied,
310
+ },
311
+ checked_at: new Date().toISOString(),
312
+ };
313
+ }
314
+
315
+ module.exports = {
316
+ // Main entry point
317
+ checkPolicyDrift: checkPolicyDrift,
318
+ // Parsing helpers (exported for unit testing)
319
+ extractYamlList: extractYamlList,
320
+ extractBlockedNetworkComments: extractBlockedNetworkComments,
321
+ parsePolicyYaml: parsePolicyYaml,
322
+ };
@@ -18,6 +18,7 @@ const {
18
18
  const HOOK_NAME = "sh-config-guard";
19
19
  const SETTINGS_FILE = path.join(".claude", "settings.json");
20
20
  const CONFIG_HASH_FILE = path.join(".claude", "logs", "config-hash.json");
21
+ const POLICIES_DIR = path.join(".claude", "policies");
21
22
 
22
23
  // ---------------------------------------------------------------------------
23
24
  // Config Analysis
@@ -63,10 +64,91 @@ function saveConfigSnapshot(snapshot) {
63
64
  fs.writeFileSync(CONFIG_HASH_FILE, JSON.stringify(snapshot, null, 2));
64
65
  }
65
66
 
67
+ /**
68
+ * Count items in a YAML list section.
69
+ * Matches a section header like "deny_read:" followed by indented list items.
70
+ * @param {string} content - YAML file content
71
+ * @param {string} sectionName - Name of the YAML section to count
72
+ * @returns {number} Number of list items in the section
73
+ */
74
+ function countYamlListItems(content, sectionName) {
75
+ const regex = new RegExp(sectionName + ":\\n((?:\\s+-\\s+.+\\n)*)", "m");
76
+ const match = content.match(regex);
77
+ if (!match) return 0;
78
+ return match[1].split("\n").filter((l) => l.trim().startsWith("- ")).length;
79
+ }
80
+
81
+ /**
82
+ * Count host: entries in network_policies section.
83
+ * @param {string} content - YAML file content
84
+ * @returns {number} Number of network endpoint entries
85
+ */
86
+ function countNetworkEndpoints(content) {
87
+ const matches = content.match(/^\s+[-\s]*host:\s/gm);
88
+ return matches ? matches.length : 0;
89
+ }
90
+
91
+ /**
92
+ * Scan .claude/policies/ for YAML files and compute SHA-256 hash of each.
93
+ * @returns {Object.<string, string>} Map of file path to SHA-256 hash
94
+ */
95
+ function extractPolicyHashes() {
96
+ const hashes = {};
97
+ if (!fs.existsSync(POLICIES_DIR)) return hashes;
98
+ try {
99
+ const files = fs
100
+ .readdirSync(POLICIES_DIR)
101
+ .filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
102
+ for (const file of files) {
103
+ const filePath = path.join(POLICIES_DIR, file);
104
+ try {
105
+ const content = fs.readFileSync(filePath, "utf8");
106
+ hashes[filePath] = sha256(content);
107
+ } catch {
108
+ /* skip unreadable */
109
+ }
110
+ }
111
+ } catch {
112
+ /* dir read error */
113
+ }
114
+ return hashes;
115
+ }
116
+
117
+ /**
118
+ * Extract security-critical counts from each YAML policy file.
119
+ * @returns {Object.<string, { deny_read_count: number, deny_write_count: number, read_write_count: number, network_endpoint_count: number }>}
120
+ */
121
+ function extractPolicyMetrics() {
122
+ const metrics = {};
123
+ if (!fs.existsSync(POLICIES_DIR)) return metrics;
124
+ try {
125
+ const files = fs
126
+ .readdirSync(POLICIES_DIR)
127
+ .filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
128
+ for (const file of files) {
129
+ const filePath = path.join(POLICIES_DIR, file);
130
+ try {
131
+ const content = fs.readFileSync(filePath, "utf8");
132
+ metrics[filePath] = {
133
+ deny_read_count: countYamlListItems(content, "deny_read"),
134
+ deny_write_count: countYamlListItems(content, "deny_write"),
135
+ read_write_count: countYamlListItems(content, "read_write"),
136
+ network_endpoint_count: countNetworkEndpoints(content),
137
+ };
138
+ } catch {
139
+ /* skip */
140
+ }
141
+ }
142
+ } catch {
143
+ /* dir read error */
144
+ }
145
+ return metrics;
146
+ }
147
+
66
148
  /**
67
149
  * Extract security-critical fields from settings.
68
150
  * @param {Object} settings
69
- * @returns {{ deny_rules: string[], hook_count: number, hook_events: string[], hook_commands: string[], sandbox: boolean, unsandboxed: boolean, disableAllHooks: boolean }}
151
+ * @returns {{ deny_rules: string[], hook_count: number, hook_events: string[], hook_commands: string[], sandbox: boolean, unsandboxed: boolean, disableAllHooks: boolean, policy_hashes: Object, policy_metrics: Object }}
70
152
  */
71
153
  function extractSecurityFields(settings) {
72
154
  const denyRules = (settings.permissions && settings.permissions.deny) || [];
@@ -100,6 +182,8 @@ function extractSecurityFields(settings) {
100
182
  : true,
101
183
  unsandboxed: Boolean(settings.allowUnsandboxedCommands),
102
184
  disableAllHooks: Boolean(settings.disableAllHooks),
185
+ policy_hashes: extractPolicyHashes(),
186
+ policy_metrics: extractPolicyMetrics(),
103
187
  };
104
188
  }
105
189
 
@@ -156,6 +240,52 @@ function detectDangerousMutations(stored, current) {
156
240
  reasons.push("disableAllHooks set to true");
157
241
  }
158
242
 
243
+ // Check 6: OpenShell policy file tampering (ADR-037 GA Phase)
244
+ const storedHashes = stored.policy_hashes || {};
245
+ const currentHashes = current.policy_hashes || {};
246
+ const storedMetrics = stored.policy_metrics || {};
247
+ const currentMetrics = current.policy_metrics || {};
248
+
249
+ for (const [filePath, storedHash] of Object.entries(storedHashes)) {
250
+ if (!(filePath in currentHashes)) {
251
+ // Policy file was deleted
252
+ reasons.push(`OpenShell policy file removed: "${filePath}"`);
253
+ continue;
254
+ }
255
+ if (currentHashes[filePath] !== storedHash) {
256
+ // Policy file changed — check for weakening
257
+ const sm = storedMetrics[filePath] || {};
258
+ const cm = currentMetrics[filePath] || {};
259
+
260
+ if (
261
+ sm.deny_read_count > 0 &&
262
+ (cm.deny_read_count || 0) < sm.deny_read_count
263
+ ) {
264
+ reasons.push(
265
+ `OpenShell policy weakened: deny_read reduced (${sm.deny_read_count} → ${cm.deny_read_count || 0}) in "${filePath}"`,
266
+ );
267
+ }
268
+ if (
269
+ sm.deny_write_count > 0 &&
270
+ (cm.deny_write_count || 0) < sm.deny_write_count
271
+ ) {
272
+ reasons.push(
273
+ `OpenShell policy weakened: deny_write reduced (${sm.deny_write_count} → ${cm.deny_write_count || 0}) in "${filePath}"`,
274
+ );
275
+ }
276
+ if ((cm.network_endpoint_count || 0) > (sm.network_endpoint_count || 0)) {
277
+ reasons.push(
278
+ `OpenShell policy weakened: network endpoints expanded (${sm.network_endpoint_count || 0} → ${cm.network_endpoint_count}) in "${filePath}"`,
279
+ );
280
+ }
281
+ if ((cm.read_write_count || 0) > (sm.read_write_count || 0)) {
282
+ reasons.push(
283
+ `OpenShell policy weakened: read_write paths expanded (${sm.read_write_count || 0} → ${cm.read_write_count}) in "${filePath}"`,
284
+ );
285
+ }
286
+ }
287
+ }
288
+
159
289
  return {
160
290
  blocked: reasons.length > 0,
161
291
  reasons,
@@ -272,4 +402,8 @@ module.exports = {
272
402
  saveConfigSnapshot,
273
403
  extractSecurityFields,
274
404
  detectDangerousMutations,
405
+ extractPolicyHashes,
406
+ extractPolicyMetrics,
407
+ countYamlListItems,
408
+ countNetworkEndpoints,
275
409
  };
@@ -17,6 +17,7 @@ const {
17
17
  } = require("./lib/sh-utils");
18
18
  const { detectOpenShell } = require("./lib/openshell-detect");
19
19
  const { checkPolicyCompatibility } = require("./lib/policy-compat");
20
+ const { checkPolicyDrift } = require("./lib/policy-drift");
20
21
 
21
22
  const HOOK_NAME = "sh-session-start";
22
23
  const CLAUDE_MD = "CLAUDE.md";
@@ -230,6 +231,38 @@ try {
230
231
  }
231
232
  }
232
233
 
234
+ // 2e: Policy drift check (ADR-037 GA Phase)
235
+ const POLICIES_DIR = path.join(".claude", "policies");
236
+ if (fs.existsSync(POLICIES_DIR)) {
237
+ try {
238
+ const driftResult = checkPolicyDrift({
239
+ specPath: PERM_SPEC_FILE,
240
+ policyDir: POLICIES_DIR,
241
+ });
242
+ session.policy_drift = driftResult;
243
+ writeSession(session);
244
+
245
+ if (driftResult.has_drift) {
246
+ contextParts.push(
247
+ `[layer-3b] WARNING: Policy drift detected — ${driftResult.warnings.length} issue(s) found`,
248
+ );
249
+ for (const warning of driftResult.warnings.slice(0, 3)) {
250
+ contextParts.push(`[layer-3b] ${warning}`);
251
+ }
252
+ if (driftResult.warnings.length > 3) {
253
+ contextParts.push(
254
+ `[layer-3b] ... and ${driftResult.warnings.length - 3} more`,
255
+ );
256
+ }
257
+ contextParts.push(
258
+ "[layer-3b] Run: npx shield-harness generate-policy to regenerate",
259
+ );
260
+ }
261
+ } catch {
262
+ // drift check failure is non-blocking
263
+ }
264
+ }
265
+
233
266
  // --- Module 3: Version Check (§5.1.4) ---
234
267
  // Store baseline hashes for instructions monitoring
235
268
  const hashes = {};
@@ -273,6 +306,14 @@ try {
273
306
  sandbox_version: openshellResult.version || null,
274
307
  sandbox_policy_enforced:
275
308
  openshellResult.available && openshellResult.container_running,
309
+ policy_drift: session.policy_drift
310
+ ? {
311
+ has_drift: session.policy_drift.has_drift,
312
+ warning_count: session.policy_drift.warnings
313
+ ? session.policy_drift.warnings.length
314
+ : 0,
315
+ }
316
+ : null,
276
317
  policy_compat: policyCompat
277
318
  ? {
278
319
  compatible: policyCompat.compatible,
@@ -90,6 +90,16 @@
90
90
  "rationale": "Permissions SoT self-protection",
91
91
  "threat_id": "T-03"
92
92
  },
93
+ {
94
+ "rule": "Edit(.claude/policies/**)",
95
+ "rationale": "OpenShell policy file protection (ADR-037 GA)",
96
+ "threat_id": "T-03"
97
+ },
98
+ {
99
+ "rule": "Write(.claude/policies/**)",
100
+ "rationale": "OpenShell policy file protection (ADR-037 GA)",
101
+ "threat_id": "T-03"
102
+ },
93
103
  {
94
104
  "rule": "Bash(rm -rf /)",
95
105
  "rationale": "System destruction prevention",
@@ -433,7 +443,7 @@
433
443
  ]
434
444
  },
435
445
  "expected_counts": {
436
- "deny": 41,
446
+ "deny": 43,
437
447
  "ask": 4,
438
448
  "allow": 49
439
449
  }
@@ -0,0 +1,105 @@
1
+ # Auto-generated by Shield Harness tier-policy-gen
2
+ # Source: permissions-spec.json v1.0.0
3
+ # Profile: standard
4
+ # Generated: 2026-03-24T06:03:59.030Z
5
+ #
6
+ # Usage:
7
+ # openshell sandbox create --policy <this-file> -- claude
8
+ #
9
+ # Static policies require sandbox recreation to change.
10
+ # Network policies can be hot-reloaded: openshell policy set <name> --policy <file> --wait
11
+
12
+ version: 1
13
+
14
+ # --- Static (locked at sandbox creation) ---
15
+
16
+ filesystem_policy:
17
+ include_workdir: true
18
+ deny_read:
19
+ - ~/.ssh
20
+ - ~/.aws
21
+ - ~/.gnupg
22
+ - **/.env
23
+ - **/.env.*
24
+ - **/credentials*
25
+ - ./**/*.pem
26
+ - ./**/*.key
27
+ - ./**/*secret*
28
+ - ~/.config/gcloud
29
+ deny_write:
30
+ - .claude/hooks
31
+ - .claude/rules
32
+ - .claude/skills
33
+ - .claude/settings.json
34
+ - .claude/permissions-spec.json
35
+ - .claude/settings.local.json
36
+ - .claude/policies
37
+ - tasks/backlog.yaml
38
+ - .shield-harness
39
+ - .claude/patterns
40
+ read_only:
41
+ - /usr
42
+ - /lib
43
+ - /etc
44
+ read_write:
45
+ - /sandbox
46
+ - /tmp
47
+
48
+ landlock:
49
+ compatibility: best_effort
50
+
51
+ process:
52
+ run_as_user: sandbox
53
+ run_as_group: sandbox
54
+
55
+ # --- Dynamic (hot-reloadable) ---
56
+
57
+ network_policies:
58
+ anthropic_api:
59
+ name: anthropic-api
60
+ endpoints:
61
+ - host: api.anthropic.com
62
+ port: 443
63
+ access: full
64
+ binaries:
65
+ - path: /usr/local/bin/claude
66
+
67
+ github:
68
+ name: github
69
+ endpoints:
70
+ - host: github.com
71
+ port: 443
72
+ access: read-only
73
+ - host: "*.githubusercontent.com"
74
+ port: 443
75
+ access: read-only
76
+ binaries:
77
+ - path: /usr/bin/git
78
+
79
+ npm_registry:
80
+ name: npm-registry
81
+ endpoints:
82
+ - host: registry.npmjs.org
83
+ port: 443
84
+ access: read-only
85
+ binaries:
86
+ - path: /usr/bin/npm
87
+ - path: /usr/bin/node
88
+
89
+ # Blocked network operations (from permissions-spec.json deny rules):
90
+ # - curl *
91
+ # - wget *
92
+ # - Invoke-WebRequest *
93
+ # - nc *
94
+ # - ncat *
95
+ # - nmap *
96
+ # - git push --force *
97
+ # - npm publish *
98
+
99
+ # Blocked process operations (from permissions-spec.json deny rules):
100
+ # - rm -rf /
101
+ # - rm -rf ~
102
+ # - del /s /q C:\\
103
+ # - format *
104
+ # - cat */.ssh/*
105
+ # - type *\\.ssh\\*
package/README.ja.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  **Claude Code の全操作を自動防御するセキュリティハーネス**
6
6
 
7
- > 承認ダイアログなしで安全な自律開発を実現
7
+ > フック駆動の自動判定で安全な自律開発を実現
8
8
 
9
- > **Alpha (v0.1.0)**: セキュリティモデルは開発中です。パーミッションルールと設計ドキュメントの整合作業を進めています。本番利用は推奨しません。
9
+ > **v0.4.0**: 22 フック、4 層防御(L1 権限 + L2 フック + L3 サンドボックス + L3b OpenShell)、391 テスト(OWASP AITG 攻撃シミュレーション 108 テスト含む)。
10
10
 
11
11
  [![English](https://img.shields.io/badge/lang-English-blue?style=flat-square)](README.md)
12
12
  [![日本語](https://img.shields.io/badge/lang-日本語-red?style=flat-square)](#)
@@ -16,7 +16,7 @@
16
16
  ## Shield Harness とは
17
17
 
18
18
  Claude Code の全操作を自動防御するセキュリティハーネス。
19
- 承認ダイアログなしで安全な自律開発を実現します。`.claude/` ディレクトリに展開される hooks + rules + permissions による多層防御でエージェントを統制します。
19
+ フック駆動の自動判定で安全な自律開発を実現します。`.claude/` ディレクトリに展開される hooks + rules + permissions による多層防御でエージェントを統制します。
20
20
 
21
21
  ## クイックスタート
22
22
 
@@ -27,13 +27,13 @@ npx shield-harness init [--profile minimal|standard|strict]
27
27
  ## なぜ Shield Harness なのか
28
28
 
29
29
  - **フック駆動の防御**: 22 のセキュリティフックが Claude Code の全操作を監視
30
- - **承認レスモード**: hooks に全セキュリティ判定を委譲し、人間の承認ダイアログを排除
30
+ - **自動セキュリティ判定**: hooks が全セキュリティ判断をリアルタイムで処理 — 手動承認のボトルネックなし
31
31
  - **fail-close 原則**: 安全条件を確認できない場合は自動的に停止
32
32
  - **証跡記録**: SHA-256 ハッシュチェーンで全 allow/deny 決定を改ざん不能な形で記録
33
33
 
34
34
  ## アーキテクチャ概要
35
35
 
36
- 3 層防御モデル:
36
+ 4 層防御モデル:
37
37
 
38
38
  | 層 | 防御 | 実装 |
39
39
  | -------- | ---------------------- | ------------------------------------------------------------- |
@@ -52,30 +52,30 @@ npx shield-harness init [--profile minimal|standard|strict]
52
52
 
53
53
  ## フックカタログ
54
54
 
55
- | # | フック | イベント | 責務 |
56
- | --- | ---------------- | --------------------- | --------------------------------------------- |
57
- | 1 | permission | PreToolUse | ツール使用の 4 カテゴリ分類 |
58
- | 2 | gate | PreToolUse | Bash コマンドの 7 攻撃ベクトル検査 |
59
- | 3 | injection-guard | PreToolUse | 9 カテゴリ 50+ パターンのインジェクション検出 |
60
- | 4 | data-boundary | PreToolUse | 本番データ境界 + 管轄追跡 |
61
- | 5 | quiet-inject | PreToolUse | quiet フラグ自動注入 |
62
- | 6 | evidence | PostToolUse | SHA-256 ハッシュチェーン証跡 |
63
- | 7 | output-control | PostToolUse | 出力トランケーション + トークン予算 |
64
- | 8 | dep-audit | PostToolUse | パッケージインストール検出 |
65
- | 9 | lint-on-save | PostToolUse | 自動 lint 実行 |
66
- | 10 | session-start | SessionStart | セッション初期化 + 整合性ベースライン |
67
- | 11 | session-end | SessionEnd | クリーンアップ + 統計 |
68
- | 12 | circuit-breaker | Stop | リトライ上限 (3 回) |
69
- | 13 | config-guard | ConfigChange | 設定変更の監視 |
70
- | 14 | user-prompt | UserPromptSubmit | ユーザー入力のインジェクション検査 |
71
- | 15 | permission-learn | PermissionRequest | 権限学習ガード |
72
- | 16 | elicitation | Elicitation | フィッシング + スコープガード |
73
- | 17 | subagent | SubagentStart | サブエージェント予算制約 (25%) |
74
- | 18 | instructions | InstructionsLoaded | ルールファイル整合性監視 |
75
- | 19 | precompact | PreCompact | コンパクション前バックアップ |
76
- | 20 | postcompact | PostCompact | コンパクション後復元 + 検証 |
77
- | 21 | worktree | WorktreeCreate/Remove | セキュリティ伝播 + 証跡マージ |
78
- | 22 | task-gate | TaskCompleted | テストゲート |
55
+ | # | フック | イベント | 責務 |
56
+ | --- | ---------------- | --------------------- | ----------------------------------------------- |
57
+ | 1 | permission | PreToolUse | ツール使用の 4 カテゴリ分類 |
58
+ | 2 | gate | PreToolUse | Bash コマンドの 7 攻撃ベクトル検査 |
59
+ | 3 | injection-guard | PreToolUse | 9 カテゴリ 50+ パターンのインジェクション検出 |
60
+ | 4 | data-boundary | PreToolUse | 本番データ境界 + 管轄追跡 |
61
+ | 5 | quiet-inject | PreToolUse | quiet フラグ自動注入 |
62
+ | 6 | evidence | PostToolUse | SHA-256 ハッシュチェーン証跡 |
63
+ | 7 | output-control | PostToolUse | 出力トランケーション + トークン予算 |
64
+ | 8 | dep-audit | PostToolUse | パッケージインストール検出 |
65
+ | 9 | lint-on-save | PostToolUse | 自動 lint 実行 |
66
+ | 10 | session-start | SessionStart | セッション初期化 + 整合性ベースライン |
67
+ | 11 | session-end | SessionEnd | クリーンアップ + 統計 |
68
+ | 12 | circuit-breaker | Stop | リトライ上限 (3 回) |
69
+ | 13 | config-guard | ConfigChange | 設定変更の監視 + OpenShell ポリシーファイル保護 |
70
+ | 14 | user-prompt | UserPromptSubmit | ユーザー入力のインジェクション検査 |
71
+ | 15 | permission-learn | PermissionRequest | 権限学習ガード |
72
+ | 16 | elicitation | Elicitation | フィッシング + スコープガード |
73
+ | 17 | subagent | SubagentStart | サブエージェント予算制約 (25%) |
74
+ | 18 | instructions | InstructionsLoaded | ルールファイル整合性監視 |
75
+ | 19 | precompact | PreCompact | コンパクション前バックアップ |
76
+ | 20 | postcompact | PostCompact | コンパクション後復元 + 検証 |
77
+ | 21 | worktree | WorktreeCreate/Remove | セキュリティ伝播 + 証跡マージ |
78
+ | 22 | task-gate | TaskCompleted | テストゲート |
79
79
 
80
80
  ## パイプライン
81
81
 
@@ -148,10 +148,62 @@ Windows ユーザーにとっての主なメリット:
148
148
 
149
149
  - ポリシーがエージェントプロセスの**外部**に存在 — エージェント自身がガードレールを無効化できない
150
150
  - Docker Desktop + WSL2 バックエンド(一般的な Windows 開発環境)で動作
151
- - 残余リスクを 5% から 1% 未満に低減
151
+ - Layer 1-2 のパターンマッチング限界による残余リスクを大幅に低減
152
152
  - 自由に取り外し可能 — コンテナを停止すれば Shield Harness は Layer 1-2 にフォールバック
153
153
 
154
- > **注意**: OpenShell は Alpha(v0.0.13)— API は将来変更の可能性があります。
154
+ > **注意**: OpenShell は Alpha(v0.0.13)— API は将来変更の可能性があります。Shield Harness 側の GA Phase 統合は完了済み(ADR-037): config guard によるポリシーファイル保護、ポリシードリフトチェック、全ドキュメント整備が完了しています。
155
+
156
+ #### セットアップ
157
+
158
+ **前提条件**: [Docker Desktop](https://www.docker.com/products/docker-desktop/)(Windows では WSL2 バックエンド)
159
+
160
+ ```bash
161
+ # 1. Docker Desktop をインストールし、起動を確認
162
+ # https://www.docker.com/products/docker-desktop/
163
+ docker --version
164
+
165
+ # 2. OpenShell CLI のインストール
166
+ pip install openshell
167
+
168
+ # 3. permissions-spec.json からポリシーを生成
169
+ # .claude/policies/openshell-generated.yaml が作成されます
170
+ npx shield-harness policy generate
171
+
172
+ # 4. OpenShell コンテナを起動し、その中で Claude Code を実行
173
+ # 初回実行時に Docker がサンドボックスイメージを自動取得します
174
+ # コンテナ内ではカーネルレベル制限(Landlock/Seccomp/Network NS)が自動適用されます
175
+ openshell run --policy .claude/policies/openshell-generated.yaml
176
+ ```
177
+
178
+ OpenShell コンテナ内で動作する Claude Code には、Layer 3b のカーネル強制が自動的に適用されます。Shield Harness はセッション開始時にこれを検出します(`sh-session-start.js`)— 追加設定は不要です。
179
+
180
+ OpenShell なしの場合、Shield Harness は Layer 1-2 防御にフォールバックします(フック保護に劣化なし)。
181
+
182
+ ポリシーファイルは以下で保護されます:
183
+
184
+ - `permissions.deny`: `Edit/Write(.claude/policies/**)` でエージェントによる変更をブロック
185
+ - `sandbox.denyWrite`: `.claude/policies` がファイルシステム deny リストに含まれる
186
+ - `sh-config-guard.js`: ハッシュ追跡でポリシーファイルの改竄・弱体化を検出
187
+ - `sh-session-start.js`: セッション開始時のドリフトチェックで spec-policy 整合性を検証
188
+
189
+ ## テスト
190
+
191
+ ```bash
192
+ # 全テスト実行(391 テスト、OWASP AITG 攻撃シミュレーション 108 テスト含む)
193
+ npm test
194
+
195
+ # 攻撃シミュレーションテストのみ実行
196
+ node --test tests/attack-sim-*.test.js
197
+ ```
198
+
199
+ | テストスイート | OWASP カテゴリ | テスト数 |
200
+ | ----------------------------- | -------------------------------------- | -------- |
201
+ | attack-sim-prompt-injection | AITG-APP-01: Direct Prompt Injection | 25 |
202
+ | attack-sim-indirect-injection | AITG-APP-02: Indirect Prompt Injection | 18 |
203
+ | attack-sim-data-leak | AITG-APP-03: Sensitive Data Leak | 20 |
204
+ | attack-sim-agentic-limits | AITG-APP-06: Agentic Behavior Limits | 18 |
205
+ | attack-sim-sandbox-escape | NVIDIA 3-axis: Sandbox Escape | 15 |
206
+ | attack-sim-defense-chain | SAIF: Defense-in-depth Chain | 12 |
155
207
 
156
208
  ## チャンネル連携
157
209
 
@@ -180,11 +232,11 @@ Shield Harness は [Semantic Versioning](https://semver.org/) に準拠します
180
232
  | `minor` | 新機能(後方互換)、Phase 内 must タスク全完了時 | OCSF 対応、新フック追加、CLI オプション追加 |
181
233
  | `major` | 破壊的変更 | スキーマ非互換変更、settings 構造変更 |
182
234
 
183
- **リリーストリガー**: `git tag v1.x.x && git push origin v1.x.x` で `release.yml` が自動実行(npm publish + GitHub Release)。セキュリティ修正は即座に patch リリース。
235
+ **リリーストリガー**: `git tag vX.Y.Z && git push origin vX.Y.Z` で `release.yml` が自動実行(npm publish + GitHub Release)。セキュリティ修正は即座に patch リリース。
184
236
 
185
237
  ## 参考プロジェクト
186
238
 
187
- Shield Harness は 40 以上の Claude Code セキュリティプロジェクトを調査して設計されました。主な参考:
239
+ 主な参考プロジェクト:
188
240
 
189
241
  | プロジェクト | 影響を受けた点 |
190
242
  | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  # Shield Harness
4
4
 
5
- **Auto-defense security harness for Claude Code — approval-free, safe autonomous development**
5
+ **Hook-driven auto-defense security harness for Claude Code**
6
6
 
7
- > **Alpha (v0.1.0)**: Security model is under active development. Permission rules and design documents are being aligned. Not recommended for production use yet.
7
+ > **v0.4.0**: 22 hooks, 4-layer defense (L1 Permissions + L2 Hooks + L3 Sandbox + L3b OpenShell), 391 tests including 108 OWASP AITG attack simulation tests.
8
8
 
9
9
  [![English](https://img.shields.io/badge/lang-English-blue?style=flat-square)](#)
10
10
  [![日本語](https://img.shields.io/badge/lang-日本語-red?style=flat-square)](README.ja.md)
@@ -25,13 +25,13 @@ npx shield-harness init [--profile minimal|standard|strict]
25
25
  ## Why Shield Harness
26
26
 
27
27
  - **Hooks-driven defense**: 22 security hooks monitor every Claude Code operation
28
- - **Approval-free mode**: Delegate all security decisions to hooks, eliminating human approval dialogs
28
+ - **Automated security decisions**: Hooks handle all security judgments in real time no manual approval bottleneck
29
29
  - **fail-close principle**: Automatically stops when safety conditions cannot be verified
30
30
  - **Evidence recording**: Tamper-proof SHA-256 hash chain records all allow/deny decisions
31
31
 
32
32
  ## Architecture Overview
33
33
 
34
- 3-layer defense model:
34
+ 4-layer defense model:
35
35
 
36
36
  | Layer | Defense | Implementation |
37
37
  | -------- | ------------------ | -------------------------------------------------- |
@@ -50,30 +50,30 @@ npx shield-harness init [--profile minimal|standard|strict]
50
50
 
51
51
  ## Hook Catalog
52
52
 
53
- | # | Hook | Event | Responsibility |
54
- | --- | ---------------- | --------------------- | ------------------------------------------------ |
55
- | 1 | permission | PreToolUse | 4-category tool usage classification |
56
- | 2 | gate | PreToolUse | 7 attack vector inspection for Bash commands |
57
- | 3 | injection-guard | PreToolUse | 9-category 50+ pattern injection detection |
58
- | 4 | data-boundary | PreToolUse | Production data boundary + jurisdiction tracking |
59
- | 5 | quiet-inject | PreToolUse | Auto-inject quiet flags |
60
- | 6 | evidence | PostToolUse | SHA-256 hash chain evidence |
61
- | 7 | output-control | PostToolUse | Output truncation + token budget |
62
- | 8 | dep-audit | PostToolUse | Package install detection |
63
- | 9 | lint-on-save | PostToolUse | Auto lint execution |
64
- | 10 | session-start | SessionStart | Session init + integrity baseline |
65
- | 11 | session-end | SessionEnd | Cleanup + statistics |
66
- | 12 | circuit-breaker | Stop | Retry limit (3 attempts) |
67
- | 13 | config-guard | ConfigChange | Settings change monitoring |
68
- | 14 | user-prompt | UserPromptSubmit | User input injection scanning |
69
- | 15 | permission-learn | PermissionRequest | Permission learning guard |
70
- | 16 | elicitation | Elicitation | Phishing + scope guard |
71
- | 17 | subagent | SubagentStart | Subagent budget constraint (25%) |
72
- | 18 | instructions | InstructionsLoaded | Rule file integrity monitoring |
73
- | 19 | precompact | PreCompact | Pre-compaction backup |
74
- | 20 | postcompact | PostCompact | Post-compaction restore + verify |
75
- | 21 | worktree | WorktreeCreate/Remove | Security propagation + evidence merge |
76
- | 22 | task-gate | TaskCompleted | Test gate |
53
+ | # | Hook | Event | Responsibility |
54
+ | --- | ---------------- | --------------------- | ------------------------------------------------------------- |
55
+ | 1 | permission | PreToolUse | 4-category tool usage classification |
56
+ | 2 | gate | PreToolUse | 7 attack vector inspection for Bash commands |
57
+ | 3 | injection-guard | PreToolUse | 9-category 50+ pattern injection detection |
58
+ | 4 | data-boundary | PreToolUse | Production data boundary + jurisdiction tracking |
59
+ | 5 | quiet-inject | PreToolUse | Auto-inject quiet flags |
60
+ | 6 | evidence | PostToolUse | SHA-256 hash chain evidence |
61
+ | 7 | output-control | PostToolUse | Output truncation + token budget |
62
+ | 8 | dep-audit | PostToolUse | Package install detection |
63
+ | 9 | lint-on-save | PostToolUse | Auto lint execution |
64
+ | 10 | session-start | SessionStart | Session init + integrity baseline |
65
+ | 11 | session-end | SessionEnd | Cleanup + statistics |
66
+ | 12 | circuit-breaker | Stop | Retry limit (3 attempts) |
67
+ | 13 | config-guard | ConfigChange | Settings change monitoring + OpenShell policy file protection |
68
+ | 14 | user-prompt | UserPromptSubmit | User input injection scanning |
69
+ | 15 | permission-learn | PermissionRequest | Permission learning guard |
70
+ | 16 | elicitation | Elicitation | Phishing + scope guard |
71
+ | 17 | subagent | SubagentStart | Subagent budget constraint (25%) |
72
+ | 18 | instructions | InstructionsLoaded | Rule file integrity monitoring |
73
+ | 19 | precompact | PreCompact | Pre-compaction backup |
74
+ | 20 | postcompact | PostCompact | Post-compaction restore + verify |
75
+ | 21 | worktree | WorktreeCreate/Remove | Security propagation + evidence merge |
76
+ | 22 | task-gate | TaskCompleted | Test gate |
77
77
 
78
78
  ## Pipeline
79
79
 
@@ -146,10 +146,62 @@ Key benefits for Windows users:
146
146
 
147
147
  - Policies exist **outside** the agent process — the agent cannot disable its own guardrails
148
148
  - Runs on Docker Desktop + WSL2 backend (typical Windows dev setup)
149
- - Reduces residual risk from 5% to <1%
149
+ - Significantly reduces residual risk from Layer 1-2 pattern matching limitations
150
150
  - Freely removable — stop the container and Shield Harness falls back to Layer 1-2
151
151
 
152
- > **Note**: OpenShell is Alpha (v0.0.13) — APIs may change with future releases.
152
+ > **Note**: OpenShell is Alpha (v0.0.13) — APIs may change with future releases. Shield Harness GA Phase integration is complete (ADR-037): config guard policy file protection, policy drift check, and full documentation are ready.
153
+
154
+ #### Setup
155
+
156
+ **Prerequisites**: [Docker Desktop](https://www.docker.com/products/docker-desktop/) (WSL2 backend on Windows)
157
+
158
+ ```bash
159
+ # 1. Install Docker Desktop and verify it is running
160
+ # https://www.docker.com/products/docker-desktop/
161
+ docker --version
162
+
163
+ # 2. Install OpenShell CLI
164
+ pip install openshell
165
+
166
+ # 3. Generate policy from permissions-spec.json
167
+ # Creates .claude/policies/openshell-generated.yaml
168
+ npx shield-harness policy generate
169
+
170
+ # 4. Start OpenShell container and run Claude Code inside it
171
+ # Docker pulls the sandbox image automatically on first run
172
+ # Kernel-level enforcement (Landlock/Seccomp/Network NS) is active inside the container
173
+ openshell run --policy .claude/policies/openshell-generated.yaml
174
+ ```
175
+
176
+ Claude Code running inside the OpenShell container automatically receives Layer 3b kernel enforcement. Shield Harness detects this at session start (`sh-session-start.js`) — no additional configuration required.
177
+
178
+ Without OpenShell, Shield Harness falls back to Layer 1-2 defense (no degradation in hook protection).
179
+
180
+ Policy files are protected by:
181
+
182
+ - `permissions.deny`: `Edit/Write(.claude/policies/**)` blocks agent modification
183
+ - `sandbox.denyWrite`: `.claude/policies` in filesystem deny list
184
+ - `sh-config-guard.js`: Hash tracking detects policy file tampering or weakening
185
+ - `sh-session-start.js`: Drift check at session start verifies spec-policy alignment
186
+
187
+ ## Testing
188
+
189
+ ```bash
190
+ # Run all tests (391 tests including 108 OWASP AITG attack simulations)
191
+ npm test
192
+
193
+ # Run attack simulation tests only
194
+ node --test tests/attack-sim-*.test.js
195
+ ```
196
+
197
+ | Test Suite | OWASP Category | Tests |
198
+ | ----------------------------- | -------------------------------------- | ----- |
199
+ | attack-sim-prompt-injection | AITG-APP-01: Direct Prompt Injection | 25 |
200
+ | attack-sim-indirect-injection | AITG-APP-02: Indirect Prompt Injection | 18 |
201
+ | attack-sim-data-leak | AITG-APP-03: Sensitive Data Leak | 20 |
202
+ | attack-sim-agentic-limits | AITG-APP-06: Agentic Behavior Limits | 18 |
203
+ | attack-sim-sandbox-escape | NVIDIA 3-axis: Sandbox Escape | 15 |
204
+ | attack-sim-defense-chain | SAIF: Defense-in-depth Chain | 12 |
153
205
 
154
206
  ## Channel Integration
155
207
 
@@ -178,11 +230,11 @@ Shield Harness follows [Semantic Versioning](https://semver.org/):
178
230
  | `minor` | New features (backward compatible), Phase must-tasks completed | OCSF support, new hook, CLI option |
179
231
  | `major` | Breaking changes | Schema incompatible, settings change |
180
232
 
181
- **Release trigger**: `git tag v1.x.x && git push origin v1.x.x` triggers `release.yml` (automated npm publish + GitHub Release). Security fixes trigger an immediate patch release.
233
+ **Release trigger**: `git tag vX.Y.Z && git push origin vX.Y.Z` triggers `release.yml` (automated npm publish + GitHub Release). Security fixes trigger an immediate patch release.
182
234
 
183
235
  ## References
184
236
 
185
- Shield Harness was designed by surveying 40+ Claude Code security projects. Key references:
237
+ Key references:
186
238
 
187
239
  | Project | Influence |
188
240
  | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shield-harness",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Security harness for Claude Code — hooks-driven, zero-hassle defense",
5
5
  "bin": {
6
6
  "shield-harness": "./bin/shield-harness.js"