[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 · 9 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 · 9 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 · 9 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 · 8 months ago

Any updates? @ThariqS

DreamHouseKSH · 8 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 · 8 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 · 8 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 · 8 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 · 7 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 · 7 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] · 6 months ago

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

wplong11 · 6 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] · 6 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.