statusLine: pass terminal columns to command via stdin or env, and account for visual line wrapping

Resolved 💬 19 comments Opened Jan 31, 2026 by MeroZemory Closed May 28, 2026
💡 Likely answer: A maintainer (claude[bot], contributor) responded on this thread — see the highlighted reply below.

Problem

Custom statusLine commands run as child processes with piped stdout. In this context, there is no way to detect the terminal width:

  • process.stdout.columns -> undefined (pipe, not TTY)
  • process.stderr.columns -> undefined
  • process.env.COLUMNS -> undefined
  • tput cols, mode con, stty size -> all fail in pipe context

This means the statusLine command cannot know how wide the terminal is.

Why it matters

Claude Code calculates the status line's row count by counting \n characters. However, if a line exceeds the actual terminal width, it visually wraps to the next row. Claude Code doesn't account for this visual wrapping, causing:

  1. Input area height mismatch - user presses Shift+Enter to add a new line, but the input area doesn't grow (or grows to wrong height)
  2. Excessive blank lines - vertical layout breaks with large empty spaces below the conversation

Since the statusLine command cannot detect terminal width, it cannot truncate its output to prevent wrapping.

Reproduction

  1. Add to ~/.claude/settings.json:
{
  "statusLine": {
    "type": "command",
    "command": "node -e \"console.log('A'.repeat(200))\""
  }
}
  1. Start a Claude Code session
  2. Press Shift+Enter in the input area
  3. Observe: input area height is wrong, blank lines appear below

Proposed solution

Option A: Include terminal dimensions in the stdin JSON:

{
  "terminal": { "columns": 120, "rows": 40 },
  ...existing fields...
}

Option B: Pass terminal width via environment variable (COLUMNS) when spawning the statusLine child process.

Option C (complementary): Claude Code should also account for visual line wrapping when calculating status line height internally, by dividing each line's visible width by the terminal column count.

Environment

  • Claude Code v2.1.27
  • Windows 11
  • Tested with Node.js statusLine command

View original on GitHub ↗

19 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/18785
  2. https://github.com/anthropics/claude-code/issues/10472
  3. https://github.com/anthropics/claude-code/issues/4136

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

MeroZemory · 5 months ago

This is not a duplicate of the suggested issues.

  • #18785, #10472, #4136 are general TUI rendering bugs (blank lines, scrolling) that occur without any custom statusLine.
  • This issue is specifically about the statusLine child process having no way to detect terminal width (process.stdout.columns, stderr.columns, env.COLUMNS are all undefined in the piped child process). This is a feature request to pass terminal dimensions via stdin JSON or environment variables.

The root cause is different: those issues are about Ink's viewport height calculation; this one is about the statusLine command having no access to terminal geometry, making it impossible to prevent visual line wrapping that breaks Claude Code's row count.

jameswasher · 5 months ago

This also causes vertical rendering on startup until the TUI initializes.

<img width="1290" height="838" alt="Image" src="https://github.com/user-attachments/assets/5aff6fe5-24b1-4e96-8703-f9173ae53d3e" />

jsquyres · 4 months ago

General +1 on the idea: would love it if Claude passed the column width down to the status line somehow.

JYHTHEGREAT · 4 months ago

Additional findings from Windows 11 + ConPTY + claude-hud

Confirming and expanding on this issue with concrete data from debugging claude-hud (statusLine plugin).

Width detection: all methods fail in ConPTY piped subprocess

| Method | Result | Tracks resize? |
|--------|--------|----------------|
| process.stdout.columns | undefined | N/A |
| process.stderr.columns | undefined | N/A |
| process.env.COLUMNS | undefined | N/A |
| mode con (cmd.exe) | 120 (buffer width, not window) | No - always returns initial buffer size |
| tput cols (Git Bash) | 80 (default fallback) | No - always returns 80 |

The wrap=truncate compounding issue

Even when the statusLine command wraps its output into multiple shorter lines (via separate console.log calls), Claude Code's Text wrap=truncate truncates each line with ... if it exceeds the Ink rendering width. Since the subprocess cannot detect the actual rendering width, it is impossible to produce correctly-sized output.

Tested behavior:

  • statusLine outputs 2 lines (via separate console.log calls) - Claude Code renders both lines OK
  • Any single line exceeding Ink rendering width - truncated with ...
  • Window resize - mode con still returns stale value - wrapping breaks

Strongest case for Option B (COLUMNS env var)

Setting COLUMNS in the child process environment would:

  1. Be immediately available (process.env.COLUMNS) - no subprocess overhead
  2. Track window resize (if Claude Code updates it on each invocation)
  3. Allow statusLine commands to wrap/truncate correctly
  4. Be a one-line fix on the Claude Code side

Option A (stdin JSON) would also work but requires statusLine commands to parse stdin, which not all implementations do.

Environment

  • Claude Code: latest (PE binary, Windows)
  • Windows 11 Pro 10.0.26200
  • Windows Terminal with ConPTY
  • claude-hud v0.0.9
aaronsb · 3 months ago

This affects more than just statusLine — any tool invoked via the Bash tool or hooks that renders terminal UI is blind to the actual width.

Concrete examples from claude-code-config:

  • mmaid — Mermaid diagram renderer, renders at 80 cols regardless of actual terminal width
  • chart-tool — ANSI bar charts, sparklines, histograms all assume 80 cols
  • embed-status / ways-stats / governance-report — CLI dashboards that could use the full width for tables

From inside a Bash tool call:

COLUMNS=0
tput cols → 80 (default, not real)
stty size → fails (no tty)

The fix is straightforward: Claude Code already knows the terminal dimensions — just pass COLUMNS and LINES as env vars to child processes (hooks, Bash tool, statusLine commands). This would immediately benefit any tool that renders structured terminal output.

aaronsb · 3 months ago

To clarify the severity: this doesn't just produce suboptimal output — it can actively break rendering. Tools that detect width via tput cols get 80 (the hardcoded default), but if the real terminal is narrower (split panes, tiling WMs, mobile), the output wraps unpredictably and becomes unreadable. And if wider, you're wasting half the screen.

The COLUMNS=0 value is particularly misleading — tools that check for it get zero, which some interpret as "no terminal" rather than "unknown width."

seanmartinsmith · 3 months ago

came here from my own duplicate (#41512). same need - building a responsive statusline in powershell that needs to shed segments when the terminal is narrow, can't get real dimensions from the subprocess.

+1 for option B (pass COLUMNS/LINES as env vars) as the simplest fix. would also benefit hooks and bash tool calls as aaronsb pointed out

kiki830621 · 3 months ago

+1 — this limitation is now causing significant fragmentation in the plugin ecosystem.

Impact on plugins

The popular claude-hud plugin has accumulated 11 open PRs and 5+ open issues all addressing the same root cause: subprocess can't detect terminal width. See jarrodwatts/claude-hud#416 for a comprehensive analysis.

Each plugin author is forced to ship their own workaround — injected COLUMNS=$(tput cols), arbitrary fallback constants (40, 120, 160), ancestor-TTY process tree walks, etc. — and none work reliably across macOS bare terminals (no controlling TTY), tmux, zellij, and Windows.

Real-world verification on macOS

On Terminal.app / iTerm2 (no multiplexer), Claude Code 2.0.x:

  • tput cols < /dev/ttyENXIO: Device not configured
  • stty size < /dev/ttyENXIO

So even the subprocess's "last resort" of reading from the controlling terminal doesn't work, because Claude Code's child doesn't inherit a controlling TTY.

Preferred solution

Passing COLUMNS via env var would solve this for 95% of plugins with zero plugin-side changes. Passing via stdin JSON (as this issue proposes) would also work and gives plugins the flexibility to handle per-line widths.

Even a single terminal-width field in the stdin JSON would be enough — the "visual wrapping accounting" concern is a separate problem plugins can address themselves once they know the width.

Amadeus- · 3 months ago

Honestly, I just want to be able to make my status line right-justified so that it is away from the text that Claude Code already puts on the bottom left corner.

seanmartinsmith · 2 months ago

still unaddressed?

carlospinto4p · 2 months ago

Adding data from another corner this hits: JetBrains IDE terminals (JediTerm — PyCharm, IntelliJ, etc.) on Windows.

Every probe confirmed from a subprocess matching the statusline contract (stdin piped, MSYS2 Git Bash environment):

| Method | Result |
| --- | --- |
| stty size </dev/tty | /dev/tty: No such device or address |
| [Console]::WindowWidth (PowerShell) | Invalid handle |
| mode.com con | stub 120 |
| tput cols | stub 80 |
| $env:COLUMNS / JediTerm env vars | unset (only TERMINAL_EMULATOR and TERM_SESSION_ID are exported — no width field) |

The MSYS2 /dev/tty case is notable — the subprocess was spawned with pipes (no pty), so even the POSIX-style last-resort of reading the controlling terminal is unavailable. That's the same pattern @kiki830621 reported for macOS (ENXIO), just with a different error surface.

Interim workaround on my end: a PROMPT_COMMAND hook writes tput cols to a file from the user's interactive shell, and the statusline script reads it. Works but requires pressing Enter after each resize, and only covers bash (PowerShell users need a parallel prompt function hook). Solved for my setup, but a genuinely brittle ecosystem problem.

Strong +1 on Option B (env vars) — would also fix hooks and Bash tool calls as @aaronsb noted, not just statusline.

alchemymakemoney · 2 months ago

+1 — hitting this on Claude Code 2.1.136 with a multi-row bash statusline (~14 lines: machine/branch/PR header, per-flow Metaflow progress rows, per-file git churn rows). Symptom: only line 1 reaches the TUI; lines 2+ are silently absent (not wrapped, not overlapped — just gone).

Confirms #22115's diagnosis from a different angle: the statusline child genuinely has no way to detect terminal width:

  • process.stdout.columns → undefined (pipe)
  • $COLUMNS → unset
  • tput cols → falls back to 80
  • stty size < /dev/tty/dev/tty: No such device or address (no controlling TTY for the child)

So even a well-behaved script that wants to self-truncate per line can't.

Extra wrinkle worth noting on the renderer side: our hidden lines carry OSC 8 hyperlinks (\033]8;;URL\a…\033]8;;\a) and ANSI SGR. Visible width per line is modest (~80–120 cols), but raw byte length is 300+ per line. If the row-height accounting measures bytes rather than visible width, that alone would explain why lines 2+ vanish on terminals that should comfortably fit them visually.

Asks (in order of usefulness):

  1. Pass terminal.columns in the stdin JSON (Option A in #22115). Minimum viable; lets every script self-truncate.
  2. Strip ANSI/OSC escapes before measuring visible width when computing row count and wrap, if not already done.
  3. Optional: a documented statusLine.maxLines cap so scripts know the budget.

Happy to test a prerelease.

mkalkere · 2 months ago

Sharing a downstream workaround for anyone hitting this in Python statuslines:

claude-status v0.5.7 (released today) walks a 7-step fallback chain to recover terminal width when the subprocess context hides it: stdin terminal.columns (forward-compat for whenever this issue lands) → COLUMNS env → shutil.get_terminal_sizeos.get_terminal_size(fd) for stderr/stdout/stdin → stty size < /dev/ttytput cols 2>/dev/tty → safe default. Each step is wrapped in try/except so missing tools / closed /dev/tty fall through silently, with a 1s timeout on the subprocess probes.

Real-world impact in our case: a 165-col terminal that previously rendered only ~83 chars on Line 2 now renders ~137 chars (5 sections → 11 sections). Pure stdlib, no daemon, no perf cost on the cheap paths.

Implementation: _detect_terminal_width() in cli.py. Happy to extract the helper into a standalone snippet if it'd be useful for other plugin authors hitting this.

Still very much in favor of the upstream fix — the cleanest signal will always be Anthropic putting terminal.columns in stdin JSON. Pinging in case it helps prioritize.

seanmartinsmith · 2 months ago
Sharing a downstream workaround for anyone hitting this in Python statuslines: claude-status v0.5.7 (released today) walks a 7-step fallback chain to recover terminal width when the subprocess context hides it: stdin terminal.columns (forward-compat for whenever this issue lands) → COLUMNS env → shutil.get_terminal_sizeos.get_terminal_size(fd) for stderr/stdout/stdin → stty size < /dev/ttytput cols 2>/dev/tty → safe default. Each step is wrapped in try/except so missing tools / closed /dev/tty fall through silently, with a 1s timeout on the subprocess probes. Real-world impact in our case: a 165-col terminal that previously rendered only ~83 chars on Line 2 now renders ~137 chars (5 sections → 11 sections). Pure stdlib, no daemon, no perf cost on the cheap paths. Implementation: _detect_terminal_width() in cli.py. Happy to extract the helper into a standalone snippet if it'd be useful for other plugin authors hitting this. Still very much in favor of the upstream fix — the cleanest signal will always be Anthropic putting terminal.columns in stdin JSON. Pinging in case it helps prioritize.

wouldn't mind having a snippet or gist to play around with. thanks. but this does feel like a gap in cc that could be easily implemented especially as they develop the interface and status line with multi agent support. so, continuing the comments chain here for now seems like the best path to raise awareness and hopefully get future support? 🤞

jeremy-newhouse · 2 months ago

+1 — confirming this regressed further in Claude Code 2.1.139. The release note:

Fixed a bug where a hook writing to the terminal could corrupt an on-screen interactive prompt; hooks now run without terminal access

"Hooks now run without terminal access" closes the last TTY-based escape hatch that the workaround chains in this thread rely on. Observed on macOS, 2.1.139, actual terminal 182 cols:

  • process.stdout/stderr.columnsundefined (piped, was already true)
  • COLUMNS env → unset (Claude Code does not forward it)
  • stty size < /dev/ttydevice not configured (no controlling TTY, was sometimes still reachable before 2.1.139 depending on invocation path)
  • tput cols80 (its no-TTY stub fallback)

All four signals now either fail or return a misleading stub. Any "> N" sanity threshold under 80 will silently accept the tput stub and pin statusline rendering at an 80-col layout regardless of the user's real terminal width. I tripped this directly: a conservative < 70 threshold that worked fine through 2.1.138 stopped working in 2.1.139 because tput's 80 passes the check.

**The practical effect is that after 2.1.139, COLUMNS passed via env or a terminal.columns field in stdin is the only path that can work.** The fallback chains this thread has been building up (tput, stty, /dev/tty, etc.) are now entirely dead code on macOS and, I suspect, on Linux too — just more obviously than before.

Strongly preferring Option A (terminal: { columns, rows } in stdin JSON) from the original issue. Option B (COLUMNS env) would also solve it and is arguably cheaper to ship — with the caveat that it won't adapt to mid-session terminal resizes unless the hook is re-invoked with an updated env on each refresh tick.

Happy to test a prerelease.

moonD4rk · 2 months ago

+1 from a Go statusline implementation (ccstatus, I'm the author).

Option A (terminal: { columns, rows } in stdin JSON) would be great.

Most of the thread is Node/PowerShell/Python, so just adding a Go data point: we hit the exact same wall — the statusline child has no TTY, so COLUMNS, tput cols, /dev/tty etc. all fail or lie.

What we do today is a workaround: walk up the process tree until we find an ancestor that still owns the terminal, and read the size from there. When it works, it works well — the screenshots below are from Ghostty, where the layout adapts cleanly as I resize the terminal:

<!-- IMAGE 1: ccstatus in Ghostty at one terminal width -->

<img width="1333" height="832" alt="Image" src="https://github.com/user-attachments/assets/7fbeabd9-1847-4615-89c5-1e370bf9341a" />

<!-- IMAGE 2: ccstatus in Ghostty at a different width — layout re-flows -->

<img width="364" height="832" alt="Image" src="https://github.com/user-attachments/assets/cb59579f-ad42-4fe8-83c8-93fa90c8cfc8" />

But "works in Ghostty on macOS" is the trap. It's a compromise, not a fix:

  • it does nothing on Windows / WSL / IDE terminals — exactly where this thread reports the worst breakage
  • when it can't find the width, the status line falls back to 80 columns and renders wrong, with no signal that anything went off
  • we still ship a manual terminalWidth config option to paper over the cases it misses — same hand-maintained-width situation everyone else here describes

A single terminal.columns field in the stdin JSON (which subagentStatusLine already gets as columns) would let us drop all of that. Happy to test a prerelease.

joshsymonds · 2 months ago

+1 — independent confirmation of the 2.1.139 regression from a different stack: Linux (NixOS), kitty terminal (TERM=xterm-kitty), real terminal width ~220 cols, custom Go statusline (cc-tools).

Probed from inside the Claude subprocess environment on 2.1.139:

COLUMNS                                    → "0" (set, but zero)
TMUX, SSH_TTY                              → empty
open("/dev/tty")                           → ENXIO ("no such device or address")
ioctl(TIOCGWINSZ) on stderr/stdout/stdin   → ENOTTY
tput cols                                  → 80   (terminfo default for xterm-kitty)
stty size                                  → fails (no controlling tty)

The render pins to ~80 cols against the left edge regardless of the actual terminal — exactly what @jeremy-newhouse described last week. Reproducible deterministically with echo '{...}' | cc-tools-statusline invoked from any Bash-tool-like context.

One thing worth highlighting from the table above: tput cols confidently returns 80, which silently passes any "> N" sanity threshold under 80. Statuslines that previously fell back to a sensible default (e.g., 200) when they detected "no real terminal" now happily render an 80-col layout into a 220-col window, because nothing in the chain reports failure — tput lies with a straight face.

Strong +1 on Option A (terminal.columns in stdin JSON), with the parity argument: subagentStatusLine already gets a columns field; the parent statusLine payload just doesn't.

Implementing @moond4rk's parent-process-tree walk in cc-tools locally as a workaround until then — with the documented caveat that it only helps on real-pty hosts (Linux, macOS sans IDE).

claude[bot] contributor · 1 month ago

This issue was fixed as of version 2.1.153.