@massu/core 1.10.7 → 1.12.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 (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,243 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // PreToolUse Hook: Pre-Deletion Feature Impact Check
7
- // Detects file deletion patterns (rm, git rm, Write with empty content)
8
- // and runs sentinel impact analysis. Blocks if critical features orphaned.
9
- // Also protects knowledge system files from accidental deletion (P7-005).
10
- // Must complete in <500ms.
11
- // ============================================================
12
-
13
- import Database from 'better-sqlite3';
14
- import { resolve } from 'path';
15
- import { existsSync } from 'fs';
16
- import { getFeatureImpact } from '../sentinel-db.ts';
17
- import { getProjectRoot, getResolvedPaths } from '../config.ts';
18
- import { t } from '../lib/sql-table-names.ts';
19
-
20
- interface HookInput {
21
- session_id: string;
22
- tool_name: string;
23
- tool_input: {
24
- command?: string;
25
- file_path?: string;
26
- content?: string;
27
- };
28
- }
29
-
30
- const PROJECT_ROOT = getProjectRoot();
31
-
32
- // Knowledge system files that must never be silently deleted.
33
- // These files underpin knowledge indexing and memory retrieval.
34
- const KNOWLEDGE_PROTECTED_FILES = [
35
- 'knowledge-db.ts',
36
- 'knowledge-indexer.ts',
37
- 'knowledge-tools.ts',
38
- ];
39
-
40
- function getDataDb(): Database.Database | null {
41
- const dbPath = getResolvedPaths().dataDbPath;
42
- if (!existsSync(dbPath)) return null;
43
- try {
44
- const db = new Database(dbPath, { readonly: true });
45
- db.pragma('journal_mode = WAL');
46
- return db;
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- /**
53
- * P7-005: Check if the tool call targets a protected knowledge system file.
54
- * Returns a warning message if a protected file would be deleted/emptied.
55
- */
56
- function checkKnowledgeFileProtection(input: HookInput): string | null {
57
- const candidateFiles: string[] = [];
58
-
59
- if (input.tool_name === 'Bash' && input.tool_input.command) {
60
- const cmd = input.tool_input.command;
61
- const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
62
- if (rmMatch) {
63
- const parts = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
64
- candidateFiles.push(...parts);
65
- }
66
- }
67
-
68
- if (input.tool_name === 'Write' && input.tool_input.file_path) {
69
- const content = input.tool_input.content || '';
70
- if (content.trim().length === 0) {
71
- candidateFiles.push(input.tool_input.file_path);
72
- }
73
- }
74
-
75
- for (const f of candidateFiles) {
76
- const basename = f.split('/').pop() ?? f;
77
- if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
78
- return (
79
- `KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. ` +
80
- `Deleting it will break knowledge indexing and memory retrieval. ` +
81
- `Create a replacement before removing.`
82
- );
83
- }
84
- }
85
-
86
- return null;
87
- }
88
-
89
- function extractDeletedFiles(input: HookInput): string[] {
90
- const files: string[] = [];
91
-
92
- if (input.tool_name === 'Bash' && input.tool_input.command) {
93
- const cmd = input.tool_input.command;
94
-
95
- // Detect rm commands
96
- const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
97
- if (rmMatch) {
98
- const paths = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
99
- for (const p of paths) {
100
- const relPath = p.startsWith('src/') ? p : p.replace(PROJECT_ROOT + '/', '');
101
- if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
102
- files.push(relPath);
103
- }
104
- }
105
- }
106
- }
107
-
108
- // Detect Write tool with empty content (file replacement that empties)
109
- if (input.tool_name === 'Write' && input.tool_input.file_path) {
110
- const content = input.tool_input.content || '';
111
- if (content.trim().length === 0) {
112
- const relPath = input.tool_input.file_path.replace(PROJECT_ROOT + '/', '');
113
- if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
114
- files.push(relPath);
115
- }
116
- }
117
- }
118
-
119
- return files;
120
- }
121
-
122
- async function main(): Promise<void> {
123
- try {
124
- const input = await readStdin();
125
- const hookInput = JSON.parse(input) as HookInput;
126
-
127
- // P7-005: Check knowledge file protection before anything else
128
- const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
129
- if (knowledgeWarning) {
130
- process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
131
- process.exit(0);
132
- return;
133
- }
134
-
135
- const deletedFiles = extractDeletedFiles(hookInput);
136
- if (deletedFiles.length === 0) {
137
- process.exit(0);
138
- return;
139
- }
140
-
141
- const db = getDataDb();
142
- if (!db) {
143
- // No database available - can't check
144
- process.exit(0);
145
- return;
146
- }
147
-
148
- // The sentinel registry table name (defined by sentinel-db schema).
149
- // P-H032: now config-driven via t() helper from lib/sql-table-names.ts.
150
- const SENTINEL_TABLE = t('sentinel');
151
-
152
- try {
153
- // Check if any sentinel tables exist
154
- const tableExists = db.prepare(
155
- `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
156
- ).get(SENTINEL_TABLE);
157
-
158
- if (!tableExists) {
159
- process.exit(0);
160
- return;
161
- }
162
-
163
- const impact = getFeatureImpact(db, deletedFiles);
164
-
165
- if (impact.blocked) {
166
- const msg = [
167
- `SENTINEL IMPACT WARNING: Deleting ${deletedFiles.length} file(s) would affect features:`,
168
- '',
169
- ];
170
-
171
- if (impact.orphaned.length > 0) {
172
- msg.push(`ORPHANED (${impact.orphaned.length} features - no primary components left):`);
173
- for (const item of impact.orphaned) {
174
- msg.push(` - ${item.feature.feature_key} [${item.feature.priority}]: ${item.feature.title}`);
175
- }
176
- }
177
-
178
- if (impact.degraded.length > 0) {
179
- msg.push(`DEGRADED (${impact.degraded.length} features - some components removed):`);
180
- for (const item of impact.degraded) {
181
- msg.push(` - ${item.feature.feature_key}: ${item.feature.title}`);
182
- }
183
- }
184
-
185
- msg.push('');
186
- msg.push('Create a migration plan before deleting these files.');
187
-
188
- // Output warning but don't block (user can proceed)
189
- process.stdout.write(JSON.stringify({ message: msg.join('\n') }));
190
- }
191
- // Check Python import graph for deleted .py files
192
- const pyFiles = deletedFiles.filter(f => f.endsWith('.py'));
193
- if (pyFiles.length > 0) {
194
- try {
195
- for (const pyFile of pyFiles) {
196
- const importers = db.prepare(
197
- `SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
198
- ).all(pyFile) as { source_file: string }[];
199
-
200
- const routes = db.prepare(
201
- `SELECT method, path FROM ${t('py_routes')} WHERE file = ?`
202
- ).all(pyFile) as { method: string; path: string }[];
203
-
204
- const models = db.prepare(
205
- `SELECT class_name FROM ${t('py_models')} WHERE file = ?`
206
- ).all(pyFile) as { class_name: string }[];
207
-
208
- if (importers.length > 0 || routes.length > 0 || models.length > 0) {
209
- const parts: string[] = [];
210
- if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
211
- if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
212
- if (models.length > 0) parts.push(`defines ${models.length} models`);
213
-
214
- const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(', ')}. Check dependents before deleting.`;
215
- process.stdout.write(JSON.stringify({ message: msg }));
216
- }
217
- }
218
- } catch {
219
- // Python tables may not exist yet
220
- }
221
- }
222
- } finally {
223
- db.close();
224
- }
225
- } catch {
226
- // Hooks must never crash
227
- }
228
-
229
- process.exit(0);
230
- }
231
-
232
- function readStdin(): Promise<string> {
233
- return new Promise((resolve) => {
234
- let data = '';
235
- process.stdin.setEncoding('utf-8');
236
- process.stdin.on('data', (chunk) => { data += chunk; });
237
- process.stdin.on('end', () => resolve(data));
238
- // Timeout to prevent hanging
239
- setTimeout(() => resolve(data), 400);
240
- });
241
- }
242
-
243
- main();
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // PostToolUse Hook: Quality Event Recorder
7
- // Parses tool responses for quality signals (test failures,
8
- // type errors, build failures) and records them for analytics.
9
- // Must complete in <500ms.
10
- // ============================================================
11
-
12
- import { getMemoryDb } from '../memory-db.ts';
13
-
14
- interface HookInput {
15
- session_id: string;
16
- transcript_path: string;
17
- cwd: string;
18
- hook_event_name: string;
19
- tool_name: string;
20
- tool_input: Record<string, unknown>;
21
- tool_response: string;
22
- }
23
-
24
- interface QualitySignal {
25
- event_type: string;
26
- details: string;
27
- }
28
-
29
- const TEST_FAILURE_PATTERNS: RegExp[] = [
30
- /\bFAIL\b/,
31
- /✗/,
32
- /\bfailed\b/i,
33
- /\bError:/,
34
- ];
35
-
36
- const TYPE_ERROR_PATTERNS: RegExp[] = [
37
- /error TS\d+/,
38
- /\btsc\b.*error/i,
39
- ];
40
-
41
- const BUILD_FAILURE_PATTERNS: RegExp[] = [
42
- /Build failed/i,
43
- /\besbuild\b.*error/i,
44
- /\besbuild\b.*failed/i,
45
- ];
46
-
47
- function detectQualitySignals(toolResponse: string): QualitySignal[] {
48
- const signals: QualitySignal[] = [];
49
- const response = toolResponse ?? '';
50
-
51
- for (const pattern of TEST_FAILURE_PATTERNS) {
52
- if (pattern.test(response)) {
53
- const match = response.match(pattern);
54
- signals.push({
55
- event_type: 'test_failure',
56
- details: match ? match[0].slice(0, 500) : 'Test failure detected',
57
- });
58
- break; // One signal per category
59
- }
60
- }
61
-
62
- for (const pattern of TYPE_ERROR_PATTERNS) {
63
- if (pattern.test(response)) {
64
- const match = response.match(pattern);
65
- signals.push({
66
- event_type: 'type_error',
67
- details: match ? match[0].slice(0, 500) : 'TypeScript error detected',
68
- });
69
- break;
70
- }
71
- }
72
-
73
- for (const pattern of BUILD_FAILURE_PATTERNS) {
74
- if (pattern.test(response)) {
75
- const match = response.match(pattern);
76
- signals.push({
77
- event_type: 'build_failure',
78
- details: match ? match[0].slice(0, 500) : 'Build failure detected',
79
- });
80
- break;
81
- }
82
- }
83
-
84
- return signals;
85
- }
86
-
87
- async function main(): Promise<void> {
88
- try {
89
- const input = await readStdin();
90
- const hookInput = JSON.parse(input) as HookInput;
91
- const { session_id, tool_name, tool_response } = hookInput;
92
-
93
- const signals = detectQualitySignals(tool_response);
94
- if (signals.length === 0) {
95
- process.exit(0);
96
- return;
97
- }
98
-
99
- const db = getMemoryDb();
100
- try {
101
- const stmt = db.prepare(`
102
- INSERT INTO quality_events (session_id, event_type, tool_name, details)
103
- VALUES (?, ?, ?, ?)
104
- `);
105
- for (const signal of signals) {
106
- stmt.run(session_id, signal.event_type, tool_name, signal.details);
107
- }
108
- } finally {
109
- db.close();
110
- }
111
- } catch (_e) {
112
- // Best-effort: never block Claude Code
113
- }
114
- process.exit(0);
115
- }
116
-
117
- function readStdin(): Promise<string> {
118
- return new Promise((resolve) => {
119
- let data = '';
120
- process.stdin.setEncoding('utf-8');
121
- process.stdin.on('data', (chunk: string) => { data += chunk; });
122
- process.stdin.on('end', () => resolve(data));
123
- setTimeout(() => resolve(data), 3000);
124
- });
125
- }
126
-
127
- main();
@@ -1,159 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // PostToolUse Hook: Rule-to-Enforcement Pipeline
7
- // When a new prevention rule (feedback_*.md) is written,
8
- // automatically triggers the final step: enforcement placement.
9
- //
10
- // Part of the Auto-Learning Pipeline:
11
- // Fix Detected → Incident Report → Rule Derived → [ENFORCEMENT]
12
- //
13
- // Triggers on: Write to memory/feedback_*.md (configurable)
14
- // Must complete in <500ms.
15
- // ============================================================
16
-
17
- import { existsSync, readFileSync, readdirSync } from 'fs';
18
- import { basename, resolve } from 'path';
19
- import { getProjectRoot, getConfig } from '../config.ts';
20
-
21
- interface HookInput {
22
- session_id: string;
23
- tool_name: string;
24
- tool_input: { file_path?: string; content?: string };
25
- }
26
-
27
- async function main(): Promise<void> {
28
- try {
29
- const input = await readStdin();
30
- const hookInput = JSON.parse(input) as HookInput;
31
- const filePath = hookInput.tool_input?.file_path;
32
-
33
- if (!filePath) {
34
- process.exit(0);
35
- return;
36
- }
37
-
38
- const config = getConfig();
39
- if (config.autoLearning?.enabled === false) {
40
- process.exit(0);
41
- return;
42
- }
43
-
44
- const root = getProjectRoot();
45
- const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
46
- const enforcementDir = config.autoLearning?.enforcementHooksDir ?? 'scripts/hooks';
47
- // Only trigger on feedback rule files
48
- const relPath = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
49
- const fileName = basename(filePath);
50
-
51
- // Match feedback_*.md in either relative or absolute memory paths
52
- if (!fileName.startsWith('feedback_') || !fileName.endsWith('.md')) {
53
- process.exit(0);
54
- return;
55
- }
56
-
57
- // Must be in a memory-like directory
58
- const claudeDir = config.conventions?.claudeDirName ?? '.claude';
59
- if (!relPath.includes(memoryDir) && !relPath.includes('memory/') && !relPath.includes(claudeDir + '/')) {
60
- process.exit(0);
61
- return;
62
- }
63
-
64
- if (!existsSync(filePath)) {
65
- process.exit(0);
66
- return;
67
- }
68
-
69
- if (config.autoLearning?.pipeline?.requireEnforcement === false) {
70
- process.exit(0);
71
- return;
72
- }
73
-
74
- // Extract rule details from the file
75
- const content = readFileSync(filePath, 'utf-8');
76
- const nameMatch = content.match(/^name:\s*(.+)/m);
77
- const descMatch = content.match(/^description:\s*(.+)/m);
78
- const ruleName = nameMatch?.[1]?.trim() ?? fileName;
79
- const ruleDesc = descMatch?.[1]?.trim() ?? '';
80
-
81
- // Check if this rule already has enforcement in any hook file
82
- const enforcementDirAbs = resolve(root, enforcementDir);
83
- let hasEnforcement = false;
84
- if (existsSync(enforcementDirAbs)) {
85
- const hookFiles = readdirSync(enforcementDirAbs).filter(f => f.endsWith('.sh') || f.endsWith('.ts') || f.endsWith('.js'));
86
- for (const hookFile of hookFiles) {
87
- try {
88
- const hookContent = readFileSync(resolve(enforcementDirAbs, hookFile), 'utf-8');
89
- if (hookContent.includes(fileName)) {
90
- hasEnforcement = true;
91
- break;
92
- }
93
- } catch { /* ignore read errors */ }
94
- }
95
- }
96
-
97
- if (hasEnforcement) {
98
- // Already has enforcement — no action needed
99
- process.exit(0);
100
- return;
101
- }
102
-
103
- // Output enforcement placement instructions
104
- const lines: string[] = [];
105
- lines.push('');
106
- lines.push('============================================================================');
107
- lines.push(' AUTO-LEARNING: New Rule Created — Enforcement Placement Required');
108
- lines.push('============================================================================');
109
- lines.push('');
110
- lines.push(` Rule: ${ruleName}`);
111
- lines.push(` File: ${filePath}`);
112
- if (ruleDesc) {
113
- lines.push(` Description: ${ruleDesc}`);
114
- }
115
- lines.push('');
116
- lines.push(' This rule has NO automated enforcement yet. Add it now.');
117
- lines.push('');
118
- lines.push(' ANALYZE the rule and determine enforcement layer(s):');
119
- lines.push('');
120
- lines.push(' 1. STATICALLY DETECTABLE? (grep/regex can find violations in code)');
121
- lines.push(` → Add check to: ${enforcementDir}/pattern-feedback hook`);
122
- lines.push(` → Also add to pre-commit hook if critical`);
123
- lines.push('');
124
- lines.push(' 2. ABOUT EDITING CERTAIN FILES? (auth, infra, routers, etc.)');
125
- lines.push(` → Add warning to: ${enforcementDir}/blast-radius hook`);
126
- lines.push('');
127
- lines.push(' 3. ABOUT DANGEROUS COMMANDS? (kill, rm, destructive ops)');
128
- lines.push(` → Add block to: ${enforcementDir}/dangerous-command hook`);
129
- lines.push('');
130
- lines.push(' 4. NEEDS RUNTIME MONITORING? (can only be detected at runtime)');
131
- lines.push(' → Create a monitoring/audit producer');
132
- lines.push('');
133
- lines.push(' 5. AI-GUIDANCE ONLY? (philosophy, process, judgment calls)');
134
- lines.push(' → Memory rule is sufficient (already created)');
135
- lines.push('');
136
- lines.push(' AFTER adding enforcement, test the hook to verify it detects violations.');
137
- lines.push('');
138
- lines.push(' This step is MANDATORY per the auto-learning pipeline.');
139
- lines.push('============================================================================');
140
- lines.push('');
141
-
142
- console.log(lines.join('\n'));
143
- } catch {
144
- // Best-effort: never block Claude Code
145
- }
146
- process.exit(0);
147
- }
148
-
149
- function readStdin(): Promise<string> {
150
- return new Promise((resolve) => {
151
- let data = '';
152
- process.stdin.setEncoding('utf-8');
153
- process.stdin.on('data', (chunk: string) => { data += chunk; });
154
- process.stdin.on('end', () => resolve(data));
155
- setTimeout(() => resolve(data), 3000);
156
- });
157
- }
158
-
159
- main();
@@ -1,153 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Massu. All rights reserved.
3
- // Licensed under BSL 1.1 - see LICENSE file for details.
4
-
5
- // ============================================================
6
- // PreToolUse Hook: Security Gate
7
- // Validates tool calls against security policies.
8
- // Checks Bash commands for dangerous patterns and Write/Edit
9
- // tool calls for protected file paths.
10
- // Must complete in <500ms.
11
- // ============================================================
12
-
13
- // Force module mode for TypeScript (no external deps needed)
14
- export {};
15
-
16
- interface HookInput {
17
- session_id: string;
18
- tool_name: string;
19
- tool_input: {
20
- command?: string;
21
- file_path?: string;
22
- content?: string;
23
- new_string?: string;
24
- };
25
- }
26
-
27
- const DANGEROUS_BASH_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
28
- { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: 'rm -rf /' },
29
- { pattern: /rm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(?:\s|$)/, label: 'rm -rf /' },
30
- { pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/, label: 'curl | bash (remote code execution)' },
31
- { pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/, label: 'wget | bash (remote code execution)' },
32
- { pattern: /chmod\s+777/, label: 'chmod 777 (world-writable permissions)' },
33
- { pattern: /chmod\s+-R\s+777/, label: 'chmod -R 777 (world-writable permissions)' },
34
- { pattern: />\s*\/etc\/passwd/, label: 'write to /etc/passwd' },
35
- { pattern: />\s*\/etc\/shadow/, label: 'write to /etc/shadow' },
36
- { pattern: />\s*\/etc\/sudoers/, label: 'write to /etc/sudoers' },
37
- { pattern: /dd\s+if=.*of=\/dev\/(?:sda|sdb|hda|hdb|nvme)/, label: 'dd to raw device' },
38
- { pattern: /mkfs\s+\/dev\//, label: 'format disk device' },
39
- { pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}/, label: 'fork bomb' },
40
- { pattern: /eval\s+.*\$\(.*curl/, label: 'eval with remote curl' },
41
- { pattern: /base64\s+-d\s+.*\|\s*(?:bash|sh|zsh)/, label: 'base64 decoded shell exec' },
42
- ];
43
-
44
- const PROTECTED_FILE_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
45
- { pattern: /\.env$/, label: '.env file' },
46
- { pattern: /\.env\./, label: '.env.* file' },
47
- { pattern: /credentials(?:\.json)?$/, label: 'credentials file' },
48
- { pattern: /\.pem$/, label: '.pem certificate/key file' },
49
- { pattern: /\.key$/, label: '.key file' },
50
- { pattern: /\.p12$/, label: '.p12 keystore file' },
51
- { pattern: /\.pfx$/, label: '.pfx keystore file' },
52
- { pattern: /id_rsa$/, label: 'RSA private key' },
53
- { pattern: /id_ed25519$/, label: 'Ed25519 private key' },
54
- { pattern: /id_ecdsa$/, label: 'ECDSA private key' },
55
- { pattern: /\.ssh\/config$/, label: 'SSH config file' },
56
- { pattern: /secrets\.yaml$/, label: 'secrets.yaml file' },
57
- { pattern: /secrets\.yml$/, label: 'secrets.yml file' },
58
- { pattern: /\.netrc$/, label: '.netrc credentials file' },
59
- { pattern: /aws\/credentials$/, label: 'AWS credentials file' },
60
- { pattern: /kubeconfig$/, label: 'Kubernetes config file' },
61
- ];
62
-
63
- function checkBashCommand(command: string): string | null {
64
- for (const { pattern, label } of DANGEROUS_BASH_PATTERNS) {
65
- if (pattern.test(command)) {
66
- return label;
67
- }
68
- }
69
- return null;
70
- }
71
-
72
- function checkFilePath(filePath: string): string | null {
73
- for (const { pattern, label } of PROTECTED_FILE_PATTERNS) {
74
- if (pattern.test(filePath)) {
75
- return label;
76
- }
77
- }
78
- return null;
79
- }
80
-
81
- const DANGEROUS_PYTHON_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
82
- { pattern: /\beval\s*\(/, label: 'Python eval() — arbitrary code execution' },
83
- { pattern: /\bexec\s*\(/, label: 'Python exec() — arbitrary code execution' },
84
- { pattern: /\b__import__\s*\(/, label: 'Python __import__() — dynamic import (potential code injection)' },
85
- { pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: 'subprocess.call(shell=True) — shell injection risk' },
86
- { pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: 'subprocess.Popen(shell=True) — shell injection risk' },
87
- { pattern: /os\.system\s*\(/, label: 'os.system() — shell injection risk' },
88
- { pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: 'f-string in SQL — SQL injection risk' },
89
- { pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: 'String formatting in SQL — SQL injection risk' },
90
- ];
91
-
92
- function checkPythonContent(content: string): string | null {
93
- for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
94
- if (pattern.test(content)) {
95
- return label;
96
- }
97
- }
98
- return null;
99
- }
100
-
101
- async function main(): Promise<void> {
102
- try {
103
- const input = await readStdin();
104
- const hookInput = JSON.parse(input) as HookInput;
105
- const { tool_name, tool_input } = hookInput;
106
-
107
- if (tool_name === 'Bash' && tool_input.command) {
108
- const violation = checkBashCommand(tool_input.command);
109
- if (violation) {
110
- process.stdout.write(JSON.stringify({
111
- message: `SECURITY GATE: Dangerous command pattern detected: ${violation}\nCommand: ${tool_input.command.slice(0, 200)}\nReview carefully before proceeding.`,
112
- }));
113
- }
114
- }
115
-
116
- if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path) {
117
- const violation = checkFilePath(tool_input.file_path);
118
- if (violation) {
119
- process.stdout.write(JSON.stringify({
120
- message: `SECURITY GATE: Attempt to write to protected file: ${violation}\nPath: ${tool_input.file_path}\nEnsure this is intentional and no secrets will be exposed.`,
121
- }));
122
- }
123
- }
124
-
125
- // Check Python file content for dangerous patterns (Write uses content, Edit uses new_string)
126
- const pyContent = tool_input.content || tool_input.new_string;
127
- if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path?.endsWith('.py') && pyContent) {
128
- const pyViolation = checkPythonContent(pyContent);
129
- if (pyViolation) {
130
- process.stdout.write(JSON.stringify({
131
- message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}\nFile: ${tool_input.file_path}\nReview carefully before proceeding.`,
132
- }));
133
- }
134
- }
135
- } catch {
136
- // Hooks must never crash
137
- }
138
-
139
- process.exit(0);
140
- }
141
-
142
- function readStdin(): Promise<string> {
143
- return new Promise((resolve) => {
144
- let data = '';
145
- process.stdin.setEncoding('utf-8');
146
- process.stdin.on('data', (chunk) => { data += chunk; });
147
- process.stdin.on('end', () => resolve(data));
148
- // Timeout to prevent hanging
149
- setTimeout(() => resolve(data), 400);
150
- });
151
- }
152
-
153
- main();