Claude Clipboard Cleaner (Hammerspoon edition) - auto-strips trailing padding + leading 2-space indent from Claude Code terminal copies A developer created a Hammerspoon-based clipboard cleaner that automatically removes trailing-space padding and leading two-space indentation from Claude Code terminal output. The tool, written in Lua, uses heuristic detection to only modify clipboard content that matches the pattern of Claude terminal copies, leaving normal text untouched. | -- Claude Clipboard Cleaner Hammerspoon edition | | -- Strips trailing-space padding and the leading 2-space indent that the | | -- Claude Code terminal adds when you select-copy its output. | | -- | | -- Self-contained: drop this file anywhere and load it from ~/.hammerspoon/init.lua: | | -- dofile "/Users/sneh/Downloads/planner/claude-clipboard-cleaner/claude-clipboard-cleaner.lua" | | -- | | -- It only rewrites the clipboard when the content clearly looks like Claude | | -- terminal output see the 50%/60% guards below , so copying normal text or | | -- code from other apps is left untouched. | | | | local M = {} | | | | local POLL INTERVAL = 0.3 -- seconds, same cadence as the original app | | local enabled = true | | local cleanCount = 0 | | | | -- MARK: detection ---------------------------------------------------------- | | | | -- 50%+ of non-empty lines end with 3+ trailing spaces. Normal text never | | -- does this; it is unique to fixed-width terminal copy. | | local function hasTrailingPadding lines | | local padded, nonEmpty = 0, 0 | | for , line in ipairs lines do | | if line ~= "" then | | nonEmpty = nonEmpty + 1 | | local trailing = line:match " %s $" or "" | | if trailing = 3 then padded = padded + 1 end | | end | | end | | if padded < 3 then return false end | | return padded / math.max nonEmpty, 1 = 0.5 | | end | | | | -- 60%+ of non-empty lines start with exactly a 2-space indent. | | local function hasLeadingTwoSpace lines | | local matched, nonEmpty = 0, 0 | | for , line in ipairs lines do | | if line ~= "" then | | nonEmpty = nonEmpty + 1 | | if line:sub 1, 2 == " " then matched = matched + 1 end | | end | | end | | if nonEmpty == 0 then return false end | | return matched / nonEmpty = 0.6 | | end | | | | -- MARK: cleaning ----------------------------------------------------------- | | | | local function cleanClaude text | | if not text then return nil end | | | | local lines = {} | | for line in text .. "\n" :gmatch " .- \n" do lines lines + 1 = line end | | if lines < 3 then return nil end | | | | local stripTrailing = hasTrailingPadding lines | | local stripLeading = hasLeadingTwoSpace lines | | if not stripTrailing or stripLeading then return nil end | | | | for i, line in ipairs lines do | | if stripTrailing then line = line:gsub "%s+$", "" end | | if line:sub 1, 2 == " " then line = line:sub 3 end | | lines i = line | | end | | | | local out = table.concat lines, "\n" | | return out ~= text and out or nil | | end | | | | M.cleanClaude = cleanClaude -- exported so it can be unit-tested | | | | -- MARK: clipboard watcher -------------------------------------------------- | | | | local lastChange = hs.pasteboard.changeCount | | | | local timer = hs.timer.new POLL INTERVAL, function | | if not enabled then return end | | local cc = hs.pasteboard.changeCount | | if cc == lastChange then return end | | lastChange = cc | | | | local text = hs.pasteboard.readString | | if not text then return end | | | | local cleaned = cleanClaude text | | if cleaned then | | hs.pasteboard.setContents cleaned | | lastChange = hs.pasteboard.changeCount -- don't re-process our own write | | cleanCount = cleanCount + 1 | | if M.menu then M.menu:setTitle "⌘C ✓" end | | hs.timer.doAfter 0.6, function | | if M.menu then M.menu:setTitle "⌘C" end | | end | | end | | end | | | | -- MARK: menu bar ----------------------------------------------------------- | | | | M.menu = hs.menubar.new | | if M.menu then | | M.menu:setTitle "⌘C" | | M.menu:setTooltip "Claude Clipboard Cleaner" | | local function buildMenu | | return { | | { title = "Cleaned: " .. cleanCount .. " copies", disabled = true }, | | { title = "-" }, | | { title = enabled and "Disable" or "Enable", fn = function | | enabled = not enabled | | M.menu:setTitle enabled and "⌘C" or "⌘C off " | | end }, | | { title = "Reload config", fn = function hs.reload end }, | | { title = "-" }, | | { title = "About", fn = function | | hs.urlevent.openURL "https://gist.github.com/spbavarva/d6e29e12d3fc8572b62c8523ffccc2a6" | | end }, | | } | | end | | M.menu:setMenu buildMenu | | end | | | | timer:start | | hs.alert.show "Claude Clipboard Cleaner: on" | | | | return M |