cd /news/developer-tools/claude-clipboard-cleaner-hammerspoonโ€ฆ ยท home โ€บ topics โ€บ developer-tools โ€บ article
[ARTICLE ยท art-35687] src=gist.github.com โ†— pub= topic=developer-tools verified=true sentiment=ยท neutral

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.

read4 min views1 publishedJun 21, 2026

| -- 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 |

โ”€โ”€ more in #developer-tools 4 stories ยท sorted by recency
โ”€โ”€ more on @hammerspoon 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain โ€” perfect for shipping the agent you just read about.

$git push zahid main
โ†’ Live at https://your-agent.zahid.host โœ“
Get free account โ†’ Pricing
from โ‚ฌ0/mo ยท no card required
LIVE [news/claude-clipboard-cleโ€ฆ] indexed:0 read:4min 2026-06-21 ยท โ€”