I was deep into a refactor. Claude Code had been solid for the first hour — renaming modules, updating imports, rewriting tests. Clean work. Then it started suggesting imports from files that didn't exist.
Not typos. Fully invented module paths. Confident syntax pointing at nothing.
I didn't catch it immediately. The code looked right. The structure made sense. But the files it was referencing? Never existed. I burned 20 minutes debugging phantom imports before I realized what was happening.
The context window was full.
The Problem Nobody Warns You About
Claude Code has a finite context window. Every message you send, every file it reads, every diff it generates — all of it accumulates. When that window approaches capacity, the model starts compressing older context. Details get fuzzy. Specifics get replaced with plausible-sounding guesses.
And here's the dangerous part: it doesn't look wrong. The code still has correct syntax. Variable names still follow your conventions. The structure still matches your project. But the actual content — file paths, function signatures, import locations — starts drifting from reality.
I've seen it:
- Hallucinated imports — referencing modules that don't exist, with paths that look plausible
- Phantom files — trying to edit files it read 40 messages ago, using outdated content
- Mixed-up logic — combining patterns from different parts of the codebase into something that compiles but does the wrong thing
- Confident nonsense — generating entire blocks of code that reference APIs or configs from earlier context that have since been compressed away
The worst part? There's no warning. No error. No "hey, I'm running low on context." It just quietly degrades.
The Fix: Make Context Visible
The solution was embarrassingly simple. I needed to see the context usage — not buried in some debug menu, but right there in the terminal, updating in real-time.
Claude Code supports custom statuslines. A shell script that receives session data as JSON on stdin and prints whatever you want to the bottom of the terminal. I wrote one that shows a progress bar for context usage.
Now my terminal looks like this:
[Opus] my-project
[####------] 42% | $0.85 | 12m 34s
That progress bar changed everything.
What Changed
With context usage always visible, I developed a simple habit: when context hits ~70%, start a new session.
Not 90%. Not 80%. 70%.
Why that early? Because degradation doesn't start at 100%. By the time the window is full, the model has already been compressing and losing detail for a while. At 70%, the quality is still solid but you're on borrowed time. Starting fresh with a /compact or a new session at that point means you never enter the danger zone.
Before the statusline, my workflow looked like this:
- Start a session
- Work for an hour
- Notice weird output
- Debug the weird output
- Realize the context is full
- Start over, having wasted 20 minutes
Now:
- Start a session
- Work while glancing at the bar
- See it hit 70%
- Start a new session
- Keep going without interruption
The difference in wasted time is massive.
How Statuslines Work
Claude Code pipes a JSON object to your script's stdin on every update:
{
"model": { "display_name": "Opus" },
"workspace": { "current_dir": "/home/user/my-project" },
"cost": {
"total_cost_usd": 0.85,
"total_duration_ms": 754000,
"total_lines_added": 156,
"total_lines_removed": 23
},
"context_window": {
"used_percentage": 42,
"total_input_tokens": 84000,
"total_output_tokens": 12500
},
"version": "1.0.80"
}
Your script reads it, extracts what matters with jq, and prints formatted output. That's the entire contract.
Here's the context-focused script I started with:
#!/bin/bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name // "unknown"')
ctx=$(echo "$input" | jq -r '.context_window.used_percentage // 0')
# Progress bar
bar_width=10
filled=$((ctx * bar_width / 100))
empty=$((bar_width - filled))
bar=$(printf "%${filled}s" | tr " " "#")$(printf "%${empty}s" | tr " " "-")
# Color: green under 60, yellow 60-80, red above 80
if [ "$ctx" -ge 80 ]; then
CLR="\033[31m"
elif [ "$ctx" -ge 60 ]; then
CLR="\033[33m"
else
CLR="\033[32m"
fi
RST="\033[0m"
echo -e "[${model}] ${CLR}[${bar}] ${ctx}%${RST}"
Green when you're safe. Yellow when you should wrap up. Red when you need to stop and start fresh. You can't miss it.
Setting It Up
Three steps:
- Save the script to
~/.claude/statusline.sh - Run
chmod +x ~/.claude/statusline.sh - Add to
~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
}
}
Note: the key is statusLine (camelCase), not statusline. Restart Claude Code and the bar appears.
I Built a Tool So You Don't Have to Write Bash
After setting up my own statusline, I realized most people aren't going to hand-write bash scripts with ANSI escape codes for a status bar. So I built a Statusline Builder.
Pick a template, toggle the segments you want, choose a color theme, and copy the script. The live preview shows exactly what it'll look like. 30 seconds, no bash knowledge required.
The builder has five presets:
- Minimal — just the model and context percentage
- Standard — model, directory, context, and cost
- Rich — two lines with a progress bar, cost, and duration
- DevOps — context plus lines added/removed
- Colorful — color-coded bar with threshold warnings
You can also mix and match 11 different segments: model, directory, context bar, cost, duration, lines changed, tokens, version.
The Real Lesson
The context window is the single most important resource in a Claude Code session. More important than the model. More important than the prompt. When it runs out, everything degrades — silently.
You wouldn't run a production system without monitoring memory usage. Don't run Claude Code without monitoring context usage.
A progress bar at the bottom of the terminal is all it takes. It's the cheapest insurance you'll ever set up.