I freqently copy output from Claude Code or GitHub Copilot CLI to store it in my personal notes for followup or to share a message on Slack.
In doing so, I'd often paste it into TextEdit first to manually remove the trailing whitespace or fix the linebreaks that the AI tools inserted at unnatural places.
It's a first-world level annoyance. But I do it many times a day, so I made ai-clean to do it for me, and I made it freely available assuming someone else out there must have the same annoyance.
What it does
Whenever I want to copy the output of Claude Code or Copilot CLI, I swap to another open Terminal tab and run ai-clean. It trims the whitespace and fixes the linebreaks. Then I paste it into Notes or Slack.
There's a live demo at ai-clean.dev — paste your own messy clipboard contents and see what happens. It runs the same Go cleanup
pipeline as the CLI, compiled to WebAssembly, all client-side.
How it works
The cleanup runs as a pipeline of heuristics:
- (Optional) strip ANSI / OSC escape sequences.
- Remove pure box-drawing border lines (
┌─┐,└─┘,═══). - Strip leading chrome — dedent the minimum-common leading whitespace, then strip a uniform leading border character (
│,|,>, etc.) when it appears on ≥80% of non-empty lines. - Strip trailing chrome — mirror of step 3 for the right side.
- Rejoin wrapped lines (conservatively: preserves code blocks and markdown tables).
- Collapse runs of 3+ blank lines down to 2.
Steps 2–5 run inside a fix-point loop. Each stage can produce input that another stage would clean further — a trailing-strip can turn a mixed line into pure box chrome; a rejoin can expose leading whitespace from the merged tail. So the loop runs until a full pass makes no change. Convergence is guaranteed because
every changing pass strictly shrinks the document.
The invariant that keeps it honest
Clean(Clean(x), opts) == Clean(x, opts) must hold for all inputs. A second pass should be a no-op.
There's a FuzzClean test that throws random bytes at the pipeline and asserts the invariant holds. It's caught and helped me fix bugs that wouldn't have been found otherwise:
- Residue from nested-border peeling when an inner border survives the outer border's strip.
- Lone
\rcharacters not getting normalized to\n, breaking idempotency on the second pass. - Mixed-content lines turning into pure box chrome after a strip — which the box-border pre-pass would then remove on the next iteration, but not on the first.
If you touch the pipeline, you run the fuzzer for 30 seconds. It's caught every behavioral regression so far.
A few opinionated choices
-
Pure Go, no Cgo. Cross-compilation is
GOOS=… GOARCH=… go buildwith no special handling. The clipboard library (atotto/clipboard) was chosen specifically because it avoids Cgo by shelling out to system helpers on Linux. - 80% threshold for border detection, not 100%. Real-world bordered output sometimes has one occasional missing border line. 100% misses those; 80% catches them without false positives.
-
Markdown-table guard. If a candidate
|border is present but rows also have interior|characters (likely a markdown table), the strip is skipped. This was a bug I shipped and then fixed.
Try it
- Browser demo: ai-clean.dev
- Source: github.com/TheAndruu/ai-clean
- Install:
brew install TheAndruu/tap/ai-clean
MIT licensed. Issues, PRs, and roasts on the heuristics welcome.

Top comments (0)