skills/slack-copy/SKILL.md
Put formatted content on the macOS clipboard so it renders correctly when pasted into a Slack message composer (bold/italic/lists via HTML, tables via TSV).
npx skillsauth add benwaffle/skills slack-copyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Slack's macOS desktop message composer is picky about what it renders from a pasted clipboard. Empirically verified behavior:
| Format | Slack message composer? | How to put it on the clipboard |
|---|---|---|
| Bold, italic, strikethrough | ✅ via HTML | <b>, <i>, <s> on public.html |
| Bulleted / numbered list | ✅ via HTML | <ul><li>, <ol><li> |
| Inline code | ✅ via HTML | <code> |
| Code block | ✅ via HTML | <pre> (line breaks preserved; <pre><code> is equivalent — no syntax highlighting either way) |
| Blockquote | ✅ via HTML | <blockquote> (renders with the left bar; <code> inside it works) |
| Links | ✅ via HTML | <a href="…">…</a> |
| Table | ❌ HTML <table> is stripped | Use tab-separated plain text (TSV). Slack renders TSV as a table. |
| Headings, colors, custom fonts | ❌ | Not supported in the composer; use a canvas. |
Three big gotchas:
<meta charset="utf-8"> prefix breaks Slack's HTML paste handler — strip it. Inline-formatting HTML must start directly with the first element (<b>, <ul>, etc.).
Tables: Slack ignores HTML <table> entirely. The way to get a table is plain-text TSV (cells separated by \t, rows by \n). Put that on public.utf8-plain-text. (HTML can still be on the same pasteboard with a separate marker; Slack will pick plain text for tables.)
Slack collapses block-level whitespace — <p> and the implicit block break around <ul>/<ol> are not honored. To get visible section breaks you must insert explicit <br> tags: <br> after a header before its list, and <br><br> for a blank line between sections. Without them, "What changed" runs straight into the first list item.
Inline whitespace from your source HTML is honored — opposite problem from #3. Slack preserves:
<br> (a newline-then-indent in your heredoc shows up as a leading space on the next line)Easiest fixes: keep HTML on a single line with no whitespace between <br> and the next token, and write the file with printf '%s' (no trailing newline) instead of cat <<'EOF' (which always appends one). If you must use a heredoc for readability, post-process: tr -d '\n' < raw.html > clean.html or sed -E 's/<br>[[:space:]]+/<br>/g'.
When the user asks to "put X on the clipboard for Slack", "copy a table for Slack", "format for Slack", or invokes /slack-copy, use this skill.
Two channels matter for Slack: public.html (rich) and public.utf8-plain-text (fallback). Use scripts/set-clipboard.py to set both at once with the right UTIs:
uv run --with pyobjc-framework-Cocoa \
python ~/dev/skills/skills/slack-copy/scripts/set-clipboard.py \
<html-file> <plain-text-file>
The script clears the pasteboard, writes both representations to one NSPasteboardItem, and prints the resulting types so you can verify.
For TSV-only (tables), pbcopy < table.tsv is enough — Slack only needs the plain channel for tables.
Generate an HTML fragment with no <meta> or <html> wrapper, and a clean plain-text fallback:
Note the <br> after Affected: — without it, "Affected:" runs straight into the first bullet.
cat > /tmp/msg.html <<'EOF'
<b>Heads up:</b> the rollout is delayed.<br><br>
<b>Affected</b><br>
<ul>
<li>service <code>provisioner</code> in <i>prod2</i></li>
<li>see <a href="https://example/ticket">ticket 1234</a></li>
</ul>
EOF
cat > /tmp/msg.txt <<'EOF'
Heads up: the rollout is delayed. Affected:
- service `provisioner` in prod2
- see ticket 1234 (https://example/ticket)
EOF
uv run --with pyobjc-framework-Cocoa \
python ~/dev/skills/skills/slack-copy/scripts/set-clipboard.py /tmp/msg.html /tmp/msg.txt
Just tabs and newlines. Header row optional. pbcopy is enough:
{
printf 'Time (UTC)\tService\tStatus\n'
printf '23:22:16\tprovisioner\tdegraded\n'
printf '23:36:10\trgw\trecovered\n'
} | pbcopy
Or pass through column -s$'\t' -t first only if you want it human-readable in a terminal — don't do that for the clipboard, Slack needs literal tabs.
Slack doesn't render an HTML table even when other HTML works, so a single paste can't include both styled text and a table. Options:
*bold*, _italic_, \code``) typed inline, and only paste the TSV.| a | b | table works.When experimenting with what Slack accepts, always use different content for HTML vs plain-text fallback so you can tell which one ended up in the message. If you put the same text in both and a "table" renders, you can't tell whether Slack honored your HTML or just rendered tab-separated plain text. Use a sentinel:
printf '<b>HTML-BRANCH</b>' > /tmp/h
printf 'PLAIN-BRANCH' > /tmp/p
uv run --with pyobjc-framework-Cocoa \
python ~/dev/skills/skills/slack-copy/scripts/set-clipboard.py /tmp/h /tmp/p
After pasting, "HTML-BRANCH" bolded = HTML honored. "PLAIN-BRANCH" plain = HTML rejected, fallback used.
To inspect what's actually on the pasteboard:
osascript -e 'clipboard info' # lists UTIs and sizes
pbpaste # plain text
osascript -e 'try
return (the clipboard as «class HTML») as string
on error e
return "no HTML: " & e
end try'
<br> show as leading spaces; trailing newline shows as an empty final line. Use printf '%s' '…all-on-one-line…' or post-process.<table> in a message composer — always stripped.textutil -convert rtf) on public.rtf — Slack ignores it for tables.<meta charset> prefix — breaks even simple <b>/<i> rendering.<thead>/<tbody> wrappers — doesn't help (and <table> is stripped anyway).<!--StartFragment--> markers — Slack still strips tables.<p> for paragraph breaks — Slack collapses them. Use <br> / <br><br> instead.<ul>/<ol> to introduce vertical space — Slack jams the preceding text right against the first list item. Insert <br> between them.If the user is pasting into a Slack canvas rather than a message, the rules differ — canvases render markdown natively, including pipe-style tables. For canvas paste, generate markdown:
| col1 | col2 |
|---|---|
| a | b |
and pbcopy it.
development
Generate self-contained HTML slideshows for technical concepts with diagrams, code highlighting, math, and charts
tools
Generate an interactive HTML report of your Claude Code usage (sessions, messages, tokens, active days, streaks, peak times, model usage by day, top projects/tools) by parsing ~/.claude/projects/*.jsonl. Use when the user asks for their Claude stats, usage, activity, streaks, token breakdown, or wants to see how they use Claude over time.
databases
Query and analyze Apple .logarchive files using the macOS `log show` command
development
Android Debug Bridge (ADB) assistant for inspecting, debugging, and managing Android devices