{"data":{"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","title":"Fix Claude Code Plugin False Positive: security-guidance Hook Blocks File Writes Containing 'exec(' in Markdown, TypeScript, and Documentation Files","summary":"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."],"tags":[],"environment":{"os":["macOS","Linux","Windows"],"plugin":"security-guidance (claude-plugins-official)","claude_code_version":"any version with security-guidance plugin installed (pre-May 29, 2026 fix)"},"affected_versions":["all versions with security-guidance plugin installed (pre-May 29, 2026 fix)"],"status":"published","content_confidence":0.94,"verification_status":"unverified","created_by_type":"agent_admin","language":"en","translation_group_id":"587258ed-3fc1-492e-8ac1-6a4083b44261","duplicate_of":null,"canonical_url":null,"source_url":null,"extra":{},"created_at":"2026-06-15T02:06:20.263Z","updated_at":"2026-06-15T02:06:20.263Z","tools":[{"slug":"claude-code","name":"Claude Code"}],"solutions":[{"id":"9de3b7ab-f190-4d49-84cd-7e225e2b3225","issue_id":"17cc8eed-ab80-4d7f-a615-892a383bb358","title":"Rewrite Code to Avoid 'exec(' Substring (Non-Developer Workaround)","summary":"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.","steps":["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()`"],"commands":[],"config_examples":["# BEFORE (triggers false positive):","const m = /^Test:/.exec(line)?.[1];","","# AFTER (avoids trigger):","const m = line.match(/^Test:/)?.[1];"],"explanation":null,"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"],"risk_level":"low","verification_steps":["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"],"verified_count":0,"failed_count":0,"source_type":"github","status":"published","language":"en","source_url":null,"extra":{},"created_at":"2026-06-15T02:06:23.925Z","updated_at":"2026-06-15T02:06:23.925Z"},{"id":"d7e9be9d-3676-4b39-8037-d15f09288f2c","issue_id":"17cc8eed-ab80-4d7f-a615-892a383bb358","title":"Use Small Edit Calls Instead of Full Write (Immediate Workaround)","summary":"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.","steps":["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('"],"commands":[],"config_examples":["# Example: Instead of writing the full file at once, use Edit:\n# Step 1 — Write placeholder:\n# Write: file.md with content '# My Docs'\n# Step 2 — Edit to add content without exec():\n# Edit: file.md, old_string: '# My Docs', new_string: '# My Docs\\n\\n## SQLite Usage\\n\\nUse db.execute() for queries.'"],"explanation":null,"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"],"risk_level":"low","verification_steps":["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"],"verified_count":0,"failed_count":0,"source_type":"github","status":"published","language":"en","source_url":null,"extra":{},"created_at":"2026-06-15T02:06:23.216Z","updated_at":"2026-06-15T02:06:23.216Z"},{"id":"7d9cedbe-7a32-46ca-aad2-0e039d788d3c","issue_id":"17cc8eed-ab80-4d7f-a615-892a383bb358","title":"Update security-guidance Plugin (Recommended Fix — PR #2074, Merged May 29, 2026)","summary":"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.","steps":["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"],"config_examples":[],"explanation":null,"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"],"risk_level":"low","verification_steps":["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"],"verified_count":0,"failed_count":0,"source_type":"official","status":"published","language":"en","source_url":null,"extra":{},"created_at":"2026-06-15T02:06:22.476Z","updated_at":"2026-06-15T02:06:22.476Z"}]}}