shield-harness 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude/hooks/lib/session-modules/.gitkeep +0 -0
  2. package/.claude/hooks/lib/sh-utils.js +241 -0
  3. package/.claude/hooks/lint-on-save.js +240 -0
  4. package/.claude/hooks/sh-circuit-breaker.js +111 -0
  5. package/.claude/hooks/sh-config-guard.js +252 -0
  6. package/.claude/hooks/sh-data-boundary.js +315 -0
  7. package/.claude/hooks/sh-dep-audit.js +101 -0
  8. package/.claude/hooks/sh-elicitation.js +241 -0
  9. package/.claude/hooks/sh-evidence.js +193 -0
  10. package/.claude/hooks/sh-gate.js +330 -0
  11. package/.claude/hooks/sh-injection-guard.js +165 -0
  12. package/.claude/hooks/sh-instructions.js +210 -0
  13. package/.claude/hooks/sh-output-control.js +183 -0
  14. package/.claude/hooks/sh-permission-learn.js +223 -0
  15. package/.claude/hooks/sh-permission.js +157 -0
  16. package/.claude/hooks/sh-pipeline.js +639 -0
  17. package/.claude/hooks/sh-postcompact.js +173 -0
  18. package/.claude/hooks/sh-precompact.js +114 -0
  19. package/.claude/hooks/sh-quiet-inject.js +147 -0
  20. package/.claude/hooks/sh-session-end.js +143 -0
  21. package/.claude/hooks/sh-session-start.js +196 -0
  22. package/.claude/hooks/sh-subagent.js +86 -0
  23. package/.claude/hooks/sh-task-gate.js +138 -0
  24. package/.claude/hooks/sh-user-prompt.js +181 -0
  25. package/.claude/hooks/sh-worktree.js +227 -0
  26. package/.claude/patterns/injection-patterns.json +137 -0
  27. package/.claude/rules/binding-governance.md +62 -0
  28. package/.claude/rules/channel-security.md +90 -0
  29. package/.claude/rules/coding-principles.md +79 -0
  30. package/.claude/rules/dev-environment.md +37 -0
  31. package/.claude/rules/implementation-context.md +112 -0
  32. package/.claude/rules/language.md +26 -0
  33. package/.claude/rules/security.md +109 -0
  34. package/.claude/rules/testing.md +43 -0
  35. package/LICENSE +21 -0
  36. package/README.ja.md +107 -0
  37. package/README.md +105 -0
  38. package/bin/shield-harness.js +141 -0
  39. package/package.json +33 -0
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ // sh-postcompact.js — Post-compaction state restoration & verification
3
+ // Spec: DETAILED_DESIGN.md §5.8
4
+ // Event: PostCompact
5
+ // Matcher: auto
6
+ // Target response time: < 200ms
7
+ "use strict";
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const {
12
+ readHookInput,
13
+ allow,
14
+ sha256,
15
+ readSession,
16
+ writeSession,
17
+ appendEvidence,
18
+ SH_DIR,
19
+ } = require("./lib/sh-utils");
20
+
21
+ const HOOK_NAME = "sh-postcompact";
22
+ const BACKUP_DIR = path.join(SH_DIR, "compact-backup");
23
+ const CLAUDE_MD = "CLAUDE.md";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Restore Logic
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Restore session state from compact-backup.
31
+ * Merges backup into current session (backup values take precedence for key fields).
32
+ * @returns {Object} restored session
33
+ */
34
+ function restoreSessionState() {
35
+ const backupFile = path.join(BACKUP_DIR, "session.json");
36
+ const currentSession = readSession();
37
+
38
+ if (!fs.existsSync(backupFile)) {
39
+ return currentSession;
40
+ }
41
+
42
+ try {
43
+ const backup = JSON.parse(fs.readFileSync(backupFile, "utf8"));
44
+ // Merge: preserve backup's critical fields
45
+ return {
46
+ ...currentSession,
47
+ ...backup,
48
+ // Always update these from current state
49
+ last_compact: new Date().toISOString(),
50
+ };
51
+ } catch {
52
+ return currentSession;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Verify CLAUDE.md integrity after compaction.
58
+ * @param {Object} session - Session with baseline hash
59
+ * @returns {{ valid: boolean, message: string }}
60
+ */
61
+ function verifyCLAUDEMD(session) {
62
+ if (!fs.existsSync(CLAUDE_MD)) {
63
+ return { valid: false, message: "CLAUDE.md not found" };
64
+ }
65
+
66
+ const currentHash = sha256(fs.readFileSync(CLAUDE_MD, "utf8"));
67
+
68
+ // If we don't have a baseline, just record it
69
+ if (!session.claude_md_hash) {
70
+ return {
71
+ valid: true,
72
+ message: `CLAUDE.md hash recorded: ${currentHash.slice(0, 12)}...`,
73
+ };
74
+ }
75
+
76
+ if (currentHash !== session.claude_md_hash) {
77
+ return {
78
+ valid: false,
79
+ message: `WARNING: CLAUDE.md has been modified since session start! Expected: ${session.claude_md_hash.slice(0, 12)}..., Got: ${currentHash.slice(0, 12)}...`,
80
+ };
81
+ }
82
+
83
+ return { valid: true, message: "CLAUDE.md integrity verified" };
84
+ }
85
+
86
+ /**
87
+ * Build restoration context for systemMessage injection.
88
+ * @param {Object} session
89
+ * @param {{ valid: boolean, message: string }} integrityCheck
90
+ * @returns {string}
91
+ */
92
+ function buildRestorationContext(session, integrityCheck) {
93
+ const parts = [];
94
+
95
+ parts.push("=== Shield Harness Post-Compaction Restore ===");
96
+
97
+ // Integrity check result
98
+ if (!integrityCheck.valid) {
99
+ parts.push(`⚠ ${integrityCheck.message}`);
100
+ } else {
101
+ parts.push(`✓ ${integrityCheck.message}`);
102
+ }
103
+
104
+ // Session state
105
+ if (session.session_start) {
106
+ parts.push(`Session started: ${session.session_start}`);
107
+ }
108
+ if (session.token_budget) {
109
+ parts.push(
110
+ `Token budget: ${session.token_budget.used || 0}/${session.token_budget.session_limit || "?"}`,
111
+ );
112
+ }
113
+
114
+ // Key reminders
115
+ parts.push("");
116
+ parts.push("Key files to re-read if needed:");
117
+ parts.push(" - CLAUDE.md (project instructions)");
118
+ parts.push(" - .claude/rules/ (security & coding rules)");
119
+ parts.push(" - tasks/backlog.yaml (task SoT)");
120
+ parts.push(" - docs/DETAILED_DESIGN.md (hook specifications)");
121
+ parts.push("==========================================");
122
+
123
+ return parts.join("\n");
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Main
128
+ // ---------------------------------------------------------------------------
129
+
130
+ try {
131
+ const input = readHookInput();
132
+
133
+ // Restore session state from backup
134
+ const session = restoreSessionState();
135
+
136
+ // Verify CLAUDE.md integrity
137
+ const integrityCheck = verifyCLAUDEMD(session);
138
+
139
+ // Update and save session
140
+ writeSession(session);
141
+
142
+ // Record evidence
143
+ try {
144
+ appendEvidence({
145
+ hook: HOOK_NAME,
146
+ event: "PostCompact",
147
+ decision: "allow",
148
+ integrity_valid: integrityCheck.valid,
149
+ integrity_message: integrityCheck.message,
150
+ session_id: input.sessionId,
151
+ });
152
+ } catch {
153
+ // Evidence failure is non-blocking
154
+ }
155
+
156
+ // Inject restoration context
157
+ const context = buildRestorationContext(session, integrityCheck);
158
+ allow(context);
159
+ } catch (_err) {
160
+ // Operational hook — fail-open
161
+ allow();
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Exports (for testing)
166
+ // ---------------------------------------------------------------------------
167
+
168
+ module.exports = {
169
+ restoreSessionState,
170
+ verifyCLAUDEMD,
171
+ buildRestorationContext,
172
+ BACKUP_DIR,
173
+ };
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // sh-precompact.js — Pre-compaction state backup & context injection
3
+ // Spec: DETAILED_DESIGN.md §5.8
4
+ // Event: PreCompact
5
+ // Matcher: auto
6
+ // Target response time: < 200ms
7
+ "use strict";
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const {
12
+ readHookInput,
13
+ allow,
14
+ readSession,
15
+ appendEvidence,
16
+ SESSION_FILE,
17
+ SH_DIR,
18
+ } = require("./lib/sh-utils");
19
+
20
+ const HOOK_NAME = "sh-precompact";
21
+ const BACKUP_DIR = path.join(SH_DIR, "compact-backup");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Backup Logic
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Copy session.json to compact-backup directory.
29
+ */
30
+ function backupSessionState() {
31
+ if (!fs.existsSync(BACKUP_DIR)) {
32
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
33
+ }
34
+
35
+ // Backup session.json
36
+ if (fs.existsSync(SESSION_FILE)) {
37
+ fs.copyFileSync(SESSION_FILE, path.join(BACKUP_DIR, "session.json"));
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Build context output for injection into compacted conversation.
43
+ * @param {Object} session - Current session state
44
+ * @returns {string}
45
+ */
46
+ function buildContextOutput(session) {
47
+ const parts = [];
48
+
49
+ parts.push("=== Shield Harness Pre-Compaction Snapshot ===");
50
+
51
+ // Session state
52
+ if (session.session_start) {
53
+ parts.push(`Session started: ${session.session_start}`);
54
+ }
55
+ if (session.token_budget) {
56
+ parts.push(
57
+ `Token budget used: ${session.token_budget.used || 0}/${session.token_budget.session_limit || "?"}`,
58
+ );
59
+ }
60
+
61
+ // Key project files reminder
62
+ parts.push("");
63
+ parts.push("Key files:");
64
+ parts.push(" - CLAUDE.md (project instructions)");
65
+ parts.push(" - .claude/rules/ (security & coding rules)");
66
+ parts.push(" - tasks/backlog.yaml (task SoT — read-only)");
67
+ parts.push(" - docs/DETAILED_DESIGN.md (hook specifications)");
68
+
69
+ parts.push("=========================================");
70
+
71
+ return parts.join("\n");
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Main
76
+ // ---------------------------------------------------------------------------
77
+
78
+ try {
79
+ const input = readHookInput();
80
+ const session = readSession();
81
+
82
+ // Backup state before compaction
83
+ backupSessionState();
84
+
85
+ // Record evidence
86
+ try {
87
+ appendEvidence({
88
+ hook: HOOK_NAME,
89
+ event: "PreCompact",
90
+ decision: "allow",
91
+ backup_created: true,
92
+ session_id: input.sessionId,
93
+ });
94
+ } catch {
95
+ // Evidence failure is non-blocking
96
+ }
97
+
98
+ // Inject context
99
+ const context = buildContextOutput(session);
100
+ allow(context);
101
+ } catch (_err) {
102
+ // Operational hook — fail-open
103
+ allow();
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Exports (for testing)
108
+ // ---------------------------------------------------------------------------
109
+
110
+ module.exports = {
111
+ backupSessionState,
112
+ buildContextOutput,
113
+ BACKUP_DIR,
114
+ };
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ // sh-quiet-inject.js — Auto-inject quiet flags to save tokens
3
+ // Spec: DETAILED_DESIGN.md §3.5
4
+ // Hook event: PreToolUse
5
+ // Matcher: Bash
6
+ // Target response time: < 10ms
7
+ "use strict";
8
+
9
+ const { readHookInput, allow, allowWithUpdate } = require("./lib/sh-utils");
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Quiet Injection Rules
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * Each rule: { pattern, flag, verboseCheck }
17
+ * - pattern: RegExp matching the command (e.g., /\bgit\s+clone\b/)
18
+ * - flag: The quiet flag to inject (e.g., "-q", "--silent")
19
+ * - verboseCheck: RegExp matching verbose flags that should prevent injection
20
+ */
21
+ const QUIET_RULES = [
22
+ // Git commands: inject -q unless -v/--verbose present
23
+ {
24
+ pattern: /\bgit\s+clone\b/,
25
+ flag: "-q",
26
+ verboseCheck: /\s-v\b|\s--verbose\b/,
27
+ },
28
+ {
29
+ pattern: /\bgit\s+fetch\b/,
30
+ flag: "-q",
31
+ verboseCheck: /\s-v\b|\s--verbose\b/,
32
+ },
33
+ {
34
+ pattern: /\bgit\s+pull\b/,
35
+ flag: "-q",
36
+ verboseCheck: /\s-v\b|\s--verbose\b/,
37
+ },
38
+ {
39
+ pattern: /\bgit\s+push\b/,
40
+ flag: "-q",
41
+ verboseCheck: /\s-v\b|\s--verbose\b/,
42
+ },
43
+
44
+ // npm commands: inject --silent unless --verbose present
45
+ {
46
+ pattern: /\bnpm\s+install\b/,
47
+ flag: "--silent",
48
+ verboseCheck: /\s--verbose\b/,
49
+ },
50
+ {
51
+ pattern: /\bnpm\s+ci\b/,
52
+ flag: "--silent",
53
+ verboseCheck: /\s--verbose\b/,
54
+ },
55
+
56
+ // cargo build: inject -q unless --verbose/-v present
57
+ {
58
+ pattern: /\bcargo\s+build\b/,
59
+ flag: "-q",
60
+ verboseCheck: /\s--verbose\b|\s-v\b/,
61
+ },
62
+
63
+ // pip install: inject -q unless --verbose/-v present
64
+ {
65
+ pattern: /\bpip3?\s+install\b/,
66
+ flag: "-q",
67
+ verboseCheck: /\s--verbose\b|\s-v\b/,
68
+ },
69
+
70
+ // docker pull: inject -q unless --verbose/-v present
71
+ {
72
+ pattern: /\bdocker\s+pull\b/,
73
+ flag: "-q",
74
+ verboseCheck: /\s--verbose\b|\s-v\b/,
75
+ },
76
+ ];
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Injection Logic
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /**
83
+ * Attempt to inject a quiet flag into a command string.
84
+ *
85
+ * Finds the matching subcommand (e.g., "git clone") and inserts the flag
86
+ * immediately after it.
87
+ *
88
+ * @param {string} command - The original command string.
89
+ * @returns {{ modified: boolean, command: string }}
90
+ */
91
+ function injectQuietFlag(command) {
92
+ for (const rule of QUIET_RULES) {
93
+ const match = command.match(rule.pattern);
94
+ if (!match) continue;
95
+
96
+ // Skip if already has quiet flag injected
97
+ // Check for common quiet flags in the command
98
+ if (/\s-q\b|\s--quiet\b|\s--silent\b/.test(command)) continue;
99
+
100
+ // Skip if verbose flag is present
101
+ if (rule.verboseCheck.test(command)) continue;
102
+
103
+ // Insert flag right after the matched subcommand
104
+ const insertPos = match.index + match[0].length;
105
+ const modified =
106
+ command.slice(0, insertPos) + " " + rule.flag + command.slice(insertPos);
107
+
108
+ return { modified: true, command: modified };
109
+ }
110
+
111
+ return { modified: false, command };
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Main
116
+ // ---------------------------------------------------------------------------
117
+
118
+ try {
119
+ const input = readHookInput();
120
+ const command = (input.toolInput.command || "").trim();
121
+
122
+ // Empty command — nothing to inject
123
+ if (!command) {
124
+ allow();
125
+ }
126
+
127
+ const result = injectQuietFlag(command);
128
+
129
+ if (result.modified) {
130
+ allowWithUpdate({ command: result.command });
131
+ } else {
132
+ allow();
133
+ }
134
+ } catch (_err) {
135
+ // Operational hook, not security — on error, just allow.
136
+ // Worst case: extra output tokens, not a security breach.
137
+ allow();
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Exports (for testing)
142
+ // ---------------------------------------------------------------------------
143
+
144
+ module.exports = {
145
+ QUIET_RULES,
146
+ injectQuietFlag,
147
+ };
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // sh-session-end.js — Session cleanup & statistics
3
+ // Spec: DETAILED_DESIGN.md §5.8
4
+ // Event: SessionEnd
5
+ // Target response time: < 200ms
6
+ "use strict";
7
+
8
+ const fs = require("fs");
9
+ const {
10
+ readHookInput,
11
+ allow,
12
+ readSession,
13
+ writeSession,
14
+ appendEvidence,
15
+ EVIDENCE_FILE,
16
+ } = require("./lib/sh-utils");
17
+
18
+ const HOOK_NAME = "sh-session-end";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Statistics computation
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Compute session statistics from evidence ledger.
26
+ * @param {string} sessionId
27
+ * @returns {{ toolCalls: number, denials: number, topTools: string[], duration: string }}
28
+ */
29
+ function computeStats(sessionId) {
30
+ const stats = { toolCalls: 0, denials: 0, topTools: [], duration: "unknown" };
31
+
32
+ try {
33
+ if (!fs.existsSync(EVIDENCE_FILE)) return stats;
34
+
35
+ const lines = fs.readFileSync(EVIDENCE_FILE, "utf8").trim().split("\n");
36
+ const toolCounts = {};
37
+ let sessionStart = null;
38
+
39
+ for (const line of lines) {
40
+ if (!line) continue;
41
+ try {
42
+ const entry = JSON.parse(line);
43
+ // Only count entries from this session
44
+ if (entry.session_id && entry.session_id !== sessionId) continue;
45
+
46
+ if (entry.event === "SessionStart") {
47
+ sessionStart = entry.recorded_at;
48
+ }
49
+ if (entry.tool) {
50
+ stats.toolCalls++;
51
+ toolCounts[entry.tool] = (toolCounts[entry.tool] || 0) + 1;
52
+ }
53
+ if (entry.decision === "deny") {
54
+ stats.denials++;
55
+ }
56
+ } catch {
57
+ // Skip malformed lines
58
+ }
59
+ }
60
+
61
+ // Top 3 tools
62
+ stats.topTools = Object.entries(toolCounts)
63
+ .sort((a, b) => b[1] - a[1])
64
+ .slice(0, 3)
65
+ .map(([tool, count]) => `${tool}(${count})`);
66
+
67
+ // Duration
68
+ if (sessionStart) {
69
+ const startMs = new Date(sessionStart).getTime();
70
+ const endMs = Date.now();
71
+ const diffMin = Math.round((endMs - startMs) / 60000);
72
+ if (diffMin < 60) {
73
+ stats.duration = `${diffMin}m`;
74
+ } else {
75
+ const h = Math.floor(diffMin / 60);
76
+ const m = diffMin % 60;
77
+ stats.duration = `${h}h${m}m`;
78
+ }
79
+ }
80
+ } catch {
81
+ // Stats computation failure is non-critical
82
+ }
83
+
84
+ return stats;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Main
89
+ // ---------------------------------------------------------------------------
90
+
91
+ try {
92
+ const input = readHookInput();
93
+ const { sessionId } = input;
94
+
95
+ // Compute session stats
96
+ const stats = computeStats(sessionId);
97
+
98
+ // Record close marker in evidence ledger
99
+ try {
100
+ appendEvidence({
101
+ hook: HOOK_NAME,
102
+ event: "SessionEnd",
103
+ decision: "allow",
104
+ summary: {
105
+ tool_calls: stats.toolCalls,
106
+ denials: stats.denials,
107
+ top_tools: stats.topTools,
108
+ duration: stats.duration,
109
+ },
110
+ session_id: sessionId,
111
+ });
112
+ } catch {
113
+ // Evidence failure is non-blocking
114
+ }
115
+
116
+ // Reset session.json
117
+ const session = readSession();
118
+ session.retry_count = 0;
119
+ session.stop_hook_active = false;
120
+ session.session_end = new Date().toISOString();
121
+ writeSession(session);
122
+
123
+ // Output summary
124
+ const summary = [
125
+ `[${HOOK_NAME}] Session closed.`,
126
+ ` Tool calls: ${stats.toolCalls}, Denials: ${stats.denials}`,
127
+ ` Top tools: ${stats.topTools.join(", ") || "none"}`,
128
+ ` Duration: ${stats.duration}`,
129
+ ].join("\n");
130
+
131
+ allow(summary);
132
+ } catch (_err) {
133
+ // Operational hook — fail-open
134
+ allow();
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Exports (for testing)
139
+ // ---------------------------------------------------------------------------
140
+
141
+ module.exports = {
142
+ computeStats,
143
+ };