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)
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.
- Replace `const result = /pattern/.exec(str)` with `const result = str.match(/pattern/)`
- Replace `db.exec(schema)` references in documentation with `db.execute(schema)` or quote as `db.exec()`
- For shell-scripting examples, use `$(command)` instead of referencing `exec()` directly
- 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
Use Small Edit Calls Instead of Full Write (Immediate Workaround)
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.
- Write a placeholder file that does NOT contain 'exec(': e.g., '# Documentation' as the initial Write
- Use Edit tool calls to add content in small chunks, one section at a time
- If a section contains 'exec(', rewrite it to use alternative syntax (e.g., String.match() instead of RegExp.exec())
- 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
Update security-guidance Plugin (Recommended Fix — PR #2074, Merged May 29, 2026)
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.
- Update Claude Code plugins: run `/plugin update security-guidance` in Claude Code, or reinstall the latest plugin version
- If the plugin was manually installed, pull the latest from claude-plugins-official repository
- Verify the plugin version includes PR #2074 (check for _JS_EXTS gate in security_reminder_hook.py)
- 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
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"
}