Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions claude_code_log/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <local-command-caveat>-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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see 1.5.0 is brewing ;-)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realised we should have done it ages ago 😅 should probably cut a release once a week after every few PRs!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cboos also wanted to email you about publishing to PyPI, but the address on your Github profile bounced my email. Mine is working, please drop me a message!

}

cache_ver = version.parse(cache_version)
Expand Down
3 changes: 3 additions & 0 deletions claude_code_log/factories/system_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"<local-command-caveat>",
"[Request interrupted by user for tool use]",
"<local-command-stdout>",
]
Expand Down
78 changes: 78 additions & 0 deletions test/test_message_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -175,7 +176,84 @@ def test_system_message_filtering():
test_file_path.unlink()


def test_wrapped_caveat_message_filtering():
"""Caveats wrapped in <local-command-caveat> tags (Claude Code ≥ ~2.1)
are filtered from transcripts and never used as session previews."""
wrapped_caveat_text = (
"<local-command-caveat>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.</local-command-caveat>"
)

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!")
Loading