{"slug": "claude-clipboard-cleaner-hammerspoon-edition-auto-strips-trailing-padding-2-from", "title": "Claude Clipboard Cleaner (Hammerspoon edition) - auto-strips trailing padding + leading 2-space indent from Claude Code terminal copies", "summary": "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.", "body_md": "|\n-- Claude Clipboard Cleaner (Hammerspoon edition) |\n|\n-- Strips trailing-space padding and the leading 2-space indent that the |\n|\n-- Claude Code terminal adds when you select-copy its output. |\n|\n-- |\n|\n-- Self-contained: drop this file anywhere and load it from ~/.hammerspoon/init.lua: |\n|\n-- dofile(\"/Users/sneh/Downloads/planner/claude-clipboard-cleaner/claude-clipboard-cleaner.lua\") |\n|\n-- |\n|\n-- It only rewrites the clipboard when the content clearly looks like Claude |\n|\n-- terminal output (see the 50%/60% guards below), so copying normal text or |\n|\n-- code from other apps is left untouched. |\n|\n|\n|\nlocal M = {} |\n|\n|\n|\nlocal POLL_INTERVAL = 0.3 -- seconds, same cadence as the original app |\n|\nlocal enabled = true |\n|\nlocal cleanCount = 0 |\n|\n|\n|\n-- MARK: detection ---------------------------------------------------------- |\n|\n|\n|\n-- 50%+ of non-empty lines end with 3+ trailing spaces. Normal text never |\n|\n-- does this; it is unique to fixed-width terminal copy. |\n|\nlocal function hasTrailingPadding(lines) |\n|\nlocal padded, nonEmpty = 0, 0 |\n|\nfor _, line in ipairs(lines) do |\n|\nif line ~= \"\" then |\n|\nnonEmpty = nonEmpty + 1 |\n|\nlocal trailing = #(line:match(\"(%s*)$\") or \"\") |\n|\nif trailing >= 3 then padded = padded + 1 end |\n|\nend |\n|\nend |\n|\nif padded < 3 then return false end |\n|\nreturn (padded / math.max(nonEmpty, 1)) >= 0.5 |\n|\nend |\n|\n|\n|\n-- 60%+ of non-empty lines start with exactly a 2-space indent. |\n|\nlocal function hasLeadingTwoSpace(lines) |\n|\nlocal matched, nonEmpty = 0, 0 |\n|\nfor _, line in ipairs(lines) do |\n|\nif line ~= \"\" then |\n|\nnonEmpty = nonEmpty + 1 |\n|\nif line:sub(1, 2) == \" \" then matched = matched + 1 end |\n|\nend |\n|\nend |\n|\nif nonEmpty == 0 then return false end |\n|\nreturn (matched / nonEmpty) >= 0.6 |\n|\nend |\n|\n|\n|\n-- MARK: cleaning ----------------------------------------------------------- |\n|\n|\n|\nlocal function cleanClaude(text) |\n|\nif not text then return nil end |\n|\n|\n|\nlocal lines = {} |\n|\nfor line in (text .. \"\\n\"):gmatch(\"(.-)\\n\") do lines[#lines + 1] = line end |\n|\nif #lines < 3 then return nil end |\n|\n|\n|\nlocal stripTrailing = hasTrailingPadding(lines) |\n|\nlocal stripLeading = hasLeadingTwoSpace(lines) |\n|\nif not (stripTrailing or stripLeading) then return nil end |\n|\n|\n|\nfor i, line in ipairs(lines) do |\n|\nif stripTrailing then line = line:gsub(\"%s+$\", \"\") end |\n|\nif line:sub(1, 2) == \" \" then line = line:sub(3) end |\n|\nlines[i] = line |\n|\nend |\n|\n|\n|\nlocal out = table.concat(lines, \"\\n\") |\n|\nreturn out ~= text and out or nil |\n|\nend |\n|\n|\n|\nM.cleanClaude = cleanClaude -- exported so it can be unit-tested |\n|\n|\n|\n-- MARK: clipboard watcher -------------------------------------------------- |\n|\n|\n|\nlocal lastChange = hs.pasteboard.changeCount() |\n|\n|\n|\nlocal timer = hs.timer.new(POLL_INTERVAL, function() |\n|\nif not enabled then return end |\n|\nlocal cc = hs.pasteboard.changeCount() |\n|\nif cc == lastChange then return end |\n|\nlastChange = cc |\n|\n|\n|\nlocal text = hs.pasteboard.readString() |\n|\nif not text then return end |\n|\n|\n|\nlocal cleaned = cleanClaude(text) |\n|\nif cleaned then |\n|\nhs.pasteboard.setContents(cleaned) |\n|\nlastChange = hs.pasteboard.changeCount() -- don't re-process our own write |\n|\ncleanCount = cleanCount + 1 |\n|\nif M.menu then M.menu:setTitle(\"⌘C ✓\") end |\n|\nhs.timer.doAfter(0.6, function() |\n|\nif M.menu then M.menu:setTitle(\"⌘C\") end |\n|\nend) |\n|\nend |\n|\nend) |\n|\n|\n|\n-- MARK: menu bar ----------------------------------------------------------- |\n|\n|\n|\nM.menu = hs.menubar.new() |\n|\nif M.menu then |\n|\nM.menu:setTitle(\"⌘C\") |\n|\nM.menu:setTooltip(\"Claude Clipboard Cleaner\") |\n|\nlocal function buildMenu() |\n|\nreturn { |\n|\n{ title = \"Cleaned: \" .. cleanCount .. \" copies\", disabled = true }, |\n|\n{ title = \"-\" }, |\n|\n{ title = enabled and \"Disable\" or \"Enable\", fn = function() |\n|\nenabled = not enabled |\n|\nM.menu:setTitle(enabled and \"⌘C\" or \"⌘C (off)\") |\n|\nend }, |\n|\n{ title = \"Reload config\", fn = function() hs.reload() end }, |\n|\n{ title = \"-\" }, |\n|\n{ title = \"About\", fn = function() |\n|\nhs.urlevent.openURL(\"https://gist.github.com/spbavarva/d6e29e12d3fc8572b62c8523ffccc2a6\") |\n|\nend }, |\n|\n} |\n|\nend |\n|\nM.menu:setMenu(buildMenu) |\n|\nend |\n|\n|\n|\ntimer:start() |\n|\nhs.alert.show(\"Claude Clipboard Cleaner: on\") |\n|\n|\n|\nreturn M |", "url": "https://wpnews.pro/news/claude-clipboard-cleaner-hammerspoon-edition-auto-strips-trailing-padding-2-from", "canonical_source": "https://gist.github.com/spbavarva/d6e29e12d3fc8572b62c8523ffccc2a6", "published_at": "2026-06-21 15:37:29+00:00", "updated_at": "2026-06-21 16:03:24.663664+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models"], "entities": ["Hammerspoon", "Claude Code", "Claude"], "alternates": {"html": "https://wpnews.pro/news/claude-clipboard-cleaner-hammerspoon-edition-auto-strips-trailing-padding-2-from", "markdown": "https://wpnews.pro/news/claude-clipboard-cleaner-hammerspoon-edition-auto-strips-trailing-padding-2-from.md", "text": "https://wpnews.pro/news/claude-clipboard-cleaner-hammerspoon-edition-auto-strips-trailing-padding-2-from.txt", "jsonld": "https://wpnews.pro/news/claude-clipboard-cleaner-hammerspoon-edition-auto-strips-trailing-padding-2-from.jsonld"}}