@coworker-jp/aidr 0.1.316 → 0.1.321

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coworker-jp/aidr",
3
- "version": "0.1.316",
3
+ "version": "0.1.321",
4
4
  "description": "AIDR setup CLI - installs ai-scanner hooks for 19+ AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,10 @@ const SCRIPT_FILES = [
21
21
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
22
22
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
23
23
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
24
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
24
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
25
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
26
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
27
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
25
28
 
26
29
  export const meta = {
27
30
  name: "claude",
@@ -35,7 +35,10 @@ const SCRIPT_FILES = [
35
35
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
36
36
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
37
37
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
38
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
38
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
39
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
40
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
41
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
39
42
  const ROOT_FILES = ["AGENTS.md"];
40
43
 
41
44
  export const meta = {
@@ -36,7 +36,10 @@ const SCRIPT_FILES = [
36
36
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
37
37
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
38
38
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
39
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
39
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
40
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
41
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
42
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
40
43
 
41
44
  export const meta = {
42
45
  name: "cursor",
@@ -13,7 +13,10 @@ const HOOK_FILES = ["aidr-pre.sh"];
13
13
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
14
14
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
15
15
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
16
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
16
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
17
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
18
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
19
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
17
20
 
18
21
  export const meta = {
19
22
  name: "gemini",
@@ -26,11 +29,13 @@ export async function install(home, accessKey, opts = {}) {
26
29
  const { dryRun = false, scope = "project" } = opts;
27
30
  const base = meta.configDir(home);
28
31
  const hooks = path.join(base, "hooks");
29
- const binary = path.join(home, meta.agentDir, "bin", "ai-scanner");
32
+ // #2366: hook が呼ぶのは `ai-harness` (8 コンテキスト)。`ai-scanner` は
33
+ // 定期インベントリ送信で、`scan read-input` 等のサブコマンドを持たない。
34
+ const binary = path.join(home, meta.agentDir, "bin", "ai-harness");
30
35
  const settingsPath = path.join(base, "settings.json");
31
36
  const preHookPath = path.join(hooks, "aidr-pre.sh");
32
37
  const [settingsRes, hookRes] = await Promise.all([
33
- writeJsonMerge(settingsPath, buildGeminiSettings(accessKey, binary), mergeGeminiSettings, { dryRun, backup: scope !== "project" }),
38
+ writeJsonMerge(settingsPath, buildGeminiSettings(accessKey, hooks), mergeGeminiSettings, { dryRun, backup: scope !== "project" }),
34
39
  writeFileSafe(preHookPath, T.getGeminiPreHookSh(binary), { dryRun, force: true, mode: 0o755 }),
35
40
  ]);
36
41
  return {
@@ -26,7 +26,10 @@ const SCRIPT_FILES = [
26
26
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
27
27
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
28
28
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
29
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
29
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
30
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
31
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
32
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
30
33
 
31
34
  export const meta = {
32
35
  name: "kiro",
@@ -10,7 +10,10 @@ import { pathExists, rmdirIfEmpty } from "../fs-utils.mjs";
10
10
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
11
11
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
12
12
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
13
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
13
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
14
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
15
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
16
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
14
17
 
15
18
  export const meta = {
16
19
  name: "standalone",
@@ -33,7 +33,10 @@ const SCRIPT_FILES = [
33
33
  // `rmdirIfEmpty` が効かないので、アンインストール後にディレクトリと記録が居座る。
34
34
  // 7 エージェントすべて同じ表を持ち、`tests/bin-dir-artifacts.test.mjs` が
35
35
  // 「表 ⇔ テンプレートが実際に書く名前」を両方向で照合する。
36
- const BIN_FILES = ["ai-scanner", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
36
+ // #2366: 配布物は 2 本。`ai-harness` が hook の 8 コンテキストを持ち、
37
+ // `ai-scanner` が定期インベントリ送信を持つ。**両方を消す対象に入れる** ——
38
+ // 片方だけ残すと bin ディレクトリが空にならず `rmdirIfEmpty` が効かない。
39
+ const BIN_FILES = ["ai-scanner", "ai-harness", "opengrep", ".ai-scanner-unscanned", ".ai-scanner-last-pull"];
37
40
 
38
41
  export const meta = {
39
42
  name: "windsurf",
@@ -282,8 +282,18 @@ export async function fetchAsset(binUrl, destPath, { accessKey, checksumOpts } =
282
282
  return { path: destPath, sha256, verified };
283
283
  }
284
284
 
285
+ //: The product served at the BARE platform key. Every other product is
286
+ //: addressed as `<product>/<platform>`; see `fetchBinary` for why this one is
287
+ //: not, and why that asymmetry is deliberate rather than an oversight.
288
+ export const DEFAULT_PRODUCT = "ai-scanner";
289
+
290
+ //: The hook binary (issue #2348 split, wired up in #2366). This is what every
291
+ //: hook adapter invokes — the eight tool-call scan contexts live here, NOT in
292
+ //: `ai-scanner`, which is the periodic endpoint-inventory collector.
293
+ export const HARNESS_PRODUCT = "ai-harness";
294
+
285
295
  /**
286
- * Download ai-scanner for the current platform into `destPath`.
296
+ * Download one scanner-family binary for the current platform into `destPath`.
287
297
  *
288
298
  * TWO KEYS, ONE FALLBACK (issue #1687)
289
299
  * ------------------------------------
@@ -312,19 +322,28 @@ export async function fetchAsset(binUrl, destPath, { accessKey, checksumOpts } =
312
322
  * `servedUnpinned: true` means the caller must NOT claim the pinned version was
313
323
  * installed — what landed is whatever the endpoint currently publishes.
314
324
  */
315
- export async function fetchBinary(destPath, { env = "prod", accessKey, version, baseUrl } = {}) {
325
+ export async function fetchBinary(destPath, { env = "prod", accessKey, version, baseUrl, product = DEFAULT_PRODUCT } = {}) {
316
326
  const platform = detectPlatform();
317
327
  const base = baseUrl || downloadBase(env);
318
- const unpinnedUrl = `${base}/${platform}`;
328
+ // ONE ROUTE, TWO PRODUCTS (issue #2366)
329
+ // ------------------------------------
330
+ // `ai-scanner` keeps the BARE platform key it has always had — that key is a
331
+ // wire contract with every endpoint already in the field, and the binary's own
332
+ // self-updater composes it. Everything else is addressed under its own name.
333
+ // Composing `ai-scanner/<platform>` "for symmetry" would ask a Lambda that
334
+ // predates the split for a path it rejects, and the endpoints that would hit
335
+ // it are exactly the ones with no other way in.
336
+ const seg = product === DEFAULT_PRODUCT ? "" : `${product}/`;
337
+ const unpinnedUrl = `${base}/${seg}${platform}`;
319
338
 
320
339
  if (!version) {
321
340
  const result = await fetchAsset(unpinnedUrl, destPath, { accessKey });
322
- return { ...result, platform, requestedVersion: null, servedUnpinned: false };
341
+ return { ...result, platform, product, requestedVersion: null, servedUnpinned: false };
323
342
  }
324
343
 
325
344
  try {
326
- const result = await fetchAsset(`${base}/v${version}/${platform}`, destPath, { accessKey });
327
- return { ...result, platform, requestedVersion: version, servedUnpinned: false };
345
+ const result = await fetchAsset(`${base}/v${version}/${seg}${platform}`, destPath, { accessKey });
346
+ return { ...result, platform, product, requestedVersion: version, servedUnpinned: false };
328
347
  } catch (e) {
329
348
  // A pinned release whose CHECKSUM is a definite 404 is incomplete in exactly
330
349
  // the same way as one whose binary is (#1773): half a release is not a
@@ -333,7 +352,7 @@ export async function fetchBinary(destPath, { env = "prod", accessKey, version,
333
352
  // unverified install, because `fetchAsset` refuses that on both keys.
334
353
  if (e?.status !== 404) throw e;
335
354
  const result = await fetchAsset(unpinnedUrl, destPath, { accessKey });
336
- return { ...result, platform, requestedVersion: version, servedUnpinned: true };
355
+ return { ...result, platform, product, requestedVersion: version, servedUnpinned: true };
337
356
  }
338
357
  }
339
358
 
package/src/cli.mjs CHANGED
@@ -10,7 +10,7 @@ import { Command } from "commander";
10
10
  const execFileP = promisify(execFile);
11
11
  import { AGENT_ORDER, AGENT_REGISTRY, loadAgent } from "./agents/index.mjs";
12
12
  import { verifyKey, maskKey } from "./verify.mjs";
13
- import { fetchBinary, fetchOpengrep, assertVerified } from "./binary-fetcher.mjs";
13
+ import { fetchBinary, fetchOpengrep, assertVerified, HARNESS_PRODUCT } from "./binary-fetcher.mjs";
14
14
  import { detectInstalled } from "./detect.mjs";
15
15
  import { evaluateInstallGate, formatLocalPcGuidance } from "./env-gate.mjs";
16
16
  import { resolveCallerHome, isRunningUnderSudo, chownToCaller } from "./sudo-user.mjs";
@@ -201,15 +201,33 @@ async function cmdInstall(opts) {
201
201
  version: PINNED_VERSION,
202
202
  };
203
203
 
204
- // ai-scanner + bundled Opengrep (LGPL-2.1). Both land in the same bin
205
- // dir so ai-scanner's sibling-lookup finds opengrep without relying on
206
- // PATH. Users can point AI_SCANNER_OPENGREP elsewhere to substitute
207
- // their own LGPL-2.1 build.
204
+ // THREE files, one bin dir (issue #2366).
205
+ //
206
+ // ai-harness the hook binary — the eight tool-call scan contexts.
207
+ // Every adapter this installer writes invokes THIS one,
208
+ // so it is fetched first: it is what "this machine is
209
+ // being scanned" means.
210
+ // ai-scanner the periodic endpoint-inventory collector, and the
211
+ // self-update vehicle start_scanner.sh drives.
212
+ // opengrep bundled Opengrep (LGPL-2.1), a sibling so the scanner's
213
+ // sibling-lookup finds it without relying on PATH. Users
214
+ // can point AI_SCANNER_OPENGREP elsewhere to substitute
215
+ // their own LGPL-2.1 build.
216
+ const harnessRes = assertVerified(
217
+ await fetchBinary(path.join(primaryDir, "ai-harness"),
218
+ { ...fetchOpts, product: HARNESS_PRODUCT }),
219
+ "the ai-harness binary",
220
+ );
221
+ // Set by the HOOK binary, not the periodic one: this flag is what
222
+ // decides between "nothing on this machine scans" and "scanning is in
223
+ // place, one piece is missing". Tying it to ai-scanner would report the
224
+ // reassuring half of that pair while every tool call ran unscanned.
225
+ binaryOutcome.scannerInstalled = true;
226
+ console.log(`binary: ${harnessRes.path} (${harnessRes.platform}, sha256 verified)`);
208
227
  const scannerRes = assertVerified(
209
228
  await fetchBinary(path.join(primaryDir, "ai-scanner"), fetchOpts),
210
229
  "the ai-scanner binary",
211
230
  );
212
- binaryOutcome.scannerInstalled = true;
213
231
  // Stated unconditionally, because it is now a condition of getting here
214
232
  // rather than a caption on a file that may or may not have been checked
215
233
  // (#1773): `fetchAsset` throws unless the bytes matched the published
@@ -231,7 +249,7 @@ async function cmdInstall(opts) {
231
249
  console.log(`binary: ${opengrepRes.path} (opengrep ${opengrepRes.platform}, sha256 verified)`);
232
250
 
233
251
  const copyOps = restDirs.flatMap(restDir =>
234
- ["ai-scanner", "opengrep"].map(async (file) => {
252
+ ["ai-harness", "ai-scanner", "opengrep"].map(async (file) => {
235
253
  const src = path.join(primaryDir, file);
236
254
  const dest = path.join(restDir, file);
237
255
  await fs.mkdir(restDir, { recursive: true });
@@ -366,7 +384,10 @@ async function cmdInstall(opts) {
366
384
  const stranded = needBinary.filter((r) => !r.selfHeals).map((r) => r.displayName);
367
385
  const binPaths = (await Promise.all(needBinary.map(async (r) => {
368
386
  const mod = await loadAgent(r.name);
369
- return mod.meta.agentDir ? path.join(home, mod.meta.agentDir, "bin", "ai-scanner") : null;
387
+ // The hook binary: this list is read out to someone checking whether the
388
+ // machine is scanned, and `ai-scanner` sitting there would answer "yes"
389
+ // to a question about a file that runs no hooks (#2366).
390
+ return mod.meta.agentDir ? path.join(home, mod.meta.agentDir, "bin", "ai-harness") : null;
370
391
  }))).filter(Boolean);
371
392
  console.error(formatMissingBinaryStatus({
372
393
  reason, selfHealing, stranded, binPaths, installCommand: INSTALL_COMMAND_HINT,
@@ -599,13 +620,24 @@ async function cmdDoctor() {
599
620
  const mod = await loadAgent(name);
600
621
  const agentDir = mod.meta.agentDir;
601
622
  if (!agentDir) return null;
602
- const bin = path.join(home, agentDir, "bin", "ai-scanner");
603
- let present = false;
604
- try {
605
- await fs.access(bin, fs.constants.F_OK);
606
- present = true;
607
- } catch {}
608
- const out = [`binary: ${bin} ${present ? "[present]" : "[missing]"}`];
623
+ // TWO binaries since the split (issue #2366), and only ONE of them decides
624
+ // whether tool calls are scanned. `ai-harness` carries the eight hook
625
+ // contexts; `ai-scanner` is the periodic collector. Reporting only the
626
+ // latter would print "[present]" on a machine whose every hook fail-opens.
627
+ const bin = path.join(home, agentDir, "bin", "ai-harness");
628
+ const periodicBin = path.join(home, agentDir, "bin", "ai-scanner");
629
+ const exists = async (p) => {
630
+ try { await fs.access(p, fs.constants.F_OK); return true; } catch { return false; }
631
+ };
632
+ const present = await exists(bin);
633
+ const periodicPresent = await exists(periodicBin);
634
+ const out = [
635
+ `tool-call scanner: ${bin} ${present ? "[present]" : "[missing]"}`,
636
+ `periodic collector: ${periodicBin} ${periodicPresent ? "[present]" : "[missing]"}`,
637
+ ];
638
+ if (!present) {
639
+ out.push(` Tool calls on this machine are NOT being scanned. Re-run the installer (${INSTALL_COMMAND_HINT}), or contact your IT administrator.`);
640
+ }
609
641
  // What the hooks recorded while there was no binary (#1774). Without this,
610
642
  // "[missing]" was the only trace, and it said nothing about how long the
611
643
  // machine had been unscanned or how much had gone through unscanned —
@@ -41,7 +41,10 @@ export function formatMissingBinaryStatus({
41
41
  const lines = [
42
42
  "",
43
43
  RULE,
44
- "This machine is NOT being scanned yet — the ai-scanner binary is not in place.",
44
+ // Names `ai-harness`, because that is the binary that scans a tool call
45
+ // (issue #2366). Saying "ai-scanner" here would send the reader to check a
46
+ // file that can be perfectly present on a machine where nothing is scanned.
47
+ "This machine is NOT being scanned yet — the ai-harness binary is not in place.",
45
48
  "",
46
49
  ` what stopped it: ${reason}`,
47
50
  "",
package/src/merge.mjs CHANGED
@@ -24,8 +24,17 @@
24
24
  export const CLAUDE_OUR_COMMAND_RE =
25
25
  /^(?:"\$(?:CLAUDE_PROJECT_DIR|HOME)"|\$(?:CLAUDE_PROJECT_DIR|HOME))\/\.claude\/coworker-ai\/(?:start_scanner|stop_scanner|scan_[a-z_]+|log_web_urls)\.sh(?:\s|$|"|')/;
26
26
 
27
+ // Matches BOTH the adapter we install today AND the broken pre-#2354 command.
28
+ // **The legacy alternative is load-bearing, not tidiness**: an endpoint that
29
+ // already ran the old installer has the broken command sitting in its
30
+ // `settings.json`. `mergeGeminiSettings` / `unmergeGeminiSettings` use this
31
+ // predicate to recognise our own entries — if it stopped matching the old form,
32
+ // an upgrade would **leave the broken hook in place and add the new one beside
33
+ // it**, so every tool call would still hit a command that exits 2, and
34
+ // uninstall would leave it behind forever. Drop the legacy branch only once no
35
+ // endpoint in the field can still carry it.
27
36
  export const GEMINI_OUR_COMMAND_RE =
28
- /ai-scanner["']?\s+scan\s+hook-gemini\b/;
37
+ /(?:\/\.gemini\/hooks\/aidr-(?:pre|post)\.sh|ai-scanner["']?\s+scan\s+hook-gemini\b)/;
29
38
 
30
39
  // Matches our Codex hook wrappers whether installed at the repo root
31
40
  // (.codex/coworker-ai/pre_shell.sh) or absolute user-scope path
@@ -229,7 +238,27 @@ export function buildWindsurfHooksJson(scope = "project", absHome = "") {
229
238
  };
230
239
  }
231
240
 
232
- export function buildGeminiSettings(accessKey, binary) {
241
+ // **2026-09-08 (issue #2354): this pointed at a subcommand that does not exist.**
242
+ // It emitted `<binary> scan hook-gemini --stage pre|post`; the scanner has no
243
+ // `hook-gemini` subcommand, so every invocation died with
244
+ // `error: unrecognized subcommand` and **exit code 2**. Install reported
245
+ // success, `settings.json` filled with hooks, and **not one tool call was ever
246
+ // scanned** — while the working adapter (`~/.gemini/hooks/aidr-pre.sh`) was
247
+ // written to disk on every install and referenced by nothing.
248
+ //
249
+ // Two things were wrong, not one. Exit 2 is also the *block* code, and this was
250
+ // the **only** agent calling the binary directly rather than through a shell
251
+ // wrapper — so it sat outside the fail-open invariant that every other agent
252
+ // gets from `run_scanner` (which maps a timeout's 124 to 0). Pointing at the
253
+ // adapter fixes both at once: the scan actually runs, **and** it runs inside
254
+ // the wrapper.
255
+ //
256
+ // `AfterTool` is gone rather than re-pointed: **there is no post adapter.**
257
+ // `agents/gemini.mjs` installs `aidr-pre.sh` only, and inventing post coverage
258
+ // here would be new agent coverage (#2350's scope), not a defect fix. Declaring
259
+ // only what exists is the point — the previous declaration is exactly how a
260
+ // hook that never ran looked installed.
261
+ export function buildGeminiSettings(accessKey, hooksDir) {
233
262
  // timeout is in milliseconds (Gemini convention). Default 60000 (1 min)
234
263
  // is too long for an inline hook — cap at 15000 so the editor kills a
235
264
  // hung scanner cleanly. The shell-side run_scanner cap fires first under
@@ -241,10 +270,7 @@ export function buildGeminiSettings(accessKey, binary) {
241
270
  },
242
271
  hooks: {
243
272
  BeforeTool: [
244
- { hooks: [{ type: "command", command: `"${binary}" scan hook-gemini --stage pre`, timeout: 15000 }] },
245
- ],
246
- AfterTool: [
247
- { hooks: [{ type: "command", command: `"${binary}" scan hook-gemini --stage post`, timeout: 15000 }] },
273
+ { hooks: [{ type: "command", command: `"${hooksDir}/aidr-pre.sh"`, timeout: 15000 }] },
248
274
  ],
249
275
  },
250
276
  };
package/src/templates.mjs CHANGED
@@ -32,6 +32,16 @@ export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api
32
32
  # - Fail-open: never block Claude Code startup
33
33
  BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
34
34
  BIN_DIR="$(dirname "$BIN")"
35
+ # TWO binaries since the split (issue #2366). \`ai-harness\` runs the eight
36
+ # tool-call scan contexts — it is the one every hook adapter invokes, so it is
37
+ # the one whose absence means "this session is not being scanned". \`ai-scanner\`
38
+ # is the periodic inventory collector and the self-update vehicle. They are
39
+ # built from one crate and therefore must stay on ONE version; the skew check
40
+ # further down is what enforces that.
41
+ #
42
+ # Two env names, never one shared: pointing a single override at one product
43
+ # would silently leave the other pointing at a file that is not it.
44
+ HARNESS="\${AI_HARNESS_BIN:-$BIN_DIR/ai-harness}"
35
45
  # The self-update throttle belongs to the binary it throttles, so it is derived
36
46
  # from \$BIN — the one source this script already trusts for "where is my copy"
37
47
  # (\`.ai-scanner-unscanned\` is placed the same way). It used to be pinned to
@@ -132,7 +142,8 @@ sha256_of() {
132
142
  # \`$UNVERIFIED_REASON\` carries WHY, so the message can name it.
133
143
  download_binary() {
134
144
  local url="$1"
135
- local tmp=$(mktemp "\${BIN_DIR}/ai-scanner.XXXXXX")
145
+ local dest="$2"
146
+ local tmp=$(mktemp "\${BIN_DIR}/.download.XXXXXX")
136
147
  local auth_args=()
137
148
  if [ -n "\${AI_SCANNER_ACCESS_KEY:-}" ]; then
138
149
  auth_args=(-H "x-access-key: \${AI_SCANNER_ACCESS_KEY}")
@@ -166,10 +177,33 @@ download_binary() {
166
177
  # macOS: strip Gatekeeper quarantine so unsigned binary can execute
167
178
  # (no-op on Linux where xattr isn't typically present)
168
179
  xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true
169
- mv -f "$tmp" "$BIN"
180
+ mv -f "$tmp" "$dest"
170
181
  return 0
171
182
  }
172
183
 
184
+ # Fetch one product into place if it is not already there. The download route
185
+ # serves \`ai-scanner\` at the bare platform key (unchanged wire contract) and
186
+ # every other product under its own name.
187
+ #
188
+ # Says "installed" only when it actually installed something. Printing it on
189
+ # every session start for a file that was already there turns the line into
190
+ # noise, and the next reader stops distinguishing it from the one that matters.
191
+ # $1 = absolute destination, $2 = product name
192
+ ensure_binary() {
193
+ local dest="$1" product="$2" url
194
+ [ -x "$dest" ] && return 0
195
+ if [ "$product" = "ai-scanner" ]; then
196
+ url="\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}"
197
+ else
198
+ url="\${DOWNLOAD_BASE}/\${product}/\${PLATFORM_SLUG}"
199
+ fi
200
+ if download_binary "$url" "$dest"; then
201
+ echo "$product binary installed (\${PLATFORM_SLUG}, checksum verified)" >&2
202
+ return 0
203
+ fi
204
+ return 1
205
+ }
206
+
173
207
  # What has already run unscanned on this machine, if anything (#1774). The scan
174
208
  # hooks append to this file every time they exit without a binary; this is the
175
209
  # first moment anyone can say it out loud, and it is deleted the moment a binary
@@ -234,26 +268,42 @@ case "$OS" in
234
268
  ;;
235
269
  esac
236
270
 
237
- # Binary missing → fetch it
238
- if [ ! -x "$BIN" ]; then
239
- if download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}"; then
240
- echo "ai-scanner binary installed (\${PLATFORM_SLUG}, checksum verified)" >&2
241
- rm -f "$UNSCANNED_STAMP"
271
+ # The HOOK binary first: tool-call scanning depends on this one, so its absence
272
+ # is what the session-level message is about.
273
+ #
274
+ # docs/product/UI_COPY.md 【非交渉】: the fact, stated so it cannot be read as
275
+ # harmless, and then the next action. The old single line ("Warning: Failed to
276
+ # download …") said neither what it meant for the session nor what to do, and it
277
+ # was the ONLY trace anywhere (#1774).
278
+ SCANNING=0
279
+ if ensure_binary "$HARNESS" ai-harness; then
280
+ SCANNING=1
281
+ # A binary IS in place. Clear any record of having run unscanned, so the
282
+ # record only ever describes the machine's CURRENT state.
283
+ rm -f "$UNSCANNED_STAMP"
284
+ else
285
+ echo "ai-scanner: this session is NOT being scanned — the tool-call scanner (ai-harness) is not installed on this machine and could not be installed now: \${UNVERIFIED_REASON:-the download failed}." >&2
286
+ report_unscanned_history
287
+ echo "ai-scanner: tool calls keep working, and they run unscanned until it is in place. Re-run the installer (${installCommand}), or contact your IT administrator with this message." >&2
288
+ fi
289
+
290
+ # The PERIODIC binary. Its absence does NOT stop tool-call scanning, so it gets
291
+ # its own, narrower sentence rather than borrowing the one above — saying "this
292
+ # session is NOT being scanned" when it IS would train the reader to ignore the
293
+ # line that means it.
294
+ if ! ensure_binary "$BIN" ai-scanner; then
295
+ # Only claims scanning is unaffected when it actually is — the harness may
296
+ # have failed too, and the line above is the one that says so.
297
+ if [ "$SCANNING" = 1 ]; then
298
+ echo "ai-scanner: tool calls are still being scanned, but the periodic endpoint-inventory collector could not be installed: \${UNVERIFIED_REASON:-the download failed}. Re-run the installer (${installCommand}) if this endpoint stops appearing in the portal." >&2
242
299
  else
243
- # docs/product/UI_COPY.md 【非交渉】: the fact, stated so it cannot be
244
- # read as harmless, and then the next action. The old single line
245
- # ("Warning: Failed to download …") said neither what it meant for the
246
- # session nor what to do, and it was the ONLY trace anywhere (#1774).
247
- echo "ai-scanner: this session is NOT being scanned — the scanner is not installed on this machine and could not be installed now: \${UNVERIFIED_REASON:-the download failed}." >&2
248
- report_unscanned_history
249
- echo "ai-scanner: tool calls keep working, and they run unscanned until it is in place. Re-run the installer (${installCommand}), or contact your IT administrator with this message." >&2
300
+ echo "ai-scanner: the periodic endpoint-inventory collector could not be installed either: \${UNVERIFIED_REASON:-the download failed}. Re-run the installer (${installCommand}), or contact your IT administrator with this message." >&2
250
301
  fi
251
- exit 0
252
302
  fi
253
303
 
254
- # A binary IS in place. Clear any record of having run unscanned, so the record
255
- # only ever describes the machine's CURRENT state.
256
- rm -f "$UNSCANNED_STAMP"
304
+ # Neither is in place: nothing below can run, and the message above already said
305
+ # so with its next action.
306
+ [ -x "$BIN" ] || exit 0
257
307
 
258
308
  # Binary exists — throttled self-update check.
259
309
  #
@@ -276,13 +326,38 @@ if should_check_update; then
276
326
  have_host=$(timeout "$VERSION_TIMEOUT" "$BIN" version 2>/dev/null | sed -n 's#^server: [a-z]*://\\([^/]*\\).*#\\1#p' | head -1)
277
327
  if [ -n "$have_host" ] && [ -n "$want_host" ] && [ "$have_host" != "$want_host" ]; then
278
328
  echo "ai-scanner: the installed binary talks to $have_host, but this installation provisions from $want_host, so its access key can never be accepted (detection logs are refused and discarded). Re-downloading the matching binary." >&2
279
- download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" || \\
329
+ download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" "$BIN" || \\
280
330
  echo "ai-scanner: could not replace the mismatched binary: \${UNVERIFIED_REASON:-the download failed}. Scanning continues locally, but detection logs will keep being refused. Re-run the installer, or contact your IT administrator with this message." >&2
281
331
  elif [ -z "$have_host" ]; then
282
332
  echo "ai-scanner: could not read which license server the installed binary uses, so the environment match was NOT checked. If detection logs are being refused, re-run the installer." >&2
283
333
  fi
284
334
  timeout "$UPDATE_TIMEOUT" "$BIN" update 2>/dev/null || \\
285
335
  echo "ai-scanner: the scanner could not update itself just now. It keeps scanning with the rules it already has; if this repeats, re-run the installer or contact your IT administrator." >&2
336
+
337
+ # Keep the hook binary on the SAME build as the periodic one (issue #2366).
338
+ # They come from one crate and share one version, so a skew means an update
339
+ # moved only ai-scanner and left the harness behind.
340
+ #
341
+ # \`"$HARNESS" update\` is deliberately NOT used (issue #2368): that
342
+ # self-update resolves the bare platform key, which serves ai-scanner —
343
+ # running it would overwrite the harness with the periodic binary and take
344
+ # every hook down with it. Refreshing through the same checksum-verified
345
+ # download path avoids that. **Do not "simplify" this into an update call.**
346
+ #
347
+ # \`GAP-UNOBSERVED-OVERWRITE\`: the overwrite itself has NOT been observed.
348
+ # Reproducing it needs \`/verify\` to advertise a newer version, and v0.1.319
349
+ # has zero keys in S3, so the precondition does not exist yet. What is
350
+ # written above is the mechanism read off \`cli/mod.rs::cmd_update\` — which is
351
+ # enough to route around, and NOT enough to call measured.
352
+ if [ -x "$HARNESS" ]; then
353
+ scanner_ver=$(timeout "$VERSION_TIMEOUT" "$BIN" version 2>/dev/null | awk 'NR==1{print $2}')
354
+ harness_ver=$(timeout "$VERSION_TIMEOUT" "$HARNESS" version 2>/dev/null | awk 'NR==1{print $2}')
355
+ if [ -n "$scanner_ver" ] && [ -n "$harness_ver" ] && [ "$scanner_ver" != "$harness_ver" ]; then
356
+ echo "ai-scanner: the tool-call scanner is on $harness_ver while this endpoint is on $scanner_ver. Refreshing it so both run one build." >&2
357
+ download_binary "\${DOWNLOAD_BASE}/ai-harness/\${PLATFORM_SLUG}" "$HARNESS" || \\
358
+ echo "ai-scanner: could not refresh the tool-call scanner: \${UNVERIFIED_REASON:-the download failed}. Scanning continues on $harness_ver; re-run the installer (${installCommand}) if this repeats." >&2
359
+ fi
360
+ fi
286
361
  date +%s > "$PULL_STAMP"
287
362
  fi
288
363
 
@@ -368,7 +443,7 @@ const SCAN_HELPERS = `run_scanner() {
368
443
  export const SCAN_BASH_SH = `#!/bin/bash
369
444
  # PostToolUse hook for Bash: Scan command output.
370
445
  # Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
371
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
446
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
372
447
  ${UNSCANNED_NOTE}
373
448
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
374
449
 
@@ -385,7 +460,7 @@ jq -r '.tool_response // ""' <<<"$INPUT" \\
385
460
  export const SCAN_BASH_INPUT_SH = `#!/bin/bash
386
461
  # PreToolUse hook for Bash: Scan command before execution.
387
462
  # Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
388
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
463
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
389
464
  ${UNSCANNED_NOTE}
390
465
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
391
466
 
@@ -403,7 +478,7 @@ export const SCAN_EDIT_SH = `#!/bin/bash
403
478
  # PreToolUse hook for Edit/MultiEdit: Scan new content before write.
404
479
  # Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
405
480
  # Only the Edit-vs-MultiEdit content extraction is Claude Code specific.
406
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
481
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
407
482
  ${UNSCANNED_NOTE}
408
483
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
409
484
 
@@ -429,7 +504,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
429
504
  export const SCAN_READ_SH = `#!/bin/bash
430
505
  # PostToolUse hook for Read: Scan file content Claude just read.
431
506
  # Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
432
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
507
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
433
508
  ${UNSCANNED_NOTE}
434
509
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
435
510
 
@@ -449,7 +524,7 @@ jq -r '.tool_response.file.content // .tool_response // ""' <<<"$INPUT" \\
449
524
  export const SCAN_EDIT_OUTPUT_SH = `#!/bin/bash
450
525
  # PostToolUse hook for Edit/MultiEdit/Write: Scan final file content.
451
526
  # Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
452
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
527
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
453
528
  ${UNSCANNED_NOTE}
454
529
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
455
530
 
@@ -470,7 +545,7 @@ export const SCAN_READ_INPUT_SH = `#!/bin/bash
470
545
  # This is a thin Claude Code adapter — all scanning logic lives in the Rust
471
546
  # binary, including actor resolution, exclude-dir filtering, content cap, and
472
547
  # detection-log writing.
473
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
548
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
474
549
  ${UNSCANNED_NOTE}
475
550
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
476
551
 
@@ -488,7 +563,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
488
563
  export const SCAN_WEBFETCH_SH = `#!/bin/bash
489
564
  # PreToolUse hook for WebFetch: Scan URL before Claude fetches it.
490
565
  # Thin Claude Code adapter — Rust handles phishing scoring + YARA + detection log.
491
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
566
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
492
567
  ${UNSCANNED_NOTE}
493
568
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
494
569
 
@@ -505,7 +580,7 @@ run_scanner scan webfetch --url "$URL" </dev/null
505
580
  export const SCAN_WEBSEARCH_SH = `#!/bin/bash
506
581
  # PostToolUse hook for WebSearch: Scan search results for phishing URLs.
507
582
  # Thin Claude Code adapter — Rust handles cap, phishing scoring, detection log.
508
- BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
583
+ BIN="\${AI_HARNESS_BIN:-$HOME/.claude/bin/ai-harness}"
509
584
  ${UNSCANNED_NOTE}
510
585
  [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
511
586
 
@@ -551,6 +626,12 @@ export function agentEnvSh(accessKey, agentDir) {
551
626
  return `#!/bin/bash
552
627
  SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
553
628
  export AI_SCANNER_BIN="\${AI_SCANNER_BIN:-$HOME/${agentDir}/bin/ai-scanner}"
629
+ # The hook binary (issue #2366). Every adapter below scans through THIS one:
630
+ # the eight tool-call contexts live in \`ai-harness\`, not in \`ai-scanner\`
631
+ # (which is the periodic endpoint-inventory collector). Two names, never one
632
+ # shared override — pointing a single variable at one product would leave the
633
+ # other silently addressing a file that is not it.
634
+ export AI_HARNESS_BIN="\${AI_HARNESS_BIN:-$HOME/${agentDir}/bin/ai-harness}"
554
635
  export AI_SCANNER_ACCESS_KEY="${accessKey}"
555
636
  export AI_SCANNER_LEVEL="\${AI_SCANNER_LEVEL:-high}"
556
637
  export AI_SCANNER_EXCLUDE_DIRS="\${AI_SCANNER_EXCLUDE_DIRS:-}"
@@ -569,11 +650,11 @@ ${UNSCANNED_NOTE}
569
650
 
570
651
  run_scanner() {
571
652
  if command -v timeout >/dev/null 2>&1; then
572
- timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_SCANNER_BIN}" "$@"
653
+ timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_HARNESS_BIN}" "$@"
573
654
  elif command -v gtimeout >/dev/null 2>&1; then
574
- gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_SCANNER_BIN}" "$@"
655
+ gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_HARNESS_BIN}" "$@"
575
656
  else
576
- "\${AI_SCANNER_BIN}" "$@"
657
+ "\${AI_HARNESS_BIN}" "$@"
577
658
  fi
578
659
  local rc=$?
579
660
  [ $rc -eq 124 ] && return 0
@@ -593,7 +674,7 @@ source "$SCRIPT_DIR/env.sh"
593
674
  cat > /dev/null
594
675
  bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
595
676
  cat <<EOF
596
- {"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
677
+ {"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_HARNESS_BIN":"\${AI_HARNESS_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
597
678
  EOF
598
679
  `;
599
680
  }
@@ -601,7 +682,7 @@ EOF
601
682
  export const CURSOR_PRE_SHELL_SH = `#!/bin/bash
602
683
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
603
684
  source "$SCRIPT_DIR/env.sh"
604
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
685
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
605
686
  export AI_SCANNER_DETECTION_LOG
606
687
  jq -r '.tool_input.command // ""' | run_scanner scan bash-input
607
688
  `;
@@ -609,7 +690,7 @@ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
609
690
  export const CURSOR_PRE_READ_SH = `#!/bin/bash
610
691
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
611
692
  source "$SCRIPT_DIR/env.sh"
612
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
693
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
613
694
  export AI_SCANNER_DETECTION_LOG
614
695
  FILE_PATH=$(jq -r '.tool_input.file_path // ""')
615
696
  [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
@@ -619,7 +700,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
619
700
  export const CURSOR_PRE_EDIT_SH = `#!/bin/bash
620
701
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
621
702
  source "$SCRIPT_DIR/env.sh"
622
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
703
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
623
704
  export AI_SCANNER_DETECTION_LOG
624
705
  INPUT=$(cat)
625
706
  FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
@@ -636,7 +717,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
636
717
  export const CURSOR_POST_SHELL_SH = `#!/bin/bash
637
718
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
638
719
  source "$SCRIPT_DIR/env.sh"
639
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
720
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
640
721
  export AI_SCANNER_DETECTION_LOG
641
722
  INPUT=$(cat)
642
723
  COMMAND=$(jq -r '.tool_output // ""' <<<"$INPUT" | head -1 && jq -r '.tool_input.command // ""' <<<"$INPUT")
@@ -646,7 +727,7 @@ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan bash-output --command
646
727
  export const CURSOR_POST_READ_SH = `#!/bin/bash
647
728
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
648
729
  source "$SCRIPT_DIR/env.sh"
649
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
730
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
650
731
  export AI_SCANNER_DETECTION_LOG
651
732
  INPUT=$(cat)
652
733
  FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
@@ -657,7 +738,7 @@ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan read-output --file "$F
657
738
  export const CURSOR_POST_WRITE_SH = `#!/bin/bash
658
739
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
659
740
  source "$SCRIPT_DIR/env.sh"
660
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
741
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
661
742
  export AI_SCANNER_DETECTION_LOG
662
743
  INPUT=$(cat)
663
744
  FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
@@ -672,7 +753,7 @@ run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
672
753
  export const WINDSURF_ENSURE_SCANNER_SH = `#!/bin/bash
673
754
  SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
674
755
  source "$SCRIPT_DIR/env.sh"
675
- BIN="$AI_SCANNER_BIN"
756
+ BIN="$AI_HARNESS_BIN"
676
757
  LOCK_FILE="/tmp/.ai-scanner-ensure.lock"
677
758
  if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
678
759
  return 0 2>/dev/null || true
@@ -688,7 +769,7 @@ return 0 2>/dev/null || true
688
769
  export const WINDSURF_PRE_COMMAND_SH = `#!/bin/bash
689
770
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
690
771
  source "$SCRIPT_DIR/ensure_scanner.sh"
691
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
772
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
692
773
  export AI_SCANNER_DETECTION_LOG
693
774
  CONTENT=$(jq -r '.tool_info.command_line // ""')
694
775
  [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
@@ -698,7 +779,7 @@ printf '%s' "$CONTENT" | run_scanner scan bash-input
698
779
  export const WINDSURF_PRE_READ_SH = `#!/bin/bash
699
780
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
700
781
  source "$SCRIPT_DIR/ensure_scanner.sh"
701
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
782
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
702
783
  export AI_SCANNER_DETECTION_LOG
703
784
  FILE_PATH=$(jq -r '.tool_info.file_path // ""')
704
785
  [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
@@ -708,7 +789,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
708
789
  export const WINDSURF_PRE_WRITE_SH = `#!/bin/bash
709
790
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
710
791
  source "$SCRIPT_DIR/ensure_scanner.sh"
711
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
792
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
712
793
  export AI_SCANNER_DETECTION_LOG
713
794
  INPUT=$(cat)
714
795
  FILE_PATH=$(jq -r '.tool_info.file_path // ""' <<<"$INPUT")
@@ -720,7 +801,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
720
801
  export const WINDSURF_POST_READ_SH = `#!/bin/bash
721
802
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
722
803
  source "$SCRIPT_DIR/ensure_scanner.sh"
723
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
804
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
724
805
  export AI_SCANNER_DETECTION_LOG
725
806
  FILE_PATH=$(jq -r '.tool_info.file_path // ""')
726
807
  [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
@@ -730,7 +811,7 @@ run_scanner scan read-output --file "$FILE_PATH" < "$FILE_PATH"
730
811
  export const WINDSURF_POST_WRITE_SH = `#!/bin/bash
731
812
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
732
813
  source "$SCRIPT_DIR/ensure_scanner.sh"
733
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
814
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
734
815
  export AI_SCANNER_DETECTION_LOG
735
816
  FILE_PATH=$(jq -r '.tool_info.file_path // ""')
736
817
  [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
@@ -773,7 +854,7 @@ exec "$SCRIPT_DIR/start_scanner.sh"
773
854
  export const KIRO_PRE_SHELL_SH = `#!/bin/bash
774
855
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
775
856
  source "$SCRIPT_DIR/env.sh"
776
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
857
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
777
858
  export AI_SCANNER_DETECTION_LOG
778
859
  jq -r '.tool_input.command // ""' | run_scanner scan bash-input
779
860
  `;
@@ -781,7 +862,7 @@ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
781
862
  export const KIRO_PRE_READ_SH = `#!/bin/bash
782
863
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
783
864
  source "$SCRIPT_DIR/env.sh"
784
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
865
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
785
866
  export AI_SCANNER_DETECTION_LOG
786
867
  FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""')
787
868
  [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
@@ -791,7 +872,7 @@ run_scanner scan read-input --file "$FILE_PATH" </dev/null
791
872
  export const KIRO_PRE_WRITE_SH = `#!/bin/bash
792
873
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
793
874
  source "$SCRIPT_DIR/env.sh"
794
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
875
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
795
876
  export AI_SCANNER_DETECTION_LOG
796
877
  INPUT=$(cat)
797
878
  FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
@@ -803,7 +884,7 @@ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
803
884
  export const KIRO_POST_SHELL_SH = `#!/bin/bash
804
885
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
805
886
  source "$SCRIPT_DIR/env.sh"
806
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
887
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
807
888
  export AI_SCANNER_DETECTION_LOG
808
889
  INPUT=$(cat)
809
890
  COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
@@ -814,7 +895,7 @@ printf '%s' "$RESPONSE" | run_scanner scan bash-output --command "$COMMAND"
814
895
  export const KIRO_POST_READ_SH = `#!/bin/bash
815
896
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
816
897
  source "$SCRIPT_DIR/env.sh"
817
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
898
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
818
899
  export AI_SCANNER_DETECTION_LOG
819
900
  INPUT=$(cat)
820
901
  FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""' <<<"$INPUT")
@@ -826,7 +907,7 @@ printf '%s' "$RESPONSE" | run_scanner scan read-output --file "$FILE_PATH"
826
907
  export const KIRO_POST_WRITE_SH = `#!/bin/bash
827
908
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
828
909
  source "$SCRIPT_DIR/env.sh"
829
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
910
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
830
911
  export AI_SCANNER_DETECTION_LOG
831
912
  INPUT=$(cat)
832
913
  FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
@@ -902,7 +983,7 @@ export const CODEX_SESSION_INIT_SH = `#!/bin/bash
902
983
  SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
903
984
  source "$SCRIPT_DIR/env.sh"
904
985
  cat > /dev/null # drain stdin so Codex doesn't block on a write
905
- BIN="$AI_SCANNER_BIN"
986
+ BIN="$AI_HARNESS_BIN"
906
987
  LOCK_FILE="/tmp/.ai-scanner-codex-init.lock"
907
988
  if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
908
989
  exit 0
@@ -919,7 +1000,7 @@ export const CODEX_PRE_SHELL_SH = `#!/bin/bash
919
1000
  # Codex PreToolUse Bash hook: scan command and return block JSON to Codex.
920
1001
  SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
921
1002
  source "$SCRIPT_DIR/env.sh"
922
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
1003
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
923
1004
  export AI_SCANNER_DETECTION_LOG
924
1005
  INPUT=$(cat)
925
1006
  CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
@@ -949,7 +1030,7 @@ export const CODEX_POST_SHELL_SH = `#!/bin/bash
949
1030
  # Codex PostToolUse Bash hook: scan command output and return feedback JSON.
950
1031
  SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
951
1032
  source "$SCRIPT_DIR/env.sh"
952
- BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
1033
+ BIN="$AI_HARNESS_BIN"; [ ! -x "$BIN" ] && { note_unscanned; exit 0; }
953
1034
  export AI_SCANNER_DETECTION_LOG
954
1035
  INPUT=$(cat)
955
1036
  CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)