[BUG] Korean input characters disappear on iOS mobile SSH

Status Closed — not planned
Reported on v2.0.76
Maintainer reply None cached
Activity 13 comments · opened Dec 29, 2025 · closed Mar 27, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

When connecting via Termius on iOS and running Claude Code over SSH,
Korean input characters disappear at the moment a consonant and vowel are combined.

  • Consonant only: works
  • Vowel only: works
  • Consonant + vowel (e.g. + ): character disappears

⚠️ In the same Termius environment:

  • ✅ Android: works correctly
  • ❌ iOS: issue occurs

Other CLI/TUI tools such as Gemini CLI and Codex CLI work correctly even on iOS.

Screen Recording

https://github.com/user-attachments/assets/5d2d9796-7ecd-435e-8eb1-0abf334a9f7c

Environment

  • Client
  • ❌ iOS + Termius (issue occurs)
  • ✅ Android + Termius (works)
  • Server
  • MacOS (SSH)
  • Affected application
  • Claude Code
  • Not affected
  • MacOS Terminal / iTerm2
  • Gemini CLI
  • Codex CLI

Actual Behavior

  • During Korean jamo composition, the input buffer deletes the character and the final composed character is not rendered

Technical Analysis (Important)

iOS Termius input behavior

When typing the Korean character on iOS Termius, the actual byte stream received by the server (logged in raw TTY mode) is:

e3 84 b1 7f ea b0 80

Interpretation:

  • e3 84 b1 → (U+3131)
  • 7f → DEL (backspace)
  • ea b0 80 → (U+AC00)

This means iOS Termius does not send IME preedit composition to the server.
Instead, it emulates composition using a delete + re-insert sequence:
→ delete → .

Claude Code appears to mis-handle this sequence:

  1. Receives → inserts into buffer
  2. Receives DEL → deletes
  3. Receives final character → insertion is dropped due to internal input state or cursor calculation

As a result, no character remains on screen.

Why this does not occur on Android

On Android Termius:

  • Korean IME composition is completed on the client side
  • SSH sends only the final composed UTF-8 characters

This matches the behavior of macOS terminals, so Claude Code never receives the delete + re-insert sequence and works correctly.

Why this does not occur on macOS

  • macOS Terminal / iTerm2 complete IME composition locally
  • SSH transmits only finalized UTF-8 characters
  • Claude Code never encounters the problematic delete + insert sequence

Additional Notes

  • Changing Termius backspace transmission from DEL (0x7f) to Ctrl-H (0x08):
  • Does not fully resolve the issue in Claude Code
  • Can introduce input issues in other terminal applications

Conclusion

  • This is not a locale, UTF-8, or font configuration issue
  • The issue reproduces only on iOS, while Android and macOS work correctly
  • Claude Code’s raw TUI input handling does not correctly process

the delete + re-insert based IME composition sequence used by iOS Termius

  • To support iOS mobile SSH environments, Claude Code needs:
  • Safe handling of IME composition or delete + insert sequences
  • Or an alternative line-based / IME-safe input mode

Impact

  • Korean (and likely other CJK) input is effectively unusable in Claude Code on iOS
  • Copy/paste is the only practical workaround

What Should Happen?

Expected Behavior

  • Korean input should be composed and rendered correctly (, , , etc.)

Error Messages/Logs

Steps to Reproduce

Steps to Reproduce (100% on iOS)

  1. Connect to a Linux server via SSH using Termius on iOS
  2. Run claude code
  3. Try to input Korean text in the prompt
  4. Example: input
  • Type → briefly appears
  • Type → the character disappears

Test for checking text input

$ cat > /tmp/ttylog.py <<'PY'
import sys, tty, termios

fd = sys.stdin.fileno()
if not sys.stdin.isatty():
    print("stdin is not a TTY. Run this in an interactive SSH shell.", file=sys.stderr)
    sys.exit(1)

old = termios.tcgetattr(fd)
try:
    tty.setraw(fd)
    print("RAW tty logger started. (Press Ctrl-\\ to quit)")
    while True:
        b = sys.stdin.buffer.read(1)
        if not b:
            break
        sys.stdout.write(b.hex() + " ")
        sys.stdout.flush()
finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, old)
PY

$ python3 /tmp/ttylog.py

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.0.76

Platform

Anthropic API

Operating System

Other

Terminal/Shell

Other

Additional Information

_No response_

View original on GitHub ↗

13 Comments

wplong11 · 8 months ago

Analysis Report: Why Codex CLI Does Not Experience Korean Input Issues on iOS Termius

Executive Summary

This report analyzes how Codex CLI successfully handles the "delete + re-insert" Korean input sequence from iOS Termius, while Claude Code fails to do so. The analysis is intended to serve as a reference for improving Claude Code's input handling.

---

Problem Context

iOS Termius Korean Input Behavior

When typing Korean character (ga) on iOS Termius, the following byte sequence is sent to the server:

e3 84 b1  →  7f  →  ea b0 80
   ㄱ        DEL       가
(U+3131)           (U+AC00)

Interpretation:

  1. Initial consonant is sent
  2. DEL (backspace) is sent to remove the incomplete character
  3. Composed character is sent

This "delete + re-insert" pattern is how iOS Termius implements IME composition over SSH, unlike Android Termius or macOS terminals which complete composition locally.

---

Why Codex CLI Works: Three Key Design Principles

1. Grapheme Cluster-Based Backspace Handling

File: codex-rs/tui/src/bottom_pane/textarea.rs:454-466, 824-848

// textarea.rs:454-466
pub fn delete_backward(&mut self, n: usize) {
    if n == 0 || self.cursor_pos == 0 {
        return;
    }
    let mut target = self.cursor_pos;
    for _ in 0..n {
        target = self.prev_atomic_boundary(target);  // KEY: uses grapheme boundary
        if target == 0 {
            break;
        }
    }
    self.replace_range(target..self.cursor_pos, "");
}
// textarea.rs:824-848
fn prev_atomic_boundary(&self, pos: usize) -> usize {
    if pos == 0 {
        return 0;
    }
    // Handle elements (image placeholders, etc.) first
    if let Some(idx) = self
        .elements
        .iter()
        .position(|e| pos > e.range.start && pos <= e.range.end)
    {
        return self.elements[idx].range.start;
    }
    // Use unicode_segmentation to find previous grapheme boundary
    let mut gc = unicode_segmentation::GraphemeCursor::new(pos, self.text.len(), false);
    match gc.prev_boundary(&self.text, 0) {
        Ok(Some(b)) => {
            if let Some(idx) = self.find_element_containing(b) {
                self.elements[idx].range.start
            } else {
                b
            }
        }
        Ok(None) => 0,
        Err(_) => pos.saturating_sub(1),
    }
}

Why This Matters:

  • Backspace operates on grapheme clusters, not bytes or code points
  • Uses unicode-segmentation crate's GraphemeCursor
  • When deleting Korean (3 bytes in UTF-8), it correctly removes the entire character

---

2. Dedicated Fast Path for Non-ASCII Characters (IME-Safe Path)

File: codex-rs/tui/src/bottom_pane/chat_composer.rs:1349-1354, 676-695

// chat_composer.rs:1349-1354
if !has_ctrl_or_alt {
    // Non-ASCII characters (e.g., from IMEs) can arrive in quick bursts and be
    // misclassified by paste heuristics. Flush any active burst buffer and insert
    // non-ASCII characters directly.
    if !ch.is_ascii() {
        return self.handle_non_ascii_char(input);  // Korean input goes here
    }
    // ... paste burst logic (only applies to ASCII) ...
}
// chat_composer.rs:676-695
#[inline]
fn handle_non_ascii_char(&mut self, input: KeyEvent) -> (InputResult, bool) {
    if let KeyEvent {
        code: KeyCode::Char(ch),
        ..
    } = input
    {
        let now = Instant::now();
        if self.paste_burst.try_append_char_if_active(ch, now) {
            return (InputResult::None, true);
        }
    }
    if let Some(pasted) = self.paste_burst.flush_before_modified_input() {
        self.handle_paste(pasted);
    }
    self.textarea.input(input);  // Insert directly without paste heuristics
    let text_after = self.textarea.text();
    self.pending_pastes
        .retain(|(placeholder, _)| text_after.contains(placeholder));
    (InputResult::None, true)
}

Why This Matters:

  • Non-ASCII characters (Korean, Japanese, Chinese, etc.) bypass paste burst detection
  • Prevents rapid IME input from being misclassified as "paste"
  • Each character is processed independently and correctly

---

3. Byte Offset Cursor with UTF-8 Boundary Validation

File: codex-rs/tui/src/bottom_pane/textarea.rs:30-37

pub struct TextArea {
    text: String,                    // UTF-8 text storage
    cursor_pos: usize,               // Byte offset (NOT character count!)
    wrap_cache: RefCell<...>,        // Cached wrapped line ranges
    preferred_col: Option<usize>,    // For vertical navigation
    elements: Vec<TextElement>,      // Protected ranges (images, placeholders)
    kill_buffer: String,             // For Ctrl+Y yank
}

Why This Matters:

  • Cursor position is managed as byte offset
  • All text operations validate UTF-8 character boundaries
  • Prevents corruption from splitting multi-byte characters

---

Processing Flow: iOS Termius Korean Input Sequence

When iOS Termius sends the sequence for :

| Step | Input | Codex Processing | Buffer State |
|------|-------|------------------|--------------|
| 1 | (U+3131) | !ch.is_ascii()handle_non_ascii_char() → insert | |
| 2 | DEL (0x7f) | delete_backward(1)prev_atomic_boundary() → find grapheme boundary → delete | ` (empty) |
| 3 |
(U+AC00) | !ch.is_ascii()handle_non_ascii_char() → insert | 가` ✓ |

Key Insight: Each step operates independently and correctly, resulting in the final composed character being displayed properly.

---

Suspected Differences with Claude Code

| Aspect | Codex CLI | Claude Code (Suspected) |
|--------|-----------|------------------------|
| Backspace unit | Grapheme Cluster | Byte or code point? |
| Non-ASCII handling | Dedicated fast path | Same path as ASCII? |
| Unicode library | unicode-segmentation | Not used or different approach? |
| Cursor management | Byte offset + boundary validation | Possible cursor calculation errors? |

---

Key Code References for Claude Code Improvement

Dependencies (Cargo.toml)

[dependencies]
unicode-segmentation = { workspace = true }
unicode-width = { workspace = true }

Critical Files and Line Numbers

  1. codex-rs/tui/src/bottom_pane/textarea.rs
  • delete_backward(): lines 454-466
  • prev_atomic_boundary(): lines 824-848
  • next_atomic_boundary(): lines 850-874
  1. codex-rs/tui/src/bottom_pane/chat_composer.rs
  • Non-ASCII branch: lines 1349-1354
  • handle_non_ascii_char(): lines 676-695
  1. codex-rs/tui/src/bottom_pane/paste_burst.rs
  • Paste detection thresholds and logic

---

Recommended Improvements for Claude Code

1. Implement Grapheme Cluster-Based Deletion

// Recommended approach using unicode-segmentation
use unicode_segmentation::GraphemeCursor;

fn delete_backward_grapheme(text: &mut String, cursor_pos: &mut usize) {
    if *cursor_pos == 0 {
        return;
    }
    
    let mut gc = GraphemeCursor::new(*cursor_pos, text.len(), false);
    if let Ok(Some(prev_boundary)) = gc.prev_boundary(text, 0) {
        text.replace_range(prev_boundary..*cursor_pos, "");
        *cursor_pos = prev_boundary;
    }
}

2. Separate Non-ASCII Character Path

// Recommended approach for IME-safe input handling
fn handle_char_input(ch: char, /* ... */) {
    if !ch.is_ascii() {
        // Bypass paste detection for IME input
        insert_char_directly(ch);
        return;
    }
    
    // Normal ASCII handling with paste detection
    handle_with_paste_detection(ch);
}

3. Ensure UTF-8 Boundary Safety

// Always validate cursor position is on valid UTF-8 boundary
fn safe_cursor_pos(text: &str, pos: usize) -> usize {
    let safe_pos = pos.min(text.len());
    if safe_pos < text.len() && !text.is_char_boundary(safe_pos) {
        text.char_indices()
            .map(|(i, _)| i)
            .take_while(|&i| i <= pos)
            .last()
            .unwrap_or(0)
    } else {
        safe_pos
    }
}

---

Conclusion

Codex CLI successfully handles the iOS Termius Korean input sequence because:

  1. Grapheme-aware backspace ensures complete characters are deleted, not partial bytes
  2. Non-ASCII fast path prevents IME input from being misclassified by paste heuristics
  3. Proper UTF-8 boundary handling prevents buffer corruption

Claude Code likely fails because it lacks one or more of these safeguards, causing the final composed character to be lost during the "delete + re-insert" sequence.

---

Repository Reference

  • Repository: Codex CLI (forked)
  • Key files analyzed:
  • codex-rs/tui/src/bottom_pane/textarea.rs
  • codex-rs/tui/src/bottom_pane/chat_composer.rs
  • codex-rs/tui/src/bottom_pane/paste_burst.rs
  • codex-rs/tui/Cargo.toml
wplong11 · 8 months ago

Analysis Report: Why gemini-cli Does Not Experience Korean Input Issues on iOS Termius

Executive Summary

This report analyzes how gemini-cli handles the "delete + reinsert" sequence sent by iOS Termius during Korean (Hangul) character composition, and why it does not experience the character loss bug that affects Claude Code.

---

Problem Context

iOS Termius Korean Input Behavior

When typing the Korean character (ga) on iOS Termius, the following byte sequence is sent to the server:

e3 84 b1  7f       ea b0 80
   ㄱ     DEL         가

Interpretation:

  • e3 84 b1 (U+3131, initial consonant)
  • 7f → DEL (backspace)
  • ea b0 80 (U+AC00, composed character)

iOS Termius implements Korean composition using a delete + reinsert pattern rather than standard IME preedit sequences.

Why Claude Code Fails

Claude Code appears to mishandle this sequence:

  1. received → inserted into buffer
  2. DEL received → deleted
  3. received → insertion fails or is dropped due to internal state or cursor calculation issues

---

Key Finding: gemini-cli's Dual-Level DEL Handling

gemini-cli handles DEL (0x7f) at two independent levels, ensuring robust handling regardless of how the input arrives.

---

Level 1: Key Event Parsing (KeypressContext.tsx:464-467)

else if (ch === '\b' || ch === '\x7f') {
  // backspace or ctrl+h
  name = 'backspace';
  meta = escaped;
}

When DEL arrives as an individual key event, it is recognized as backspace.

---

Level 2: Inline DEL Processing in Insert Function (text-buffer.ts:1947-1961)

const insert = useCallback(
  (ch: string, { paste = false }: { paste?: boolean } = {}): void => {
    // ... path handling code omitted ...
    
    let currentText = '';
    for (const char of toCodePoints(ch)) {
      if (char.codePointAt(0) === 127) {  // DEL (0x7f) detection
        if (currentText.length > 0) {
          dispatch({ type: 'insert', payload: currentText });
          currentText = '';
        }
        dispatch({ type: 'backspace' });  // Inline backspace!
      } else {
        currentText += char;
      }
    }
    if (currentText.length > 0) {
      dispatch({ type: 'insert', payload: currentText });
    }
  },
  [isValidPath, shellModeActive, singleLine],
);

This is the critical mechanism. The insert function iterates through the input string by code points and immediately dispatches a backspace action when encountering DEL.

---

Dual Safety Net in handleKeypress (text-buffer.ts:2237-2247)

// Path 1: DEL arrives as individual key event
else if (
  key.name === 'backspace' ||
  input === '\x7f' ||           // Direct DEL check!
  (key.ctrl && key.name === 'h')
)
  backspace();

// Path 2: DEL arrives within insertable content (paste, etc.)
else if (key.insertable) {
  insert(input, { paste: key.paste });  // DEL handled inside insert()
}

---

iOS Termius Sequence Processing Flow

| Step | Received Data | gemini-cli Action | Result |
|------|--------------|-------------------|--------|
| 1 | (e3 84 b1) | currentText = "ㄱ" | Accumulated in buffer |
| 2 | DEL (7f) | insert("ㄱ") → backspace() | ㄱ inserted then immediately deleted |
| 3 | (ea b0 80) | currentText = "가" | Accumulated in buffer |
| 4 | Loop ends | insert("가") | Final result: "가" displayed |

---

Why This Approach Works

1. Synchronous Transaction Processing

The insert-backspace-insert sequence is processed as a single synchronous transaction within the insert function. No rendering or async state updates intervene.

2. Code Point-Based Parsing

toCodePoints() correctly splits UTF-8 strings by code points, preventing Korean composed characters from being corrupted.

// textUtils.ts:35-64
export function toCodePoints(str: string): string[] {
  // ASCII fast path
  let isAscii = true;
  for (let i = 0; i < str.length; i++) {
    if (str.charCodeAt(i) > 127) {
      isAscii = false;
      break;
    }
  }
  if (isAscii) {
    return str.split('');
  }
  
  // Unicode: use Array.from() for proper surrogate pair handling
  const result = Array.from(str);
  return result;
}

3. Intentional DEL Preservation (textUtils.ts:94-120)

export function stripUnsafeCharacters(str: string): string {
  return toCodePoints(strippedVT)
    .filter((char) => {
      const code = char.codePointAt(0);
      
      // Remove C0 control chars (except CR/LF) that can break display
      if (code >= 0x00 && code <= 0x1f) return false;
      
      // Preserve DEL (0x7f) - it's handled functionally by applyOperations
      // and doesn't cause rendering issues when displayed
      
      return true;
    })
    .join('');
}

DEL (0x7f) is intentionally preserved because it is handled functionally, unlike other control characters that are stripped.

---

Recommended Fix for Claude Code

Root Cause (Hypothesized)

Claude Code likely processes DEL only as a separate key event and does not handle DEL inline within insert operations. This may cause:

  1. Async rendering intervening between DEL processing and subsequent character insertion
  2. Cursor position calculation errors after backspace, causing the next character insertion to fail

Solution 1: Add Inline DEL Processing to Insert Function

function insert(text: string): void {
  let currentText = '';
  for (const char of [...text]) {  // Iterate by code points
    if (char.codePointAt(0) === 0x7f) {
      if (currentText.length > 0) {
        insertText(currentText);
        currentText = '';
      }
      handleBackspace();  // Inline backspace
    } else {
      currentText += char;
    }
  }
  if (currentText.length > 0) {
    insertText(currentText);
  }
}

Solution 2: Ensure Code Point-Based String Processing

// Use code points instead of UTF-16 code units
const codePoints = Array.from(str);  // or [...str]

// For length calculation
function cpLen(str: string): number {
  return Array.from(str).length;
}

// For slicing
function cpSlice(str: string, start: number, end?: number): string {
  return Array.from(str).slice(start, end).join('');
}

Solution 3: Preserve DEL in Input Sanitization

// Do NOT strip DEL (0x7f) as it needs functional handling
function stripUnsafeCharacters(str: string): string {
  return [...str].filter(char => {
    const code = char.codePointAt(0);
    
    // Strip C0 control chars except CR/LF/TAB
    if (code >= 0x00 && code <= 0x1f) {
      return code === 0x0a || code === 0x0d || code === 0x09;
    }
    
    // Preserve DEL (0x7f) for functional handling
    // Preserve all other printable characters
    return true;
  }).join('');
}

---

Reference Files in gemini-cli

| Purpose | File Path | Lines |
|---------|-----------|-------|
| Inline DEL Processing | packages/cli/src/ui/components/shared/text-buffer.ts | 1947-1961 |
| Key Event Parsing | packages/cli/src/ui/contexts/KeypressContext.tsx | 464-467 |
| handleKeypress Handler | packages/cli/src/ui/components/shared/text-buffer.ts | 2237-2247 |
| Code Point Utilities | packages/cli/src/ui/utils/textUtils.ts | 35-74 |
| DEL Preservation Logic | packages/cli/src/ui/utils/textUtils.ts | 94-120 |

---

Conclusion

gemini-cli successfully handles the iOS Termius Korean input sequence through:

  1. Dual-level DEL handling - Both at key event level and within the insert function
  2. Synchronous transaction processing - No async operations between insert-backspace-insert
  3. Code point-based string manipulation - Proper UTF-8/Unicode handling
  4. Intentional DEL preservation - DEL is not stripped but functionally processed

Implementing similar mechanisms in Claude Code should resolve the Korean input character loss issue on iOS Termius.

DreamHouseKSH · 7 months ago

Additional reproduction case

Environment:

  • iPad + Termius (SSH)
  • LXC Container (Ubuntu 24.04, Proxmox)
  • Locale: ko_KR.UTF-8
  • Claude Code version: latest

Confirmed:

  • ❌ Claude Code: Korean input broken
  • ✅ Gemini CLI: Works fine
  • ✅ Codex CLI: Works fine

Same issue on LXC container environment. This affects more users than just macOS servers.

Hoping for a fix soon - Korean input is currently unusable on iOS.

greenheadHQ · 7 months ago

Any updates? @ThariqS

DreamHouseKSH · 7 months ago

Ipad 에서 Termius 앱을 사용하면 여전히 입력이 정상적으로 처리가 안됨.

2026. 1. 20. 오후 10:37, green @.***> 작성: shren207 left a comment (anthropics/claude-code#15705) <https://github.com/anthropics/claude-code/issues/15705#issuecomment-3772932496> Any updates? — Reply to this email directly, view it on GitHub <https://github.com/anthropics/claude-code/issues/15705#issuecomment-3772932496>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AODXLHE556FJR6QDSXMOIUT4HYVS3AVCNFSM6AAAAACQH6AOTOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONZSHEZTENBZGY>. You are receiving this because you commented.
80x24 · 6 months ago

Additional reproduction: a-Shell (iOS)

Environment:

  • iPhone + a-Shell (free iOS terminal app with built-in SSH)
  • SSH to macOS (Darwin 25.2.0)
  • Claude Code latest

Result:
Korean Hangul jamo are displayed decomposed (not composed into syllables).

Example input attempt: "안녕" → displays as "ㅇㅏㄴㄴㅕㅇ"

Same behavior as Termius on iOS. Confirms this is a Claude Code TUI issue, not specific to any particular iOS terminal app.

80x24 · 6 months ago

Correction: a-Shell vs Termius behavior difference

Two distinct failure modes on iOS:

| App | Symptom | Usable? |
|-----|---------|---------|
| Termius | Characters deleted when jamo combine (consonant+vowel → disappears) | No |
| a-Shell | Jamo displayed decomposed (ㅇㅏㄴㄴㅕㅇ instead of 안녕) but input goes through | Barely (readable) |

a-Shell is at least functional for communication, Termius is completely broken for Korean input in Claude Code.

Bae-ChangHyun · 6 months ago

Hi team, this bug makes it very difficult to use the tool on iOS. Is there any ETA on when it might be resolved?

tenderpooh · 6 months ago

I'm a Korean developer who relies on Claude Code daily via iOS terminal apps (Termius, Blink/Mosh, etc.). Korean characters get completely broken during IME composition — characters disappear or get corrupted
as I type, making it impossible to write anything in Korean.

The only workaround right now is copy-pasting from another app, which kills the workflow entirely. This affects every Korean-speaking user on iOS, and there are quite a few of us who'd love to use Claude Code
on the go.

It would mean the world to us if this could get some attention. Thank you!

AlexLee00 · 6 months ago

Additional reproduction: macOS local terminal (mild) + workaround research

Environment:

  • macOS Sequoia 25.3.0, Apple Silicon M3 (MacBook Air)
  • iTerm2 (local, no SSH)
  • Claude Code latest

Findings:

The bug reproduces not only on iOS SSH but also locally on macOS — though the severity differs:

| Setup | Symptom | Severity |
|-------|---------|----------|
| iPad + Termius SSH | Characters deleted on jamo combination | Severe (unusable) |
| macOS + iTerm2 (local) | Occasional character drop during fast IME composition | Mild |

Workarounds tested — all ineffective:

  1. rlwrap — No effect. Claude Code uses its own Ink-based TUI input handler, not readline, so rlwrap cannot intercept keystrokes at the composition stage.
  1. mosh (mobile shell) — No effect. The bug is in the client-side TUI rendering layer (Ink framework's CJK wide-character width calculation), not in the SSH/transport layer.

Root cause hypothesis:
Ink TUI calculates Korean syllables as 1-column wide instead of 2-column (same as other CJK characters). This misaligns the cursor position during IME composition, causing characters to be overwritten or dropped when the composed glyph is committed.

Impact: Affects Korean-speaking users both on iOS and macOS. The iOS experience is completely broken; macOS is usable but unreliable.

Would appreciate any update on the fix timeline. Thank you!

github-actions[bot] · 5 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

wplong11 · 5 months ago

Root Cause Analysis (from leaked source code)

Since the source code isn't publicly available, I analyzed the leaked/decompiled Claude Code source to trace this bug. Sources:

Bug Trace

When iOS Termius sends Korean input , the byte stream is: (e3 84 b1) + DEL(7f) + (ea b0 80).

Step 1 — Tokenizer (src/ink/termio/tokenize.ts):
The tokenizer's ground state only transitions on ESC (0x1B). \x7f (DEL) is not treated as a control character boundary, so the entire ㄱ\x7f가 is emitted as a single text token.

Step 2 — parseKeypress (src/ink/parse-keypress.ts:611):
Since s = 'ㄱ\x7f가' doesn't match s === '\x7f' (length ≠ 1), it falls through all conditions. Result: key.name = '', key.backspace = false.

Step 3 — useTextInput.onInput (src/hooks/useTextInput.ts:442-465) — BUG HERE:

// Fix Issue #1853: Filter DEL characters that interfere with backspace in SSH/tmux
if (!key.backspace && !key.delete && input.includes('\x7f')) {
  const delCount = (input.match(/\x7f/g) || []).length  // 1
  for (let i = 0; i < delCount; i++) {
    currentCursor = currentCursor.deleteTokenBefore() ?? currentCursor.backspace()
  }
  // ...
  return  // ← EARLY RETURN! The composed character '가' is never inserted.
}

The DEL filtering logic (added for Issue #1853) treats the entire input as "just backspaces" and returns early, completely discarding non-DEL characters () in the same input chunk.

Fix Direction

Either:

  1. Tokenizer level: Split \x7f as a separate token boundary (like ESC), so , \x7f, become three independent key events.
  2. useTextInput level: After processing DEL characters, don't return — strip the \x7f bytes and continue processing the remaining characters (insert ).

---

Side note: The fact that community members have to reverse-engineer leaked source code to diagnose bugs like this is... not ideal. If you're not going to fix issues in a timely manner, consider open-sourcing the codebase and accepting community contributions. The code is already out there anyway. Letting the community help would be a win-win.

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.