statusLine: pass terminal columns to command via stdin or env, and account for visual line wrapping
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->undefinedprocess.env.COLUMNS->undefinedtput 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:
- 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)
- 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
- Add to
~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "node -e \"console.log('A'.repeat(200))\""
}
}
- Start a Claude Code session
- Press Shift+Enter in the input area
- 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
19 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is not a duplicate of the suggested issues.
process.stdout.columns,stderr.columns,env.COLUMNSare allundefinedin 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.
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" />
General +1 on the idea: would love it if Claude passed the column width down to the status line somehow.
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:
Strongest case for Option B (COLUMNS env var)
Setting COLUMNS in the child process environment would:
Option A (stdin JSON) would also work but requires statusLine commands to parse stdin, which not all implementations do.
Environment
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:
From inside a Bash tool call:
The fix is straightforward: Claude Code already knows the terminal dimensions — just pass
COLUMNSandLINESas env vars to child processes (hooks, Bash tool, statusLine commands). This would immediately benefit any tool that renders structured terminal output.To clarify the severity: this doesn't just produce suboptimal output — it can actively break rendering. Tools that detect width via
tput colsget 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=0value is particularly misleading — tools that check for it get zero, which some interpret as "no terminal" rather than "unknown width."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
+1 — this limitation is now causing significant fragmentation in the plugin ecosystem.
Impact on plugins
The popular
claude-hudplugin 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/tty→ENXIO: Device not configuredstty size < /dev/tty→ENXIOSo 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
COLUMNSvia 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.
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.
still unaddressed?
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| stub120||
tput cols| stub80||
$env:COLUMNS/ JediTerm env vars | unset (onlyTERMINAL_EMULATORandTERM_SESSION_IDare exported — no width field) |The MSYS2
/dev/ttycase 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_COMMANDhook writestput colsto 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 parallelpromptfunction 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.
+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→ unsettput cols→ falls back to 80stty 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):
terminal.columnsin the stdin JSON (Option A in #22115). Minimum viable; lets every script self-truncate.statusLine.maxLinescap so scripts know the budget.Happy to test a prerelease.
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) →COLUMNSenv →shutil.get_terminal_size→os.get_terminal_size(fd)for stderr/stdout/stdin →stty size < /dev/tty→tput cols 2>/dev/tty→ safe default. Each step is wrapped in try/except so missing tools / closed/dev/ttyfall 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.columnsin 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? 🤞
+1 — confirming this regressed further in Claude Code 2.1.139. The release note:
"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.columns→undefined(piped, was already true)COLUMNSenv → unset (Claude Code does not forward it)stty size < /dev/tty→device not configured(no controlling TTY, was sometimes still reachable before 2.1.139 depending on invocation path)tput cols→80(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
tputstub and pin statusline rendering at an 80-col layout regardless of the user's real terminal width. I tripped this directly: a conservative< 70threshold that worked fine through 2.1.138 stopped working in 2.1.139 becausetput's 80 passes the check.**The practical effect is that after 2.1.139,
COLUMNSpassed via env or aterminal.columnsfield 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 (COLUMNSenv) 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.
+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/ttyetc. 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:
terminalWidthconfig option to paper over the cases it misses — same hand-maintained-width situation everyone else here describesA single
terminal.columnsfield in the stdin JSON (whichsubagentStatusLinealready gets ascolumns) would let us drop all of that. Happy to test a prerelease.+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:
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-statuslineinvoked from any Bash-tool-like context.One thing worth highlighting from the table above:
tput colsconfidently returns80, 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 —tputlies with a straight face.Strong +1 on Option A (
terminal.columnsin stdin JSON), with the parity argument:subagentStatusLinealready gets acolumnsfield; the parentstatusLinepayload 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).
This issue was fixed as of version 2.1.153.