| -- 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 |
Agentic Security: Standardizing Cyber Workflows for AI Developers