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

Status Fixed / completed
Maintainer reply ✓ Yes — mhegazy
Activity 6 comments · opened Apr 11, 2026 · closed May 29, 2026
💡 Likely answer: A maintainer (mhegazy, contributor) responded on this thread — see the highlighted reply below.

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 ↗

6 Comments

github-actions[bot] · 4 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/44958

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

adamkdean · 4 months ago
Found 1 possible duplicate issue: 1. [[Bug] PreToolUse:Write hook generates false positives for security warnings #44958](https://github.com/anthropics/claude-code/issues/44958) This issue will be automatically closed as a duplicate in 3 days. If your issue is a duplicate, please close it and 👍 the existing issue instead To prevent auto-closure, add a comment or 👎 this comment 🤖 Generated with Claude Code

Recommend closing other item as dupe due to lack of detailed investigation compared with this ticket.

adamkdean · 3 months ago

This is not stale, it's an ongoing bug with proposed fixes such as #47514.

How do we get fixes merged @bcherny?

evemcgivern · 3 months ago

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 of RegExp.prototype.exec() on a regex literal:

// Edit was BLOCKED because the hook saw "exec("
const artifactType =
  /^Regenerate (\w+):/.exec(snapshot.change_description ?? '')?.[1];

That's a regex method, not child_process.exec. I rewrote with String.prototype.match() to get past the hook:

// Allowed
const artifactType =
  (snapshot.change_description ?? '').match(/^Regenerate (\w+):/)?.[1];

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
  • ORM .exec() finalizers (Mongoose .find().exec(), Drizzle's .execute() slips by but variants don't)
  • DB driver .exec(sql) methods on prepared statements (e.g., better-sqlite3's native API — flagged here too, ironically)
  • Any third-party library that uses .exec(...) as a method name

The 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:

  • File-extension awareness (skip the rule entirely for .md, .txt, .json, .yaml)
  • Regex matching with word boundaries and module-aware context: \b(child_process|cp)\.exec\( and \brequire\(['"]child_process['"]\)\.exec\(
  • Per-rule language detection (Python's subprocess.run vs JS child_process.exec need different patterns)

Happy to PR if there's an appetite — the rule definition lives at plugins/security-guidance/hooks/security_reminder_hook.py:70-71 if I'm reading the file right.

mhegazy contributor · 3 months ago

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.

github-actions[bot] · 1 month ago

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.