# Resize a Terminal and Your Coding Agent Gets SIGWINCH. What Redraws, and What Breaks

> Source: <https://dev.to/gabrielanhaia/resize-a-terminal-and-your-coding-agent-gets-sigwinch-what-redraws-and-what-breaks-83b>
> Published: 2026-10-28 22:01:21+00:00

You narrow a pane to make room for a second agent, scroll up to reread the plan the first one wrote, and the same answer is there three times, each copy wrapped at a different width. That report sits in the Claude Code tracker as [issue #51828](https://github.com/anthropics/claude-code/issues/51828), opened on April 22, 2026, labelled `has repro`, and reproduced by commenters on macOS, Linux with tmux, Windows Terminal, and xterm.js under tmux.

Nothing in that bug is exotic. It falls out of a chain of small, documented steps that run every time a terminal changes size. Once you can see the chain, you can tell which part of a resize glitch belongs to the agent, which to the terminal, and which to the window around them.

A terminal emulator holds the master side of a pseudo-terminal. Your shell, and any agent you start from it, holds the other side. When you drag a split, the emulator works out the new rows and columns and tells the kernel with the `TIOCSWINSZ` ioctl. [node-pty](https://github.com/microsoft/node-pty), which the README says powers VS Code's terminal among others, does that in its [`PtyResize` function](https://github.com/microsoft/node-pty/blob/main/src/unix/pty.cc): one `ioctl(fd, TIOCSWINSZ, &winp)` call.

The kernel stores the size in a `struct winsize` with `ws_row` and `ws_col` fields. Then, per the [ioctl man page](https://man7.org/linux/man-pages/man2/TIOCSWINSZ.2const.html), "When the window size changes, a SIGWINCH signal is sent to the foreground process group." The default action for `SIGWINCH` is to ignore it ([signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html)), so a program that never asked about its size keeps running, unaware.

A program that does care reads the new size back with `TIOCGWINSZ` and redraws. That last step is where everything interesting happens, because "redraw" means something very different depending on which screen the program is drawing on.

*Emulator, kernel, foreground process: the size travels down, the signal comes back up as a repaint.*

You can see each link of the chain from a shell. First, ask the kernel what it thinks the size is. [`stty size`](https://man7.org/linux/man-pages/man1/stty.1.html) prints "the number of rows and columns according to the kernel":

```
stty size
```

Run it, resize the window, run it again. The two numbers change because the emulator told the kernel, not because `stty` measured anything. You can prove the kernel just stores what it is told: `stty cols 40` sets the column count by hand, and the next `stty size` reports 40 even though your window is wider. Resize the window and the emulator overwrites it again.

Next, catch the signal. Save this as `winch.sh` and run it with `bash winch.sh`:

``` bash
#!/usr/bin/env bash
# Resize the window while this runs. Ctrl+C to stop.
trap 'echo "WINCH: $(stty size)"' WINCH
echo "start: $(stty size)"
while true; do sleep 1; done
```

Each resize prints a line. The lines can trail the drag by up to a second, and that delay is documented: the bash manual says that if bash "is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes" ([bash(1), SIGNALS](https://man7.org/linux/man-pages/man1/bash.1.html)). Here the command is `sleep 1`.

If your agent is a Node program, the third one is the version it actually sees. Node emits a [`'resize'` event](https://nodejs.org/api/tty.html) on `process.stdout` whenever its `columns` or `rows` change, and the [process docs](https://nodejs.org/api/process.html) say `SIGWINCH` itself "is delivered when the console has been resized." The script listens for both. Save as `winch.mjs`:

``` js
// node winch.mjs, then drag the pane. Ctrl+C to stop.
const out = process.stdout;
if (!out.isTTY) {
  console.error("stdout is not a TTY, run it in a terminal");
  process.exit(1);
}

let last = `${out.columns}x${out.rows}`;
console.log(`start ${last}`);

process.on("SIGWINCH", () => {
  console.log(`SIGWINCH ${new Date().toISOString()}`);
});

out.on("resize", () => {
  const now = `${out.columns}x${out.rows}`;
  console.log(`resize ${last} -> ${now}`);
  last = now;
});

process.stdin.resume(); // keep the event loop alive
```

Drag a split slowly and count the lines. How many events one drag produces depends on your terminal, not on Node, and a full-screen agent may repaint on each one. On Windows there is a caveat: the [Node process docs](https://nodejs.org/api/process.html) say `SIGWINCH` is only delivered "on write to the console when the cursor is being moved, or when a readable tty is used in raw mode."

Terminals have two screen buffers. The normal one keeps scrollback. The alternate one, entered with the xterm private mode `1049`, has none. The [xterm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) describe `CSI ? 1049 h` as: save the cursor, "switch to the Alternate Screen Buffer, clearing it first." `vim`, `less` and `htop` live there. Try it:

```
printf '\e[?1049h\e[H'  # save cursor, switch, clear
seq 1 5
sleep 3
printf '\e[?1049l'      # normal screen, cursor back
```

The numbers vanish when it switches back, and your old screen returns untouched. Nothing from the alternate screen reaches scrollback.

That split decides what a resize does. [xterm.js](https://github.com/xtermjs/xterm.js) is the library behind the VS Code integrated terminal. Its [buffer set](https://github.com/xtermjs/xterm.js/blob/master/src/common/buffer/BufferSet.ts) builds the alternate buffer with scrollback turned off ("The alt buffer should never have scrollback"), and its [buffer code](https://github.com/xtermjs/xterm.js/blob/master/src/common/buffer/Buffer.ts) only enables reflow when the buffer has scrollback. So on the normal screen the terminal rewraps old lines for you. On the alternate screen it does not, and the program has to repaint every cell itself.

**On the normal screen, hard line breaks break reflow.** A terminal can rejoin a line it wrapped itself. It cannot rejoin a line the program broke by writing a newline: the terminal marks the rows it wrapped itself, and a newline from the program looks the same as one the program meant. Commenters on #51828 describe Claude Code's classic renderer as doing this: it wraps output at the current width, then on resize moves the cursor up by the previous frame's height and redraws. Cursor-up stops at the top of the visible screen. When the frame is taller than the viewport, the rows already pushed into scrollback can't be reached, the new frame lands below them, and a resize, narrowing especially, can leave another copy behind. That analysis comes from the reporters, backed by byte-level traces, not from Anthropic, and the issue is still open with no linked fix.

**On the alternate screen, you lose the terminal's own history.** Claude Code's [fullscreen rendering](https://code.claude.com/docs/en/fullscreen) draws on the alternate screen "like `vim` or `htop`" and only renders visible messages. The docs are clear about the cost: `Cmd+f` and tmux search "don't see the conversation because it lives in the alternate screen buffer, not the native scrollback", so the app ships its own scrolling, its own search (`Ctrl+o` then `/`), and a key (`[`) that writes the transcript back into native scrollback on demand. The `/tui fullscreen` command switches renderers mid-conversation, and `/tui default` goes back. The #51828 thread lists `"tui": "fullscreen"` as the workaround that removes the duplication.

Neither choice is wrong. Normal screen keeps your terminal's search and selection and pays for it in reflow bugs. Alternate screen repaints cleanly and pays for it by rebuilding scrollback inside the app.

There is one more party in a resize, and it is the one you are dragging. An IDE that hosts terminals has its own viewport over the terminal's scrollback, and a resize moves the ground under it: the row count changes, reflow may rewrite line positions, and an agent on the normal screen may be repainting at the same moment. If you were scrolled up reading when that happens, the host has to decide where your view should end up.

I build [Hermes IDE](https://hermes-ide.com), so weigh this section with that in mind. Hermes runs each agent (Claude Code, Codex, Antigravity CLI and the rest) in a real terminal running the agent's own interface, with split panes and terminals that auto-fit to the window, which means it sits right on this layer. The [2.3.4 release](https://github.com/hermes-hq/hermes-ide/releases) on October 15, 2026 fixed exactly this: "Scrolling back through a terminal is no longer interrupted when the window resizes." The same release fixed "A session pane no longer stops working at some window widths," and folds the bars above and below a session into its header in short panes so the agent gets more rows.

*Hermes 2.3.4: Codex and Claude Code in two split panes, each in its own hermes/ worktree, with status, context use, cost and branch in the sidebar.*

That fix does not touch the agent's renderer. If an agent on the normal screen re-emits its transcript on resize, the duplicate rows are real bytes in scrollback, and no host can tell them from output the agent meant to print. Hermes is source-available under BSL 1.1 and runs on macOS, Windows and Linux. [Download it here](https://hermes-ide.com/download) or read the code on [GitHub](https://github.com/hermes-hq/hermes-ide).

Work down the chain:

`stty size` in the same pane. If it disagrees with what you see, the emulator never sent `TIOCSWINSZ`, or something in between (tmux, an SSH hop) did not pass it on.`winch.mjs` in that pane. If no `resize` lines appear, the signal is not reaching the foreground process. On Windows, see the caveat above: this script may stay silent even when resizing works.`/tui fullscreen` and see if it goes away.`Ctrl+L` redraws without losing the conversation, and `CLAUDE_CODE_ALT_SCREEN_FULL_REPAINT=1` repaints every cell each frame for terminals such as Windows Terminal that mishandle incremental writes.
Keep `winch.mjs` around. The next time an agent smears its output across scrollback, it tells you in seconds whether the signal ever arrived.

The `winch.mjs` above is the start of every TypeScript agent that lives in a terminal: it has to know its size, react to events and keep running. *AI That Acts* is where I take that further, from tool calling and plain functions to a first working agent in TypeScript that decides what to run next.

It is book 3 of *AI in TypeScript*, a five-book series that runs from your first LLM call through to agents you can leave running in production.
