@reneturcios/hookcheck 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -26,6 +26,8 @@ Reports in October 2026 described AI agents orchestrating attacks and a self-pro
26
26
  | `.github/workflows/` | run steps, as inventory, including block scalar bodies (`run: |` and `run: >`, with chomping variants). Every workflow finding carries the line number of its `run:` key |
27
27
  | `.githooks/` | hook files |
28
28
 
29
+ Every surface above is discovered by one bounded recursive walk, not only at the scan root: a surface that appears inside a subdirectory (for example `packages/foo/.claude/settings.json`, `sub/.vscode/tasks.json`, or `sub/.github/workflows/ci.yml`) is inventoried from a parent scan with its real relative path in the finding, within the discovery bound documented under Limits. A surface file is inventoried exactly once however many discovery paths reach it.
30
+
29
31
  Classification per hook:
30
32
 
31
33
  - **SAFE**: matches known safe dev patterns such as test, lint, build, typecheck, husky, or lint-staged, and matches no risky rule.
@@ -53,8 +55,7 @@ Honest boundaries, pinned by tests in test/hardening.test.ts:
53
55
  - Non-string package.json script values are skipped: npm only executes string script values, so an array or number in scripts cannot auto-run.
54
56
  - Classification reads up to 200,000 characters of a hook file or a referenced local file. Beyond that depth a payload can hide; the cap exists so a hostile file cannot make the scan unbounded.
55
57
  - GitHub workflow run steps in block scalars (`run: |`, `run: >`, and chomping variants) are parsed and classified like one line commands. Plain multi-line scalars without a block header are not joined, and reusable workflow internals are not resolved. YAML anchors, aliases, and merge keys on run steps are resolved when the anchor is defined in the same file; anchors applied to other step fields, and anchors defined in other files, are not resolved. Workflow files with CRLF line endings are parsed exactly like their LF equivalents.
56
- - Nested package.json inventory is bounded: the root package.json plus nested package.json files whose directory sits at most 4 levels below the root, visited in sorted order, capped at 200 nested files (the cap is reported as a warning when hit). node_modules, .git, dot directories, and symlinked directories are never entered, so package.json files deeper than the depth bound, or anywhere under node_modules, are not scanned.
57
- - Plugin hooks inventory is bounded the same way: `hooks/hooks.json` and `hooks/hooks-cursor.json` files whose plugin root sits at most 4 levels below the scan root, visited in sorted order, capped at 200 files (the cap is reported as a warning when hit), with node_modules, .git, dot directories, and symlinked directories never entered. Plugin hook files deeper than the bound are not scanned. Only the object command form of the plugin schema is inventoried; a hook entry written as a bare string instead of a command object is outside the documented schema and is not inventoried.
58
+ - Nested discovery is one bounded walk covering every inventoried surface: package.json files, `.claude/settings.json` and `settings.local.json` (including the stow layout `claude/settings.json`), plugin `hooks/hooks.json` and `hooks/hooks-cursor.json` files, `.vscode/tasks.json`, `.github/workflows/*.yml` and `*.yaml`, `.husky/` and `.githooks/` hook files, lefthook configs (`lefthook.yml`, `lefthook.yaml`, `.lefthook.yml`), `.pre-commit-config.yaml`, `.cursor/` and `.codex/` config files, and MCP configs (`.mcp.json`, `mcp.json`, `.cursor/mcp.json`, `.windsurf/mcp.json`, `.codex/mcp.json`, and `.vscode/mcp.json`). A surface is in scope when its project directory sits at most 4 levels below the scan root (the plugin hooks directory itself sits one level below its plugin root), visited in sorted order. Each surface family inventories at most 200 nested files; the scan root files always count. node_modules, .git, dot directories, and symlinked directories are never entered, so surfaces deeper than the depth bound, or anywhere under node_modules, are not scanned. The bound is never silent: when the walk hits the depth bound with more tree remaining, the report carries a warning naming the depth bound and counts the directories just below the bound that contain hook surface files or config directories; when a family hits the file cap, a warning names the cap and the count found. A scan cut short by either bound, by the cap, or by unreadable files is partial, says so in its warnings, and never verdicts a bare CLEAN when surfaces are known to have been missed. Only the object command form of the plugin schema is inventoried; a hook entry written as a bare string instead of a command object is outside the documented schema and is not inventoried.
58
59
  - A file or directory the scanner sets out to inventory but cannot read (for example a workflow file at mode 000) is never skipped silently: the report carries a warning naming it, and the verdict is floored at REVIEW while any part of the estate is unread, so a partial scan never presents as a complete all-clear.
59
60
  - Custom git hook directories set through core.hooksPath are not scanned: hookcheck inventories the standard `.githooks/` directory and `.husky/`, but it does not parse git config or a committed `.gitconfig` to discover a custom core.hooksPath directory. Hooks kept only in a custom hooksPath directory are outside the inventory; audit them by scanning a tree where they sit in a standard location.
60
61
  - Agent config coverage boundaries: `.gemini/settings.json` is not scanned yet. Template configs (for example `templates/settings.json.template`) are not scanned: they are inert until copied into place, at which point the copied file is scanned under its real name.
package/dist/core.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { relative } from "node:path";
2
2
  import type { Classification, HookFinding, ScanReport, Verdict } from "./types.js";
3
- export declare const VERSION = "0.1.7";
3
+ export declare const VERSION = "0.1.8";
4
4
  interface RuleDef {
5
5
  id: string;
6
6
  pattern: RegExp;
@@ -60,46 +60,53 @@ export declare function classifyCommand(command: string, lang?: Lang): {
60
60
  };
61
61
  export declare function computeVerdict(findings: HookFinding[]): Verdict;
62
62
  /**
63
- * Nested package.json discovery (P-032). Install scripts in workspace
64
- * subpackages (packages/foo, apps/web) run on a root install, so a
65
- * root-only scan misses exactly the surface a worm would persist in.
66
- * The walk is bounded so a hostile or giant tree cannot explode the
67
- * report: directories are visited in sorted order to depth
68
- * PACKAGE_MAX_DEPTH (the package.json directory depth below the root),
69
- * node_modules, .git, and dot directories are never entered, symlinked
70
- * directories are never followed (a symlink loop cannot hang the scan),
71
- * and at most PACKAGE_MAX_NESTED nested files are inventoried, with the
72
- * cap reported as a warning when it is hit.
63
+ * Unified nested discovery (P-032, P-060, P-065). Every surface the
64
+ * tool inventories at the scan root is also inventoried when it
65
+ * appears inside a subdirectory: before v0.1.8 only nested package.json
66
+ * files (P-032) and plugin hooks files (P-060) were recursed, so a
67
+ * parent scan of a monorepo or workspace silently missed nested
68
+ * .claude/settings.json, .vscode/tasks.json, workflows, husky and
69
+ * githooks directories, lefthook and pre-commit configs, and MCP
70
+ * configs, and a tree holding only nested surfaces scanned CLEAN with
71
+ * the empty estate note attached (P-065).
72
+ *
73
+ * One bounded walk enumerates every project root (each directory at
74
+ * most NESTED_MAX_DEPTH levels below the scan root, the scan root
75
+ * itself included) plus the nested package.json and plugin hooks file
76
+ * lists. Per surface scanners then run once per project root with the
77
+ * root prefix carried into every finding file path, so findings carry
78
+ * their real relative paths. The walk is bounded so a hostile or
79
+ * giant tree cannot explode the report: directories are visited in
80
+ * sorted order, node_modules, .git, and dot directories are never
81
+ * entered, symlinked directories are never followed (a symlink loop
82
+ * cannot hang the scan), and each surface family inventories at most
83
+ * NESTED_MAX_FILES_PER_SURFACE nested files, with the cap reported as
84
+ * a warning when it is hit. When directories remain below the depth
85
+ * bound the walk says so in a warning naming the bound, and it probes
86
+ * the directories just below the bound (names only, no file reads) so
87
+ * surfaces sitting there are counted in the warning instead of being
88
+ * silently dropped.
73
89
  */
90
+ export declare const NESTED_MAX_DEPTH = 4;
91
+ export declare const NESTED_MAX_FILES_PER_SURFACE = 200;
74
92
  export declare const PACKAGE_MAX_DEPTH = 4;
75
93
  export declare const PACKAGE_MAX_NESTED = 200;
76
- /**
77
- * Claude Code plugin hook files (P-060, BLOCKER). Plugins distribute
78
- * hooks in a hooks/ directory at the plugin root: hooks/hooks.json at
79
- * the repo root for a single plugin repo (obra/superpowers), or
80
- * plugins/<name>/hooks/hooks.json inside a marketplace or monorepo
81
- * (anthropics/claude-code carries 9 such files, wshobson/agents 2 among
82
- * 92 plugin directories). Before v0.1.7 none of these files was scanned
83
- * at any depth: a SessionStart curl pipe in hooks/hooks.json scanned
84
- * CLEAN with total 0 while the identical payload in .claude/settings.json
85
- * verdicts COMPROMISED-PATTERN. The file carries the same schema as the
86
- * hooks key of .claude/settings.json (an events map of command hooks:
87
- * SessionStart, PreToolUse, PostToolUse, Stop, UserPromptSubmit, and
88
- * the rest of the Claude Code event set), so commands are classified
89
- * exactly as settings hooks are. The sibling hooks/hooks-cursor.json
90
- * variant shipped by the same plugins (superpowers) carries the same
91
- * command object shape under Cursor event names, so it is inventoried
92
- * the same way.
93
- *
94
- * The walk mirrors the nested package.json discipline (P-032): sorted
95
- * order, node_modules, .git, and dot directories never entered,
96
- * symlinked directories never followed, and a cap with a warning. The
97
- * depth bound applies to the plugin root (at most PLUGIN_HOOKS_MAX_DEPTH
98
- * directories below the scan root); the hooks directory itself sits one
99
- * level below its plugin root.
100
- */
101
94
  export declare const PLUGIN_HOOKS_MAX_DEPTH = 4;
102
95
  export declare const PLUGIN_HOOKS_MAX_FILES = 200;
96
+ export interface Discovery {
97
+ /** Project root prefixes, scan root ("") first, then sorted. */
98
+ projectRoots: string[];
99
+ /** Nested package.json relative paths (scan root excluded), sorted, uncapped. */
100
+ packageFiles: string[];
101
+ /** Plugin hooks file relative paths, sorted, uncapped. */
102
+ pluginHookFiles: string[];
103
+ /** Directories just below the depth bound that were not entered. */
104
+ frontierDirs: number;
105
+ /** Frontier directories whose listing shows a hook surface file or config directory. */
106
+ surfacesBelowBound: number;
107
+ /** True when the depth bound left directories unentered. */
108
+ depthBoundHit: boolean;
109
+ }
103
110
  /**
104
111
  * Empty estate note (P-014, hookcheck half). A scan that inventories
105
112
  * nothing must say so next to the verdict, in JSON as well as in the