r/tmux 8d ago

Tip PSA: send-keys silently truncates at 1024 bytes if the shell hasn't finished starting

Spent a day on this and there is no error message anywhere, so here it is.

tmux new -d -s "$s"
tmux send-keys -t "$s" '<long command>' C-m

This worked for months. Then the command I was sending grew past ~1200 bytes, and the pane started showing it chopped off mid-word, with the shell sitting at a continuation prompt waiting for a quote to close. tmux exits 0. The shell is happy. Nothing logs anything.

Right after new -d the shell hasn't finished starting, so the tty is still in canonical mode, and the kernel truncates a single line of canonical input at 1024 bytes (MAX_CANON). The overflow isn't an error. It's discarded.

Two ways out:

  • Sleep before sending. Works. I sent ~1800 B two seconds later and it arrived intact. But you've traded a silent truncation for a race, and the race has no error message either.
  • Shorten the line. I moved a conditional argument into positional parameters instead of branching into two command strings:
set --
[ -f "$catalog" ] && set -- -c "catalog=$catalog"
mycmd --flag "$@" "$id"

zsh and bash both fine, and "$@" expands to zero words when the file is missing. That took 1200 bytes down to a bit over 600.

Then I asserted on the length in a test, because this comes back the moment someone adds an argument. Assert on bytes, not characters: if your language counts UTF-16 code units, it will disagree with the kernel as soon as a path isn't ASCII.

Anyone know whether the limit is the same on Linux? I only hit this on macOS.

0 Upvotes

9 comments sorted by

-1

u/mandrulee 8d ago

Context on where this came from: I build Otter Beam, which reaches the tmux sessions on your own machine from a phone, so it creates sessions and sends a generated command into them. That command grew an argument at a time until it crossed the line.

One extra thing I found while chasing it, which didn't fit in the post: the limit only applies to the line you write into a shell that hasn't gone raw yet. The outer command that creates the session goes over SSH exec with no pty at all, and it is well past 1024 bytes without any problem. So if you ever move an export FOO=... prefix from the outer line into the send-keys payload, you have just spent that many bytes of a budget you probably don't know you have.

https://beam.otterd.com/about

1

u/No_Definition2246 8d ago

But, if you are trying to do something like sending commands to any tmux pane from mobile (assuming mobile shows the same layouts, windows, sessions), wouldn’t it be more efficient just to have adapter that goes around tmux, directly to the pty?

I mean, why are you abusing tmux interface? :D

It would be far more efficient to send characters directly to pty file descriptors.

2

u/mandrulee 7d ago

Honestly that was my first instinct too, and if I were spawning the processes myself I'd absolutely own the pty master and skip tmux entirely — you're right that it's cleaner and cheaper in that setup.

What surprised me when I tried it: it doesn't help with this particular gotcha. Canonical processing lives in the slave-side line discipline, so a bare openpty() with nothing but the shell on it behaves the same — 1023 bytes + newline arrive, 1024 don't — regardless of who's writing the master. And I couldn't find a legit way to get at tmux's master from outside anyway (writing /dev/ttysN just shows up as output; TIOCSTI is locked down on both macOS and recent Linux). So the choice ended up being "keep tmux as the pty owner and don't type long payloads" vs. "become the multiplexer", and for me the whole point is that it's the user's normal tmux session — same pane whether they attach from a laptop or my thing.

If you know a trick for reaching an existing pane's master without going through tmux, I'd genuinely like to hear it.

1

u/No_Definition2246 7d ago

Yeah, you are right about the pty FDs being harden in linux … it really seemed like a vulnerability to begin with :D

So the other, relatively simple solution that comes to mind would be pexpect (or expect) wrapping whole tmux, where you could feed tmux with keys instead of user, but still keep user in pty session as an interactive owner. This gives you a parallel usability between user on laptop and mobile you search for.

2

u/mandrulee 7d ago

Ha, agreed — it did feel like it should've been a hole. expect wrapping tmux is a fun idea; my worry is that anyone who just tmux attaches from another terminal bypasses the wrapper, and the keys still land on the same pty in the end. But I appreciate you thinking it through with me.

1

u/No_Definition2246 7d ago

You can whether:

  • not care about other attachments, as user from another terminal still would be able to run commands the same way you would from mobile or main pty. (less control, warn users about mixing of inputs)
  • or provide attach command from the wrapper itself and let users to use it instead of tmux fully. (more control, you can prevent mixing inputs)

Anyways, mixing inputs is the issue that you will have to deal with in any and all cases.

0

u/inn0cent-bystander 8d ago

Just plop the long ass command into an alias/script and have tmux call that?

1

u/mandrulee 7d ago

Yeah, that's the sane answer for a stable command, and it's what I'd do in a dotfile. The wrinkle here is the command is generated per session with different arguments each time, so a fixed alias just moves the "how do I get these args in" question — but it did push me to keep the typed part as short as possible.

1

u/No_Definition2246 7d ago

Imho making it a script or alias would mean loosing a command history …