# Claude Clipboard Cleaner (Hammerspoon edition) - auto-strips trailing padding + leading 2-space indent from Claude Code terminal copies

> Source: <https://gist.github.com/spbavarva/d6e29e12d3fc8572b62c8523ffccc2a6>
> Published: 2026-06-21 15:37:29+00:00

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