Getting Shift+Enter to work in Claude Code over tmux

Today I was working on my Windows Laptop using windows terminal (wsl) -> ssh -> tmux -> claude code to work on a project on my local server. Shift enter is now wired into my fingers for newline but it was sending enter.

Obviously claude did all this for me but I wanted a record of it so I can set this up later.

Layer 1: tmux was discarding the modifier

Step 1: extended keys in tmux.conf

set -s extended-keys on
set -s extended-keys-format csi-u
set -as terminal-features 'xterm*:extkeys'

extkeys is negotiated when a client attaches, so reload the config and then detach and reattach. To check it landed:

tmux list-clients -F '#{client_termfeatures}'

Look for extkeys in the list. A client that attached before the config change will not have it, which looks exactly like the config not working.

Layer 2: aim at Ctrl+J instead

Even with tmux forwarding it properly, Shift+Enter still submitted.

Claude Code has a /terminal-setup command for this, but it writes keybindings into the terminal emulator's own config file, and only for a handful of editors and terminals. Over SSH it cannot reach my terminal at all, because the terminal is on the Windows machine.

The useful discovery is that Ctrl+J always inserts a newline, no setup, nothing. Ctrl+J is line feed, 0x0a, and Claude Code treats it as a newline in every terminal. That is a much better thing to aim at, so I stopped trying to make Shift+Enter work as Shift+Enter and made everything send Ctrl+J instead.

In tmux.conf:

bind -n S-Enter send-keys C-j

Layer 3: Windows Terminal never sent it in the first place

That tmux binding did nothing. Which was actually the most useful result of the whole exercise, because it proved tmux was never seeing a Shift+Enter at all. Windows Terminal was collapsing it to a plain Enter before it even left Windows.

Fix is a sendInput action in Windows Terminal's settings.json, sending a bare line feed:

{
    "command": { "action": "sendInput", "input": "\n" },
    "keys": "shift+enter"
}

The weird JSON rewrite

Here is the bit that confused me for a while. I pasted that in, saved, and Windows Terminal immediately rewrote it into this:

{
    "command":
    {
        "action": "sendInput",
        "input": "\n"
    },
    "id": "User.sendInput.DFCDAF06"
}

My "keys": "shift+enter" had vanished and been replaced by a random-looking id. It looks like the editor is fighting you and undoing your change.

It isn't. Recent versions of Windows Terminal split every binding into two halves that live in two different arrays. The actions array holds what to do and gets a generated id. The keybindings array holds what to press and refers back to that id. So the finished thing is:

"actions":
[
    {
        "command": { "action": "sendInput", "input": "\n" },
        "id": "User.sendInput.DFCDAF06"
    }
],
"keybindings":
[
    {
        "id": "User.sendInput.DFCDAF06",
        "keys": "shift+enter"
    }
]

The two halves are tied together by the id and it has to match exactly. Windows Terminal generates the id itself, so write the action first, save, let it rewrite, then copy the id it made into the keybindings entry. It applies live, no restart.

Ghostty

When I am sat at the Linux box directly I use Ghostty, which speaks CSI-u properly. It sends a real Shift+Enter, tmux recognises it, and the binding from Layer 2 turns it into Ctrl+J. Nothing to configure.

That is the split worth remembering. On the Linux box the tmux config is doing the work. On Windows the terminal sends the line feed itself and tmux never sees a Shift+Enter at all, which is why the tmux binding did nothing there.

If I ever wanted Ghostty to bypass tmux the same way, this in ~/.config/ghostty/config would do it:

keybind = shift+enter=text:\n

The final tmux config

# Pass modified keys (Shift+Enter, Ctrl+Enter, ...) through to the running
# program instead of collapsing them to the unmodified key.
set -s extended-keys on
set -s extended-keys-format csi-u
set -as terminal-features 'xterm*:extkeys'

# Claude Code always takes C-j as a newline. When the terminal sends a real
# Shift+Enter, as Ghostty does, translate it. Windows Terminal is set up to
# send the line feed itself, so this binding is not involved there.
bind -n S-Enter send-keys C-j

# Wheel scroll. Hold Shift to use the terminal's own selection and copy.
set -g mouse on

The mouse line is unrelated. Claude Code flashes a hint about it on startup.

tmux detected - scroll with PgUp/PgDn - or add 'set -g mouse on' to
~/.tmux.conf for wheel scroll

Worth knowing that turning mouse mode on means dragging to select goes into tmux's copy buffer rather than your terminal's. Hold Shift while dragging to get the normal behaviour back.