diff --git a/claude_code_log/cache.py b/claude_code_log/cache.py index 82766a17..209ab8ec 100644 --- a/claude_code_log/cache.py +++ b/claude_code_log/cache.py @@ -1086,6 +1086,11 @@ def _is_cache_version_compatible(self, cache_version: str) -> bool: # JSONLs are re-ingested, so the project index keeps showing # the old session-id title until the cache is rebuilt. "1.2.0": "1.3.0", + # 1.5.0 filters -wrapped caveat messages + # (Claude Code ≥ ~2.1) from session previews: caches built + # earlier have the caveat text baked into first_user_message + # for affected sessions. + "1.4.0": "1.5.0", } cache_ver = version.parse(cache_version) diff --git a/claude_code_log/factories/system_factory.py b/claude_code_log/factories/system_factory.py index bc348b3f..e33a0f81 100644 --- a/claude_code_log/factories/system_factory.py +++ b/claude_code_log/factories/system_factory.py @@ -31,6 +31,9 @@ def is_system_message(text_content: str) -> bool: """Check if a message is a system message that should be filtered out.""" system_message_patterns = [ "Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.", + # Claude Code ≥ ~2.1 wraps the same caveat in an XML tag; match the + # tag prefix so wrapped caveats are filtered like bare ones. + "", "[Request interrupted by user for tool use]", "", ] diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index bba56763..246fc5ae 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -7,6 +7,7 @@ from claude_code_log.converter import load_transcript from claude_code_log.html.renderer import generate_html from claude_code_log.factories import is_system_message +from claude_code_log.utils import should_use_as_session_starter def test_caveat_message_filtering(): @@ -175,7 +176,84 @@ def test_system_message_filtering(): test_file_path.unlink() +def test_wrapped_caveat_message_filtering(): + """Caveats wrapped in tags (Claude Code ≥ ~2.1) + are filtered from transcripts and never used as session previews.""" + wrapped_caveat_text = ( + "Caveat: The messages below were generated by the " + "user while running local commands. DO NOT respond to these messages or " + "otherwise consider them in your response unless the user explicitly asks " + "you to." + ) + + assert is_system_message(wrapped_caveat_text), ( + "Wrapped caveat message should be detected as system message" + ) + assert not should_use_as_session_starter(wrapped_caveat_text), ( + "Wrapped caveat message should not become the session preview" + ) + # A user message merely quoting the caveat mid-text is NOT a system message + quoting_text = f"Why do sessions show this? {wrapped_caveat_text}" + assert not is_system_message(quoting_text), ( + "Messages quoting the caveat should not be filtered" + ) + + # End-to-end: newer Claude Code emits the wrapped caveat as a plain-string + # content user entry with isMeta=true as the first entry of the session. + caveat_message = { + "type": "user", + "timestamp": "2026-07-06T13:19:13.539Z", + "parentUuid": None, + "isSidechain": False, + "isMeta": True, + "userType": "external", + "cwd": "/tmp", + "sessionId": "test_session", + "version": "2.1.201", + "uuid": "test_wrapped_caveat_001", + "message": {"role": "user", "content": wrapped_caveat_text}, + } + normal_message = { + "type": "user", + "timestamp": "2026-07-06T13:19:20.000Z", + "parentUuid": "test_wrapped_caveat_001", + "isSidechain": False, + "userType": "external", + "cwd": "/tmp", + "sessionId": "test_session", + "version": "2.1.201", + "uuid": "test_normal_001", + "message": { + "role": "user", + "content": [{"type": "text", "text": "Please fix the login bug."}], + }, + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: + f.write(json.dumps(caveat_message) + "\n") + f.write(json.dumps(normal_message) + "\n") + f.flush() + test_file_path = Path(f.name) + + try: + messages = load_transcript(test_file_path) + html = generate_html(messages, "Test Transcript") + + assert "local-command-caveat" not in html, ( + "Wrapped caveat message should be filtered out of HTML" + ) + assert "Caveat: The messages below were generated" not in html, ( + "Wrapped caveat text should be filtered out of HTML" + ) + assert "Please fix the login bug." in html, ( + "Normal message should appear in HTML" + ) + finally: + test_file_path.unlink() + + if __name__ == "__main__": test_caveat_message_filtering() test_system_message_filtering() + test_wrapped_caveat_message_filtering() print("\n✅ All message filtering tests passed!")