@swestash/agent-lens 0.5.0 → 0.5.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.
@@ -2319,7 +2319,7 @@ function classify(db, dirty) {
2319
2319
 
2320
2320
  // ../ingest/dist/detect.js
2321
2321
  import { createHash } from "crypto";
2322
- var DETECTOR_VERSION = 3;
2322
+ var DETECTOR_VERSION = 6;
2323
2323
  function evidence(s, max = 200) {
2324
2324
  const one = s.replace(/\s+/g, " ").trim();
2325
2325
  return one.length > max ? one.slice(0, max - 1) + "\u2026" : one;
@@ -2330,7 +2330,7 @@ function findingId(toolCallId, ruleId) {
2330
2330
  var SYSTEM_PATH = /(^|\s)(\/etc\/|\/usr\/|\/bin\/|\/sbin\/|\/boot\/|\/var\/|\/root\/|~\/\.ssh\/|~\/\.aws\/)/;
2331
2331
  var HOME_OR_ROOT_TARGET = /(\s|^)(~\/?|\/|\/\*|\$HOME\/?|\*)(\s|$)/;
2332
2332
  var SECRET_FILE = /(^|\/|\s)(\.env(\.[\w.-]+)?|id_rsa|id_ed25519|id_ecdsa|\.ssh\/[\w.-]+|\.aws\/credentials|\.gcp\/|gcloud\/|\.npmrc|\.pypirc|\.netrc|\.pem|\.p12|\.pfx|secrets?\.(ya?ml|json|txt)|credentials(\.json)?)\b/i;
2333
- var EXAMPLE_FILE = /\.(example|sample|template|dist)($|\.)/i;
2333
+ var EXAMPLE_FILE = /\.(example|sample|template|dist)(?![\w-])/i;
2334
2334
  var CONTENT_READ_CMD = /\b(cat|less|more|head|tail|bat|xxd|strings|base64|grep|egrep|rg|awk|sed|nl|tac|od|hexdump|cp|scp|sort)\b/i;
2335
2335
  var SECRET_VALUE_PATTERNS = [
2336
2336
  { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, label: "private-key", critical: true },
@@ -2341,23 +2341,73 @@ var SECRET_VALUE_PATTERNS = [
2341
2341
  { re: /\bsk-[A-Za-z0-9]{20,}\b/, label: "api-secret-key" }
2342
2342
  ];
2343
2343
  var isBash = (t) => t === "Bash";
2344
- var AGENT_CONFIG_PATH = /(^|\/)\.claude\//;
2344
+ var stripComments = (c) => c.replace(/(^|\s)#[^\n]*/g, "$1");
2345
+ var neutralizeEcho = (c) => c.replace(/((?:^|[;&|\n(])\s*)(?:echo|printf)\b(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^;&|\n])*/gi, "$1echo");
2346
+ var blankHeredoc = (c) => c.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g, "<<HEREDOC");
2347
+ var codeOf = (c) => neutralizeEcho(stripComments(blankHeredoc(c)));
2348
+ var blankQuoted = (c) => c.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'[^']*'/g, "''");
2349
+ var bareOf = (c) => blankQuoted(codeOf(c));
2350
+ function execGeneratedScript(cmd) {
2351
+ const targets = /* @__PURE__ */ new Set();
2352
+ for (const m of cmd.matchAll(/(?:>>?|\btee\b\s+(?:-a\s+)?)\s*["']?([A-Za-z0-9._/~-]+)/g)) {
2353
+ if (m[1] && !m[1].startsWith("/dev/"))
2354
+ targets.add(m[1]);
2355
+ }
2356
+ for (const t of targets) {
2357
+ const e = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2358
+ const interp = new RegExp(`\\b(?:sh|bash|zsh|ksh|dash|source)\\s+["']?${e}\\b|(?:^|[\\n;|&(])\\s*\\.\\s+["']?${e}\\b`);
2359
+ const hasPath = /^(\.\/|\/|~\/)/.test(t);
2360
+ const path = hasPath ? new RegExp(`(?:^|[\\n;|&(])\\s*${e}(?:\\s|$|[;&|<>])`) : new RegExp(`(?:^|[\\n;|&(])\\s*\\.\\/${e}\\b`);
2361
+ if (interp.test(cmd) || path.test(cmd))
2362
+ return t;
2363
+ }
2364
+ return null;
2365
+ }
2345
2366
  var TEMP_PATH = /^(\/tmp\/|\/var\/folders\/|\/private\/var\/folders\/)/;
2346
- function isAgentOwnedPath(p) {
2347
- if (AGENT_CONFIG_PATH.test(p) || TEMP_PATH.test(p))
2367
+ function isAgentOwnedPath(p, configDirs) {
2368
+ if (TEMP_PATH.test(p))
2369
+ return true;
2370
+ if (configDirs.some((d) => p === d || p.startsWith(d + "/")))
2348
2371
  return true;
2349
2372
  const tmp = process.env.TMPDIR;
2350
2373
  return !!tmp && p.startsWith(tmp.replace(/\/$/, "") + "/");
2351
2374
  }
2352
- function outsideProject(filePath, projectPath) {
2375
+ function outsideProject(filePath, projectPath, configDirs) {
2353
2376
  if (!filePath || !filePath.startsWith("/"))
2354
2377
  return false;
2355
- if (isAgentOwnedPath(filePath))
2378
+ if (isAgentOwnedPath(filePath, configDirs))
2356
2379
  return false;
2357
2380
  if (!projectPath)
2358
2381
  return false;
2359
2382
  return filePath !== projectPath && !filePath.startsWith(projectPath.replace(/\/$/, "") + "/");
2360
2383
  }
2384
+ var SCOPE_RANK = { unknown: 0, loopback: 1, internal: 2, external: 3 };
2385
+ function netTargetScope(rawCommand) {
2386
+ const s = rawCommand.replace(/(?:-H|--header)\s+(['"]).*?\1/gi, " ").replace(/(?:-H|--header)\s+[^\s'"]+/gi, " ");
2387
+ const authorities = [];
2388
+ for (const m of s.matchAll(/\bhttps?:\/\/(\[[0-9a-fA-F:]+\]|[^/\s"'<>|]+)/gi))
2389
+ authorities.push(m[1]);
2390
+ for (const m of s.matchAll(/(?:^|[\s"'=(])(\[[0-9a-fA-F:]+\]|[A-Za-z0-9.-]+):\d{2,5}(?=[/\s"']|$)/g))
2391
+ authorities.push(m[1]);
2392
+ let scope = "unknown";
2393
+ for (let host of authorities) {
2394
+ host = host.replace(/^\[|\]$/g, "").replace(/^[^@/]*@/, "").toLowerCase();
2395
+ if (!host.includes("::"))
2396
+ host = host.replace(/:\d+$/, "");
2397
+ let hs;
2398
+ if (host === "localhost" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || host === "0.0.0.0" || host === "::1")
2399
+ hs = "loopback";
2400
+ else if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || // RFC1918
2401
+ /^192\.168\.\d{1,3}\.\d{1,3}$/.test(host) || /^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host) || /^169\.254\.\d{1,3}\.\d{1,3}$/.test(host) || // link-local
2402
+ host.endsWith(".local") || host.endsWith(".internal") || !host.includes("."))
2403
+ hs = "internal";
2404
+ else
2405
+ hs = "external";
2406
+ if (SCOPE_RANK[hs] > SCOPE_RANK[scope])
2407
+ scope = hs;
2408
+ }
2409
+ return scope;
2410
+ }
2361
2411
  var RULES = [
2362
2412
  // --- Destructive / data-loss (OWASP ASI02 Tool Misuse) -------------------
2363
2413
  {
@@ -2368,14 +2418,14 @@ var RULES = [
2368
2418
  tools: ["Bash"],
2369
2419
  baseSeverity: "high",
2370
2420
  test(ctx) {
2371
- const c = ctx.command;
2421
+ const c = ctx.commandBare;
2372
2422
  if (!c)
2373
2423
  return null;
2374
2424
  const rf = /\brm\s+[^|;&\n]*(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|--recursive[^|;&\n]*--force|--force[^|;&\n]*--recursive)/i;
2375
2425
  if (!rf.test(c))
2376
2426
  return null;
2377
2427
  const dangerousTarget = HOME_OR_ROOT_TARGET.test(c) || /\s\/(?:\s|$)/.test(c) || /\brm\b[^|;&\n]*\*/.test(c);
2378
- return { evidence: evidence(c), severity: dangerousTarget ? "critical" : "high", mods: { dangerous_target: dangerousTarget } };
2428
+ return { evidence: evidence(ctx.command), severity: dangerousTarget ? "critical" : "high", mods: { dangerous_target: dangerousTarget } };
2379
2429
  }
2380
2430
  },
2381
2431
  {
@@ -2384,12 +2434,13 @@ var RULES = [
2384
2434
  framework_ref: "OWASP ASI02",
2385
2435
  title: "Discards local changes (git reset --hard)",
2386
2436
  tools: ["Bash"],
2387
- baseSeverity: "medium",
2437
+ baseSeverity: "low",
2438
+ // routine dev op — only uncommitted working-tree changes are lost; commits survive in reflog
2388
2439
  test(ctx) {
2389
- const c = ctx.command;
2440
+ const c = ctx.commandBare;
2390
2441
  if (!c || !/\bgit\s+reset\s+--hard\b/i.test(c))
2391
2442
  return null;
2392
- return { evidence: evidence(c) };
2443
+ return { evidence: evidence(ctx.command) };
2393
2444
  }
2394
2445
  },
2395
2446
  {
@@ -2398,13 +2449,14 @@ var RULES = [
2398
2449
  framework_ref: "OWASP ASI02",
2399
2450
  title: "Force-push rewrites remote history",
2400
2451
  tools: ["Bash"],
2401
- baseSeverity: "medium",
2452
+ baseSeverity: "low",
2453
+ // feature-branch force-push is routine; only a protected branch (below) is high
2402
2454
  test(ctx) {
2403
- const c = ctx.command;
2455
+ const c = ctx.commandBare;
2404
2456
  if (!c || !/\bgit\s+push\b[^|;&\n]*(--force\b|--force-with-lease\b|\s-f\b)/i.test(c))
2405
2457
  return null;
2406
2458
  const mainBranch = /\b(main|master|release)\b/.test(c);
2407
- return { evidence: evidence(c), severity: mainBranch ? "high" : "medium", mods: { protected_branch: mainBranch } };
2459
+ return { evidence: evidence(ctx.command), severity: mainBranch ? "high" : "low", mods: { protected_branch: mainBranch } };
2408
2460
  }
2409
2461
  },
2410
2462
  {
@@ -2415,10 +2467,11 @@ var RULES = [
2415
2467
  tools: "*",
2416
2468
  baseSeverity: "high",
2417
2469
  test(ctx) {
2418
- const hay = ctx.command ?? (typeof ctx.input?.query === "string" ? ctx.input.query : null);
2470
+ const query = typeof ctx.input?.query === "string" ? ctx.input.query : null;
2471
+ const hay = ctx.commandCode ?? query;
2419
2472
  if (!hay || !/\b(DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\s+TABLE)?)\b/i.test(hay))
2420
2473
  return null;
2421
- return { evidence: evidence(hay) };
2474
+ return { evidence: evidence(ctx.command ?? query) };
2422
2475
  }
2423
2476
  },
2424
2477
  {
@@ -2429,10 +2482,10 @@ var RULES = [
2429
2482
  tools: ["Bash"],
2430
2483
  baseSeverity: "high",
2431
2484
  test(ctx) {
2432
- const c = ctx.command;
2485
+ const c = ctx.commandBare;
2433
2486
  if (!c || !/(\bdd\s+if=|\bmkfs(\.\w+)?\b|\bshred\b)/i.test(c))
2434
2487
  return null;
2435
- return { evidence: evidence(c) };
2488
+ return { evidence: evidence(ctx.command) };
2436
2489
  }
2437
2490
  },
2438
2491
  {
@@ -2441,14 +2494,16 @@ var RULES = [
2441
2494
  framework_ref: "OWASP ASI02",
2442
2495
  title: "Overwrites a critical project file",
2443
2496
  tools: ["Write", "Edit", "MultiEdit"],
2444
- baseSeverity: "medium",
2497
+ baseSeverity: "low",
2445
2498
  test(ctx) {
2446
2499
  const f = ctx.filePath;
2447
2500
  if (!f)
2448
2501
  return null;
2449
- if (!/(^|\/)(package-lock\.json|pnpm-lock\.ya?ml|yarn\.lock|Cargo\.lock|go\.sum|\.github\/workflows\/|\.gitlab-ci\.ya?ml)\b/.test(f))
2502
+ const lock = /(^|\/)(package-lock\.json|pnpm-lock\.ya?ml|yarn\.lock|Cargo\.lock|go\.sum)\b/.test(f);
2503
+ const ci = /(^|\/)(\.github\/workflows\/|\.gitlab-ci\.ya?ml)\b/.test(f);
2504
+ if (!lock && !ci)
2450
2505
  return null;
2451
- return { evidence: evidence(f) };
2506
+ return { evidence: evidence(f), severity: ci ? "medium" : "low", mods: { kind: ci ? "ci-config" : "lockfile" } };
2452
2507
  }
2453
2508
  },
2454
2509
  // --- Credential & secret access ------------------------------------------
@@ -2460,12 +2515,18 @@ var RULES = [
2460
2515
  tools: ["Read", "Bash"],
2461
2516
  baseSeverity: "high",
2462
2517
  test(ctx) {
2463
- const hay = ctx.toolName === "Read" ? ctx.filePath : ctx.command;
2464
- if (!hay || !SECRET_FILE.test(hay) || EXAMPLE_FILE.test(hay))
2465
- return null;
2466
- if (isBash(ctx.toolName) && !CONTENT_READ_CMD.test(hay))
2518
+ if (isBash(ctx.toolName)) {
2519
+ const cmd = ctx.commandBare;
2520
+ if (!cmd)
2521
+ return null;
2522
+ const segments = cmd.split(/\|\||&&|[|;\n]/);
2523
+ const reads = segments.some((seg) => SECRET_FILE.test(seg) && !EXAMPLE_FILE.test(seg) && CONTENT_READ_CMD.test(seg));
2524
+ return reads ? { evidence: evidence(ctx.command) } : null;
2525
+ }
2526
+ const f = ctx.filePath;
2527
+ if (!f || !SECRET_FILE.test(f) || EXAMPLE_FILE.test(f))
2467
2528
  return null;
2468
- return { evidence: evidence(hay) };
2529
+ return { evidence: evidence(f) };
2469
2530
  }
2470
2531
  },
2471
2532
  {
@@ -2496,36 +2557,43 @@ var RULES = [
2496
2557
  tools: ["Bash"],
2497
2558
  baseSeverity: "critical",
2498
2559
  test(ctx) {
2499
- const c = ctx.command;
2560
+ const c = ctx.commandBare;
2500
2561
  if (!c)
2501
2562
  return null;
2502
2563
  if (!/(\||<)\s*(sudo\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\b/i.test(c))
2503
2564
  return null;
2504
2565
  if (!/\b(cat|tar|zip|gzip|base64|env|printenv|cat\s|<)\b/i.test(c))
2505
2566
  return null;
2506
- return { evidence: evidence(c) };
2567
+ return { evidence: evidence(ctx.command) };
2507
2568
  }
2508
2569
  },
2509
2570
  {
2510
2571
  id: "exfil.network_upload",
2511
2572
  category: "exfiltration",
2512
2573
  framework_ref: "MITRE ATLAS AML.T0086",
2513
- title: "Uploads data to an external host",
2574
+ title: "Uploads data to a network host",
2514
2575
  tools: ["Bash"],
2515
2576
  baseSeverity: "high",
2516
2577
  test(ctx) {
2517
- const c = ctx.command;
2578
+ const c = ctx.commandBare;
2518
2579
  if (!c)
2519
2580
  return null;
2520
- const upload = /\b(curl|wget)\b[^|;&\n]*(-X\s*POST|--data\b|--data-\w+\b|\s-d\b|-T\b|--upload-file\b|-F\b|--form\b)/i.test(c);
2581
+ const upload = /\b(curl|wget)\b[^|;&\n]*(-X\s*POST|--data\b|--data-\w+\b|\s-d\b|-T\b|--upload-file\b|-F\b|--form\b)/.test(c);
2521
2582
  if (!upload)
2522
2583
  return null;
2523
- const external = /https?:\/\/(?!(localhost|127\.0\.0\.1|0\.0\.0\.0))/i.test(c);
2524
2584
  const withFile = /(@[^\s'"]+|--upload-file|\s-T\b)/.test(c);
2585
+ const scope = netTargetScope(ctx.command);
2586
+ let severity;
2587
+ if (scope === "external")
2588
+ severity = withFile ? "critical" : "high";
2589
+ else if (scope === "unknown")
2590
+ severity = "high";
2591
+ else
2592
+ severity = withFile ? bumpSeverity(scope === "loopback" ? "info" : "low", 1) : scope === "loopback" ? "info" : "low";
2525
2593
  return {
2526
- evidence: evidence(c),
2527
- severity: withFile && external ? "critical" : "high",
2528
- mods: { external, with_file: withFile }
2594
+ evidence: evidence(ctx.command),
2595
+ severity,
2596
+ mods: { target_scope: scope, with_file: withFile }
2529
2597
  };
2530
2598
  }
2531
2599
  },
@@ -2537,12 +2605,12 @@ var RULES = [
2537
2605
  tools: ["Bash"],
2538
2606
  baseSeverity: "critical",
2539
2607
  test(ctx) {
2540
- const c = ctx.command;
2608
+ const c = ctx.commandBare;
2541
2609
  if (!c)
2542
2610
  return null;
2543
2611
  if (!/\b(nc|ncat|netcat)\b[^|;&\n]*(-e\b|-c\b|\d{2,5})|\/dev\/tcp\/|bash\s+-i\b/i.test(c))
2544
2612
  return null;
2545
- return { evidence: evidence(c) };
2613
+ return { evidence: evidence(ctx.command) };
2546
2614
  }
2547
2615
  },
2548
2616
  // --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)
@@ -2554,10 +2622,10 @@ var RULES = [
2554
2622
  tools: "*",
2555
2623
  baseSeverity: "high",
2556
2624
  test(ctx) {
2557
- const c = ctx.command;
2625
+ const c = ctx.commandBare;
2558
2626
  if (!c || !/--dangerously-skip-permissions\b|--yolo\b|--dangerously-skip\b/i.test(c))
2559
2627
  return null;
2560
- return { evidence: evidence(c) };
2628
+ return { evidence: evidence(ctx.command) };
2561
2629
  }
2562
2630
  },
2563
2631
  {
@@ -2568,10 +2636,17 @@ var RULES = [
2568
2636
  tools: ["Bash"],
2569
2637
  baseSeverity: "high",
2570
2638
  test(ctx) {
2571
- const c = ctx.command;
2572
- if (!c || !/\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh|python\d?|node)\b/i.test(c))
2639
+ const c = ctx.commandBare;
2640
+ if (!c)
2573
2641
  return null;
2574
- return { evidence: evidence(c) };
2642
+ const m = /\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(sh|bash|zsh|python\d?|node)\b([^|;&]*)/i.exec(c);
2643
+ if (!m)
2644
+ return null;
2645
+ const interp = m[1].toLowerCase();
2646
+ const args = m[2] || "";
2647
+ if (/^(?:node|python)/.test(interp) && /(?:^|\s)-(?:e|c|m|p)\b|(?:^|\s)--(?:eval|command|module|print)\b/.test(args))
2648
+ return null;
2649
+ return { evidence: evidence(ctx.command) };
2575
2650
  }
2576
2651
  },
2577
2652
  {
@@ -2583,10 +2658,28 @@ var RULES = [
2583
2658
  baseSeverity: "high",
2584
2659
  // root escalation removes a real safety barrier — higher than an in-project op
2585
2660
  test(ctx) {
2586
- const c = ctx.command;
2587
- if (!c || !/(^|\s|\|)sudo\s+/i.test(c))
2661
+ const c = ctx.commandBare;
2662
+ if (!c || !/(?:^|[\n;|&(])\s*sudo\b/i.test(c))
2663
+ return null;
2664
+ return { evidence: evidence(ctx.command) };
2665
+ }
2666
+ },
2667
+ {
2668
+ id: "privilege.exec_generated_script",
2669
+ category: "privilege-bypass",
2670
+ framework_ref: "OWASP LLM06",
2671
+ title: "Writes a shell script from inline output and executes it",
2672
+ tools: ["Bash"],
2673
+ baseSeverity: "high",
2674
+ test(ctx) {
2675
+ const raw = ctx.command;
2676
+ if (!raw)
2677
+ return null;
2678
+ const file = execGeneratedScript(stripComments(raw));
2679
+ if (!file)
2588
2680
  return null;
2589
- return { evidence: evidence(c) };
2681
+ const critical = /(?:^|["'\s;|&(])sudo\b|\brm\s+[^|;&\n]*-[a-z]*r[a-z]*f|\bchmod\s+0?777\b|\bmkfs|\bdd\s+if=/i.test(raw);
2682
+ return { evidence: evidence(raw), severity: critical ? "critical" : "high", mods: { script_file: file } };
2590
2683
  }
2591
2684
  },
2592
2685
  {
@@ -2597,10 +2690,10 @@ var RULES = [
2597
2690
  tools: ["Bash"],
2598
2691
  baseSeverity: "medium",
2599
2692
  test(ctx) {
2600
- const c = ctx.command;
2693
+ const c = ctx.commandBare;
2601
2694
  if (!c || !/\bchmod\s+(-[a-zA-Z]+\s+)*0?777\b/i.test(c))
2602
2695
  return null;
2603
- return { evidence: evidence(c) };
2696
+ return { evidence: evidence(ctx.command) };
2604
2697
  }
2605
2698
  },
2606
2699
  {
@@ -2611,12 +2704,12 @@ var RULES = [
2611
2704
  tools: ["Bash"],
2612
2705
  baseSeverity: "high",
2613
2706
  test(ctx) {
2614
- const c = ctx.command;
2707
+ const c = ctx.commandBare;
2615
2708
  if (!c)
2616
2709
  return null;
2617
2710
  if (!/\bcrontab\s+-|\bsystemctl\s+enable\b|\blaunchctl\s+load\b|>>\s*~\/\.(bashrc|zshrc|profile|bash_profile)\b|\/etc\/(cron|systemd|rc\.local)/i.test(c))
2618
2711
  return null;
2619
- return { evidence: evidence(c) };
2712
+ return { evidence: evidence(ctx.command) };
2620
2713
  }
2621
2714
  },
2622
2715
  {
@@ -2625,20 +2718,21 @@ var RULES = [
2625
2718
  framework_ref: "OWASP LLM06",
2626
2719
  title: "Writes a file outside the working project",
2627
2720
  tools: ["Write", "Edit", "MultiEdit"],
2628
- baseSeverity: "medium",
2721
+ baseSeverity: "low",
2722
+ // writing to /tmp, scratch dirs, adjacent repos is routine; only a system path (below) is high
2629
2723
  test(ctx) {
2630
2724
  const f = ctx.filePath;
2631
- if (!outsideProject(f, ctx.projectPath))
2725
+ if (!outsideProject(f, ctx.projectPath, ctx.ownedConfigDirs))
2632
2726
  return null;
2633
2727
  const system = !!f && SYSTEM_PATH.test(f);
2634
- return { evidence: evidence(f), severity: system ? "high" : "medium", mods: { project: ctx.projectPath, system_path: system } };
2728
+ return { evidence: evidence(f), severity: system ? "high" : "low", mods: { project: ctx.projectPath, system_path: system } };
2635
2729
  }
2636
2730
  }
2637
2731
  ];
2638
2732
  function appliesTo(rule, toolName) {
2639
2733
  return rule.tools === "*" || rule.tools.includes(toolName);
2640
2734
  }
2641
- function buildContext(row, projectPath) {
2735
+ function buildContext(row, projectPath, ownedConfigDirs) {
2642
2736
  let input = null;
2643
2737
  try {
2644
2738
  input = row.input_json ? JSON.parse(row.input_json) : null;
@@ -2646,8 +2740,10 @@ function buildContext(row, projectPath) {
2646
2740
  input = null;
2647
2741
  }
2648
2742
  const command = typeof input?.command === "string" ? input.command : null;
2743
+ const commandCode = command != null ? codeOf(command) : null;
2744
+ const commandBare = command != null ? bareOf(command) : null;
2649
2745
  const filePath = typeof input?.file_path === "string" ? input.file_path : null;
2650
- return { toolName: row.tool_name, command, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath };
2746
+ return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };
2651
2747
  }
2652
2748
  function detect(db, dirty) {
2653
2749
  const incremental = dirty != null;
@@ -2661,6 +2757,7 @@ function detect(db, dirty) {
2661
2757
  })(dirty);
2662
2758
  }
2663
2759
  const scope = incremental ? " WHERE tc.session_id IN (SELECT id FROM _dirty_sec)" : "";
2760
+ const ownedConfigDirs = db.prepare("SELECT config_dir FROM sources WHERE config_dir IS NOT NULL").all().map((r) => r.config_dir.replace(/\/$/, "")).filter(Boolean);
2664
2761
  const projPath = /* @__PURE__ */ new Map();
2665
2762
  for (const r of db.prepare(`SELECT s.id id, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${incremental ? " WHERE s.id IN (SELECT id FROM _dirty_sec)" : ""}`).all()) {
2666
2763
  projPath.set(r.id, r.path);
@@ -2674,7 +2771,7 @@ function detect(db, dirty) {
2674
2771
  const tx = db.transaction(() => {
2675
2772
  db.exec(delScope);
2676
2773
  for (const row of rows) {
2677
- const ctx = buildContext(row, projPath.get(row.session_id) ?? null);
2774
+ const ctx = buildContext(row, projPath.get(row.session_id) ?? null, ownedConfigDirs);
2678
2775
  for (const rule of RULES) {
2679
2776
  if (!appliesTo(rule, ctx.toolName))
2680
2777
  continue;