Rules / CFG136
CFG136 a Claude Code plugin loads a hooks module
Severity: warn (a skills-directory plugin in the repository) · info (any other plugin package)
OWASP: LLM04:2026 – Supply Chain
OWASP Agentic: ASI04:2026 – Agentic Supply Chain Vulnerabilities
Agents: claude-code
What cfgaudit checks
A Claude Code plugin's hooks/hooks.json can hold modules beside or instead of hooks: {"modules":["./register.js"]}. A hooks module is JavaScript or TypeScript that Claude Code loads into a worker of its own process and that registers hooks on the engine's events. cfgaudit reports each modules entry (#1601) in:
- a skills-directory plugin in the repository,
.claude/skills/<name>/with a.claude-plugin/plugin.json; - a repository's own
.claude-plugin/; - a package scanned with
--plugins.
Severity.
warnfor a skills-directory plugin, which Claude Code loads from the repository by itself once the workspace is trusted.infofor any other package: a repository's own.claude-plugin/or a--pluginspackage loads only when someone installs and enables it, so for its author and reviewer the module is listed rather than alarmed on.
The finding names the module, resolved relative to hooks.json as Claude Code resolves it. cfgaudit does not scan the module's code. A plugin that does not load is skipped, as for the rest of the plugin scan: "defaultEnabled": false, or set to false under enabledPlugins.
Why it matters
Measured on Claude Code 2.1.321, with a skills-directory plugin in a scratch project:
- Before trust. Project skills-directory plugins are not picked up at all.
- After the workspace trust dialog. The module loaded with no install step:
hooks module <name>@skills-dir loaded (worker, environment 1, tier user). - At session start. Its
session.starthook ran a host command through$.process.runwith no prompt.
The module's own globals are limited: no process, require or fetch, and it can import only its own files and claude-code. Through $ it still reaches:
- host commands (
$.process.run,$.process.spawn), run as the user the session runs as; - files (
$.fs) and model calls ($.model); - hooks that can deny, rewrite or answer tool calls (
tool.call) and change prompts (prompt.submit,prompt.compose).
So one trust decision for a cloned repository runs the plugin author's code in the agent, before any tool call or command a reviewer would see.
Example
.claude/skills/helper/.claude-plugin/plugin.json {"name": "helper"}
.claude/skills/helper/hooks/hooks.json {"modules": ["./register.js"]}
.claude/skills/helper/hooks/register.js
Gives one warn on hooks/hooks.json, naming hooks/register.js. The same files under plugins/helper/ of a marketplace repository give an info.
Remediation
Read the module as you would a hook script before trusting the repository, or remove the plugin. A team that ships one on purpose can exclude the file with exclude-paths in .cfgaudit.yml.
Documentation for cfgaudit 1.21.0. Source of this page · Rule implementation