make shift+enter a newline in Claude Code on Windows
I was mid-prompt in Claude Code, on a Windows box, and pressed shift+enter to
drop to a new line. My half-written message sent itself. Years of muscle memory
tell my fingers that shift+enter means newline, not send. The machine on the
far end disagreed.
change your muscle memory…
The one to reach for is ctrl+j. It drops a newline without sending, and it
works in every terminal with no setup. The
Claude Code docs put it
first, alongside typing \ and then enter.
…or change the config
The one I actually use is a Windows Terminal keybinding, because my fingers will
not unlearn the chord. It lives in settings.json:
// settings.json"actions": [ { // send a line feed: the newline byte ctrl+j produces "command": { "action": "sendInput", "input": "\n" }, "id": "User.sendInput.ShiftEnter" }],"keybindings": [ { "id": "User.sendInput.ShiftEnter", "keys": "shift+enter" }]
Now shift+enter sends exactly what Claude reads as a newline, and my hands
never learned the difference.
That is the practical bit. ctrl+j unblocks anyone. The rest is why.
down the rabbit hole
This all seemed like a hack to me. Why ctrl+j, of all keys? I asked Claude.
ctrl plus a letter sends the control byte at that letter’s position in the
alphabet. J is the tenth letter, so ctrl+j is byte 0x0A: line feed, \n,
the newline character itself. M is the thirteenth, so ctrl+m is 0x0D,
carriage return. And 0x0D is exactly what the enter key sends. So enter is
secretly ctrl+m, and ctrl+j is me typing the other newline byte by hand.
That raised a better question. If enter is just a byte, why can the terminal
not tell shift+enter apart from it? I asked Claude to dig in deeper. She
pulled a thread that runs back to the VT100. She also pointed me at
Jesse Vincent’s
write-up,
which lays the whole thing out. He had Claude research and write that one too,
which felt about right.
A terminal sends bytes, not key presses. enter sends 0x0D. What shift does
in the old encoding is to pick the alternate printable character: capital A,
the bang above the 1. enter is not printable. It is a control byte with no
alternate for shift to choose. So shift+enter sends the same 0x0D, and
from the byte stream it is indistinguishable from a bare enter.
# what reaches the app, byte for byteenter 0x0Dshift+enter 0x0D
No program can tell them apart, Claude Code included. This is a limitation older than I am, not a bug.
The collision has been fixed more than once. Terminals grew opt-in protocols
that encode the modifier as its own field, so the bytes stop colliding: xterm’s
modifyOtherKeys, then Paul Evans’s fixterms and its CSI u encoding, then
Kovid Goyal’s kitty keyboard protocol,
the modern one. Under it, shift+enter gets its own sequence, ESC[13;2u,
where 13 is enter and 2 is shift. Claude Code asks for the protocol when
it starts (CSI > 1 u) and then reads shift+enter as a key of its own.
Which explained the split I started with. The shell was never the variable. The
terminal emulator was. mintty, the one behind Git Bash, speaks the protocol and
sends shift+enter as a distinct byte with no configuration, so Claude just
works there. Windows Terminal only gained the kitty protocol in
Preview 1.25,
whose release notes name “agentic command line tools” as the reason. My stable
build is 1.24, a release behind:
$ winget upgrade Microsoft.WindowsTerminalNo available upgrade found.No newer package versions are available from the configured sources.
So 1.24 ignores Claude’s request and keeps sending bare 0x0D for both keys.
The Claude Code docs list Windows Terminal under “works without setup,” which is
true, for the build one release newer than mine.
That is what the keybinding is really for. It makes shift+enter send the same
\n byte ctrl+j does, then skips the protocol my terminal cannot speak yet.
When 1.25 reaches stable I can delete it, and shift+enter will work on its
own.
P.S. the red herring
For the first hour I blamed the remote desktop. I reach the Windows box from my
Mac over RDP, and shift+enter worked when I opened the same machine through a
VS Code tunnel but not here. So surely the remote-desktop link was eating the
modifier on the way.
It was not. I ran [Console]::ReadKey() over the RDP session, pressed the
chord, and it reported Mods=Shift. The shift press reached Windows perfectly
intact, never lost in transit. The terminal on the far end was folding it into
the same byte with or without the protocol. It took a fair bit of poking to be
sure I was chasing the wrong layer, and that working remotely had nothing to do
with it.