Skip to content

Commit 82a533f

Browse files
committed
Merge feature/ansi-sanitization: hardened ANSI regex with OSC/CSI/C1 support
- Combined hardened ANSI_ESCAPE_PATTERN from ansi-sanitization branch - Preserved content-block array rendering from content-block-array-rendering branch - Both features now work together: arrays checked first, then ANSI stripped
2 parents 5169d88 + 03c495a commit 82a533f

File tree

2 files changed

+41
-9
lines changed

2 files changed

+41
-9
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,18 @@ def get_template(name):
5353
)
5454

5555
# Regex to strip ANSI escape sequences from terminal output
56-
ANSI_ESCAPE_PATTERN = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
56+
ANSI_ESCAPE_PATTERN = re.compile(
57+
r"""
58+
\x1b(?:\].*?(?:\x07|\x1b\\) # OSC sequences
59+
|\[[0-?]*[ -/]*[@-~] # CSI sequences
60+
|[@-Z\\-_]) # 7-bit C1 control codes
61+
""",
62+
re.VERBOSE | re.DOTALL,
63+
)
5764

5865

5966
def strip_ansi(text):
60-
"""Strip ANSI escape sequences from terminal output.
61-
62-
Args:
63-
text: String that may contain ANSI escape codes.
64-
65-
Returns:
66-
The text with all ANSI escape sequences removed.
67-
"""
67+
"""Strip ANSI escape sequences from terminal output."""
6868
if not text:
6969
return text
7070
return ANSI_ESCAPE_PATTERN.sub("", text)
@@ -2246,4 +2246,5 @@ def on_progress(project_name, session_name, current, total):
22462246

22472247

22482248
def main():
2249+
# print("RUNNING LOCAL VERSION!!")
22492250
cli()

tests/test_generate_html.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
render_edit_tool,
1919
render_bash_tool,
2020
render_content_block,
21+
strip_ansi,
2122
analyze_conversation,
2223
format_tool_stats,
2324
is_tool_result_message,
@@ -284,6 +285,21 @@ def test_tool_result_error(self, snapshot_html):
284285
result = render_content_block(block)
285286
assert result == snapshot_html
286287

288+
def test_tool_result_with_ansi_codes(self):
289+
"""Test that ANSI escape codes are stripped from tool results."""
290+
block = {
291+
"type": "tool_result",
292+
"content": "\x1b[38;2;166;172;186mTests passed:\x1b[0m \x1b[32m✓\x1b[0m All 5 tests passed\n\x1b[1;31mError:\x1b[0m None",
293+
"is_error": False,
294+
}
295+
result = render_content_block(block)
296+
assert "\x1b[" not in result
297+
assert "[38;2;" not in result
298+
assert "[32m" not in result
299+
assert "[0m" not in result
300+
assert "Tests passed:" in result
301+
assert "All 5 tests passed" in result
302+
287303
def test_tool_result_with_commit(self, snapshot_html):
288304
"""Test tool result with git commit output."""
289305
# Need to set the global _github_repo for commit link rendering
@@ -369,6 +385,21 @@ def test_tool_result_content_block_array_with_tool_use(self, snapshot_html):
369385
assert result == snapshot_html
370386

371387

388+
class TestStripAnsi:
389+
"""Tests for ANSI escape stripping."""
390+
391+
def test_strips_csi_sequences(self):
392+
text = "start\x1b[?25hend\x1b[2Jdone"
393+
assert strip_ansi(text) == "startenddone"
394+
395+
def test_strips_osc_sequences(self):
396+
text = "title\x1b]0;My Title\x07end"
397+
assert strip_ansi(text) == "titleend"
398+
399+
def test_strips_osc_st_terminator(self):
400+
text = "name\x1b]0;Title\x1b\\end"
401+
assert strip_ansi(text) == "nameend"
402+
372403
class TestAnalyzeConversation:
373404
"""Tests for conversation analysis."""
374405

0 commit comments

Comments
 (0)