Skip to content

[Bug]: container exec -it leaves host tty with OPOST|ONLCR, breaking tmux/TUI rendering #2299

Description

@dcvijetic-TT

I have done the following

  • I have searched the existing issues
  • If possible, I've reproduced the issue using the 'main' branch of this project

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 agree to follow this project's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions