cfgaudit

Rules / CFG119

CFG119 a script started by an auto-run hook or task carries the markers of a malicious payload

Severity: error · warn (one marker other than obfuscation, started from several tools) OWASP: LLM04:2026 – Supply Chain Agents: claude-code, codex, continue, copilot, cursor, devin, gemini-cli, grok, opencode, qwen-code, vscode, zed

What cfgaudit checks

A committed command that runs without anyone asking for it can start a script from the repository: a Claude Code SessionStart hook running node .tooling/start.mjs, a VS Code task with runOptions.runOn: "folderOpen" running bash scripts/setup.sh, an MCP server launched as python tools/server.py, or the hooks of the other agents cfgaudit reads. The command-content rules see only the command line, so a payload moved into such a script passes them. cfgaudit reads the script (#1217), without running it, and CFG119 looks in it for the markers of a malicious payload:

Marker What it matches
Obfuscation 30 or more identifiers of the form an obfuscator gives every variable, _0x plus hex digits; or the loop an obfuscator emits to rotate its string array (while(!![]){try{); or at least 100 hex literals at a density of 5 or more per 1,000 characters in a file with a line of 5,000 characters or more (#1265); or an array of 1,000 or more character codes together with fromCharCode and eval or Function (#1270)
Download and run the URL of a release asset, an archive, a binary or a runtime installer, together with code that starts a process or makes a file executable
Credential hunt access-token prefixes (ghp_, ghs_, gho_, github_pat_, npm_, AKIA, xox?-) or credential files (.npmrc, .aws/credentials, .docker/config.json, .git-credentials, .ssh/id_), together with network code
Environment dump JSON.stringify(process.env), Object.keys(process.env), os.environ.items() or 50 or more process.env reads, together with network code

A launcher that starts a process and names a file next to it (const payload = "index.js") is followed one step further, so a small loader that downloads a runtime to run the payload beside it does not hide the payload.

The script is resolved from the command: an interpreter (node, bun run, deno run, npx tsx, python, bash, sh, pwsh -File, ruby, perl, php) followed by a path, or a script path run directly (./scripts/x.sh), with $CLAUDE_PROJECT_DIR and ${workspaceFolder} read as the repository root. The parentheses of a subshell around the command ((command -v node && node ./x.js) || ...) are not part of the path; balanced ones inside it, such as a Next.js route group app/(site)/, are. A path outside the repository, a path with any other variable and a binary file are not read. A file over 4 MB is read only at its two ends, the first and last 64 KB, for CFG119 alone, and the finding says so (#1270); the other content rules skip it. Package scripts (npm run dev) are not resolved: their code is the project's own build.

Trigger example

{
  "hooks": {
    "SessionStart": [
      { "hooks": [{ "type": "command", "command": "node .tooling/start.mjs" }] }
    ]
  }
}

with .tooling/start.mjs an obfuscated bundle (var _0x4d2c=... throughout) that contains token patterns such as ghs_ and sends data with https.request:

.tooling/start.mjs [error] CFG119: script .tooling/start.mjs, started by the hooks.SessionStart command, carries the markers of a malicious payload: it is obfuscated (412 _0x identifiers); it looks for access tokens or credential files and talks to the network. A committed script that runs when the repository is opened must not hide what it does; read it before you open the repository with an agent or editor, and if nobody in the project added it, treat the repository as compromised

Measured

The pre-release corpus run for v1.20.0 on 10,034 popular repositories that commit agent configuration found one implant, byte-identical in two unrelated projects and committed under the name of the Actions bot: a loader started by a Claude Code SessionStart hook and a VS Code folder-open task, which downloads a runtime and runs an obfuscated bundle that collects tokens and the environment. Before this rule, cfgaudit reported only CFG047 for the task and CFG067 for the hook, worded the same as for ordinary build tasks. CFG119 reports the bundle at error and the loader at warn in both projects, and nothing anywhere else in the corpus, which holds many large legitimate JavaScript files in skills and plugins (chart libraries, browser helpers, dashboards).

A second implant in the same corpus came in under a contributor's name inside an unrelated 28-file commit. A hidden folder-open task ("hide": true, "reveal": "never"), named like a lint task, runs node on a file named like a web font in a Next.js route-group path. The file is one line of obfuscated JavaScript that uses random identifiers instead of _0x names and ends with }(global,require));. CFG119 missed it twice: the subshell's closing parenthesis stuck to the path, so the file was never read, and the obfuscation marker knew only _0x names. Since #1265 it reports the task at error and names both the disguise and the rotation loop. On the full corpus CFG119 now reports the three implant repositories and nothing else. None of 886 minified bundles and large node_modules files on a developer machine matches either new obfuscation shape.

A published worm commits the same SessionStart hook to .claude/settings.json and .gemini/settings.json, plus a VS Code folder-open task, all running node .github/setup.js. The script is 4.5 to 5.3 MB in one line: an array of about a million character codes, turned into text with String.fromCharCode, decoded with a letter rotation and run with eval. In clear text it holds no URL and no module name. Before #1270 it was over the read limit and matched no marker. Now CFG119 reports it at error in all four samples checked that still carried the script. 34 of the 886 bundles measured carry a large numeric array, but none together with fromCharCode and eval, so no bundle matches. The 10,034-repository corpus gives no new finding.

Fix

Open the script and read it before you open the repository with an agent or an editor that runs its tasks. If nobody in the project added it, treat the repository as compromised: remove the files and the hook or task that starts them, rotate the tokens of everyone who opened it, and find out how the commit got in.

Documentation for cfgaudit 1.20.0. Source of this page · Rule implementation