I have done the following
Steps to reproduce
container exec -it <container> sh -c 'stty raw -echo; printf "\033[2J\033[H0123456789\nX\r\n"; sleep 3; stty sane'
Expected: X appears directly after 9, because the inner tty is raw and a bare LF only moves down a row.
Actual: X appears at column 0 of the next line.
Running the same printf on the host alone puts X after 9.
Problem description
Summary
On macOS, the host tty stays in OPOST|ONLCR mode during an interactive session. As a result, every bare LF the container sends is turned into CR LF before it reaches the terminal. Full-screen programs that use LF to move the cursor down one row (tmux, vim, Ink-based TUIs such as Claude Code) then draw at column 0 instead of the current column. You see this as:
- a new tmux pane split to the right painting over the left pane
- repaints only partly erasing earlier text, so stale characters show through
- the same corruption without tmux, running a TUI directly in the container
None of this happens when the same programs run on the host.
Cause
ProcessIO.create puts the host tty into raw mode with Terminal.setraw():
https://github.com/apple/container/blob/dc276ee/Sources/Services/ContainerAPIService/Client/ProcessIO.swift#L52
public func setraw() throws {
var attr = try Self.getattr(descriptor)
cfmakeraw(&attr)
attr.c_oflag = attr.c_oflag | tcflag_t(OPOST)
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
}
https://github.com/apple/containerization/blob/8b8cd7e/Sources/ContainerizationOS/Terminal.swift#L143-L148
On macOS, cfmakeraw() clears only the OPOST bit in c_oflag. It does not touch ONLCR, which is on by default. setraw() then turns OPOST back on, so the host tty ends up with OPOST|ONLCR and translates output again.
The guest pty already applies whatever output processing the container program asked for. tmux, for example, sets its own tty to -opost. So translating a second time on the host is always wrong for interactive sessions. For comparison, Docker's host-side raw mode (moby/term MakeRaw) clears OPOST completely.
Workaround
Clear ONLCR on the host tty before calling container. setraw() never changes that bit, so the setting survives:
saved=$(stty -g); trap 'stty "$saved"' EXIT
stty -onlcr
container exec -it <container> zsh
With this in place, the reproduction above prints X after 9, and tmux and TUI rendering inside the container are correct.
Related
Environment
- `container --version`: CLI version 1.4.1 (build: release, commit: unspeci)
- macOS: 26.6.2
- Terminal: kitty 0.49.0 created by Kovid Goyal
- Guest: Ubuntu 24.04, tmux 3.4
Code of Conduct
I have done the following
Steps to reproduce
Expected:
Xappears directly after9, because the inner tty is raw and a bare LF only moves down a row.Actual:
Xappears at column 0 of the next line.Running the same
printfon the host alone putsXafter9.Problem description
Summary
On macOS, the host tty stays in
OPOST|ONLCRmode during an interactive session. As a result, every bare LF the container sends is turned into CR LF before it reaches the terminal. Full-screen programs that use LF to move the cursor down one row (tmux, vim, Ink-based TUIs such as Claude Code) then draw at column 0 instead of the current column. You see this as:None of this happens when the same programs run on the host.
Cause
ProcessIO.createputs the host tty into raw mode withTerminal.setraw():https://github.com/apple/container/blob/dc276ee/Sources/Services/ContainerAPIService/Client/ProcessIO.swift#L52
https://github.com/apple/containerization/blob/8b8cd7e/Sources/ContainerizationOS/Terminal.swift#L143-L148
On macOS,
cfmakeraw()clears only theOPOSTbit inc_oflag. It does not touchONLCR, which is on by default.setraw()then turnsOPOSTback on, so the host tty ends up withOPOST|ONLCRand translates output again.The guest pty already applies whatever output processing the container program asked for. tmux, for example, sets its own tty to
-opost. So translating a second time on the host is always wrong for interactive sessions. For comparison, Docker's host-side raw mode (moby/termMakeRaw) clearsOPOSTcompletely.Workaround
Clear
ONLCRon the host tty before callingcontainer.setraw()never changes that bit, so the setting survives:With this in place, the reproduction above prints
Xafter9, and tmux and TUI rendering inside the container are correct.Related
podman exec(OPOST left on in the outer tty caused tmux/vim glitches)Environment
Code of Conduct