[BUG] Claude code spitting out random characters in user input area while running
Environment
- Platform (select one):
- [X] Anthropic API
- [ ] AWS Bedrock
- [ ] Google Vertex AI
- [ ] Other:
- Claude CLI version: 1.0.6
- Operating System: MacOS 15.5
- Terminal: iTerm2
Bug Description
Spitting out random characters in user input area while running.
Steps to Reproduce
- ask claude to execute something for a python source code in a pipenv based project
- when it comes out with
python -m xxxxxxxask it use pipenv run
Expected Behavior
The user input area is clear
Actual Behavior
You see random escape sequence characters in the screenshot in the user input area
I also observed this in the newest CC. I observe the following behavior:
- the random characters still appear in the terminal after killing claude (but this only happens in the CC-infected terminals)
- they are generated when I move the mouse or press its buttons
I have the same issue after CC have executed some python code
I have the same issue in 1.0.51.
I actually asked Claude desktop about it and this was the response:
This looks like your terminal is receiving and displaying raw mouse tracking escape sequences. This happens when an application enables mouse tracking mode but doesn't properly disable it when exiting.
Here are a few ways to fix this:
-
Quick reset - Try one of these:
resetor
tput reset -
Disable mouse tracking manually:
printf '\033[?1000l' -
If you're using tmux, it might be the culprit. Try:
tmux refresh-client -c -
Check if a program crashed - Some programs (like vim, less, or certain TUI applications) enable mouse tracking. If they crash or exit abnormally, they might leave the terminal in this state.
-
Kitty-specific reset:
- Press
Ctrl+Shift+Deleteto reset the terminal - Or use
kitty @ send-text --match=active '\x1b[?1000l'
- Press
To prevent this in the future:
- Make sure programs exit cleanly (don't force-kill TUI applications)
- Update your programs that use mouse input
- Consider adding this to your shell config (e.g.,
~/.bashrcor~/.zshrc):# Reset mouse tracking on each prompt PROMPT_COMMAND='printf "\033[?1000l"'
The escape sequences you're seeing (like M35;261;21M35;261;20) are the raw mouse position data that should normally be hidden and processed by applications.
When this happens, even when I exit claude the terminal gets littered as I move the mouse. So I exited claude and tried option #2 and it stopped the behavior - until I loaded claude again.
For now I've disabled auto updating claude but I'd really like to continue getting updates, so I hope this gets addressed soon.
This is very annoying, experience it here as well.
I disabled claude code extension in vscode and it seems to have resolved the issue.
This happens in Fedora 42 as well, I've tried many terminals - ptyxis, gnome-terminal, alacritty, wezterm, ghostty, rio. In Ptyxis at least I can hit Ctrl-C and clear them out, some terminals Ctrl-C and D enter junk codes too and I can't escape at all and have to just kill the terminal.
It doesn't happen right away in a fresh terminal with a fresh claude launch, but I can 100% reproduce it with certain sessions. So I've attached that session here (removed the l from jsonl cause github didn't like it)
Terminal codes were in the tool output for me in the session file. Through some work with claude I was able to build this script that fixed it for me:
#!/usr/bin/env python3
"""
Claude Session Escape Sequence Cleaner
Removes terminal escape sequences from Claude session files that can cause
mouse tracking issues and other terminal state problems.
Usage:
claude-session-cleaner file.jsonl # Clean single file
claude-session-cleaner /path/to/sessions/ # Clean all .jsonl files in folder
claude-session-cleaner /path/to/sessions/ --recursive # Clean recursively
"""
import argparse
import os
import re
import sys
from pathlib import Path
from typing import List, Tuple
def clean_escape_sequences(text: str) -> str:
"""Remove all terminal escape sequences including mouse tracking queries."""
# All possible escape sequence patterns
patterns = [
# Standard ANSI sequences
r"\\u001b\[[\d;]*m", # Color codes
r"\\u001b\[[\d;]*[HJKABCDEFGPST]", # Cursor movement, clear screen, etc.
# Private mode sequences (the dangerous ones for mouse tracking)
r"\\u001b\[\?[\d]+[hl]", # Private mode set/reset (like ?1049l, ?1000h)
r"\\u001b\[\?[\d]+\$[p]", # Private mode queries (like ?2048$p)
# Cursor position and other query sequences
r"\\u001b\[>[\d]*[a-zA-Z]", # Device status queries (like >1u)
r"\\u001b\[[\d;]*[nR]", # Position reports
# Window title sequences
r"\\u001b\][0-2];[^\\u001b]*\\u001b\\\\", # OSC sequences
# Literal escape sequences in Python strings
r"\\\\033\[[\d;]*m",
r"\\\\033\[[\d;]*[HJKABCDEFGPST]",
r"\\\\033\[\?[\d]+[hl]",
r"\\\\033\[\?[\d]+\$[p]",
r"\\\\033\[>[\d]*[a-zA-Z]",
r"\\\\033\[[\d;]*[nR]",
r"\\\\033\][0-2];[^\\\\033]*\\\\033\\\\\\\\",
# Additional color sequences that might be missed
r"\\u001b\[[\d;]*;[\d;]*;[\d;]*;[\d;]*;[\d;]*m", # Extended color sequences
# Catch-all for any remaining sequences
r"\\u001b\[[^a-zA-Z]*[a-zA-Z]",
r"\\\\033\[[^a-zA-Z]*[a-zA-Z]",
]
cleaned_text = text
for pattern in patterns:
cleaned_text = re.sub(pattern, "", cleaned_text)
return cleaned_text
def count_escape_sequences(text: str) -> int:
"""Count escape sequences in text."""
escape_patterns = [
r"\\u001b\[[^a-zA-Z]*[a-zA-Z]",
r"\\\\033\[[^a-zA-Z]*[a-zA-Z]",
]
count = 0
for pattern in escape_patterns:
count += len(re.findall(pattern, text))
return count
def clean_file(file_path: Path, backup: bool = True) -> Tuple[int, int]:
"""
Clean a single session file.
Returns (sequences_before, sequences_after)
"""
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
before_count = count_escape_sequences(content)
if before_count == 0:
return 0, 0
# Create backup if requested
if backup:
backup_path = file_path.with_suffix(f"{file_path.suffix}.backup")
backup_path.write_text(content, encoding="utf-8")
cleaned_content = clean_escape_sequences(content)
after_count = count_escape_sequences(cleaned_content)
# Write cleaned content back
with open(file_path, "w", encoding="utf-8") as f:
f.write(cleaned_content)
return before_count, after_count
except Exception as e:
print(f"ā Error processing {file_path}: {e}", file=sys.stderr)
return 0, 0
def find_session_files(path: Path, recursive: bool = False) -> List[Path]:
"""Find all .jsonl session files in path."""
if path.is_file():
return [path] if path.suffix == ".jsonl" else []
pattern = "**/*.jsonl" if recursive else "*.jsonl"
return list(path.glob(pattern))
def main():
parser = argparse.ArgumentParser(
description="Clean escape sequences from Claude session files",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__.split("Usage:")[1] if "Usage:" in __doc__ else "",
)
parser.add_argument(
"path",
type=Path,
help="File or directory path to clean"
)
parser.add_argument(
"--recursive",
"-r",
action="store_true",
help="Recursively process subdirectories"
)
parser.add_argument(
"--no-backup",
action="store_true",
help="Don't create backup files"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be cleaned without modifying files"
)
args = parser.parse_args()
if not args.path.exists():
print(f"ā Path does not exist: {args.path}", file=sys.stderr)
sys.exit(1)
# Find session files
session_files = find_session_files(args.path, args.recursive)
if not session_files:
print(f"No .jsonl files found in {args.path}")
sys.exit(0)
print(f"š Found {len(session_files)} session file(s) to process")
total_removed = 0
files_cleaned = 0
for file_path in session_files:
if args.dry_run:
try:
content = file_path.read_text(encoding="utf-8")
before_count = count_escape_sequences(content)
if before_count > 0:
print(f"š {file_path.name}: {before_count} escape sequences (dry run)")
files_cleaned += 1
total_removed += before_count
except Exception as e:
print(f"ā Error reading {file_path}: {e}", file=sys.stderr)
else:
before_count, after_count = clean_file(
file_path, backup=not args.no_backup
)
if before_count > 0:
removed = before_count - after_count
print(f"ā
{file_path.name}: removed {removed} escape sequences")
files_cleaned += 1
total_removed += removed
else:
print(f"ā {file_path.name}: already clean")
if args.dry_run:
print(f"\nš Dry run complete: {total_removed} escape sequences in {files_cleaned} files")
else:
print(f"\nš Cleaned {files_cleaned} files, removed {total_removed} escape sequences total")
if not args.no_backup and files_cleaned > 0:
print("š¾ Backup files created with .backup extension")
if __name__ == "__main__":
main()