cfgaudit

Rules / CFG111

CFG111 a permission rule or hook matcher holds an invisible character, so it does nothing

Severity: error (deny / ask rule, hook matcher) / info (allow rule) OWASP: LLM03:2026 – Excessive Agency

What cfgaudit checks

Every entry of permissions.deny, permissions.ask and permissions.allow, and every hook matcher, in Claude Code settings files and plugin hooks.json, for a character that renders as nothing: a Unicode format character (zero-width space and joiners, bidirectional controls such as U+202E, the Tags block, soft hyphen, word joiner, BOM), a control character such as NUL, or a blank Hangul filler.

{
  "permissions": {
    "deny": ["Bash(cu\u200brl:*)"]      // CFG111 error: reads as Bash(curl:*), blocks nothing
  },
  "hooks": {
    "PreToolUse": [{ "matcher": "Ba\u200bsh", "hooks": [ ... ] }]   // CFG111 error: never runs
  }
}

The message shows the entry with the character spelled out (Bash(cu<U+200B>rl:*)) and its position.

Measured on Claude Code 2.1.288

Scratch HOME, trusted scratch project, permissions.allow: ["Bash(touch:*)"], and a local endpoint replaying a touch Bash tool call:

project settings touch ran
deny: ["Bash(touch:*)"] (control) no, "Permission to use Bash ... has been denied"
the same deny with U+200B, NUL, U+202E or U+00AD inside it yes
ask: ["Bash(tou\u200bch:*)"] yes, no prompt

A PreToolUse hook with "matcher": "Bash" ran for the call; with "matcher": "Ba\u200bsh" it did not run. In none of the cases did Claude Code warn: the only log line is the rule being added, with the character in it. (2.1.287 refuses a hook matcher with an invisible character, but only for cloud-synced hooks, not settings files.)

So the entry reads as configured in every editor, in git diff and in review, and is out of force. A deny or ask rule and a hook matcher are guardrails, so that is error, and it is the Trojan Source shape: one character in a pull request disables a rule nobody sees change.

Why allow is only info

A hidden character makes a rule's text longer, so an allow rule can only match less than it appears to, never more. That carries no risk; the rule simply does not grant what it says.

What is deliberately not flagged

False-positive measurement

1085 committed settings files (seeded on deny, ask and matcher in .claude/settings*.json, plus the corpora of #609 and #610), holding 34,122 permission rules and 981 hook matchers, 146 of the rules with non-ASCII text.

The zero on deny, ask and matchers is the expected shape: the character is an accident or an attack, and neither is common. The rule exists because the attack costs one keystroke and is invisible to the reviewer.

Fix

Delete the entry and type it again. Copying a rule out of a chat, a web page or a PDF is the usual accidental source; an editor that shows invisible characters (VS Code's editor.unicodeHighlight.invisibleCharacters) makes them visible.

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