claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

[BUG] Claude code spitting out random characters in user input area while running

Open somasays opened this issue 8 months ago • 10 comments

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

Image

Spitting out random characters in user input area while running.

Steps to Reproduce

  1. ask claude to execute something for a python source code in a pipenv based project
  2. when it comes out with python -m xxxxxxx ask 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

somasays avatar Jun 02 '25 15:06 somasays

Image issue persists > claude --version > 1.0.10 (Claude Code)

somasays avatar Jun 04 '25 05:06 somasays

I also observed this in the newest CC. I observe the following behavior:

  1. the random characters still appear in the terminal after killing claude (but this only happens in the CC-infected terminals)
  2. they are generated when I move the mouse or press its buttons

sygi avatar Jun 25 '25 10:06 sygi

I have the same issue after CC have executed some python code

FBakkensen avatar Jun 27 '25 13:06 FBakkensen

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:

  1. Quick reset - Try one of these:

    reset
    

    or

    tput reset
    
  2. Disable mouse tracking manually:

    printf '\033[?1000l'
    
  3. If you're using tmux, it might be the culprit. Try:

    tmux refresh-client -c
    
  4. 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.

  5. Kitty-specific reset:

    • Press Ctrl+Shift+Delete to reset the terminal
    • Or use kitty @ send-text --match=active '\x1b[?1000l'

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., ~/.bashrc or ~/.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.

nerdo avatar Jul 13 '25 14:07 nerdo

This is very annoying, experience it here as well.

Image

Theaxiom avatar Aug 04 '25 03:08 Theaxiom

I disabled claude code extension in vscode and it seems to have resolved the issue.

Theaxiom avatar Aug 04 '25 04:08 Theaxiom

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)

Image

572bf4e0-816b-4743-990d-73d78bf46b66.json

dhitchcock avatar Sep 03 '25 16:09 dhitchcock

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()

zach-source avatar Sep 08 '25 03:09 zach-source