@indigoai-us/hq-cli 5.345.85 → 5.345.86

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.86] — 2026-10-11
6
+ - New `hq scan secrets`: an offline secret scanner for the pre-commit gate. It
7
+ checks the staged changes (or given files) against the gitleaks default rule
8
+ set, vendored from gitleaks v8.30.1 under its MIT license, plus HQ's own
9
+ token patterns. It needs no network, account or API quota, and reports the
10
+ rule, file and line without printing the value. Exit codes: 0 clean, 1
11
+ possible secret, 2 the scan could not run.
12
+
5
13
  ## [5.345.85] — 2026-10-11
6
14
  - Session-start orphan reports and reap leases are keyed by HQ root, report ended-session waiting lanes, and surface the latest completed orphan count in startup context even after a skipped sweep. `hq lanes reap --legacy` also identifies abandoned legacy conduct pool loops and only stops verified process identities when `--stop --reason` is supplied.
7
15
 
@@ -5375,6 +5375,43 @@ export declare const COMMAND_CATALOG: readonly [{
5375
5375
  }];
5376
5376
  readonly subcommands: readonly [];
5377
5377
  }];
5378
+ }, {
5379
+ readonly name: "scan";
5380
+ readonly description: "Local, offline scanners";
5381
+ readonly aliases: readonly [];
5382
+ readonly hidden: false;
5383
+ readonly helpOption: true;
5384
+ readonly helpCommand: true;
5385
+ readonly usage: "[options] [command]";
5386
+ readonly helpText: readonly [];
5387
+ readonly arguments: readonly [];
5388
+ readonly options: readonly [];
5389
+ readonly subcommands: readonly [{
5390
+ readonly name: "secrets";
5391
+ readonly description: "Scan staged changes (or files) for secrets; exits 1 on a finding";
5392
+ readonly aliases: readonly [];
5393
+ readonly hidden: false;
5394
+ readonly helpOption: true;
5395
+ readonly helpCommand: false;
5396
+ readonly usage: "[options] [files...]";
5397
+ readonly helpText: readonly [];
5398
+ readonly arguments: readonly [{
5399
+ readonly name: "files";
5400
+ readonly required: false;
5401
+ readonly variadic: true;
5402
+ readonly description: "Scan these files instead of the staged changes";
5403
+ }];
5404
+ readonly options: readonly [{
5405
+ readonly flags: "--staged";
5406
+ readonly description: "Scan the staged changes (default when no files are given)";
5407
+ readonly hidden: false;
5408
+ }, {
5409
+ readonly flags: "--json";
5410
+ readonly description: "Print findings as JSON";
5411
+ readonly hidden: false;
5412
+ }];
5413
+ readonly subcommands: readonly [];
5414
+ }];
5378
5415
  }, {
5379
5416
  readonly name: "reindex";
5380
5417
  readonly description: "Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code";
@@ -6535,6 +6535,51 @@ export const COMMAND_CATALOG = [
6535
6535
  }
6536
6536
  ]
6537
6537
  },
6538
+ {
6539
+ "name": "scan",
6540
+ "description": "Local, offline scanners",
6541
+ "aliases": [],
6542
+ "hidden": false,
6543
+ "helpOption": true,
6544
+ "helpCommand": true,
6545
+ "usage": "[options] [command]",
6546
+ "helpText": [],
6547
+ "arguments": [],
6548
+ "options": [],
6549
+ "subcommands": [
6550
+ {
6551
+ "name": "secrets",
6552
+ "description": "Scan staged changes (or files) for secrets; exits 1 on a finding",
6553
+ "aliases": [],
6554
+ "hidden": false,
6555
+ "helpOption": true,
6556
+ "helpCommand": false,
6557
+ "usage": "[options] [files...]",
6558
+ "helpText": [],
6559
+ "arguments": [
6560
+ {
6561
+ "name": "files",
6562
+ "required": false,
6563
+ "variadic": true,
6564
+ "description": "Scan these files instead of the staged changes"
6565
+ }
6566
+ ],
6567
+ "options": [
6568
+ {
6569
+ "flags": "--staged",
6570
+ "description": "Scan the staged changes (default when no files are given)",
6571
+ "hidden": false
6572
+ },
6573
+ {
6574
+ "flags": "--json",
6575
+ "description": "Print findings as JSON",
6576
+ "hidden": false
6577
+ }
6578
+ ],
6579
+ "subcommands": []
6580
+ }
6581
+ ]
6582
+ },
6538
6583
  {
6539
6584
  "name": "reindex",
6540
6585
  "description": "Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code",
@@ -358,6 +358,12 @@ export declare const REGISTRATION_PLAN: readonly [{
358
358
  readonly parent: "program";
359
359
  readonly module: "./commands/import-scan.js";
360
360
  readonly exportName: "registerImportCommand";
361
+ }, {
362
+ readonly type: "registrar";
363
+ readonly root: "scan";
364
+ readonly parent: "program";
365
+ readonly module: "./commands/scan.js";
366
+ readonly exportName: "registerScanCommand";
361
367
  }, {
362
368
  readonly type: "registrar";
363
369
  readonly root: "reindex";
@@ -62,6 +62,7 @@ export const REGISTRATION_PLAN = [
62
62
  { type: "registrar", root: "signals", parent: "program", module: "./commands/signals.js", exportName: "registerSignalsCommand" },
63
63
  { type: "registrar", root: "integrations", parent: "program", module: "./commands/integrations.js", exportName: "registerIntegrationsCommand" },
64
64
  { type: "registrar", root: "import", parent: "program", module: "./commands/import-scan.js", exportName: "registerImportCommand" },
65
+ { type: "registrar", root: "scan", parent: "program", module: "./commands/scan.js", exportName: "registerScanCommand" },
65
66
  { type: "registrar", root: "reindex", parent: "program", module: "./commands/reindex.js", exportName: "registerReindexCommand" },
66
67
  { type: "registrar", root: "rescue", parent: "program", module: "./commands/rescue.js", exportName: "registerRescueCommand" },
67
68
  { type: "registrar", root: "mcp", parent: "program", module: "./commands/mcp-status.js", exportName: "registerMcpCommand" },
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `hq scan secrets` — offline secret scanner (the HQ pre-commit gate).
3
+ *
4
+ * hq scan secrets scan the staged changes of the current repo
5
+ * hq scan secrets <file...> scan whole files instead
6
+ * hq scan secrets --json findings as JSON
7
+ *
8
+ * Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
9
+ * HQ's own token patterns. No network, no account, no quota.
10
+ *
11
+ * Exit codes: 0 clean, 1 possible secret found, 2 the scan could not run.
12
+ * A pre-commit hook blocks on any non-zero exit, so the gate fails closed.
13
+ * Output names the rule, file and line; secret values are never printed.
14
+ */
15
+ import { Command } from "commander";
16
+ interface ScanSecretsOptions {
17
+ json?: boolean;
18
+ staged?: boolean;
19
+ }
20
+ export declare function runScanSecrets(paths: string[], opts: ScanSecretsOptions, cwd?: string): number;
21
+ export declare function registerScanCommand(program: Command): void;
22
+ export {};
23
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `hq scan secrets` — offline secret scanner (the HQ pre-commit gate).
3
+ *
4
+ * hq scan secrets scan the staged changes of the current repo
5
+ * hq scan secrets <file...> scan whole files instead
6
+ * hq scan secrets --json findings as JSON
7
+ *
8
+ * Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
9
+ * HQ's own token patterns. No network, no account, no quota.
10
+ *
11
+ * Exit codes: 0 clean, 1 possible secret found, 2 the scan could not run.
12
+ * A pre-commit hook blocks on any non-zero exit, so the gate fails closed.
13
+ * Output names the rule, file and line; secret values are never printed.
14
+ */
15
+ import { ScanInputError, scanFiles, scanStaged } from "../lib/secret-scan/staged.js";
16
+ function report(findings, scope) {
17
+ if (!findings.length)
18
+ return;
19
+ const n = findings.length;
20
+ process.stderr.write(`HQ secret scan: ${n} possible secret${n === 1 ? "" : "s"} in ${scope} (values hidden)\n`);
21
+ for (const f of findings) {
22
+ process.stderr.write(` ${f.file}:${f.line} ${f.ruleId} ${f.description}\n`);
23
+ }
24
+ process.stderr.write("\nRemove the secret from the change, then stage it again. Keep real credentials in the HQ vault\n" +
25
+ "(hq secrets, hq run). A known false positive can carry a `gitleaks:allow` comment on that line.\n");
26
+ }
27
+ export function runScanSecrets(paths, opts, cwd = process.cwd()) {
28
+ let findings;
29
+ try {
30
+ findings = paths.length && !opts.staged ? scanFiles(paths) : scanStaged(cwd);
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof ScanInputError ? err.message : `scan failed: ${err instanceof Error ? err.message : String(err)}`;
34
+ if (opts.json)
35
+ process.stdout.write(`${JSON.stringify({ ok: false, error: msg })}\n`);
36
+ process.stderr.write(`HQ secret scan: ${msg}\n`);
37
+ return 2;
38
+ }
39
+ if (opts.json) {
40
+ process.stdout.write(`${JSON.stringify({ ok: findings.length === 0, findings })}\n`);
41
+ }
42
+ else {
43
+ report(findings, paths.length && !opts.staged ? "the given files" : "staged changes");
44
+ }
45
+ return findings.length ? 1 : 0;
46
+ }
47
+ export function registerScanCommand(program) {
48
+ const scan = program.command("scan").description("Local, offline scanners");
49
+ scan
50
+ .command("secrets")
51
+ .description("Scan staged changes (or files) for secrets; exits 1 on a finding")
52
+ .argument("[files...]", "Scan these files instead of the staged changes")
53
+ .option("--staged", "Scan the staged changes (default when no files are given)")
54
+ .option("--json", "Print findings as JSON")
55
+ .action((files, opts) => {
56
+ process.exitCode = runScanSecrets(files ?? [], opts);
57
+ });
58
+ }
59
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Secret detection over text fragments, following gitleaks' detection model:
3
+ * global path allowlist, per-rule path filter, keyword prefilter, regex match,
4
+ * secret-group selection, Shannon-entropy floor, allowlists (OR/AND; target
5
+ * secret, match, or line; stopwords), and the inline `gitleaks:allow` marker.
6
+ *
7
+ * Findings carry rule, file and line only. The secret value never leaves this
8
+ * module.
9
+ */
10
+ import { type CompiledRuleSet } from "./rules.js";
11
+ import type { SecretFinding } from "./types.js";
12
+ /** Inline markers that suppress a finding on that line. */
13
+ export declare const ALLOW_MARKERS: readonly ["gitleaks:allow", "hq-secret-scan:allow"];
14
+ export interface Fragment {
15
+ file: string;
16
+ text: string;
17
+ /** 1-based line number of the first line of `text` in the file. */
18
+ startLine: number;
19
+ }
20
+ /** Shannon entropy in bits per character (gitleaks' definition). */
21
+ export declare function shannonEntropy(data: string): number;
22
+ /** True when the global allowlist excludes this path from scanning. */
23
+ export declare function pathIgnored(file: string, rules?: CompiledRuleSet): boolean;
24
+ /** Path-only rules (e.g. a committed .p12 keystore); for binary or whole-file adds. */
25
+ export declare function scanPath(file: string, rules?: CompiledRuleSet): SecretFinding[];
26
+ export declare function scanFragment(frag: Fragment, rules?: CompiledRuleSet): SecretFinding[];
27
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Secret detection over text fragments, following gitleaks' detection model:
3
+ * global path allowlist, per-rule path filter, keyword prefilter, regex match,
4
+ * secret-group selection, Shannon-entropy floor, allowlists (OR/AND; target
5
+ * secret, match, or line; stopwords), and the inline `gitleaks:allow` marker.
6
+ *
7
+ * Findings carry rule, file and line only. The secret value never leaves this
8
+ * module.
9
+ */
10
+ import { compiledRules } from "./rules.js";
11
+ /** Inline markers that suppress a finding on that line. */
12
+ export const ALLOW_MARKERS = ["gitleaks:allow", "hq-secret-scan:allow"];
13
+ /** Shannon entropy in bits per character (gitleaks' definition). */
14
+ export function shannonEntropy(data) {
15
+ if (!data)
16
+ return 0;
17
+ const counts = new Map();
18
+ for (const ch of data)
19
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
20
+ const len = Buffer.byteLength(data, "utf8");
21
+ let entropy = 0;
22
+ for (const count of counts.values()) {
23
+ const freq = count / len;
24
+ entropy -= freq * Math.log2(freq);
25
+ }
26
+ return entropy;
27
+ }
28
+ function allowed(al, c) {
29
+ const target = al.target === "match" ? c.match : al.target === "line" ? c.line : c.secret;
30
+ const checks = [];
31
+ if (al.paths.length)
32
+ checks.push(al.paths.some((p) => p.test(c.file)));
33
+ if (al.regexes.length)
34
+ checks.push(al.regexes.some((r) => r.test(target)));
35
+ if (al.stopwords.length) {
36
+ const lower = c.secret.toLowerCase();
37
+ checks.push(al.stopwords.some((s) => lower.includes(s)));
38
+ }
39
+ if (!checks.length)
40
+ return false;
41
+ return al.condition === "AND" ? checks.every(Boolean) : checks.some(Boolean);
42
+ }
43
+ function lineBounds(text, start, end) {
44
+ const from = text.lastIndexOf("\n", start - 1) + 1;
45
+ const nl = text.indexOf("\n", Math.max(end - 1, start));
46
+ const to = nl < 0 ? text.length : nl;
47
+ let offset = 0;
48
+ for (let i = 0; i < from; i++)
49
+ if (text.charCodeAt(i) === 10)
50
+ offset++;
51
+ return { line: text.slice(from, to), offset };
52
+ }
53
+ /** True when the global allowlist excludes this path from scanning. */
54
+ export function pathIgnored(file, rules = compiledRules()) {
55
+ return rules.global.paths.some((p) => p.test(file));
56
+ }
57
+ /** Path-only rules (e.g. a committed .p12 keystore); for binary or whole-file adds. */
58
+ export function scanPath(file, rules = compiledRules()) {
59
+ if (pathIgnored(file, rules))
60
+ return [];
61
+ return rules.rules
62
+ .filter((r) => !r.regex && r.path?.test(file))
63
+ .map((r) => ({ ruleId: r.id, description: r.description, file, line: 1 }));
64
+ }
65
+ export function scanFragment(frag, rules = compiledRules()) {
66
+ const { file, text, startLine } = frag;
67
+ if (!text || pathIgnored(file, rules))
68
+ return [];
69
+ const lower = text.toLowerCase();
70
+ const seen = new Set();
71
+ const findings = [];
72
+ for (const rule of rules.rules) {
73
+ if (!rule.regex)
74
+ continue;
75
+ if (rule.path && !rule.path.test(file))
76
+ continue;
77
+ if (rule.keywords.length && !rule.keywords.some((k) => lower.includes(k)))
78
+ continue;
79
+ const re = rule.regex;
80
+ re.lastIndex = 0;
81
+ let m;
82
+ while ((m = re.exec(text)) !== null) {
83
+ if (m[0].length === 0) {
84
+ re.lastIndex++;
85
+ continue;
86
+ }
87
+ let secret = m[0];
88
+ if (m.length > 1) {
89
+ if (rule.secretGroup && rule.secretGroup > 0)
90
+ secret = m[rule.secretGroup] ?? "";
91
+ else
92
+ secret = m.slice(1).find((g) => g !== undefined && g.length > 0) ?? m[0];
93
+ }
94
+ if (!secret)
95
+ continue;
96
+ if (rule.entropy !== undefined && rule.entropy !== 0 && shannonEntropy(secret) <= rule.entropy)
97
+ continue;
98
+ const { line, offset } = lineBounds(text, m.index, m.index + m[0].length);
99
+ if (ALLOW_MARKERS.some((mk) => line.includes(mk)))
100
+ continue;
101
+ const cand = { file, secret, match: m[0], line };
102
+ if (allowed(rules.global, cand))
103
+ continue;
104
+ if (rule.allowlists.some((al) => allowed(al, cand)))
105
+ continue;
106
+ const lineNo = startLine + offset;
107
+ const key = `${rule.id}:${lineNo}`;
108
+ if (seen.has(key))
109
+ continue;
110
+ seen.add(key);
111
+ findings.push({ ruleId: rule.id, description: rule.description, file, line: lineNo });
112
+ }
113
+ }
114
+ return dedupeByLine(findings).sort((a, b) => a.line - b.line || a.ruleId.localeCompare(b.ruleId));
115
+ }
116
+ /**
117
+ * One line, one report: when a provider-specific rule fired on a line, drop the
118
+ * catch-all `generic-api-key` and HQ fallback (`hq-*`) findings for that line,
119
+ * as gitleaks does for its generic rule.
120
+ */
121
+ function dedupeByLine(findings) {
122
+ const fallback = (id) => id === "generic-api-key" || id.startsWith("hq-");
123
+ const specific = new Set(findings.filter((f) => !fallback(f.ruleId)).map((f) => f.line));
124
+ const out = findings.filter((f) => !fallback(f.ruleId) || !specific.has(f.line));
125
+ // Between the two fallbacks, prefer the HQ rule (names the provider).
126
+ const hqLines = new Set(out.filter((f) => f.ruleId.startsWith("hq-")).map((f) => f.line));
127
+ return out.filter((f) => f.ruleId !== "generic-api-key" || !hqLines.has(f.line));
128
+ }
129
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1,6 @@
1
+ import type { GitleaksConfig } from "./types.js";
2
+ export declare const GITLEAKS_UPSTREAM_VERSION = "v8.30.1";
3
+ /** Upstream license; shipped with the derived rules as MIT requires. */
4
+ export declare const GITLEAKS_LICENSE = "MIT License\n\nCopyright (c) 2019 Zachary Rice\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n";
5
+ export declare const GITLEAKS_CONFIG: GitleaksConfig;
6
+ //# sourceMappingURL=gitleaks-rules.generated.d.ts.map