Stop Reformatting Markdown When Pasting into Slack

productivity
Author

Alex Guglielmone Nemi

Published

January 16, 2026

Pain Point

Slack only pastes rich formatting when the clipboard advertises text/html, otherwise it treats everything as plain text.

If my file sample.md looks like this:

:robot_face: Tech updates :robot_face:

# Some Title

- **Project**: Did x in [google](https://google.com).
  - aaa
  - bbb
      - ccc 
  - ddd

# Another title

- Another launch
  - details

Compare pasting directly on the left and what we want on the right.

Side by Side comparison between markdown in plain text and the slack rich formatted version

Solution

Put HTML onto the clipboard the same way a browser would, so Slack pastes it as rich content instead of plain text.

This requires a recent xclip build that supports advertising text/html correctly.

  1. Build xclip from source to get the latest features around html
  2. pip install beautifulsoup4 lxml
  3. Run pandoc sample.md -t html
  4. Optionally modify the HTML to fix things like lists.
  5. and pipe it to xclip -selection clipboard -t text/html

What it looks like:

pandoc -f gfm -t html sample.md \
| python -c '
import sys
from bs4 import BeautifulSoup as BS, Tag

s = BS(sys.stdin.read(), "lxml")

def inline_html(tag: Tag) -> str:
    return "".join(str(x) for x in tag.contents).strip()

out_lines = []

def emit_block(tag: Tag):
    name = tag.name.lower()

    if name in ("h1","h2","h3","h4","h5","h6"):
        txt = tag.get_text(strip=True)
        if txt:
            out_lines.append(f"<strong>{txt}</strong>")
            out_lines.append("<br/>")
        return

    if name in ("p",):
        txt = inline_html(tag)
        if txt:
            out_lines.append(txt)
            out_lines.append("<br/>")
        return

    if name in ("ul","ol"):
        walk_list(tag, 0)
        out_lines.append("<br/>")
        return

def walk_list(lst: Tag, level: int):
    for li in lst.find_all("li", recursive=False):
        # separate nested lists
        nested = [c for c in li.find_all(["ul","ol"], recursive=False)]
        for n in nested:
            n.extract()

        text = inline_html(li)
        if text:
            indent = "&nbsp;" * (4 * level)  # 4 = indent width per level
            bullet = "&#8226;"               # •
            out_lines.append(f"{indent}{bullet} {text}<br/>")

        for n in nested:
            walk_list(n, level + 1)

body = s.body if s.body else s
for child in list(body.children):
    if isinstance(child, Tag):
        emit_block(child)

print("".join(out_lines), end="")
' \
| xclip -sel clipboard -t text/html -alt-text "Updates"

Note: The Python step is only needed if you want to fix Slack’s broken handling of nested lists. For simple formatting (bold, links, headings), Pandoc -> xclip alone is enough.

Inspiration

Authoring Markdown externally and pasting the ‘pretty’ output into Slack (on Linux) does the same thing without the extra formatting to fix the nested lists.

Annex

How to build latest xclip from source in Ubuntu

sudo apt install autoconf automake libtool libxmu-dev
git clone https://github.com/astrand/xclip
cd xclip
autoreconf -fi
./configure --prefix=/usr/local
make
sudo make install

WSL and powershell are different beasts

This won’t work on WSL and slack as-is. You likely need to do it from powershell using a third-party program

WSL cannot directly populate the Windows clipboard with rich HTML in a way Slack accepts; an intermediate Windows application re-copies the content with additional clipboard formats.

  1. Powershell 5
Get-Content out.html -Raw | Set-Clipboard -AsHtml
  1. Open LibreOffice Writer or any GUI and paste.
  2. Select that and copy
  3. Paste into slack