[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.
6 Comments
Found 1 possible duplicate issue:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Recommend closing other item as dupe due to lack of detailed investigation compared with this ticket.
This is not stale, it's an ongoing bug with proposed fixes such as #47514.
How do we get fixes merged @bcherny?
Additional repro — false positive on TypeScript code (not just markdown) — 2026-05-22
Hit this today in a TypeScript route file. The substring
exec(matched my use ofRegExp.prototype.exec()on a regex literal:That's a regex method, not
child_process.exec. I rewrote withString.prototype.match()to get past the hook:Both forms are semantically equivalent for this use case and neither has anything to do with command execution.
Why this matters beyond docs
The original report covers markdown false positives. This shows the same root cause hits real TypeScript code paths involving any of:
RegExp.prototype.exec()— covered by today's repro.exec()finalizers (Mongoose.find().exec(), Drizzle's.execute()slips by but variants don't).exec(sql)methods on prepared statements (e.g.,better-sqlite3's native API — flagged here too, ironically).exec(...)as a method nameThe bare
"exec("substring is too permissive. A pattern that requires a preceding identifier from a known dangerous module (e.g.,child_process.exec(,require('child_process').exec(,import.*child_process.*\.exec\() would catch the actual security risk without false-positiving on every.exec(call in the language.Suggested fix shape
Same direction as the original report — replace flat substring matching with one of:
.md,.txt,.json,.yaml)\b(child_process|cp)\.exec\(and\brequire\(['"]child_process['"]\)\.exec\(subprocess.runvs JSchild_process.execneed different patterns)Happy to PR if there's an appetite — the rule definition lives at
plugins/security-guidance/hooks/security_reminder_hook.py:70-71if I'm reading the file right.Fixed by anthropics/claude-plugins-official#2074 (merged today). The XSS / browser-DOM substring rules now require JS-family extensions. If the "exec(" you're seeing is from a different Python-targeting rule firing on Markdown, that's a separate fix that hasn't shipped yet — please reopen if so, with the rule name from the warning.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.