@posthog/wizard 2.38.2 → 2.39.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 (96) hide show
  1. package/README.md +70 -13
  2. package/dist/{add-mcp-server-to-clients-CHSzsAG8.js → add-mcp-server-to-clients-Ci0I1vbS.js} +5 -5
  3. package/dist/{add-mcp-server-to-clients-CHSzsAG8.js.map → add-mcp-server-to-clients-Ci0I1vbS.js.map} +1 -1
  4. package/dist/{agent-interface-DExARWay.js → agent-interface-BR3zAUpM.js} +30 -623
  5. package/dist/agent-interface-BR3zAUpM.js.map +1 -0
  6. package/dist/{agent-runner-CMfTmM3B.js → agent-runner-B1fzwFzc.js} +33 -18
  7. package/dist/agent-runner-B1fzwFzc.js.map +1 -0
  8. package/dist/{analytics-Ciob-x8a.js → analytics-C7Oc3yWZ.js} +3 -2
  9. package/dist/analytics-C7Oc3yWZ.js.map +1 -0
  10. package/dist/analytics-tVERo8XR.js +2 -0
  11. package/dist/{api-T_WHs3qZ.js → api-DSG-l8Ql.js} +3 -3
  12. package/dist/{api-T_WHs3qZ.js.map → api-DSG-l8Ql.js.map} +1 -1
  13. package/dist/bin.js +387 -105
  14. package/dist/bin.js.map +1 -1
  15. package/dist/{ci-install-DJN5aT7W.js → ci-install-DeAd2HYP.js} +4 -4
  16. package/dist/{ci-install-DJN5aT7W.js.map → ci-install-DeAd2HYP.js.map} +1 -1
  17. package/dist/{debug-D1lORUFs.js → debug-BZnZ7Gvh.js} +37 -10
  18. package/dist/debug-BZnZ7Gvh.js.map +1 -0
  19. package/dist/{debug-Cne-cLxd.js → debug-DtgfvuKF.js} +1 -1
  20. package/dist/{defaults-a0e6IRjP.js → defaults-BxiMLkQY.js} +1 -1
  21. package/dist/{defaults-a0e6IRjP.js.map → defaults-BxiMLkQY.js.map} +1 -1
  22. package/dist/{env-api-key-DEl3LJBv.js → env-api-key-DDVkPQmo.js} +1 -1
  23. package/dist/{env-api-key-DEl3LJBv.js.map → env-api-key-DDVkPQmo.js.map} +1 -1
  24. package/dist/{environment-BEsDCA2q.js → environment-QNoUem74.js} +3 -3
  25. package/dist/{environment-BEsDCA2q.js.map → environment-QNoUem74.js.map} +1 -1
  26. package/dist/{file-utils-Cn3dzlo-.js → file-utils-UqdMTR9x.js} +2 -2
  27. package/dist/{file-utils-Cn3dzlo-.js.map → file-utils-UqdMTR9x.js.map} +1 -1
  28. package/dist/{headless-ui-DGunpK-s.js → headless-ui-DtFktXIa.js} +2 -2
  29. package/dist/{headless-ui-DGunpK-s.js.map → headless-ui-DtFktXIa.js.map} +1 -1
  30. package/dist/{host-resolution-BnUJZKId.js → host-resolution-DjQvur4X.js} +3 -3
  31. package/dist/{host-resolution-BnUJZKId.js.map → host-resolution-DjQvur4X.js.map} +1 -1
  32. package/dist/{interactive-BB2IDSWP.js → interactive-_8v2Ymw2.js} +2 -2
  33. package/dist/{interactive-BB2IDSWP.js.map → interactive-_8v2Ymw2.js.map} +1 -1
  34. package/dist/{mcp-CgA5vyiS.js → mcp-BX34188X.js} +2 -2
  35. package/dist/{mcp-CgA5vyiS.js.map → mcp-BX34188X.js.map} +1 -1
  36. package/dist/{mcp-prompt-streaming-CGAkq9qR.js → mcp-prompt-streaming-l1zsmQQc.js} +4 -4
  37. package/dist/{mcp-prompt-streaming-CGAkq9qR.js.map → mcp-prompt-streaming-l1zsmQQc.js.map} +1 -1
  38. package/dist/{non-interactive-Dko5F3vh.js → non-interactive-BBCGIDP5.js} +2 -2
  39. package/dist/{non-interactive-Dko5F3vh.js.map → non-interactive-BBCGIDP5.js.map} +1 -1
  40. package/dist/{package-manager-WKKQADHM.js → package-manager-FY9qPU4z.js} +2 -2
  41. package/dist/{package-manager-WKKQADHM.js.map → package-manager-FY9qPU4z.js.map} +1 -1
  42. package/dist/{playground-UtT6zbjE.js → playground-B9Gesj-w.js} +5 -5
  43. package/dist/{playground-UtT6zbjE.js.map → playground-B9Gesj-w.js.map} +1 -1
  44. package/dist/{posthog-BESrI6-T.js → posthog-BHbtnNQE.js} +1 -1
  45. package/dist/{posthog-BESrI6-T.js.map → posthog-BHbtnNQE.js.map} +1 -1
  46. package/dist/{posthog-integration-CWXSng0W.js → posthog-integration-CMNtIV97.js} +14 -14
  47. package/dist/{posthog-integration-CWXSng0W.js.map → posthog-integration-CMNtIV97.js.map} +1 -1
  48. package/dist/{provisioning-CgomM26T.js → provisioning-BInfIJy5.js} +4 -4
  49. package/dist/{provisioning-CgomM26T.js.map → provisioning-BInfIJy5.js.map} +1 -1
  50. package/dist/{registry-BQIX-25O.js → registry-BsPnELr-.js} +4 -4
  51. package/dist/{registry-BQIX-25O.js.map → registry-BsPnELr-.js.map} +1 -1
  52. package/dist/security-CSlaLY6P.js +204 -0
  53. package/dist/security-CSlaLY6P.js.map +1 -0
  54. package/dist/{setup-utils-DLGNk8T6.js → setup-utils-DfRLklZ3.js} +9 -9
  55. package/dist/{setup-utils-DLGNk8T6.js.map → setup-utils-DfRLklZ3.js.map} +1 -1
  56. package/dist/{start-tui-BjCKghQq.js → start-tui-H_BSAFdp.js} +29 -27
  57. package/dist/start-tui-H_BSAFdp.js.map +1 -0
  58. package/dist/{steps-XjV9CiqH.js → steps-z89LzfJG.js} +7 -7
  59. package/dist/{steps-XjV9CiqH.js.map → steps-z89LzfJG.js.map} +1 -1
  60. package/dist/{store-CDIxU0rr.js → store-B3B76DgD.js} +4 -4
  61. package/dist/{store-CDIxU0rr.js.map → store-B3B76DgD.js.map} +1 -1
  62. package/dist/{subagent-S5Yh16IJ.js → subagent-CV59ljO1.js} +2 -2
  63. package/dist/{subagent-S5Yh16IJ.js.map → subagent-CV59ljO1.js.map} +1 -1
  64. package/dist/{task-stream-rC4IEUHC.js → task-stream-d-R9J0g4.js} +2 -2
  65. package/dist/{task-stream-rC4IEUHC.js.map → task-stream-d-R9J0g4.js.map} +1 -1
  66. package/dist/{tasks-wbjOdifO.js → tasks-DkX454Mg.js} +2 -2
  67. package/dist/{tasks-wbjOdifO.js.map → tasks-DkX454Mg.js.map} +1 -1
  68. package/dist/{telemetry-BfnR7gtf.js → telemetry-BCIe6Kli.js} +3 -3
  69. package/dist/{telemetry-BfnR7gtf.js.map → telemetry-BCIe6Kli.js.map} +1 -1
  70. package/dist/{terminal-BvRwVeQp.js → terminal-CPXW44sU.js} +13 -12
  71. package/dist/{terminal-BvRwVeQp.js.map → terminal-CPXW44sU.js.map} +1 -1
  72. package/dist/{tools-CbtYlZV4.js → tools-C4BqVrt6.js} +4 -4
  73. package/dist/{tools-CbtYlZV4.js.map → tools-C4BqVrt6.js.map} +1 -1
  74. package/dist/types-evL_rPob.js +46 -0
  75. package/dist/types-evL_rPob.js.map +1 -0
  76. package/dist/{urls-CilgPbS-.js → urls-D1AehCBN.js} +2 -2
  77. package/dist/{urls-CilgPbS-.js.map → urls-D1AehCBN.js.map} +1 -1
  78. package/dist/warlock-smoke-test.ts +165 -0
  79. package/dist/{wizard-abort-Cp5Y7f8C.js → wizard-abort-5fOIcY8w.js} +3 -3
  80. package/dist/{wizard-abort-Cp5Y7f8C.js.map → wizard-abort-5fOIcY8w.js.map} +1 -1
  81. package/dist/{wizard-abort-DHrEGqaC.js → wizard-abort-Ce2RceZ1.js} +1 -1
  82. package/dist/{wizard-tools-DQTfAzSV.js → wizard-tools-xvIrJeNN.js} +26 -50
  83. package/dist/wizard-tools-xvIrJeNN.js.map +1 -0
  84. package/dist/yara-hooks-7MEXlgqm.js +2 -0
  85. package/dist/yara-hooks-DNn1Jqih.js +635 -0
  86. package/dist/yara-hooks-DNn1Jqih.js.map +1 -0
  87. package/package.json +4 -3
  88. package/dist/agent-interface-DExARWay.js.map +0 -1
  89. package/dist/agent-runner-CMfTmM3B.js.map +0 -1
  90. package/dist/analytics-Ciob-x8a.js.map +0 -1
  91. package/dist/analytics-DbfiRvPS.js +0 -2
  92. package/dist/debug-D1lORUFs.js.map +0 -1
  93. package/dist/security-pJOYY5LO.js +0 -431
  94. package/dist/security-pJOYY5LO.js.map +0 -1
  95. package/dist/start-tui-BjCKghQq.js.map +0 -1
  96. package/dist/wizard-tools-DQTfAzSV.js.map +0 -1
@@ -0,0 +1,635 @@
1
+ import { f as getUI, s as logToFile, u as WIZARD_YARA_REPORT_FILE } from "./debug-BZnZ7Gvh.js";
2
+ import { n as analytics } from "./analytics-C7Oc3yWZ.js";
3
+ import { r as AUDIT_REPORT_FILE } from "./types-evL_rPob.js";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import fg from "fast-glob";
7
+ import { createHash } from "crypto";
8
+ //#region src/lib/skill-install.ts
9
+ /**
10
+ * Check if command is a PostHog skill installation from MCP.
11
+ * We control the MCP server, so we only need to verify:
12
+ * 1. It installs to .claude/skills/
13
+ * 2. It downloads from our GitHub releases or localhost (dev)
14
+ *
15
+ * Extracted to its own module to avoid a circular dependency
16
+ * between agent-interface.ts and yara-hooks.ts.
17
+ */
18
+ function isSkillInstallCommand(command) {
19
+ if (!command.startsWith("mkdir -p .claude/skills/")) return false;
20
+ const urlMatch = command.match(/curl -sL ['"]([^'"]+)['"]/);
21
+ if (!urlMatch) return false;
22
+ const url = urlMatch[1];
23
+ return url.startsWith("https://github.com/PostHog/context-mill/releases/") || /^http:\/\/localhost:\d+\//.test(url);
24
+ }
25
+ //#endregion
26
+ //#region src/lib/programs/events-audit/constants.ts
27
+ /**
28
+ * Leaf-level constants for the events-audit program.
29
+ *
30
+ * Kept separate from `index.ts` so files like `yara-hooks.ts` can import
31
+ * the filename constants without dragging in `index.ts`'s heavier imports
32
+ * (agent-runner, audit/seed, etc.) — which can create import cycles.
33
+ */
34
+ const SETUP_REPORT_FILE = "posthog-events-audit-report.md";
35
+ const EVENT_INVENTORY_FILE = ".posthog-events-inventory.json";
36
+ /** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */
37
+ const EVENT_INVENTORY_PART_PATTERN = /^\.posthog-events-inventory\.part-\d+\.json$/;
38
+ //#endregion
39
+ //#region src/lib/programs/posthog-integration/constants.ts
40
+ /**
41
+ * Leaf-level constants for the posthog-integration program.
42
+ *
43
+ * Kept separate from `index.ts` so files like `yara-hooks.ts` can import
44
+ * the filename constants without dragging in `index.ts`'s heavier imports
45
+ * (agent-interface, framework-config, etc.) — which would create an import
46
+ * cycle through agent-interface → yara-hooks.
47
+ */
48
+ const EVENT_PLAN_FILE = ".posthog-events.json";
49
+ //#endregion
50
+ //#region src/lib/yara-hooks.ts
51
+ /**
52
+ * YARA hook wiring for the Claude Agent SDK.
53
+ *
54
+ * Creates PreToolUse and PostToolUse hook callback arrays that integrate the
55
+ * real @posthog/warlock security scanner (YARA-X via WASM) into the wizard's
56
+ * agent loop. These hooks are registered in the SDK's query() options alongside
57
+ * the existing Stop hook.
58
+ *
59
+ * PreToolUse hooks block dangerous commands before execution. PostToolUse hooks
60
+ * detect violations in written code and prompt injection in read content, and
61
+ * scan context-mill skill downloads.
62
+ *
63
+ * Warlock owns the rules; this file owns the *policy* — how a match maps to a
64
+ * block / revert / terminate response, plus the optional LLM triage pass that
65
+ * filters false positives before we act.
66
+ *
67
+ * Naming: "yara" is the technique — scanning content against YARA rules — so the
68
+ * wizard-side wiring keeps that name. "warlock" is the engine package that runs
69
+ * the rules. Both names showing up here is intentional.
70
+ *
71
+ * This is Layer 2 (L2) in the wizard's defense-in-depth model, complementing the
72
+ * prompt-based commandments (L0) and the canUseTool() allowlist (L1).
73
+ */
74
+ let warlockModulePromise = null;
75
+ function getWarlock() {
76
+ if (!warlockModulePromise) warlockModulePromise = import("@posthog/warlock").catch((err) => {
77
+ warlockModulePromise = null;
78
+ throw err;
79
+ });
80
+ return warlockModulePromise;
81
+ }
82
+ /**
83
+ * Pay the WASM-init + rule-compile cost up front, off the hook path, so the
84
+ * first real tool-call scan doesn't eat cold-start under a hook timeout.
85
+ * Best-effort: a failure here is non-fatal — hooks still fail closed per scan.
86
+ */
87
+ async function prewarmYaraScanner() {
88
+ try {
89
+ await (await getWarlock()).scan("");
90
+ logToFile("[YARA] warlock pre-warmed");
91
+ } catch (err) {
92
+ logToFile("[YARA] warlock pre-warm failed:", err);
93
+ }
94
+ }
95
+ let scanCount = 0;
96
+ const scanViolations = [];
97
+ function recordScan() {
98
+ scanCount++;
99
+ }
100
+ /**
101
+ * Log a match to the run log + PostHog, and record it in the run's scan report.
102
+ * Single entry point so every violation is reported consistently.
103
+ */
104
+ function recordMatch(phase, tool, match, action) {
105
+ const triage = match.triage;
106
+ logYaraMatch(phase, tool, match, action);
107
+ scanViolations.push({
108
+ rule: match.rule,
109
+ severity: match.metadata.severity ?? "unknown",
110
+ category: match.metadata.category ?? "unknown",
111
+ action,
112
+ phase,
113
+ tool,
114
+ description: match.metadata.description ?? "",
115
+ triageVerdict: triage?.verdict,
116
+ triageReason: triage?.reason
117
+ });
118
+ }
119
+ /**
120
+ * Scan bookkeeping for harnesses that run a scanner outside these hooks (pi):
121
+ * counts the scan and routes violations through `recordMatch`, so they reach
122
+ * the run log, per-match telemetry, and the end-of-run scan report.
123
+ */
124
+ function recordExternalScan(phase, tool, violations, action) {
125
+ recordScan();
126
+ for (const v of violations) recordMatch(phase, tool, {
127
+ rule: v.rule,
128
+ metadata: {
129
+ severity: v.severity,
130
+ category: v.category,
131
+ description: v.description
132
+ },
133
+ matchedStrings: []
134
+ }, action);
135
+ }
136
+ /** Format the scan report summary. Returns null if no scans occurred */
137
+ function formatScanReport() {
138
+ if (scanCount === 0) return null;
139
+ const lines = ["", "— YARA Scanner Summary —"];
140
+ const violationCount = scanViolations.length;
141
+ const cleanCount = scanCount - violationCount;
142
+ lines.push(`✓ ${scanCount} tool calls scanned, ${violationCount} violation${violationCount !== 1 ? "s" : ""} detected`);
143
+ if (violationCount > 0) {
144
+ lines.push("");
145
+ for (const v of scanViolations) {
146
+ const tag = v.action.toUpperCase();
147
+ lines.push(` [${tag}] ${v.rule} (${v.severity.toUpperCase()}) — ${v.phase}:${v.tool}`);
148
+ }
149
+ }
150
+ if (cleanCount > 0) {
151
+ lines.push("");
152
+ lines.push(`No violations: ✓ ${cleanCount} clean scan${cleanCount !== 1 ? "s" : ""}`);
153
+ }
154
+ lines.push("");
155
+ return lines.join("\n");
156
+ }
157
+ /** Write the scan report to a JSON file. Returns the file path, or null if no scans occurred. */
158
+ function writeScanReport() {
159
+ if (scanCount === 0) return null;
160
+ const report = {
161
+ summary: {
162
+ totalScans: scanCount,
163
+ violations: scanViolations.length,
164
+ clean: scanCount - scanViolations.length
165
+ },
166
+ violations: scanViolations
167
+ };
168
+ try {
169
+ fs.writeFileSync(WIZARD_YARA_REPORT_FILE, JSON.stringify(report, null, 2));
170
+ } catch (err) {
171
+ logToFile("[YARA] Failed to write scan report:", err);
172
+ return null;
173
+ }
174
+ return WIZARD_YARA_REPORT_FILE;
175
+ }
176
+ const CATEGORY_DESCRIPTIONS = {
177
+ prompt_injection: "hit hidden instructions in a file it read, a possible attempt to hijack the setup",
178
+ exfiltration: "tried to send data to an outside location",
179
+ destructive_operations: "tried to run a destructive command",
180
+ supply_chain: "tried to pull in untrusted code",
181
+ posthog_pii: "tried to write personal data (PII) into your project",
182
+ posthog_hardcoded_key: "tried to hardcode a PostHog key into your project",
183
+ hardcoded_secret: "tried to hardcode a secret into your project"
184
+ };
185
+ /** Most recent violation that terminated the run, or null. */
186
+ function lastTerminatingViolation() {
187
+ for (let i = scanViolations.length - 1; i >= 0; i--) if (scanViolations[i].action === "aborted") return scanViolations[i];
188
+ return null;
189
+ }
190
+ /**
191
+ * Build the user-facing copy for the security abort screen: what the scanner
192
+ * caught and what we did about it. This screen renders only because the run was
193
+ * stopped, so the "what we did" half is constant. Falls back to a generic line
194
+ * when no specific violation was recorded (e.g. the scanner itself errored and
195
+ * failed closed before a match was logged).
196
+ */
197
+ function formatYaraAbortMessage() {
198
+ const v = lastTerminatingViolation();
199
+ return `Security check stopped the setup.
200
+
201
+ The agent ${(v && CATEGORY_DESCRIPTIONS[v.category]) ?? "did something our security scanner flagged"}. We stopped the run to keep your project safe.\nNo unsafe changes were left in your project.
202
+
203
+ If this looks wrong, let us know: wizard@posthog.com`;
204
+ }
205
+ /** Timeout for scan hooks (PreToolUse, PostToolUse Write/Edit/Read/Grep) */
206
+ const HOOK_TIMEOUT_MS = 3e4;
207
+ /** Timeout for the skill install hook (filesystem I/O + multiple scans) */
208
+ const SKILL_SCAN_HOOK_TIMEOUT_MS = 3e4;
209
+ /**
210
+ * Chunk size for scanning (100 KB). Oversized content is scanned in
211
+ * overlapping chunks so coverage is complete — nothing is silently truncated.
212
+ * The size also bounds what a single triage call pastes into the LLM prompt
213
+ * (~25K tokens), so triage always sees the chunk its matches came from.
214
+ */
215
+ const SCAN_CHUNK_SIZE = 1e5;
216
+ /**
217
+ * Overlap between adjacent chunks so a pattern straddling a chunk boundary
218
+ * still lands whole inside at least one chunk. YARA rule strings are at most
219
+ * a few hundred bytes; 4 KB is generous.
220
+ */
221
+ const SCAN_CHUNK_OVERLAP = 4096;
222
+ function logYaraMatch(phase, tool, match, action) {
223
+ const severity = match.metadata.severity ?? "unknown";
224
+ const category = match.metadata.category ?? "unknown";
225
+ const ruleAction = match.metadata.action ?? "unknown";
226
+ const description = match.metadata.description ?? "";
227
+ logToFile(`[YARA] ${phase}:${tool} [${action.toUpperCase()}] rule "${match.rule}" (severity: ${severity}, category: ${category}, action: ${ruleAction})\n Description: ${description}`);
228
+ analytics.wizardCapture("yara rule matched", {
229
+ rule: match.rule,
230
+ severity: match.metadata.severity,
231
+ category: match.metadata.category,
232
+ rule_action: match.metadata.action,
233
+ action,
234
+ phase,
235
+ tool,
236
+ description: match.metadata.description,
237
+ triage_verdict: match.triage?.verdict
238
+ });
239
+ }
240
+ /**
241
+ * Send the run's full scan report to PostHog so maintainers can see why a run
242
+ * was flagged, warned, or aborted. This is the maintainer-facing report — it is
243
+ * NEVER shown to the user. No-op if nothing was scanned. The free-text triage
244
+ * reason is omitted to avoid sending scanned content off the user's machine.
245
+ */
246
+ function captureScanReport() {
247
+ if (scanCount === 0) return;
248
+ analytics.wizardCapture("yara scan report", {
249
+ total_scans: scanCount,
250
+ violation_count: scanViolations.length,
251
+ clean_count: scanCount - scanViolations.length,
252
+ violations: scanViolations.map((v) => ({
253
+ rule: v.rule,
254
+ severity: v.severity,
255
+ category: v.category,
256
+ action: v.action,
257
+ phase: v.phase,
258
+ tool: v.tool,
259
+ description: v.description,
260
+ triage_verdict: v.triageVerdict
261
+ }))
262
+ });
263
+ scanCount = 0;
264
+ scanViolations.length = 0;
265
+ }
266
+ /**
267
+ * End-of-run flush for the scan report. Wired once at the runner seam so every
268
+ * harness (linear, orchestrator, and any future shape) reports without having
269
+ * to know warlock exists — scanning is already harness-agnostic (it runs from
270
+ * SDK Pre/PostToolUse hooks), and this keeps reporting at a single shared seam.
271
+ *
272
+ * Order matters: write the local --yara-report file FIRST (it reads scanCount /
273
+ * scanViolations), THEN send telemetry. captureScanReport() zeroes scan state,
274
+ * which is what makes this whole function idempotent — a second call from
275
+ * another termination path (e.g. finally after an abort already flushed) finds
276
+ * scanCount === 0 and every step no-ops.
277
+ */
278
+ function flushScanReport(session) {
279
+ if (session.yaraReport) {
280
+ const reportPath = writeScanReport();
281
+ if (reportPath) {
282
+ const summary = formatScanReport();
283
+ getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ""}`);
284
+ }
285
+ }
286
+ captureScanReport();
287
+ }
288
+ const WIZARD_DOC_BASENAMES = new Set([
289
+ EVENT_INVENTORY_FILE,
290
+ SETUP_REPORT_FILE,
291
+ AUDIT_REPORT_FILE,
292
+ EVENT_PLAN_FILE
293
+ ]);
294
+ const WIZARD_DOC_PATTERNS = [EVENT_INVENTORY_PART_PATTERN];
295
+ function isWizardDocumentationPath(filePath) {
296
+ if (!filePath) return false;
297
+ const basename = path.basename(filePath);
298
+ if (WIZARD_DOC_BASENAMES.has(basename)) return true;
299
+ return WIZARD_DOC_PATTERNS.some((re) => re.test(basename));
300
+ }
301
+ /** Identical-payload attempt at which the agent is told to move on. */
302
+ const REPEAT_BLOCK_MOVE_ON_ATTEMPT = 3;
303
+ /** Per-run tracker of YARA-blocked payloads (keyed by tool + content hash). */
304
+ function createRepeatBlockTracker() {
305
+ const counts = /* @__PURE__ */ new Map();
306
+ return { attempt(tool, content) {
307
+ const key = `${tool} ${createHash("sha256").update(content).digest("hex")}`;
308
+ const next = (counts.get(key) ?? 0) + 1;
309
+ counts.set(key, next);
310
+ return next;
311
+ } };
312
+ }
313
+ /**
314
+ * The block reason for the given attempt: the plain reason the first time,
315
+ * an explicit "this exact retry can never work" on the second, and a
316
+ * "report it and move on" instruction from the third on.
317
+ */
318
+ function repeatBlockReason(attempt, tool, baseReason) {
319
+ if (attempt <= 1) return baseReason;
320
+ if (attempt < REPEAT_BLOCK_MOVE_ON_ATTEMPT) return `${baseReason} This exact ${tool} content was ALREADY blocked — the scan is deterministic, so retrying identical content will always block again. Change the code to remove the flagged pattern instead of retrying.`;
321
+ return `${baseReason} This exact ${tool} content has now been blocked ${attempt} times. STOP retrying it. Note the blocked change in the setup report and move on to the next task.`;
322
+ }
323
+ const SEVERITY_RANK = {
324
+ critical: 4,
325
+ high: 3,
326
+ medium: 2,
327
+ low: 1
328
+ };
329
+ /** Return the highest-severity match from a list of matches. */
330
+ function highestSeverityMatch(matches) {
331
+ return matches.reduce((worst, m) => (SEVERITY_RANK[m.metadata.severity ?? ""] ?? 0) > (SEVERITY_RANK[worst.metadata.severity ?? ""] ?? 0) ? m : worst);
332
+ }
333
+ /**
334
+ * Keep only matches whose rule targets this content surface. Rules carry a
335
+ * `scan_context` of 'command' | 'input' | 'output'. An undefined context is
336
+ * treated as "applies everywhere" (fail-safe — a rule that forgets the tag
337
+ * still gets enforced rather than silently skipped).
338
+ */
339
+ function matchesForContext(matches, ctx) {
340
+ return matches.filter((m) => {
341
+ const c = m.metadata.scan_context;
342
+ return c === ctx || c === void 0;
343
+ });
344
+ }
345
+ /**
346
+ * Drop false positives via warlock's LLM triage. Fail-closed: if no provider is
347
+ * available, or the triage call throws, every match is treated as real — we
348
+ * never silently suppress a flagged match.
349
+ *
350
+ * Every overruled match is reported to PostHog (rule metadata only, never the
351
+ * free-text reason) so maintainers can alert on triage-overrule patterns — the
352
+ * signal that someone is either tripping a noisy rule or trying to talk the
353
+ * triage model out of a real finding.
354
+ */
355
+ async function triageFilter(content, matches, ctx, llmProvider) {
356
+ if (matches.length === 0) return [];
357
+ if (!llmProvider) {
358
+ logToFile(`[YARA] triage skipped (no provider) — treating ${matches.length} match(es) as real`);
359
+ return matches;
360
+ }
361
+ try {
362
+ const triaged = await (await getWarlock()).triageMatches(content, matches, llmProvider);
363
+ const kept = triaged.filter((m) => m.triage.verdict === "true_positive");
364
+ for (const m of triaged) {
365
+ if (m.triage.verdict === "true_positive") continue;
366
+ logToFile(`[YARA] triage overruled rule "${m.rule}" (${m.metadata.severity ?? "unknown"}) — not acting on it`);
367
+ analytics.wizardCapture("yara triage overruled", {
368
+ rule: m.rule,
369
+ severity: m.metadata.severity,
370
+ category: m.metadata.category,
371
+ scan_context: ctx
372
+ });
373
+ }
374
+ logToFile(`[YARA] triage: ${matches.length} flagged → ${kept.length} kept as real`);
375
+ return kept;
376
+ } catch (err) {
377
+ logToFile("[YARA] triage failed — treating all matches as real:", err);
378
+ return matches;
379
+ }
380
+ }
381
+ /**
382
+ * Split oversized content into overlapping chunks so the scanner covers all
383
+ * of it. Content that fits in one chunk is returned as-is.
384
+ */
385
+ function chunkContent(content) {
386
+ if (content.length <= SCAN_CHUNK_SIZE) return [content];
387
+ const chunks = [];
388
+ const step = SCAN_CHUNK_SIZE - SCAN_CHUNK_OVERLAP;
389
+ for (let start = 0; start < content.length; start += step) {
390
+ chunks.push(content.slice(start, start + SCAN_CHUNK_SIZE));
391
+ if (start + SCAN_CHUNK_SIZE >= content.length) break;
392
+ }
393
+ return chunks;
394
+ }
395
+ /**
396
+ * Scan content against warlock rules — chunked when oversized, so nothing is
397
+ * skipped — and keep only matches for this content surface. Chunks scan
398
+ * sequentially (the WASM engine is single-threaded; parallelism buys nothing).
399
+ * Returns each flagged chunk with its matches so triage can later judge every
400
+ * match against the exact content it came from.
401
+ */
402
+ async function scanForContext(content, ctx) {
403
+ const warlock = await getWarlock();
404
+ const chunks = chunkContent(content);
405
+ if (chunks.length > 1) {
406
+ logToFile(`[YARA] content is ${content.length} chars — scanning ${chunks.length} overlapping chunks`);
407
+ analytics.wizardCapture("yara scan chunked", {
408
+ content_length: content.length,
409
+ chunk_count: chunks.length,
410
+ scan_context: ctx
411
+ });
412
+ }
413
+ const flagged = [];
414
+ for (const chunk of chunks) {
415
+ const result = await warlock.scan(chunk);
416
+ if (!result.matched) continue;
417
+ const matches = matchesForContext(result.matches, ctx);
418
+ if (matches.length === 0) continue;
419
+ flagged.push({
420
+ chunk,
421
+ matches
422
+ });
423
+ }
424
+ return flagged;
425
+ }
426
+ /** The overlap between chunks can surface the same rule twice; count it once. */
427
+ function dedupeByRule(matches) {
428
+ const seen = /* @__PURE__ */ new Set();
429
+ return matches.filter((m) => {
430
+ if (seen.has(m.rule)) return false;
431
+ seen.add(m.rule);
432
+ return true;
433
+ });
434
+ }
435
+ /**
436
+ * Triage each flagged chunk against its own content — the evidence is always
437
+ * inside the window the LLM sees. Triage calls run in parallel (each is just
438
+ * an HTTP round trip) so N flagged chunks cost ~1× triage latency, not N×.
439
+ */
440
+ async function triageFlagged(flagged, ctx, llmProvider) {
441
+ return dedupeByRule((await Promise.all(flagged.map(({ chunk, matches }) => triageFilter(chunk, matches, ctx, llmProvider)))).flat());
442
+ }
443
+ /**
444
+ * Scan content, filter to the relevant context, triage. Returns real matches.
445
+ * Exported for harnesses that run their own hook loop (pi) so every harness
446
+ * scans through the same warlock engine, chunking, and triage policy.
447
+ */
448
+ async function scanAndTriage(content, ctx, llmProvider) {
449
+ return triageFlagged(await scanForContext(content, ctx), ctx, llmProvider);
450
+ }
451
+ /**
452
+ * Create PreToolUse hook matchers for YARA scanning.
453
+ * Scans Bash commands before execution for exfiltration, destructive
454
+ * operations, and supply chain violations ('command'-context rules).
455
+ */
456
+ function createPreToolUseYaraHooks(_llmProvider) {
457
+ const repeatTracker = createRepeatBlockTracker();
458
+ return [{
459
+ hooks: [async (input) => {
460
+ try {
461
+ if (input.tool_name !== "Bash") return {};
462
+ const toolInput = input.tool_input;
463
+ const command = typeof toolInput?.command === "string" ? toolInput.command : "";
464
+ if (!command) return {};
465
+ recordScan();
466
+ const matches = await scanAndTriage(command, "command", void 0);
467
+ if (matches.length === 0) return {};
468
+ const match = highestSeverityMatch(matches);
469
+ recordMatch("PreToolUse", "Bash", match, "blocked");
470
+ return {
471
+ decision: "block",
472
+ reason: repeatBlockReason(repeatTracker.attempt("Bash", command), "Bash", `[YARA] ${match.rule}: ${match.metadata.description ?? "security policy violation"}. Command blocked for security.`)
473
+ };
474
+ } catch (error) {
475
+ logToFile("[YARA] PreToolUse hook error:", error);
476
+ return {
477
+ decision: "block",
478
+ reason: "[YARA] Scanner error — command blocked as a precaution."
479
+ };
480
+ }
481
+ }],
482
+ timeout: HOOK_TIMEOUT_MS
483
+ }];
484
+ }
485
+ /**
486
+ * Create PostToolUse hook matchers for YARA scanning.
487
+ *
488
+ * Three matchers:
489
+ * 1. Write/Edit — scan written content ('output'-context rules: PII, secrets)
490
+ * 2. Read/Grep — scan read content ('input'-context rules: prompt injection)
491
+ * 3. Bash (skill install) — scan downloaded skill files for poisoned content
492
+ *
493
+ * `onTerminate` is invoked on the terminal paths (critical prompt injection in
494
+ * read content, a poisoned skill, or a scanner error under fail-closed). It is
495
+ * what ACTUALLY stops the run — returning `stopReason` from a PostToolUse hook
496
+ * is not honored by the SDK, so the caller wires this to the run's
497
+ * AbortController (see agent-interface).
498
+ */
499
+ function createPostToolUseYaraHooks(llmProvider, onTerminate) {
500
+ return [
501
+ {
502
+ hooks: [async (input) => {
503
+ try {
504
+ const toolName = input.tool_name;
505
+ if (toolName !== "Write" && toolName !== "Edit") return {};
506
+ const toolInput = input.tool_input;
507
+ const content = toolName === "Write" ? toolInput?.content ?? "" : toolInput?.new_string ?? "";
508
+ if (!content) return {};
509
+ recordScan();
510
+ const flagged = await scanForContext(content, "output");
511
+ if (flagged.length === 0) return {};
512
+ const filePath = toolInput?.file_path;
513
+ const activeFlagged = isWizardDocumentationPath(filePath) ? flagged.map(({ chunk, matches }) => ({
514
+ chunk,
515
+ matches: matches.filter((m) => m.metadata.category !== "posthog_pii")
516
+ })).filter(({ matches }) => matches.length > 0) : flagged;
517
+ if (activeFlagged.length === 0) {
518
+ logToFile(`[YARA] posthog_pii match suppressed on wizard doc ${path.basename(filePath ?? "")} (rule: ${flagged[0]?.matches[0]?.rule})`);
519
+ return {};
520
+ }
521
+ const matches = await triageFlagged(activeFlagged, "output", llmProvider);
522
+ if (matches.length === 0) return {};
523
+ const match = highestSeverityMatch(matches);
524
+ recordMatch("PostToolUse", toolName, match, "reverted");
525
+ return { hookSpecificOutput: {
526
+ hookEventName: "PostToolUse",
527
+ additionalContext: `[YARA VIOLATION] ${match.rule}: ${match.metadata.description ?? ""}. You MUST revert this change immediately. The content you just wrote violates security policy.`
528
+ } };
529
+ } catch (error) {
530
+ logToFile("[YARA] PostToolUse Write/Edit hook error:", error);
531
+ const reason = "[YARA] Scanner error while scanning written content — session terminated as a precaution.";
532
+ onTerminate(reason);
533
+ return { stopReason: reason };
534
+ }
535
+ }],
536
+ timeout: HOOK_TIMEOUT_MS
537
+ },
538
+ {
539
+ hooks: [async (input) => {
540
+ try {
541
+ const toolName = input.tool_name;
542
+ if (toolName !== "Read" && toolName !== "Grep") return {};
543
+ const toolResponse = input.tool_response;
544
+ if (toolResponse == null) return {};
545
+ const content = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
546
+ if (!content) return {};
547
+ recordScan();
548
+ const matches = await scanAndTriage(content, "input", llmProvider);
549
+ if (matches.length === 0) return {};
550
+ const match = highestSeverityMatch(matches);
551
+ if (match.metadata.severity === "critical" || match.metadata.action === "block") {
552
+ recordMatch("PostToolUse", toolName, match, "aborted");
553
+ const reason = `[YARA CRITICAL] ${match.rule}: Prompt injection detected in file content. Agent context is potentially poisoned. Session terminated for safety.`;
554
+ onTerminate(reason);
555
+ return { stopReason: reason };
556
+ }
557
+ recordMatch("PostToolUse", toolName, match, "warned");
558
+ return { hookSpecificOutput: {
559
+ hookEventName: "PostToolUse",
560
+ additionalContext: `[YARA WARNING] ${match.rule}: ${match.metadata.description ?? ""}`
561
+ } };
562
+ } catch (error) {
563
+ logToFile("[YARA] PostToolUse Read/Grep hook error:", error);
564
+ const reason = "[YARA] Scanner error while scanning read content — session terminated as a precaution.";
565
+ onTerminate(reason);
566
+ return { stopReason: reason };
567
+ }
568
+ }],
569
+ timeout: HOOK_TIMEOUT_MS
570
+ },
571
+ {
572
+ hooks: [async (input) => {
573
+ try {
574
+ if (input.tool_name !== "Bash") return {};
575
+ const toolInput = input.tool_input;
576
+ const command = typeof toolInput?.command === "string" ? toolInput.command : "";
577
+ if (!isSkillInstallCommand(command)) return {};
578
+ const dirMatch = command.match(/mkdir -p (.claude\/skills\/[^\s&]+)/);
579
+ if (!dirMatch) return {};
580
+ const skillDir = dirMatch[1];
581
+ const cwd = input.cwd ?? process.cwd();
582
+ recordScan();
583
+ const matches = await scanSkillFiles(cwd, skillDir, llmProvider);
584
+ if (matches.length === 0) return {};
585
+ const match = highestSeverityMatch(matches);
586
+ recordMatch("PostToolUse", "Bash (skill install)", match, "aborted");
587
+ const reason = `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${match.rule}. The downloaded skill contains potential prompt injection. Session terminated for safety.`;
588
+ onTerminate(reason);
589
+ return { stopReason: reason };
590
+ } catch (error) {
591
+ logToFile("[YARA] PostToolUse skill install hook error:", error);
592
+ const reason = "[YARA] Scanner error while scanning skill files — session terminated as a precaution.";
593
+ onTerminate(reason);
594
+ return { stopReason: reason };
595
+ }
596
+ }],
597
+ timeout: SKILL_SCAN_HOOK_TIMEOUT_MS
598
+ }
599
+ ];
600
+ }
601
+ /**
602
+ * Read and scan all text files in a skill directory for prompt injection.
603
+ * Scans each file sequentially (single-threaded WASM — parallelism buys
604
+ * nothing), unions the 'input'-context matches, then triages once across the
605
+ * combined content. Returns the real (post-triage) matches.
606
+ */
607
+ async function scanSkillFiles(cwd, skillDir, llmProvider) {
608
+ const absoluteDir = path.resolve(cwd, skillDir);
609
+ if (!fs.existsSync(absoluteDir)) {
610
+ logToFile(`[YARA] Skill directory does not exist: ${absoluteDir}`);
611
+ return [];
612
+ }
613
+ const files = await fg("**/*.{md,txt,yaml,yml,json,js,ts,py,rb,sh}", {
614
+ cwd: absoluteDir,
615
+ absolute: true
616
+ });
617
+ const fileContents = [];
618
+ for (const filePath of files) try {
619
+ fileContents.push(fs.readFileSync(filePath, "utf-8"));
620
+ } catch (err) {
621
+ logToFile(`[YARA] Could not read skill file ${filePath}:`, err);
622
+ }
623
+ if (fileContents.length === 0) {
624
+ logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);
625
+ return [];
626
+ }
627
+ logToFile(`[YARA] Scanning ${fileContents.length} files in skill directory: ${skillDir}`);
628
+ const flagged = [];
629
+ for (const content of fileContents) flagged.push(...await scanForContext(content, "input"));
630
+ return triageFlagged(flagged, "input", llmProvider);
631
+ }
632
+ //#endregion
633
+ export { flushScanReport as a, isWizardDocumentationPath as c, repeatBlockReason as d, scanAndTriage as f, SETUP_REPORT_FILE as h, createRepeatBlockTracker as i, prewarmYaraScanner as l, EVENT_PLAN_FILE as m, createPostToolUseYaraHooks as n, formatScanReport as o, writeScanReport as p, createPreToolUseYaraHooks as r, formatYaraAbortMessage as s, captureScanReport as t, recordExternalScan as u };
634
+
635
+ //# sourceMappingURL=yara-hooks-DNn1Jqih.js.map