[BUG] security-guidance hook blocks markdown/doc writes containing "exec(" substring

Resolved 💬 5 comments Opened Apr 11, 2026 by adamkdean Closed May 29, 2026

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_exec rule definition.
  • Line 71: "substrings": ["child_process.exec", "exec(", "execSync("] — bare "exec(" is the offender.
  • Lines 183–199: check_patterns() performs if substring in content over the raw content with no file-extension gate.
  • Line 196: the offending naive match.
  • Lines 204–214: extract_content_from_input() — for Write, returns the full content field, 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

  1. Install the security-guidance plugin.
  2. Ask Claude Code to write any .md file whose body contains the text exec( — for example a documentation file that describes SQLite usage with a snippet like db.exec(schema), or prose that quotes child.exec(...) in an example block.
  3. The hook matches the child_process_exec rule, prints the command-injection reminder to stderr, and exits 2.
  4. The Write is blocked.

Workarounds currently available

  • Write a placeholder body first, then use small Edit calls whose individual new_string values don't contain exec( (the Edit path of extract_content_from_input only scans new_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's db.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( and pickle rules share the same latent issue: any documentation file that quotes those identifiers in code samples will be blocked.

Suggested fixes

  1. Scope by extension (preferred). Add a path_check to the child_process_exec rule (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.
  2. 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.
  3. 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 python3 on 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.

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗