From ab277abe7cd53f3a214c018c942d3e874d42ee16 Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Fri, 3 Apr 2026 23:34:17 +0900 Subject: [PATCH] feat(plugin): conditional prompt injection for MCP/standalone (#1213) Branch build_system_prompt by runtime mode: MCP mode injects parse_mode + dispatch enforcement rules, standalone mode uses hook-based instructions without referencing MCP-only tools. - Import is_mcp_available from runtime_mode - Add _BASE_RULES_STANDALONE for non-MCP environments - dispatchEnforcement section only in MCP mode - 7 new tests: standalone (5) + explicit MCP (2) - All 37 prompt_injection + runtime tests pass --- .../hooks/lib/prompt_injection.py | 14 +++++- .../tests/test_prompt_injection.py | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/claude-code-plugin/hooks/lib/prompt_injection.py b/packages/claude-code-plugin/hooks/lib/prompt_injection.py index 61f4e1cf..8f2a7090 100644 --- a/packages/claude-code-plugin/hooks/lib/prompt_injection.py +++ b/packages/claude-code-plugin/hooks/lib/prompt_injection.py @@ -6,6 +6,8 @@ """ from typing import Any, Dict +from runtime_mode import is_mcp_available + CHAR_LIMIT = 2000 # --- Section builders (each returns a string or empty) --- @@ -16,6 +18,12 @@ "Follow TDD cycle: RED (failing test) -> GREEN (minimal code) -> REFACTOR." ) +_BASE_RULES_STANDALONE = ( + "[CodingBuddy] When user message starts with PLAN/ACT/EVAL/AUTO, " + "follow the mode instructions provided by the mode detection hook. " + "Follow TDD cycle: RED (failing test) -> GREEN (minimal code) -> REFACTOR." +) + _DISPATCH_ENFORCEMENT = ( "[CodingBuddy] When parse_mode returns dispatch=\"auto\", " "you MUST dispatch ALL recommended specialist agents. " @@ -102,12 +110,14 @@ def build_system_prompt(self, config: Dict[str, Any], cwd: str) -> str: if not isinstance(sections_cfg, dict): sections_cfg = {} + mcp_mode = is_mcp_available() + parts: list[str] = [] if sections_cfg.get("baseRules", True): - parts.append(_BASE_RULES) + parts.append(_BASE_RULES if mcp_mode else _BASE_RULES_STANDALONE) - if sections_cfg.get("dispatchEnforcement", True): + if mcp_mode and sections_cfg.get("dispatchEnforcement", True): parts.append(_DISPATCH_ENFORCEMENT) if sections_cfg.get("qualityGates", True): diff --git a/packages/claude-code-plugin/tests/test_prompt_injection.py b/packages/claude-code-plugin/tests/test_prompt_injection.py index a9c368af..e5793dd5 100644 --- a/packages/claude-code-plugin/tests/test_prompt_injection.py +++ b/packages/claude-code-plugin/tests/test_prompt_injection.py @@ -1,6 +1,7 @@ """Tests for PromptInjector — system prompt injection module (#828).""" import os import sys +from unittest import mock import pytest # Ensure hooks/lib is on path @@ -12,6 +13,13 @@ from prompt_injection import PromptInjector +@pytest.fixture(autouse=True) +def _mock_mcp_available(): + """Default: mock is_mcp_available to return True (MCP mode) for all existing tests.""" + with mock.patch("prompt_injection.is_mcp_available", return_value=True): + yield + + @pytest.fixture def injector(): return PromptInjector() @@ -225,3 +233,44 @@ def test_returns_empty_when_no_config(self, injector): def test_returns_empty_when_none_config(self, injector): result = injector.build_system_prompt({"promptInjection": None}, "/tmp") assert result == "" + + +class TestStandaloneMode: + """Tests for standalone mode (MCP not available).""" + + @pytest.fixture(autouse=True) + def _standalone(self): + with mock.patch("prompt_injection.is_mcp_available", return_value=False): + yield + + def test_standalone_contains_mode_hook_instruction(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert "follow the mode instructions provided by the mode detection hook" in result + + def test_standalone_no_parse_mode_must(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert "MUST call parse_mode" not in result + + def test_standalone_no_dispatch_enforcement(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert "dispatch" not in result.lower() or "dispatch=\"auto\"" not in result + + def test_standalone_still_has_tdd(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert "TDD" in result + + def test_standalone_still_has_quality_gates(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert "lint" in result.lower() or "test" in result.lower() + + +class TestMcpMode: + """Explicit MCP mode tests — verify original behavior is preserved.""" + + def test_mcp_contains_parse_mode_must(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert "MUST call parse_mode FIRST" in result + + def test_mcp_contains_dispatch_enforcement(self, injector, base_config): + result = injector.build_system_prompt(base_config, "/tmp") + assert 'dispatch="auto"' in result