[BUG] security-guidance hook blocks markdown/doc writes containing "exec(" substring
Summary
The security-guidance plugin's security_reminder_hook.py has a child_process_exec rule whose substring list includes bare "exec(". The hook runs plain substring matching over the entire file content with no awareness of file extension, so any Markdown (or other non-code) file containing the text exec( — for example a code sample like db.exec(schema) in documentation about better-sqlite3 — causes the PreToolUse hook to print the command-injection warning to stderr and exit with code 2, blocking the Write tool call.
Location
plugins/security-guidance/hooks/security_reminder_hook.py
- Lines 70–90:
child_process_execrule definition. - Line 71:
"substrings": ["child_process.exec", "exec(", "execSync("]— bare"exec("is the offender. - Lines 183–199:
check_patterns()performsif substring in contentover the raw content with no file-extension gate. - Line 196: the offending naive match.
- Lines 204–214:
extract_content_from_input()— forWrite, returns the fullcontentfield, so the entire Markdown body is subjected to the substring match. - Lines 272–273: on match, prints the reminder to stderr and
sys.exit(2), which blocks the tool call.
plugins/security-guidance/hooks/hooks.json wires the hook with "matcher": "Edit|Write|MultiEdit", so it runs on every file write regardless of extension.
Reproduction
- Install the
security-guidanceplugin. - Ask Claude Code to write any
.mdfile whose body contains the textexec(— for example a documentation file that describes SQLite usage with a snippet likedb.exec(schema), or prose that quoteschild.exec(...)in an example block. - The hook matches the
child_process_execrule, prints the command-injection reminder to stderr, and exits 2. - The
Writeis blocked.
Workarounds currently available
- Write a placeholder body first, then use small
Editcalls whose individualnew_stringvalues don't containexec((theEditpath ofextract_content_from_inputonly scansnew_string, not the whole file). - Disable the hook globally via
ENABLE_SECURITY_REMINDER=0(nuclear — loses all other rules too). - Retry the same file path in the same session: the
(file_path, rule_name)state key is recorded on first match, so the second attempt passes. Confusing and inconsistent.
Impact
- Blocks legitimate documentation writes that mention
exec(in any context — prose, code fences, references to SQLite'sdb.exec(), shell-scripting docs, hook-internals docs, and so on. - Surfaces as an opaque block: the user sees a command-injection warning unrelated to what they are writing.
- The
react_dangerously_set_html,document_write_xss,innerHTML_xss,eval(andpicklerules share the same latent issue: any documentation file that quotes those identifiers in code samples will be blocked.
Suggested fixes
- Scope by extension (preferred). Add a
path_checkto thechild_process_execrule (and the other content-based code rules) that skips common documentation/plaintext extensions:.md,.mdx,.rst,.txt,.adoc, and similar. Keeps the rule in force for real source files. - Tighten the substring list. Drop bare
"exec("and keep only"child_process.exec"and"execSync(". Loses coverage of aliased imports (const { exec } = require('child_process')) but kills the false positive. - Global extension gate in
check_patterns(). Short-circuit all content-based rules when the file path has a documentation extension. Substring matching over prose is inherently lossy; a path-level gate fixes the whole class at once.
Option 1 is the cleanest localised fix. Option 3 is the cleanest systemic fix.
Notes
- Issues #40172 and #46449 concern a different failure mode in the same hook (hardcoded
python3on Windows). This report is unrelated to platform and reproduces on macOS.
Attribution
This false positive was detected by a human while using Claude Code. The hook source investigation, root-cause analysis, and the filing of this issue were performed by Claude Opus 4.6 (1M context) under human direction.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗