<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrew</title>
    <description>The latest articles on DEV Community by Andrew (@theandruu).</description>
    <link>https://dev.to/theandruu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901115%2F2c089b67-bbf9-479a-8319-7cd81fb1e563.png</url>
      <title>DEV Community: Andrew</title>
      <link>https://dev.to/theandruu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/theandruu"/>
    <language>en</language>
    <item>
      <title>Why I built a CLI to clean up text copied out of AI coding tools</title>
      <dc:creator>Andrew</dc:creator>
      <pubDate>Mon, 27 Apr 2026 19:41:33 +0000</pubDate>
      <link>https://dev.to/theandruu/why-i-built-a-cli-to-clean-up-text-copied-out-of-ai-coding-tools-1c4m</link>
      <guid>https://dev.to/theandruu/why-i-built-a-cli-to-clean-up-text-copied-out-of-ai-coding-tools-1c4m</guid>
      <description>&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It's a first-world level annoyance. But I do it many times a day, so I made &lt;a href="https://github.com/TheAndruu/ai-clean" rel="noopener noreferrer"&gt;ai-clean&lt;/a&gt; to do it for me, and I made it freely available assuming someone else out there must have the same annoyance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Whenever I want to copy the output of Claude Code or Copilot CLI, I swap to another open Terminal tab and run &lt;code&gt;ai-clean&lt;/code&gt;.  It trims the whitespace and fixes the linebreaks.  Then I paste it into Notes or Slack.  &lt;/p&gt;

&lt;p&gt;There's a live demo at &lt;a href="https://ai-clean.dev" rel="noopener noreferrer"&gt;ai-clean.dev&lt;/a&gt; — paste your own messy clipboard contents and see what happens. It runs the same Go cleanup&lt;br&gt;
pipeline as the CLI, compiled to WebAssembly, all client-side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5yux7b58zetlnesmwm0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5yux7b58zetlnesmwm0.gif" alt="demo" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The cleanup runs as a pipeline of heuristics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;(Optional) strip ANSI / OSC escape sequences.&lt;/li&gt;
&lt;li&gt;Remove pure box-drawing border lines (&lt;code&gt;┌─┐&lt;/code&gt;, &lt;code&gt;└─┘&lt;/code&gt;, &lt;code&gt;═══&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Strip leading chrome — dedent the minimum-common leading whitespace, then strip a uniform leading border character (&lt;code&gt;│&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, etc.) when it appears on ≥80% of non-empty lines.&lt;/li&gt;
&lt;li&gt;Strip trailing chrome — mirror of step 3 for the right side.&lt;/li&gt;
&lt;li&gt;Rejoin wrapped lines (conservatively: preserves code blocks and markdown tables).&lt;/li&gt;
&lt;li&gt;Collapse runs of 3+ blank lines down to 2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Steps 2–5 run inside a fix-point loop&lt;/strong&gt;. 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&lt;br&gt;
every changing pass strictly shrinks the document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invariant that keeps it honest
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Clean(Clean(x), opts) == Clean(x, opts)&lt;/code&gt; must hold for all inputs. A second pass should be a no-op.&lt;/p&gt;

&lt;p&gt;There's a &lt;code&gt;FuzzClean&lt;/code&gt; 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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Residue from nested-border peeling when an inner border survives the outer border's strip.&lt;/li&gt;
&lt;li&gt;Lone &lt;code&gt;\r&lt;/code&gt; characters not getting normalized to &lt;code&gt;\n&lt;/code&gt;, breaking idempotency on the second pass.&lt;/li&gt;
&lt;li&gt;Mixed-content lines turning into pure box chrome after a strip — which the box-border pre-pass would then remove on the &lt;em&gt;next&lt;/em&gt; iteration, but not on the first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you touch the pipeline, you run the fuzzer for 30 seconds. It's caught every behavioral regression so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few opinionated choices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure Go, no Cgo.&lt;/strong&gt; Cross-compilation is &lt;code&gt;GOOS=… GOARCH=… go build&lt;/code&gt; with no special handling. The clipboard library (&lt;a href="https://github.com/atotto/clipboard" rel="noopener noreferrer"&gt;atotto/clipboard&lt;/a&gt;) was chosen specifically because it avoids Cgo by shelling out to system helpers on Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80% threshold for border detection, not 100%.&lt;/strong&gt; Real-world bordered output sometimes has one occasional missing border line. 100% misses those; 80% catches them without false positives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown-table guard.&lt;/strong&gt; If a candidate &lt;code&gt;|&lt;/code&gt; border is present but rows also have interior &lt;code&gt;|&lt;/code&gt; characters (likely a markdown table), the strip is skipped. This was a bug I shipped and then fixed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Browser demo: &lt;a href="https://ai-clean.dev" rel="noopener noreferrer"&gt;ai-clean.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/TheAndruu/ai-clean" rel="noopener noreferrer"&gt;github.com/TheAndruu/ai-clean&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;brew install TheAndruu/tap/ai-clean&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Issues, PRs, and roasts on the heuristics welcome.&lt;/p&gt;

</description>
      <category>go</category>
      <category>cli</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
