When you bind Ctrl+M to a custom action in Claude Code running inside Ghostty, it works. Open the same Claude Code in GNOME Terminal, and the binding silently breaks — Ctrl+M behaves like Enter instead. The difference isn’t in Claude Code or your configuration. It’s in the bytes. Ghostty transmits \x1b[109;5u for Ctrl+M via the Kitty Keyboard Protocol, while GNOME Terminal sends 0x0D — the exact same byte as Enter. Your shortcut works in one terminal and vanishes in another because the underlying byte sequences are fundamentally different.
Terminal keyboard input flows through three layers before reaching your application: ASCII control character encoding, the kernel’s TTY driver (configured via termios), and application-level processing (GNU Readline, Fish, Claude Code). Understanding this stack explains why Ctrl+M is identical to Enter at the ASCII level, why Ctrl+C can’t be rebound in most tools, and why modern terminals like Ghostty and Kitty can break free from 1960s constraints that GNOME Terminal still enforces. This article traces a single keypress from your keyboard through all three layers — explaining what each layer does, what it doesn’t, and where the gaps between standards and reality live.
The Terminal Input Pipeline: From Keyboard to Application
When you press a key in a terminal emulator like Ghostty, Kitty, or Alacritty, the byte travels through a well-defined pipeline:
Terminal Emulator (Ghostty) → captures keyboard event, writes bytes to PTY master
↓
TTY Driver (kernel) → PTY slave side, intercepts or passes bytes per termios settings
↓
Application (Fish, etc.) → in raw mode, handles all keypresses itself
The terminal emulator is the modern equivalent of a physical teletypewriter. The PTY (Pseudo-TTY) pair is the modern equivalent of the serial cable. The TTY driver is the same kernel module that processed serial data from physical teletypewriters in the 1970s.
1. TTY and Terminal: A History of Names
TTY stands for TeleTYpewriter — a physical electromechanical device with a keyboard and a print head, connected to a computer via serial cable. Terminal is its successor concept. Early terminals were also hardware (like the DEC VT100), replacing paper with a CRT screen.
Hardware Evolution
| Era | Device | Interface | Display |
|---|---|---|---|
| 1960s | Physical teletypewriter (TTY) | Serial port | Paper |
| 1970s | Video terminal (VT100) | Serial port | CRT screen |
| 1980s | Terminal emulator (xterm) | PTY (Pseudo-TTY) | X11 window |
| 2020s | Ghostty / Kitty / Alacritty | PTY | GPU-accelerated window |
The kernel’s TTY driver and PTY interface were designed for physical teletypewriters and have been carried forward unchanged. Today, Ghostty is “the emulated teletypewriter,” the PTY is “the serial cable,” and the TTY driver is “the kernel module that processes serial data.”
Name Legacy
| Name | Original meaning | Current meaning |
|---|---|---|
| TTY | Teletypewriter | Kernel terminal driver module |
| PTY | Pseudo-teletypewriter | Software-emulated TTY (master/slave pair) |
| terminal | Physical terminal device | Terminal emulator software (Ghostty) |
| console | System console (directly connected terminal) | TUI or system primary terminal |
stty | Set teletype | Configure TTY driver parameters |
/dev/tty | TTY device file | Current terminal’s device file |
2. ASCII Control Character Encoding
The encoding rule for Ctrl-modified keys was defined in the 1960s ASCII standard:
Ctrl + key = key & 0x1F (clear bits 5 and 6, i.e., AND with 0b00011111)
This rule applies to characters in the ASCII range 0x40–0x5F (uppercase letters A–Z and symbols @, [, \, ], ^, _). It does not apply to lowercase letters or characters outside this range. The equivalent subtraction shorthand ASCII − 64 only holds within this range.
| Key | Calculation | Byte | ASCII name | Meaning |
|---|---|---|---|---|
Ctrl+M | 0x4D & 0x1F = 0x0D | 0x0D | CR | Identical to Enter |
Ctrl+J | 0x4A & 0x1F = 0x0A | 0x0A | LF | Line feed |
Ctrl+I | 0x49 & 0x1F = 0x09 | 0x09 | HT | Identical to Tab |
Ctrl+[ | 0x5B & 0x1F = 0x1B | 0x1B | ESC | Escape |
Ctrl+C | 0x43 & 0x1F = 0x03 | 0x03 | ETX | End of text |
Ctrl+D | 0x44 & 0x1F = 0x04 | 0x04 | EOT | End of transmission |
Ctrl+M = Enter and Ctrl+I = Tab are not tool configurations. At the byte level, they are the same value. This was defined by the ASCII standard (ANSI X3.4-1963) and cannot be changed by any application.
3. The TTY Driver (termios)
The TTY driver is a kernel-level software layer that sits between the terminal and the process. POSIX (IEEE 1003.1 / ISO/IEC 9945) defines its behavior through the termios structure:
struct termios {
tcflag_t c_iflag; // Input mode flags
tcflag_t c_oflag; // Output mode flags
tcflag_t c_cflag; // Control mode flags
tcflag_t c_lflag; // Local mode flags (echo, icanon, ...)
cc_t c_cc[NCCS]; // Special character array
};
The c_cc Array (Linux, 19 entries)
| Index | stty name | Default | Meaning |
|---|---|---|---|
| VINTR | intr | ^C | Send SIGINT |
| VQUIT | quit | ^\ | Send SIGQUIT |
| VERASE | erase | ^? | Delete previous character |
| VKILL | kill | ^U | Delete entire line |
| VEOF | eof | ^D | EOF marker / Flush buffer |
| VSTART | start | ^Q | Resume output (XON) |
| VSTOP | stop | ^S | Suspend output (XOFF) |
| VSUSP | susp | ^Z | Send SIGTSTP |
| VEOL | eol | <undef> | End-of-line character |
| VMIN | min | 1 | Minimum characters for read() (non-canonical only) |
| VTIME | time | 0 | read() timeout (non-canonical only) |
In the POSIX termios structure, Canonical and Non-canonical modes are mutually exclusive, so the driver reuses memory slots in the c_cc array. VMIN typically shares the same index as VEOF (^D), and VTIME shares with VEOL. This is why system programmers must be careful when switching modes.
The stty Command
stty (set teletype) is the user-space tool for configuring the termios structure. It calls tcgetattr() and tcsetattr() under the hood:
$ stty -a
intr = ^C → SIGINT (interrupt)
quit = ^\ → SIGQUIT (quit + core dump)
susp = ^Z → SIGTSTP (suspend)
erase = ^? → Delete previous character
kill = ^U → Delete entire line
eof = ^D → Flush buffer / EOF marker
Note that Ctrl+D (VEOF) is not a signal. In Canonical mode, it instructs the TTY driver to immediately “flush” its internal buffer and return all collected bytes to the waiting read() system call. If the buffer is empty (i.e., you press ^D on a new line), read() returns 0 bytes. By POSIX convention, an application interprets a 0-byte return from read() as an End-of-File (EOF) condition.
4. Canonical Mode: Line Editing for Teletypewriters
Canonical mode (also called cooked mode) is the TTY driver’s default line-editing mode. The TTY driver maintains an internal input buffer on behalf of the user — the application only receives the complete line after Enter is pressed.
This design was built for physical teletypewriters, where paper cannot be erased. Editing could only happen in the TTY driver’s internal buffer. What appeared on paper was a separate, irreversible record.
On early 1960s teletypewriters, the erase character was ^H (BS, 0x08), and the convention for marking a deletion on paper was to print #. The ^? (DEL, 0x7F) became the standard erase character later, established by DEC VT-series terminals in the 1970s.
The origin of ^? (DEL) is physical: in the era of punched paper tape, a character was recorded by punching holes into a tape. Once a hole was punched, it couldn’t be “un-punched.” The binary representation of 0x7F is 1111111 (seven holes punched). If an operator made a mistake, they would back up the tape and punch all seven holes (the “Rubout” character), which the computer was then programmed to ignore. This physical constraint is why DEL became the standard for erasing.
Modern Linux systems default to ^? for VERASE, while some older BSD-derived systems still default to ^H.
| Key | TTY buffer operation | Effect on paper (1960s TTY era) |
|---|---|---|
^H (erase, early TTYs) | Delete last character | Back up one position, print # to mark deletion |
^W (werase) | Delete last word | Overwrite characters one by one |
^U (kill) | Clear entire line | Print @ to mark line as void, newline to retype |
^R (reprint) | Re-output buffer contents | Reprint the clean current buffer |
Consider this input sequence: fix the bu + ^W + ^W + the bug
What appears on paper (with strikeouts):
fix the bu####the bug
^^^^
Two ^W strikeouts covering "bu" and "the"
TTY buffer (invisible to the operator, evolving step by step):
fix the bu ← typed up to here
fix the ← ^W deleted "bu"
fix ← ^W deleted "the"
fix the bug ← typed "the bug"
There’s a critical distinction here:
- Paper = display (irreversible, can only strike over)
- TTY buffer = data (freely modifiable, invisible to user)
The paper and the buffer are not synchronized. The paper shows the full history with strikeouts. The buffer holds only the clean current state. This is why ^R (reprint) exists — the operator forgets what’s actually in the buffer and needs a clean reprint.
When video terminals (VT100) arrived with cursor movement and screen erase capabilities, the TTY driver could send BS SPACE BS sequences to actually erase characters on screen. Paper and buffer finally became visually synchronized. Modern terminal emulators (Ghostty) inherit this mechanism.
5. TTY Interception vs Application Handling
The TTY driver intercepts bytes it recognizes and handles them itself (sending signals, editing the line). Bytes it doesn’t recognize are passed through to the application:
Keyboard Ctrl+C → 0x03 → TTY intercepts → sends SIGINT → process receives signal
Keyboard Ctrl+A → 0x01 → TTY passes through → reaches readline/fish → application handles it
This means:
Ctrl+C(SIGINT),Ctrl+Z(SIGTSTP),Ctrl+\(SIGQUIT) — handled by the TTY layerCtrl+A(beginning of line),Ctrl+E(end of line),Ctrl+K(kill to end of line) — handled by Readline or the application layerCtrl+U,Ctrl+W,Ctrl+D— handled by the TTY in canonical mode, but typically redefined by applications like Readline (e.g.,Ctrl+Ubecomes “delete to beginning of line” instead of “delete entire line”)
6. Readline and POSIX: What’s Standardized and What Isn’t
Readline is not a standard. It’s GNU Readline — a line-editing library originally written by Brian Fox for the GNU Project (publicly released with Bash around 1988–1989). Its keybindings come from Emacs conventions, not from any specification.
Historical Timeline
| Year | Milestone | Editing capability |
|---|---|---|
| 1970s | Unix sh | termios cooked mode only (^U kill line, ^W kill word, ^H/^? backspace). No cursor movement. |
| 1978 | BSD csh | Bill Joy introduces command history substitution (!!, !$), but still no interactive line editing |
| ~1981 | tcsh | Ken Greer adds Emacs-style interactive line editing (Ctrl+A home, Ctrl+E end) to csh |
| ~1983 | ksh (AT&T) | David Korn independently adds emacs-mode and vi-mode line editing; developed separately from tcsh |
| 1988–89 | GNU Readline | Brian Fox creates reusable line-editing library for Bash. Adopts Emacs key conventions established by tcsh/ksh. Chet Ramey becomes primary maintainer ~1990. |
| 1990s | POSIX (IEEE 1003.1) | Standardizes termios interface and sh command language syntax. Does not standardize line-editing keybindings. |
The Standardization Gap
POSIX termios — TTY driver behavior (standardized)
↑ not covered by any standard ↑
BSD tcsh / GNU Readline — Line-editing keybindings (de facto standard, not a formal specification)
↑ each tool implements its own ↑
Fish / Claude Code — Don't use GNU Readline; implement line editing independently
Key points:
- POSIX does not define
Ctrl+A= beginning of line orCtrl+E= end of line. POSIX only specifies the semantics ofc_ccentries likeVERASE,VKILL, andVWERASE. - Emacs keybindings are a de facto standard, propagated through tcsh/ksh → GNU Readline → Bash. No specification defines them.
- Fish doesn’t use GNU Readline. It implements its own line editor but follows the same Emacs key conventions (
Ctrl+A/E/B/F/N/P) because they’re muscle memory at this point. - Claude Code does the same — implementing input processing independently, following Emacs conventions rather than any formal specification.
The keybindings you see in terminal tools are the product of 40 years of convention propagated from Emacs → tcsh → GNU Readline. They are not the product of any ISO or POSIX specification.
7. Raw Mode: When Applications Take Over
Cooked Mode vs Raw Mode
Raw Mode is a termios configuration that disables all TTY driver processing, letting the application receive raw bytes directly. Cooked Mode (canonical mode) is the default where the TTY driver handles everything:
| Behavior | Cooked Mode (default) | Raw Mode |
|---|---|---|
| Line buffering | Input delivered to app after Enter | Each byte delivered immediately |
^C → SIGINT | TTY intercepts, sends signal | Byte 0x03 passed to app directly |
^U / ^W | TTY performs line/word deletion | Bytes passed to app directly |
^D EOF | TTY triggers EOF condition | Byte 0x04 passed to app directly |
^S / ^Q | TTY suspends/resumes output | Bytes passed to app directly |
| Echo | TTY auto-echoes input | Application decides whether to echo |
^V | TTY escapes next character | Byte passed to app directly |
Linux and BSD systems provide cfmakeraw() as a convenience function to switch to Raw Mode in one call. Under the hood, it disables icanon, echo, ISIG, IXON, and other flag bits in the termios structure. Note that cfmakeraw() is not part of the POSIX standard — it originated as a BSD extension and was later adopted by Linux glibc. On strictly POSIX-only environments, you must set the flags manually via tcsetattr().
The Three Roles
Terminal Emulator (Ghostty) — Captures keyboard events, converts to bytes, writes to PTY master
↓
TTY Driver (kernel) — PTY slave side, intercepts or passes bytes per termios settings
↓
Application (Fish, Claude Code) — Calls tcsetattr() to switch raw mode, handles all keypresses itself
The terminal emulator doesn’t participate in Raw Mode switching. It only converts keyboard events to bytes and sends them through the PTY. It’s the applications — Fish, Claude Code — that call tcsetattr() to modify termios flags and take over keypress handling.
Overlap with c_cc Definitions
TTY c_cc | TTY behavior | Application behavior | Conflict? |
|---|---|---|---|
^C | SIGINT | Fish: cancel, Claude Code: cancel | No — tools preserve TTY handling |
^D | VEOF (exit) | Fish: exit | No — same semantics |
^U | VKILL (delete entire line) | Delete to line start | No — semantic refinement |
^W | VWERASE (delete previous word) | Delete previous word | No — same semantics, app handles in Raw Mode |
^S | VSTOP (suspend output) | Claude Code: stash | No — IXON disabled in Raw Mode |
^R | VREPRINT | Claude Code: search history | No — app handles in Raw Mode |
No actual conflicts. In Raw Mode, the application takes over all keypress handling. The c_cc definitions stop applying. The keybindings are semantically consistent with TTY defaults by deliberate design.
8. Ctrl+J vs Ctrl+M: A Deep Dive
The difference between these two keys reveals how termios flags affect what applications see:
| Key | Byte | Terminal default | Can be bound separately? |
|---|---|---|---|
Ctrl+M | 0x0D (CR) | Identical to Enter | No in Cooked Mode; Only with modern protocols in Raw Mode |
Ctrl+J | 0x0A (LF) | Different from Enter | Yes — different byte |
In Cooked Mode
The TTY driver enables the icrnl (Input CR to NL) flag by default. This translates incoming 0x0D (CR) to 0x0A (LF). To the application, Ctrl+M and Ctrl+J both produce byte 0x0A — they are indistinguishable.
Why does this translation exist? On physical teletypewriters, CR (0x0D, Carriage Return) and LF (0x0A, Line Feed) were two separate mechanical actions: CR moved the print head back to the left edge, LF rolled the paper up one line. Starting a new line required both. Unix adopted \n (LF) as its line ending, while Windows uses \r\n (CR+LF). The TTY driver’s icrnl flag bridges this gap — it converts the Enter key’s 0x0D (CR) into 0x0A (LF) so that Unix applications only see \n.
In Raw Mode
When an application switches to Raw Mode, icrnl is disabled. This allows the application to distinguish between Ctrl+J (0x0A) and Enter/Ctrl+M (0x0D).
However, there is a common misconception that Raw Mode alone allows you to distinguish Ctrl+M from Enter. In standard terminal configurations, both keys send the exact same byte (0x0D). If the bytes are identical, the application cannot distinguish them even in Raw Mode. To truly separate Enter from Ctrl+M, you must use a terminal emulator that supports modern keyboard protocols.
9. Modern Solutions: Kitty Keyboard Protocol & CSI u
The reason we struggle with Shift+Enter or distinguishing Tab from Ctrl+I is the rigid 1963 ASCII encoding. To solve this without breaking backward compatibility, modern terminal emulators like Ghostty, Kitty, and Alacritty support enhanced keyboard protocols (like the Kitty Keyboard Protocol or the CSI u standard).
Instead of sending a single legacy byte like 0x0D (CR) for Enter, these protocols can send a rich escape sequence:
- Legacy Enter:
\x0D - Modern Shift+Enter (CSI u):
\x1b[13;2u
The application (if it supports these protocols) can then parse these escape sequences to provide fully independent keybindings for every possible key combination, finally moving beyond the constraints of teletypewriter-era hardware.
10. Debugging Terminal Input: Seeing the Bytes Yourself
You can observe terminal input processing at each layer using standard Unix tools.
View Current TTY Settings
stty -a
This shows all termios flags and c_cc character mappings. Look for icanon (canonical mode on/off), echo (echo on/off), and the special character assignments.
Observe Raw Keypress Bytes
# Press keys, then Ctrl+C to exit
cat | xxd
Each keypress shows its raw hex value. Try Ctrl+M (you’ll see 0d) vs Ctrl+J (you’ll see 0a). In cooked mode, both may appear as 0a due to icrnl.
Switch to Raw Mode and Observe
import sys, tty, termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while True:
ch = sys.stdin.read(1)
print('byte: %02x (%r)' % (ord(ch), ch), file=sys.stderr)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
In Raw Mode, you’ll see Ctrl+M as 0d and Ctrl+J as 0a — confirming they’re distinct bytes when icrnl is off.
Answering the Opening Question
Run the script above in Ghostty and press Ctrl+M. You won’t see 0x0D — you’ll see:
byte: 1b ('\x1b') byte: 5b ('[') byte: 31 ('1') byte: 30 ('0') byte: 39 ('9') byte: 3b (';') byte: 35 ('5') byte: 75 ('u')
This is \x1b[109;5u — the Kitty Keyboard Protocol encoding for “key m + Ctrl modifier.” Decoded:
\x1b [ 1 0 9 ; 5 u
ESC CSI ├─────┘ ├──┘ └─ CSI u format terminator
│ │ │
│ │ └─ modifier 5 = Ctrl
│ └─ Unicode codepoint 109 = 'm'
└─ ASCII code 0x1b, Same as Ctrl+[
Ghostty doesn’t compress Ctrl+M into the legacy 0x0D. So Claude Code can tell Ctrl+M apart from Enter.
Press Enter and Ctrl+J for comparison:
| Key | Legacy byte | Ghostty sends |
|---|---|---|
Enter | 0x0D | 0x0D — legacy byte |
Ctrl+M | 0x0D (same) | \x1b[109;5u — completely different |
Ctrl+J | 0x0A | 0x0A — legacy byte |
Which terminals can distinguish them depends on protocol support:
| Terminal | Enter | Ctrl+M | Distinguishable? |
|---|---|---|---|
| Ghostty | 0x0D | \x1b[109;5u | Yes |
| Kitty | 0x0D | \x1b[109;5u | Yes (protocol creator) |
| WezTerm | 0x0D | \x1b[109;5u | Yes |
| Alacritty | Partial | Depends on version | Partial |
| GNOME Terminal (VTE) | 0x0D | 0x0D | No |
| xterm | 0x0D | 0x0D | No |
This is why your Ctrl+M binding works in Ghostty but silently breaks in GNOME Terminal. The three-layer stack — ASCII, TTY driver, application — hasn’t changed. But the terminal emulator has inserted a fourth layer: the keyboard protocol. And that layer determines whether Ctrl+M and Enter are the same byte or not.
Frequently Asked Questions
Why can’t I rebind Ctrl+M independently from Enter?
In cooked mode, you can’t: Ctrl+M produces 0x0D (CR), and the TTY driver’s icrnl flag translates it to 0x0A (LF) — same as Ctrl+J — before it reaches the application. The bytes are identical by the time any application sees them.
In Raw Mode (used by Fish, Claude Code), icrnl is disabled, but Ctrl+M and Enter still typically send the same byte (0x0D). To distinguish them, you need a terminal emulator and an application that both support modern protocols like Kitty Keyboard Protocol or CSI u, which send unique escape sequences for different key combinations.
What’s the difference between Ctrl+J and Ctrl+M?
Ctrl+J produces 0x0A (Line Feed) and Ctrl+M produces 0x0D (Carriage Return) — they are different bytes. However, in cooked mode, the TTY driver’s icrnl flag translates 0x0D to 0x0A, making them indistinguishable to applications. In Raw Mode (used by Fish, Claude Code), icrnl is disabled and the bytes arrive separately, allowing independent keybindings.
Do Fish and Claude Code use GNU Readline?
No. Each implements its own line editing and input processing. However, they both follow the same Emacs keybinding conventions (Ctrl+A for beginning of line, Ctrl+E for end, etc.) because these are 40-year muscle-memory standards, not Readline-specific features.
What does stty raw actually do?
It disables all TTY driver processing: turns off icanon (canonical mode line editing), echo (automatic character display), ISIG (signal generation for ^C, ^Z, ^\), IXON (XON/XOFF flow control), and the icrnl flag (CR-to-NL translation). The application then receives every byte exactly as typed. On Linux and BSD systems, the cfmakeraw() function does this in one call — note that cfmakeraw() is a BSD/Linux extension, not part of the POSIX standard.
Why does Ctrl+U delete to line start instead of the whole line?
In POSIX termios, VKILL (Ctrl+U) is defined as “delete entire line.” But when an application like Bash (via GNU Readline) or Fish switches to Raw Mode, the TTY’s c_cc handling is bypassed. The application redefines Ctrl+U as “kill to beginning of line” (keeping characters after the cursor), which is more useful in practice. This is a semantic refinement, not a conflict.
Conclusion
Terminal keyboard input processing is a three-layer system: ASCII defines the byte-level encoding (unchanged since 1963), the TTY driver (configured via termios) provides line editing and signal handling inherited from teletypewriter design, and the application layer (Readline or custom) handles everything else.
The keybindings in modern terminal tools — Ctrl+A for beginning of line, Ctrl+E for end, Ctrl+K to kill forward — come from 40 years of Emacs convention, not from POSIX or any formal standard. Applications in Raw Mode bypass the TTY driver’s c_cc definitions entirely, but preserve their semantics by design.
Understanding this stack demystifies why some shortcuts can’t be rebound (they’re the same byte at the ASCII level), why Shift+Enter requires Raw Mode to work, and why every terminal tool independently implements the same Emacs keybindings.
Further Reading
- The TTY Demystified — Linus Akesson’s canonical reference on TTY architecture
- A Brief Introduction to termios — Nelson Elhage’s deep dive on the termios API
- A History of the TTY — Historical context from physical teletypewriters to modern terminals
- termios(3) man page — Complete POSIX termios reference