fix(file_tools): strip ANSI escape codes from write_file and patch content (#2532)

Models occasionally copy ANSI escape sequences from terminal output
or display formatting into file content, breaking shebangs and
injecting binary characters into scripts.

Strip ANSI codes (CSI, OSC, simple escapes) from:
- write_file content
- patch old_string, new_string, and V4A patch content

The check is fast (skips entirely if no ESC byte present).

Reported by Andi Jaeger.
This commit is contained in:
Teknium 2026-03-22 11:17:06 -07:00 committed by GitHub
parent cd2280d1a3
commit fa6f069577
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,6 +5,7 @@ import errno
import json import json
import logging import logging
import os import os
import re
import threading import threading
from typing import Optional from typing import Optional
from tools.file_operations import ShellFileOperations from tools.file_operations import ShellFileOperations
@ -12,6 +13,18 @@ from agent.redact import redact_sensitive_text
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Regex to match ANSI escape sequences (CSI codes, OSC codes, simple escapes).
# Models occasionally copy these from terminal output into file content.
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][A-B012]|\x1b[=>]")
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape sequences from text destined for file writes."""
if not text or "\x1b" not in text:
return text
return _ANSI_ESCAPE_RE.sub("", text)
_EXPECTED_WRITE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS} _EXPECTED_WRITE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS}
@ -288,6 +301,7 @@ def notify_other_tool_call(task_id: str = "default"):
def write_file_tool(path: str, content: str, task_id: str = "default") -> str: def write_file_tool(path: str, content: str, task_id: str = "default") -> str:
"""Write content to a file.""" """Write content to a file."""
try: try:
content = _strip_ansi(content)
file_ops = _get_file_ops(task_id) file_ops = _get_file_ops(task_id)
result = file_ops.write_file(path, content) result = file_ops.write_file(path, content)
return json.dumps(result.to_dict(), ensure_ascii=False) return json.dumps(result.to_dict(), ensure_ascii=False)
@ -311,10 +325,13 @@ def patch_tool(mode: str = "replace", path: str = None, old_string: str = None,
return json.dumps({"error": "path required"}) return json.dumps({"error": "path required"})
if old_string is None or new_string is None: if old_string is None or new_string is None:
return json.dumps({"error": "old_string and new_string required"}) return json.dumps({"error": "old_string and new_string required"})
old_string = _strip_ansi(old_string)
new_string = _strip_ansi(new_string)
result = file_ops.patch_replace(path, old_string, new_string, replace_all) result = file_ops.patch_replace(path, old_string, new_string, replace_all)
elif mode == "patch": elif mode == "patch":
if not patch: if not patch:
return json.dumps({"error": "patch content required"}) return json.dumps({"error": "patch content required"})
patch = _strip_ansi(patch)
result = file_ops.patch_v4a(patch) result = file_ops.patch_v4a(patch)
else: else:
return json.dumps({"error": f"Unknown mode: {mode}"}) return json.dumps({"error": f"Unknown mode: {mode}"})