agenthusk 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable AgentHusk changes are documented here.
4
+
5
+ ## 0.1.0 - 2026-06-01
6
+
7
+ ### Added
8
+
9
+ - Bounded, local-first scanning for 10 AI coding-agent storage roots.
10
+ - Secret-shaped value fingerprints scoped to a single report.
11
+ - Default path anonymization with an explicit unsafe `--show-paths` option.
12
+ - Residue, local-permission, MCP-configuration, and coverage-gap findings.
13
+ - Self-contained HTML reports, JSON reports, and aggregate-only SVG share cards.
14
+ - Synthetic demo, regression tests, CI, packaged CLI smoke test, and launch materials.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentHusk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # AgentHusk
2
+
3
+ **Find secret-shaped residue left in local AI coding-agent storage before it becomes an incident.**
4
+
5
+ AgentHusk is a local-first forensic scanner. It inspects known agent directories without intentionally modifying source artifacts, flags secrets and risky residue, and keeps matched content values out of its default anonymized reports. It writes local report artifacts containing short fingerprints so repeated matches can be grouped without copying a matched credential into another artifact.
6
+
7
+ No external API. No upload. No telemetry.
8
+
9
+ AgentHusk does not start configured MCP servers, execute discovered commands, or inspect live agent traffic. Its scope is deliberately narrower: static local residue after the agent has run.
10
+
11
+ ![AgentHusk report preview](docs/assets/agenthusk-social.svg)
12
+
13
+ ## 30-second quickstart
14
+
15
+ Requires Node.js 20 or later.
16
+
17
+ ```sh
18
+ cd /path/to/agenthusk
19
+ node src/cli.js demo
20
+ node src/cli.js scan
21
+ ```
22
+
23
+ `demo` creates a safe synthetic report. `scan` inspects the supported agent directories that exist under your home directory and writes a local report for review.
24
+
25
+ After an npm release, the intended equivalent commands are:
26
+
27
+ ```sh
28
+ npx agenthusk demo
29
+ npx agenthusk scan
30
+ ```
31
+
32
+ ## What it detects
33
+
34
+ AgentHusk currently looks for:
35
+
36
+ - Secret-shaped values, including common API keys, access tokens, bearer tokens, webhook URLs, and assigned secret values.
37
+ - The same detected secret appearing in more than one scanned file, grouped by a short fingerprint.
38
+ - Environment-file copies, shell-history files, and locally retained agent session transcripts.
39
+ - Sensitive residue files readable by other local users and agent directories traversable by other local users.
40
+ - MCP server configuration declarations that deserve a trust-boundary review.
41
+ - Coverage gaps such as oversized files that were not content-scanned.
42
+
43
+ Findings are leads for review, not proof of compromise.
44
+
45
+ The report's aggregate risk signal is a visual triage aid, not a security grade or proof that a machine is safe. Use the finding evidence and coverage section to decide what to inspect locally.
46
+
47
+ ## Trust model
48
+
49
+ AgentHusk is designed to add less sensitive residue than it finds:
50
+
51
+ - AgentHusk does not intentionally edit, delete, quarantine, rotate, or upload scanned source artifacts. It does write the requested local report artifacts.
52
+ - The scanner does not call external APIs and does not send telemetry.
53
+ - Matched content values are not copied into content-derived report fields. A per-run random HMAC key produces a 32-character fingerprint for grouping matches within that report.
54
+ - Fingerprints are intentionally not stable across separate runs.
55
+ - Reports anonymize source paths by default. They still contain metadata such as anonymized paths, line numbers, permission modes, and finding details. Review reports before sharing them.
56
+ - Raw source paths are included only when you explicitly pass the unsafe `--show-paths` option. A raw path can itself contain sensitive text, including a value also present in file content.
57
+ - The original secret, if any, remains in the source file until you remediate it.
58
+
59
+ Run scans against a snapshot or copied tree when possible, as an ordinary user. Avoid scanning a live writable tree, FUSE or network mount, or running AgentHusk with elevated privileges. The scanner skips symlinks, but it is not designed to defend against a concurrently changing or adversarial filesystem.
60
+
61
+ ## Supported agents
62
+
63
+ The default scan checks these known locations under the current user's home directory:
64
+
65
+ | Agent | Location |
66
+ | --- | --- |
67
+ | Codex | `~/.codex` |
68
+ | Claude Code | `~/.claude` |
69
+ | Gemini | `~/.gemini` |
70
+ | OpenClaw | `~/.openclaw` |
71
+ | Hermes | `~/.hermes` |
72
+ | Cursor | `~/.cursor` |
73
+ | Windsurf | `~/.windsurf` |
74
+ | OpenCode | `~/.config/opencode` |
75
+ | Continue | `~/.continue` |
76
+ | Cline | `~/.cline` |
77
+
78
+ Support means that AgentHusk knows where to look. It does not imply affiliation with, endorsement by, or complete coverage of any agent.
79
+
80
+ ## Useful options
81
+
82
+ By default, AgentHusk writes `agenthusk-report.html` and `agenthusk-report.json` in the current directory. On POSIX platforms, reports are created with owner-only mode `0600`. Windows ACL enforcement is not implemented.
83
+
84
+ ```sh
85
+ node src/cli.js scan --root /path/to/review --max-files 5000
86
+ node src/cli.js scan --root /path/to/review-copy --max-bytes 8388608
87
+ node src/cli.js scan --card agenthusk-card.svg
88
+ node src/cli.js demo --out demo/report.html --json demo/report.json --card demo/card.svg
89
+ ```
90
+
91
+ Use `--root` to scan an explicit directory instead of the default known roots; repeat it to scan more than one. Run `node src/cli.js help` for the full option list.
92
+
93
+ Paths are anonymized by default so reports are safer to review and share. Use `--max-bytes` to change the per-file content-inspection limit. The SVG share card omits paths entirely.
94
+
95
+ `--show-paths` is an unsafe option for local investigation only. For roots under your home directory it includes local relative paths. For external explicit roots it keeps the absolute root hidden but includes descendant names. A path can itself contain sensitive text, so do not use this option for artifacts that may be shared.
96
+
97
+ ## Limits
98
+
99
+ AgentHusk is a narrow forensic aid, not a secret manager, malware scanner, or compliance control.
100
+
101
+ - Pattern matching can produce false positives and false negatives.
102
+ - Files larger than 4 MiB are skipped for content inspection by default.
103
+ - Aggregate content reads are capped at 64 MiB by default.
104
+ - A scan is capped at 20,000 visited files and a maximum traversal depth of 14 by default.
105
+ - Discovered symlinks, symlinked final roots, and roots under the selected home with symlinked descendant components are skipped. `.git`, `node_modules`, and `coverage` directories are not traversed.
106
+ - Binary files are not content-scanned.
107
+ - The scanner cannot detect secrets that do not match its current rules, residue outside scanned roots, or credentials that already left the machine.
108
+ - Scanning a live writable tree, FUSE mount, network mount, or attacker-controlled filesystem can produce inconsistent results or expose the scanner to filesystem races. Prefer a snapshot or copied tree.
109
+ - A clean report is not evidence that a machine or account is safe.
110
+
111
+ ## What to do with a finding
112
+
113
+ 1. Review the referenced source file locally. Do not paste the underlying value into an issue.
114
+ 2. Rotate or revoke a credential if exposure is possible or retention is unexpected.
115
+ 3. Remove stale residue or restrict permissions where appropriate.
116
+ 4. Re-run the scan and inspect the new local report.
117
+
118
+ ## Development
119
+
120
+ ```sh
121
+ npm test
122
+ npm run check
123
+ npm run smoke:pack
124
+ ```
125
+
126
+ See [CONTRIBUTING.md](CONTRIBUTING.md) before changing detection or redaction behavior. The scanner boundary is documented in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Planned work is tracked in [docs/ROADMAP.md](docs/ROADMAP.md). For usage questions, read [SUPPORT.md](SUPPORT.md). For security reports, read [SECURITY.md](SECURITY.md). Maintainers can use [docs/RELEASE.md](docs/RELEASE.md) for the publication checklist.
127
+
128
+ ## License
129
+
130
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,50 @@
1
+ # Security Policy
2
+
3
+ AgentHusk scans security-sensitive local files. Treat bugs that expose source content or weaken redaction as security issues.
4
+
5
+ ## Supported versions
6
+
7
+ AgentHusk is pre-1.0 software. Security fixes are applied to the latest release line only.
8
+
9
+ | Version | Supported |
10
+ | --- | --- |
11
+ | Latest `0.x` release | Yes |
12
+ | Older releases | No |
13
+
14
+ ## Reporting a vulnerability
15
+
16
+ Do not open a public issue for a vulnerability or include real secrets, tokens, report files, or sensitive paths in public discussion.
17
+
18
+ Use the repository's [private vulnerability-reporting form](https://github.com/seipass/agenthusk/security/advisories/new) when it is available. If it is not available, contact a maintainer privately through the channel listed on the repository profile. If no private channel is listed, open a minimal issue asking for a private contact method without disclosing vulnerability details.
19
+
20
+ Include:
21
+
22
+ - The affected AgentHusk version or commit.
23
+ - The operating system and Node.js version.
24
+ - A minimal reproduction using synthetic placeholders only.
25
+ - The expected and actual behavior.
26
+ - Whether a generated report, console output, or file path may reveal sensitive content.
27
+
28
+ ## Security invariants
29
+
30
+ Changes should preserve these properties:
31
+
32
+ - Scanned source artifacts are not intentionally modified. Requested local report artifacts are written.
33
+ - No external API calls, uploads, or telemetry are introduced into the scanning path.
34
+ - Default anonymized reports and normal CLI output do not contain matched content values.
35
+ - Source paths are anonymized by default. Raw source paths appear only when the user explicitly passes the unsafe `--show-paths` option.
36
+ - Secret fingerprints use a per-run random key and are not stable cross-run identifiers.
37
+ - Discovered symlinks are not followed during traversal. Symlinked final roots and roots under the selected home with symlinked descendant components are skipped.
38
+ - Test fixtures contain synthetic values only.
39
+
40
+ ## Report handling
41
+
42
+ AgentHusk default anonymized reports omit matched content values, but they are not automatically safe public artifacts. Reports can still include line numbers, permission modes, agent names, and forensic findings. Reports generated with the unsafe `--show-paths` option also include raw source paths, which can themselves contain sensitive text or a value also present in file content. Review reports before sharing them outside the machine where they were generated.
43
+
44
+ ## Safe scanning environment
45
+
46
+ Prefer scanning a snapshot or copied tree as an ordinary user. Avoid scanning a live writable tree, FUSE or network mount, or attacker-controlled filesystem. Do not run AgentHusk with elevated privileges unless you have a specific reason and understand the expanded exposure. Symlink skipping is not a complete defense against a filesystem that changes concurrently during traversal.
47
+
48
+ ## Response expectations
49
+
50
+ Maintainers will validate the report, determine severity, prepare a fix, and coordinate disclosure when appropriate. Response timing depends on maintainer availability; no fixed service-level agreement is promised.
@@ -0,0 +1,74 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
2
+ <title id="title">AgentHusk local-first agent forensic report</title>
3
+ <desc id="desc">A value-hidden scan summary with 7 local findings and a risk signal of 92. The signal is not proof.</desc>
4
+ <defs>
5
+ <pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
6
+ <path d="M24 0H0V24" fill="none" stroke="#171713" stroke-opacity=".09"/>
7
+ </pattern>
8
+ <filter id="noise">
9
+ <feTurbulence baseFrequency=".8" numOctaves="3" seed="17" stitchTiles="stitch"/>
10
+ <feColorMatrix type="saturate" values="0"/>
11
+ <feComponentTransfer><feFuncA type="table" tableValues="0 .12"/></feComponentTransfer>
12
+ </filter>
13
+ <style>
14
+ .serif { font-family: Georgia, "Times New Roman", serif; }
15
+ .mono { font-family: "Courier New", Courier, monospace; }
16
+ .label { font: 700 12px "Courier New", Courier, monospace; letter-spacing: 2px; fill: #bd4a35; }
17
+ .metric { font: 700 48px Georgia, "Times New Roman", serif; letter-spacing: -4px; fill: #fff9ed; }
18
+ .metric-label { font: 700 10px "Courier New", Courier, monospace; letter-spacing: 1.3px; fill: #776f61; }
19
+ .node-label { font: 700 12px Georgia, "Times New Roman", serif; fill: #171713; }
20
+ .node-count { font: 700 9px "Courier New", Courier, monospace; letter-spacing: 1px; fill: #776f61; }
21
+ </style>
22
+ </defs>
23
+ <rect width="1200" height="630" fill="#f3ead8"/>
24
+ <rect width="1200" height="630" fill="url(#grid)"/>
25
+ <rect width="1200" height="630" filter="url(#noise)" opacity=".65"/>
26
+ <rect width="26" height="630" fill="#ed6a4d"/>
27
+ <rect x="1072" width="128" height="630" fill="#171713"/>
28
+ <rect x="1101" width="1" height="630" fill="#f3b63f" fill-opacity=".4"/>
29
+
30
+ <text x="72" y="79" class="serif" font-size="58" font-weight="700" letter-spacing="-5" fill="#171713">agenthusk</text>
31
+ <text x="76" y="108" class="label">LOCAL-FIRST AGENT FORENSICS // 2026-06-01</text>
32
+ <text x="74" y="208" class="serif" font-size="94" font-weight="700" letter-spacing="-8" fill="#171713">Residue leaves</text>
33
+ <text x="74" y="292" class="serif" font-size="94" font-style="italic" letter-spacing="-8" fill="#ed6a4d">an agent husk.</text>
34
+ <text x="76" y="353" class="mono" font-size="15" fill="#514b41">LOCAL SCAN / 7 FINDINGS / COVERAGE CAP: NOT HIT</text>
35
+ <text x="76" y="378" class="mono" font-size="12" letter-spacing="1" fill="#776f61">1,842 FILES VISITED / 906 TEXT FILES INSPECTED / VALUE-HIDDEN</text>
36
+
37
+ <path d="M74 413H813" stroke="#171713" stroke-width="2"/>
38
+ <text x="74" y="444" class="label">AGENT EXPOSURE MAP / EVIDENCE DENSITY</text>
39
+ <circle cx="76" cy="473" r="26" fill="none" stroke="#f3b63f" stroke-opacity=".25"/>
40
+ <circle cx="76" cy="473" r="18" fill="#f3b63f"/>
41
+ <text x="76" y="536" class="node-label" text-anchor="middle">Codex</text>
42
+ <text x="76" y="557" class="node-count" text-anchor="middle">1 SIGNALS</text>
43
+ <circle cx="224" cy="473" r="26" fill="none" stroke="#e27650" stroke-opacity=".25"/>
44
+ <circle cx="224" cy="473" r="18" fill="#e27650"/>
45
+ <text x="224" y="536" class="node-label" text-anchor="middle">Claude Code</text>
46
+ <text x="224" y="557" class="node-count" text-anchor="middle">1 SIGNALS</text>
47
+ <circle cx="372" cy="473" r="32" fill="none" stroke="#6fb5ff" stroke-opacity=".25"/>
48
+ <circle cx="372" cy="473" r="24" fill="#6fb5ff"/>
49
+ <text x="372" y="536" class="node-label" text-anchor="middle">Gemini</text>
50
+ <text x="372" y="557" class="node-count" text-anchor="middle">3 SIGNALS</text>
51
+ <circle cx="520" cy="473" r="29" fill="none" stroke="#e96666" stroke-opacity=".25"/>
52
+ <circle cx="520" cy="473" r="21" fill="#e96666"/>
53
+ <text x="520" y="536" class="node-label" text-anchor="middle">OpenClaw</text>
54
+ <text x="520" y="557" class="node-count" text-anchor="middle">2 SIGNALS</text>
55
+
56
+ <rect x="840" y="56" width="285" height="518" fill="#171713"/>
57
+ <text x="875" y="100" class="mono" font-size="12" font-weight="700" letter-spacing="2" fill="#f3b63f">RISK SIGNAL / 100</text>
58
+ <text x="875" y="123" class="mono" font-size="10" font-weight="700" letter-spacing="1.4" fill="#cfc4b0">SIGNAL, NOT PROOF</text>
59
+ <circle cx="985" cy="252" r="92" fill="none" stroke="#ffffff" stroke-opacity=".14" stroke-width="18"/>
60
+ <circle cx="985" cy="252" r="92" fill="none" stroke="#ed6a4d" stroke-width="18" pathLength="100" stroke-dasharray="92 100" transform="rotate(-90 985 252)"/>
61
+ <text x="985" y="279" class="serif" font-size="102" font-weight="700" letter-spacing="-10" text-anchor="middle" fill="#fff9ed">92</text>
62
+ <text x="985" y="334" class="mono" font-size="12" font-weight="700" letter-spacing="2" text-anchor="middle" fill="#ed6a4d">CRITICAL TRIAGE BAND</text>
63
+
64
+ <path d="M874 376H1091" stroke="#ffffff" stroke-opacity=".2"/>
65
+ <text x="875" y="423" class="metric" fill="#fff9ed">7</text>
66
+ <text x="875" y="446" class="metric-label" fill="#cfc4b0">FINDINGS</text>
67
+ <text x="958" y="423" class="metric" fill="#fff9ed">2</text>
68
+ <text x="958" y="446" class="metric-label" fill="#cfc4b0">CRITICAL</text>
69
+ <text x="1033" y="423" class="metric" fill="#fff9ed">3</text>
70
+ <text x="1033" y="446" class="metric-label" fill="#cfc4b0">HIGH</text>
71
+ <rect x="874" y="485" width="218" height="51" fill="#f3b63f"/>
72
+ <text x="983" y="507" class="mono" font-size="10" font-weight="700" letter-spacing="1.4" text-anchor="middle" fill="#171713">VALUE-HIDDEN GUARANTEE</text>
73
+ <text x="983" y="524" class="mono" font-size="9" font-weight="700" letter-spacing=".7" text-anchor="middle" fill="#171713">MATCHED VALUES STAY HIDDEN</text>
74
+ </svg>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "agenthusk",
3
+ "version": "0.1.0",
4
+ "description": "Local-first forensic scanner for secrets and risky residue left by AI coding agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agenthusk": "src/cli.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "docs/assets/agenthusk-social.svg",
12
+ "CHANGELOG.md",
13
+ "README.md",
14
+ "LICENSE",
15
+ "SECURITY.md"
16
+ ],
17
+ "scripts": {
18
+ "demo": "node src/cli.js demo --out demo/agenthusk-demo.html --json demo/agenthusk-demo.json",
19
+ "scan": "node src/cli.js scan",
20
+ "test": "node --test",
21
+ "check": "node --check src/cli.js && node --check src/scanner.js && node --check src/report.js && node --check src/demo.js && node --check scripts/smoke-pack.js",
22
+ "smoke:pack": "node scripts/smoke-pack.js",
23
+ "release:check": "npm test && npm run check && npm run demo && npm run smoke:pack && npm publish --dry-run"
24
+ },
25
+ "keywords": [
26
+ "ai-agent",
27
+ "security",
28
+ "secrets",
29
+ "mcp",
30
+ "forensics",
31
+ "local-first",
32
+ "codex",
33
+ "claude-code",
34
+ "gemini-cli"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/seipass/agenthusk.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/seipass/agenthusk/issues"
42
+ },
43
+ "homepage": "https://github.com/seipass/agenthusk#readme",
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }
package/src/catalog.js ADDED
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+
3
+ export const AGENT_CATALOG = [
4
+ { id: "codex", label: "Codex", color: "#f3b63f", paths: [".codex"] },
5
+ { id: "claude", label: "Claude Code", color: "#e27650", paths: [".claude"] },
6
+ { id: "gemini", label: "Gemini", color: "#6fb5ff", paths: [".gemini"] },
7
+ { id: "openclaw", label: "OpenClaw", color: "#e96666", paths: [".openclaw"] },
8
+ { id: "hermes", label: "Hermes", color: "#dc8fff", paths: [".hermes"] },
9
+ { id: "cursor", label: "Cursor", color: "#a78bfa", paths: [".cursor"] },
10
+ { id: "windsurf", label: "Windsurf", color: "#4fc3b3", paths: [".windsurf"] },
11
+ {
12
+ id: "opencode",
13
+ label: "OpenCode",
14
+ color: "#5bd2a6",
15
+ paths: [path.join(".config", "opencode")]
16
+ },
17
+ { id: "continue", label: "Continue", color: "#8ad56b", paths: [".continue"] },
18
+ { id: "cline", label: "Cline", color: "#ff8c73", paths: [".cline"] }
19
+ ];
20
+
21
+ export function knownRoots(homeDir) {
22
+ return AGENT_CATALOG.flatMap(agent =>
23
+ agent.paths.map(relativePath => ({
24
+ ...agent,
25
+ path: path.join(homeDir, relativePath)
26
+ }))
27
+ );
28
+ }
package/src/cli.js ADDED
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createDemoReport } from "./demo.js";
8
+ import { renderHtmlReport, renderShareCard } from "./report.js";
9
+ import { ScanUsageError, scan } from "./scanner.js";
10
+
11
+ const DEFAULT_HTML_PATH = "agenthusk-report.html";
12
+ const DEFAULT_JSON_PATH = "agenthusk-report.json";
13
+ export const VERSION = JSON.parse(
14
+ fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")
15
+ ).version;
16
+
17
+ export const USAGE = `Usage:
18
+ agenthusk scan [options]
19
+ agenthusk demo [options]
20
+ agenthusk help
21
+ agenthusk --version
22
+
23
+ Options:
24
+ --home <directory> Override the home directory used by scan
25
+ --root <directory> Scan a specific root; may be repeated
26
+ --out <file> Write the HTML report to this file
27
+ --json <file> Write the JSON report to this file
28
+ --card <file> Write an SVG share card to this file
29
+ --max-files <count> Stop scanning after this many files
30
+ --max-bytes <count> Skip content inspection for files larger than this
31
+ --show-paths Unsafe: include relative paths in a private report
32
+ --no-html Do not write an HTML report
33
+ --version Show the AgentHusk version
34
+ --help Show this help
35
+ `;
36
+
37
+ class CliUsageError extends Error {}
38
+
39
+ function takeValue(args, option) {
40
+ const value = args.shift();
41
+ if (!value || value.startsWith("--")) {
42
+ throw new CliUsageError(`Missing value for ${option}.`);
43
+ }
44
+ return value;
45
+ }
46
+
47
+ export function parseArguments(argv) {
48
+ const args = [...argv];
49
+ const command = args.shift() ?? "help";
50
+ const options = {
51
+ command,
52
+ homeDir: undefined,
53
+ roots: [],
54
+ htmlPath: DEFAULT_HTML_PATH,
55
+ jsonPath: DEFAULT_JSON_PATH,
56
+ cardPath: undefined,
57
+ maxFiles: undefined,
58
+ maxBytes: undefined,
59
+ showPaths: false,
60
+ html: true
61
+ };
62
+
63
+ if (command === "--help" || command === "-h") {
64
+ return { ...options, command: "help" };
65
+ }
66
+ if (command === "--version" || command === "-v") {
67
+ return { ...options, command: "version" };
68
+ }
69
+ if (!["scan", "demo", "help", "version"].includes(command)) {
70
+ throw new CliUsageError("Unknown command.");
71
+ }
72
+
73
+ while (args.length > 0) {
74
+ const option = args.shift();
75
+ switch (option) {
76
+ case "--home":
77
+ options.homeDir = takeValue(args, option);
78
+ break;
79
+ case "--root":
80
+ options.roots.push(takeValue(args, option));
81
+ break;
82
+ case "--out":
83
+ options.htmlPath = takeValue(args, option);
84
+ break;
85
+ case "--json":
86
+ options.jsonPath = takeValue(args, option);
87
+ break;
88
+ case "--card":
89
+ options.cardPath = takeValue(args, option);
90
+ break;
91
+ case "--max-files": {
92
+ const maxFiles = Number(takeValue(args, option));
93
+ if (!Number.isSafeInteger(maxFiles) || maxFiles <= 0) {
94
+ throw new CliUsageError("--max-files must be a positive integer.");
95
+ }
96
+ options.maxFiles = maxFiles;
97
+ break;
98
+ }
99
+ case "--max-bytes": {
100
+ const maxBytes = Number(takeValue(args, option));
101
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
102
+ throw new CliUsageError("--max-bytes must be a positive integer.");
103
+ }
104
+ options.maxBytes = maxBytes;
105
+ break;
106
+ }
107
+ case "--no-html":
108
+ options.html = false;
109
+ break;
110
+ case "--show-paths":
111
+ options.showPaths = true;
112
+ break;
113
+ case "--help":
114
+ case "-h":
115
+ options.command = "help";
116
+ break;
117
+ case "--version":
118
+ case "-v":
119
+ options.command = "version";
120
+ break;
121
+ default:
122
+ throw new CliUsageError("Unknown option.");
123
+ }
124
+ }
125
+
126
+ return options;
127
+ }
128
+
129
+ export function atomicWrite(filePath, contents) {
130
+ const destination = path.resolve(filePath);
131
+ const directory = path.dirname(destination);
132
+ const temporaryPath = path.join(
133
+ directory,
134
+ `.${path.basename(destination)}.${process.pid}.${crypto.randomUUID()}.tmp`
135
+ );
136
+
137
+ fs.mkdirSync(directory, { recursive: true });
138
+ try {
139
+ fs.writeFileSync(temporaryPath, contents, {
140
+ encoding: "utf8",
141
+ flag: "wx",
142
+ mode: 0o600
143
+ });
144
+ fs.renameSync(temporaryPath, destination);
145
+ } finally {
146
+ try {
147
+ fs.unlinkSync(temporaryPath);
148
+ } catch {
149
+ // The rename succeeded or the temporary file was never created.
150
+ }
151
+ }
152
+ }
153
+
154
+ function createReport(options) {
155
+ if (options.command === "demo") return createDemoReport();
156
+ return scan({
157
+ homeDir: options.homeDir,
158
+ roots: options.roots.length > 0 ? options.roots : undefined,
159
+ maxFiles: options.maxFiles,
160
+ maxContentBytes: options.maxBytes,
161
+ showPaths: options.showPaths
162
+ });
163
+ }
164
+
165
+ export function runCli(
166
+ argv = process.argv.slice(2),
167
+ streams = { stdout: process.stdout, stderr: process.stderr }
168
+ ) {
169
+ try {
170
+ const options = parseArguments(argv);
171
+ if (options.command === "help") {
172
+ streams.stdout.write(USAGE);
173
+ return 0;
174
+ }
175
+ if (options.command === "version") {
176
+ streams.stdout.write(`${VERSION}\n`);
177
+ return 0;
178
+ }
179
+
180
+ const report = createReport(options);
181
+ const artifacts = [
182
+ [options.jsonPath, `${JSON.stringify(report, null, 2)}\n`]
183
+ ];
184
+ if (options.html) artifacts.push([options.htmlPath, renderHtmlReport(report)]);
185
+ if (options.cardPath) artifacts.push([options.cardPath, renderShareCard(report)]);
186
+
187
+ for (const [filePath, contents] of artifacts) atomicWrite(filePath, contents);
188
+ const coverage = report.stats.coverageIncomplete ? "coverage gaps reported" : "no cap hit";
189
+ streams.stdout.write(
190
+ `AgentHusk report generated. ${report.agents.length} roots scanned, ${report.findings.length} findings, ${coverage}.\n`
191
+ );
192
+ return 0;
193
+ } catch (error) {
194
+ if (error instanceof CliUsageError || error instanceof ScanUsageError) {
195
+ streams.stderr.write(`agenthusk: ${error.message}\n\n${USAGE}`);
196
+ return 2;
197
+ }
198
+ streams.stderr.write("agenthusk: unable to generate report.\n");
199
+ return 1;
200
+ }
201
+ }
202
+
203
+ function resolveEntryPoint(filePath) {
204
+ try {
205
+ return fs.realpathSync.native(filePath);
206
+ } catch {
207
+ return path.resolve(filePath);
208
+ }
209
+ }
210
+
211
+ const isEntryPoint = process.argv[1]
212
+ && resolveEntryPoint(process.argv[1]) === fileURLToPath(import.meta.url);
213
+
214
+ if (isEntryPoint) process.exitCode = runCli();