cfgaudit

Rules / CFG015

CFG015 hook command contains shell substitution

Severity: warn (escalates to error if a network call appears inside the substitution) OWASP: LLM01:2026 – Prompt Injection

What cfgaudit checks

cfgaudit scans every command-bearing key in settings.json — hooks (hooks.*[].hooks[].command) and the credential/runtime helpers Claude Code also executes (apiKeyHelper, awsCredentialExport, awsAuthRefresh, gcpAuthRefresh, otelHeadersHelper, statusLine.command, subagentStatusLine.command, fileSuggestion.command) and each MCP server's headersHelper — for shell-evaluated substitutions:

The output of a substituted command is spliced into the shell line at runtime. When any input is attacker-controlled (filename Claude chose, tool output, an injected commit message, …), the substitution becomes a command-injection sink.

A finding is emitted at warn when a substitution is present and at error when the substituted command itself reaches the network (curl, wget, nc, ncat, ssh, scp, rsync, ftp, telnet, nslookup, dig, host). Network calls inside a substitution mean the value spliced into the hook is whatever some external endpoint chose to return.

This rule is complementary to:

Multiple substitutions in one command are consolidated into a single finding; the message lists each substitution string.

Not reported:

Trigger examples

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "notify-send \"$(cat /etc/passwd)\"" },
          { "type": "command", "command": "echo `date`" }
        ]
      }
    ]
  }
}

Both fire. The first is warn (no network call); the second is also warn. Replace cat /etc/passwd with curl … and the severity becomes error.

Safe alternative

Hooks should be deterministic and not depend on values evaluated at hook time. If you need dynamic data, read it from stdin, the event JSON Claude Code pipes into the hook, into a variable and pass that in double quotes (f=$(jq -r '.tool_input.file_path'); tool "$f"), or use a program that reads stdin itself; do not let attacker-controlled inputs flow into a $(…) that is spliced into the command:

{
  "type": "command",
  "command": "/usr/local/bin/notify-from-stdin"
}

References

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