●WORKFLOWS — Dynamic workflows are GA in Claude Code: Claude plans the work, runs hundreds of parallel subagents in one session, and verifies outputs before reporting back●SCALE — Paired with Opus 4.8, Claude Code can carry codebase-scale migrations across hundreds of thousands of lines from kickoff to merge●ULTRACODE — The ultracode setting, via the effort menu, sets effort to xhigh and lets Claude decide automatically when to use a workflow●EFFORT — claude.ai adds an effort control beside the model selector; pick extra (xhigh in Code) or max for harder, long-running tasks●WFSIZE — A Dynamic workflow size setting in /config controls how large Claude makes workflows (small, medium, or large agent counts)●OPUS48 — Claude Opus 4.8 remains generally available, improving on 4.7 across coding, agentic skills, reasoning, and knowledge work●WORKFLOWS — Dynamic workflows are GA in Claude Code: Claude plans the work, runs hundreds of parallel subagents in one session, and verifies outputs before reporting back●SCALE — Paired with Opus 4.8, Claude Code can carry codebase-scale migrations across hundreds of thousands of lines from kickoff to merge●ULTRACODE — The ultracode setting, via the effort menu, sets effort to xhigh and lets Claude decide automatically when to use a workflow●EFFORT — claude.ai adds an effort control beside the model selector; pick extra (xhigh in Code) or max for harder, long-running tasks●WFSIZE — A Dynamic workflow size setting in /config controls how large Claude makes workflows (small, medium, or large agent counts)●OPUS48 — Claude Opus 4.8 remains generally available, improving on 4.7 across coding, agentic skills, reasoning, and knowledge work
Switching Claude Code to Plain-Text Rendering — Three Ways In, and What Your Logs Actually Weigh
I opened last night's Claude Code log and found the same spinner line stacked dozens of times. Here are the three ways to turn on screen reader mode, a short script that measures what escape sequences and redraws really cost you, and what you give up when the rendering goes plain.
I opened last night's log over coffee and just stared at it for a while. The file was 1,800 lines, and most of them were the same progress line. That spinner next to "Thinking…" had been dutifully stacking one row per frame.
On screen, that line only ever existed once. The cursor kept returning to the start, the same spot got repainted, and my eyes read it as a single line. The log file was simply being honest about every repaint along the way.
What the terminal shows you and what flows down the pipe are two different artifacts. I thought I understood that. I had the size of the gap badly wrong until I counted my own log.
There Is a Setting That Changes the Rendering Itself
Claude Code has an opt-in setting called screen reader mode. As the name says, it exists for people using screen readers, and turning it on switches output to plain-text rendering.
It also matters if you never touch assistive technology. A screen that a screen reader struggles to announce and a log that grep or diff struggles to handle fail for the same reason. Cursor moves that repaint the same position, escape sequences that add color, wrapping tuned to your terminal width — helpful when a human is watching, pure noise when a machine reads it later.
So screen reader mode is an accessibility setting, and it is also an answer to a different question: is anyone going to read this output again after the run? I came at it from the second question, not the first.
Three Ways In, and When Each One Fits
There are three activation paths. One would seem like enough, except that they differ in scope, which means in practice you want all three.
Path
How
Scope
When I reach for it
CLI flag
claude --ax-screen-reader
That session only
First try. Undoing it means restarting
Env var
CLAUDE_AX_SCREEN_READER=1
That shell and its children
Prefixing only the runs I want captured
Settings file
"axScreenReader": true
Every session in that environment
Once it has earned permanence
The environment variable earns the most use in day-to-day operation. Because it goes in front of the command, it applies to exactly the runs whose output I want to keep.
# Plain rendering for this one run, raw output saved to diskCLAUDE_AX_SCREEN_READER=1 claude -p "list duplicated constants under src/config" \ > "$HOME/logs/claude-$(date +%Y%m%d-%H%M%S).log" 2>&1
For the settings file, it is one line of JSON.
{ "axScreenReader": true}
Try the flag, move to the env var for the runs that need it, and only then write it into settings if you want it everywhere. Starting at the settings file is how you end up unsure which rendering mode you are actually in when something looks off.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Pick between the three activation paths (--ax-screen-reader, CLAUDE_AX_SCREEN_READER, the axScreenReader setting) based on whether you want a one-off, a per-run switch, or a permanent change
✦A 40-line script that weighs a log in four stages, so you decide your capture format from numbers rather than a hunch
✦A clear picture of what plain rendering costs you — in-place progress, diff coloring, width-aware wrapping — so you know when to switch back
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
I did not want to decide this by feel, so I measured. Four stages: raw bytes, bytes after stripping ANSI sequences, bytes after collapsing carriage-return repaints, and bytes after folding adjacent duplicate lines.
#!/usr/bin/env python3"""claude-log-weigh.py — measure what decoration and redraws cost in a Claude Code log. usage: python3 claude-log-weigh.py <logfile>"""import reimport sysfrom pathlib import Path# CSI sequences (color, cursor moves) and OSC sequences (title setting, etc.)ANSI_RE = re.compile(rb"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")def collapse_cr(data: bytes) -> bytes: """Fold in-line \\r repaints down to what the terminal actually left on screen.""" out = [] for line in data.split(b"\n"): out.append(line.split(b"\r")[-1] if b"\r" in line else line) return b"\n".join(out)def dedupe_adjacent(lines: list) -> list: """Fold runs of identical adjacent lines into one (spinner leftovers).""" result = [] for line in lines: if not result or result[-1] != line: result.append(line) return resultdef main(path: str) -> None: raw = Path(path).read_bytes() stripped = ANSI_RE.sub(b"", raw) collapsed = collapse_cr(stripped) final = b"\n".join(dedupe_adjacent(collapsed.split(b"\n"))) stages = [ ("raw", raw), ("ansi-stripped", stripped), ("cr-collapsed", collapsed), ("dedup-adjacent", final), ] base = len(raw) for name, data in stages: size = len(data) lines = data.count(b"\n") + 1 print(f"{name:16} {size:9,d} bytes {lines:6,d} lines ({size / base:5.1%} of raw)") out = Path(path).with_suffix(".clean.log") out.write_bytes(final) print(f"\nwrote: {out}")if __name__ == "__main__": if len(sys.argv) != 2: sys.exit("usage: python3 claude-log-weigh.py <logfile>") main(sys.argv[1])
collapse_cr is the part that matters. Stripping ANSI alone does not remove the "same line dozens of times" problem I opened with — those rows are not decoration, they are the history of the cursor returning to column zero and rewriting. Decoration and repaint have to be counted as two separate things.
Running a refactoring session log captured with default rendering (1.9 MB raw, roughly 24,000 lines) through the script on my machine:
Stage
Size
Lines
Share of raw
raw
~1.9 MB
~24,000
100%
ansi-stripped
~0.9 MB
~24,000
~48%
cr-collapsed
~0.6 MB
~24,000
~30%
dedup-adjacent
~0.2 MB
~2,600
~11%
Capturing the same work again with CLAUDE_AX_SCREEN_READER=1 in front produced a raw log in the low 0.2 MB range — about the size of the fully processed version. Turning the setting on is close to doing all four stages at capture time instead of afterward.
The 11% is not the point; that number moves with the work. What changed my thinking was the breakdown: about half the weight was decoration, and another chunk was repaint. I had assumed escape sequences were the whole story, and I was half right at best. The line count only dropped at the final fold.
Some Things You Lose
It is not free. Plain rendering takes things away, and three of them made me want the default back.
In-place progress. Spinners and progress indicators append rows instead of overwriting one. The screen keeps scrolling downward, which makes watching a long run feel restless.
Diff coloring. Added and removed lines in an edit proposal are distinguished by markers rather than color. Skimming a diff got noticeably slower for me. On the other hand, copying that same output into a bug report is easier, because no decoration comes along for the ride.
Width-aware wrapping. Formatting tuned to terminal width is more restrained, which hurts in a narrow pane. I usually run in one half of a split, so this one landed.
Interactive work where your hands are on the keyboard wants the default. Runs you intend to read later want plain. That is the whole line, but it is worth confirming the line matches your own habits before you flip the settings file on permanently.
Pin It to the Unattended Side
For me the answer was: default for interactive, plain for anything unattended. That is exactly where the environment variable path shines. One wrapper script guarantees every non-interactive Claude Code run goes through it.
#!/usr/bin/env bash# claude-batch — wrapper for unattended runs. Keeps logs plain.set -euo pipefailLOG_DIR="${CLAUDE_BATCH_LOG_DIR:-$HOME/logs/claude}"mkdir -p "$LOG_DIR"LOG="$LOG_DIR/$(date +%Y%m%d-%H%M%S)-$$.log"# Plain rendering plus color suppression. Both, so nothing slips throughexport CLAUDE_AX_SCREEN_READER=1export NO_COLOR=1{ echo "# started: $(date -Iseconds)" echo "# argv: $*"} >> "$LOG"# Redirect rather than pipe, so the exit code survivesset +eclaude "$@" >> "$LOG" 2>&1STATUS=$?set -eecho "# finished: $(date -Iseconds) status=${STATUS}" >> "$LOG"echo "$LOG"exit "$STATUS"
The set +e fence exists so the caller receives Claude Code's own exit status. Write it as claude ... | tee instead and you get the exit code of the last command in the pipeline, which means failures sail through as successes. In unattended work that mix-up sits undetected for days.
NO_COLOR rides along too. Screen reader mode feels like it should be enough, but it does not govern the commands Claude Code invokes — your test runner, your linter. If you want the log plain, suppress from one layer further out.
When many subagents run in parallel, the sheer volume of log jumps. For deciding how much to hand over in the first place, I worked through that separately in Choosing Dynamic Workflow Size and Effort From a Ledger, Not a Hunch. Getting the capture format right is the prep work that comes before that ledger.
How This Relates to the Rendering Stalls
Earlier in July, a fix landed for terminals freezing or lagging on keystrokes while streaming responses that contain very long lists, tables, paragraphs, or code blocks. Plenty of people will recognize the symptom. I hit it more than once, always right as a long generated table started to flow.
With that fix in, there is no longer any reason to use screen reader mode as a workaround for stalls. Worth stating plainly: this setting is not a performance escape hatch, it changes the output format. Different goal, so borrowing it for that will disappoint you.
That said, now that the stall is fixed, long outputs stream more often — and the heavier logs follow. Order of operations: update first so you have the fix, then make plain rendering the default for runs you intend to read later. The two do not conflict.
Where I Landed
After about two weeks, this is the shape it settled into.
Situation
Rendering
How
Hands-on interactive work
Default
Nothing specified
Overnight, unattended batches
Plain
Wrapper sets CLAUDE_AX_SCREEN_READER=1
Handing a bug log to someone
Plain
One-off --ax-screen-reader
Pasting long output elsewhere
Plain
Same
I never did write it into the settings file. As an indie developer my ratio of interactive to unattended work swings day by day, so keeping the switch in an environment variable stayed easier to live with. If you keep a separate machine or container purely for unattended runs, putting axScreenReader in that environment's settings is the cleaner move.
On the keybinding side, the same update added vimInsertModeRemaps, which lets you map a two-key insert-mode sequence such as jj to Escape. A different axis from rendering, but pointing the same direction: pulling the terminal experience back toward you. When new knobs appear, each one is worth a single deliberate try.
One Next Step
Take your most recent unattended log and run it through the script above. If dedup-adjacent comes in under half of raw, more than half of that file is repaint history. Once you can see that, the decision about the setting makes itself.
I did not arrive at this setting through accessibility. Even so, discovering that the "just emit it plainly" option built for assistive technology was also the right answer for machine-readable logs felt quietly correct. Separating who you are making things readable for changes what a setting means. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.