KBCodeKB
Claude CodeUnverified

Fix Claude Code Plugin False Positive: security-guidance Hook Blocks File Writes Containing 'exec(' in Markdown, TypeScript, and Documentation Files

The security-guidance plugin (claude-plugins-official) has a child_process_exec rule whose substring list includes bare 'exec('. The hook performs plain substring matching over ENTIRE file content with no file-extension awareness. Any Markdown (.md), TypeScript (.ts), or documentation file containing 'exec(' — including legitimate uses like SQLite's db.exec(schema), RegExp.prototype.exec(), or code samples in documentation — triggers the PreToolUse Write hook to print a command-injection warning to stderr and exit with code 2, BLOCKING the file write entirely. The same latent issue affects 6 other XSS/browser-DOM rules (new_function_injection, react_dangerously_set_html, document_write_xss, innerHTML_xss, outerHTML_xss, insertAdjacentHTML_xss) which fire on documentation files containing trigger substrings like 'new Function', 'dangerouslySetInnerHTML', 'document.write', '.innerHTML ='. This is NOT a Claude Code core bug — it is a security-guidance plugin false positive caused by substring matching without file-extension gates. Anthropic staff (mhegazy) confirmed and fixed via merged PR anthropics/claude-plugins-official#2074 (May 29, 2026): all XSS/browser-DOM substring rules now require JS-family file extensions (.js/.ts/.jsx/.tsx). The child_process_exec rule with bare 'exec(' was separately addressed. For users who cannot update the plugin immediately, workarounds include using small Edit calls instead of Write, disabling the hook via ENABLE_SECURITY_REMINDER=0, rewriting exec() to use alternative APIs (String.match() instead of RegExp.exec()), or using a placeholder-body-first then Edit workflow.

Symptoms

  • Claude Code Write tool is blocked with a command-injection warning when writing ANY file containing the substring 'exec(' — even in Markdown docs, TypeScript source, or prose
  • The error message references 'child_process.exec' or command injection even though the file content has nothing to do with shell commands or child processes
  • Writing documentation about SQLite (db.exec()), JavaScript regex (RegExp.prototype.exec()), or shell scripting examples triggers the hook and blocks the write
  • The same file path succeeds on a second attempt because the (file_path, rule_name) state key is recorded on first match — confusing and inconsistent behavior

Error signatures

child_process.exec
exec(
security_reminder_hook.py
command injection
security-guidance
PreToolUse hook exited with code 2

Possible causes

  • The security-guidance plugin's child_process_exec rule (in security_reminder_hook.py, line 71) defines substrings: ['child_process.exec', 'exec(', 'execSync(']. The bare 'exec(' substring matches ANY occurrence of 'exec(' in ANY file content — including db.exec(schema) in SQLite docs, RegExp.prototype.exec() in TypeScript source, or 'exec(' inside code fences in Markdown. The hook's check_patterns() function (line 183-199) performs a simple 'if substring in content' check with NO file-extension gate. For Write tool calls, extract_content_from_input() (line 204-214) returns the FULL content field, subjecting the entire file body to substring matching.
  • The hooks.json configuration wires the hook with matcher: 'Edit|Write|MultiEdit', so it runs on EVERY file write regardless of file extension. Six other XSS/browser-DOM rules (new_function_injection, react_dangerously_set_html, document_write_xss, innerHTML_xss, outerHTML_xss, insertAdjacentHTML_xss) share the same latent issue — they trigger on documentation files containing 'new Function', 'dangerouslySetInnerHTML', 'document.write', '.innerHTML =' etc.
  • The hook was designed for security scanning of actual source code files but the substring matching approach is inherently lossy when applied to prose/documentation. Unlike AST-based analysis, substring matching cannot distinguish between 'exec(' as a method call in code and 'exec(' as a quoted string in documentation.

Solutions

Rewrite Code to Avoid 'exec(' Substring (Non-Developer Workaround)

risk: lowgithubpublished

If you are writing TypeScript/JavaScript code that uses RegExp.prototype.exec(), rewrite it to use String.prototype.match() which does not contain the 'exec(' substring. For SQLite documentation, use db.execute() or db.run() instead of db.exec(). This is a simple syntax change that completely avoids the false positive.

  1. Replace `const result = /pattern/.exec(str)` with `const result = str.match(/pattern/)`
  2. Replace `db.exec(schema)` references in documentation with `db.execute(schema)` or quote as `db.exec()`
  3. For shell-scripting examples, use `$(command)` instead of referencing `exec()` directly
  4. For Python subprocess docs, use `subprocess.run()` instead of `os.exec()`

Config examples

# BEFORE (triggers false positive):
const m = /^Test:/.exec(line)?.[1];
# AFTER (avoids trigger):
const m = line.match(/^Test:/)?.[1];

Risks

  • String.match() returns different structure than RegExp.exec() — first element is full match, not the regex result object
  • For iterative regex with /g flag, match() returns all matches at once, while exec() returns one at a time with lastIndex tracking — behavior differs significantly

Verification

  • Step 1: Rewrite your code using the alternative syntax shown above
  • Step 2: Ask Claude Code to write the file → expect: Write succeeds without command-injection warning
  • Step 3: Run your test suite → expect: all tests pass with the rewritten syntax
0 verified0 failed

Use Small Edit Calls Instead of Full Write (Immediate Workaround)

risk: lowgithubpublished

The hook's extract_content_from_input() for Edit tool calls only scans the new_string field, not the entire file. By writing a minimal placeholder file first (e.g., a single line without 'exec('), then using small Edit calls to add the remaining content, you can bypass the false positive. The Edit path only triggers if new_string contains the trigger substring, which is much easier to avoid than the full file body.

  1. Write a placeholder file that does NOT contain 'exec(': e.g., '# Documentation' as the initial Write
  2. Use Edit tool calls to add content in small chunks, one section at a time
  3. If a section contains 'exec(', rewrite it to use alternative syntax (e.g., String.match() instead of RegExp.exec())
  4. Or use MultiEdit with individual new_string values that don't contain 'exec('

Config examples

# Example: Instead of writing the full file at once, use Edit:
# Step 1 — Write placeholder:
# Write: file.md with content '# My Docs'
# Step 2 — Edit to add content without exec():
# Edit: file.md, old_string: '# My Docs', new_string: '# My Docs\n\n## SQLite Usage\n\nUse db.execute() for queries.'

Risks

  • Time-consuming for large files with many sections
  • Easy to accidentally include 'exec(' in an Edit and get blocked anyway
  • The second-attempt-succeeds behavior (state key recorded on first match) is unreliable and confusing

Verification

  • Step 1: Write a placeholder file with `echo '# Test' > /tmp/test.md` → expect no hook error
  • Step 2: Ask Claude Code to Edit the file and add content without 'exec(' → expect: Edit succeeds
0 verified0 failed

Update security-guidance Plugin (Recommended Fix — PR #2074, Merged May 29, 2026)

risk: lowofficialpublished

Anthropic merged PR #2074 in claude-plugins-official on May 29, 2026, which gates all XSS/browser-DOM substring rules to JS-family file extensions (.js/.ts/.jsx/.tsx). Markdown (.md), Python (.py), text (.txt), and other non-JS files will no longer trigger false positives for 'new Function', 'dangerouslySetInnerHTML', 'document.write', '.innerHTML =', '.outerHTML =', '.insertAdjacentHTML('. Update the security-guidance plugin to the latest version to get this fix.

  1. Update Claude Code plugins: run `/plugin update security-guidance` in Claude Code, or reinstall the latest plugin version
  2. If the plugin was manually installed, pull the latest from claude-plugins-official repository
  3. Verify the plugin version includes PR #2074 (check for _JS_EXTS gate in security_reminder_hook.py)
  4. Attempt to write a Markdown file containing 'exec(' or 'new Function' — the write should now succeed

Commands

# In Claude Code:
/plugin update security-guidance
# Verify fix by checking hook source:
grep -n '_JS_EXTS' ~/.claude/plugins/security-guidance/hooks/security_reminder_hook.py

Risks

  • Plugin update may require restarting Claude Code
  • The bare 'exec(' substring in the child_process_exec rule was addressed in a separate fix; if you still see false positives for 'exec(', the fix may not be in your plugin version yet

Verification

  • Step 1: Run `/plugin update security-guidance` in Claude Code → expect: plugin updates successfully
  • Step 2: Ask Claude Code to write a test .md file containing 'db.exec(schema)' → expect: Write succeeds without command-injection warning
  • Step 3: Run `grep '_JS_EXTS' ~/.claude/plugins/security-guidance/hooks/security_reminder_hook.py` → expect: at least one match showing the extension gate is present
0 verified0 failed

Agent JSON

Canonical machine-readable representation of this issue:

{
  "issue_id": "17cc8eed-ab80-4d7f-a615-892a383bb358",
  "slug": "fix-claude-code-plugin-false-positive-security-guidance-hook-blocks-file-writes-containing-exec-in-markdown-typescript-a-a1mxa3",
  "verification_status": "unverified",
  "canonical_json": "https://codekb.dev/v1/issues/fix-claude-code-plugin-false-positive-security-guidance-hook-blocks-file-writes-containing-exec-in-markdown-typescript-a-a1mxa3"
}
← Back to all issuesPowered by CodeKB