Feature Request: Add /copy command to copy messages to clipboard

Open 💬 25 comments Opened Aug 10, 2025 by NightMachinery

Feature Request: /copy [N=1] slash command

Summary

Add a /copy slash command that copies the last N Claude Code messages as Markdown to the OS clipboard.

Proposed Usage

/copy        # Copies the last message (Claude's response)
/copy 3      # Copies the last 3 messages (alternating user/Claude)
/copy 5      # Copies the last 5 messages

Expected Behavior

  • Default N=1 (copies just Claude's last response)
  • Messages should be copied as properly formatted Markdown
  • Should include both user messages and Claude responses when N > 1
  • Should integrate with OS clipboard (works with Cmd+V/Ctrl+V)
  • Should preserve code blocks, formatting, and structure

Use Cases

  • Quickly share Claude's responses in documentation
  • Copy code snippets and explanations for use elsewhere
  • Archive conversation snippets for later reference
  • Share troubleshooting responses with team members

Implementation Notes

  • Should work across platforms (macOS, Linux, Windows)
  • Consider using a clipboard library like pyperclip or similar
  • Messages should maintain original formatting and structure
  • Could potentially support additional formats (plain text, HTML) as options

This would significantly improve the workflow when working with Claude Code responses that need to be shared or documented elsewhere.

View original on GitHub ↗

25 Comments

github-actions[bot] · 11 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/859

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

EmmettHan · 11 months ago

need this

gabrielbryk · 10 months ago

Need this too, would be nice if it was native but found this
https://github.com/Twizzes/copy-claude-response

ekil1100 · 9 months ago

need this

weltonrodrigo · 8 months ago

Please add this because a lot of times I want to copy the claude message to a colleague on slack.

github-actions[bot] · 7 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

shirgoldbird · 7 months ago

This would be extremely helpful!

pokey · 7 months ago

Fwiw a hack I use: if the message has some uniquely identifiable piece, you can grep for that in ~/.claude. Here's a one-liner to copy the markdown to clipboard:

rg -FIN 'some unique substring from the message' ~/.claude | jq -r '.message.content[].text' | pbcopy
markusjura · 6 months ago

Another workaround is to create your own /copy command. I use the following that also also supports:

  • preprocessing the response replacing typical AI formatting such as em dashes and **bold inline heading** - description text format
  • converting to Slack message format

````
---
description: Copy last response to clipboard
allowed-tools: Bash
---

Copy the last assistant response to the clipboard.

Arguments

$ARGUMENTS

Instructions

  1. Identify your last response in this conversation (the message immediately before the user invoked this command).
  1. Preprocess the content - Apply these reformatting rules before any format conversion:
  • **bold** - text**bold:** text (replace dash separator with colon after bold terms)
  • Em dash () → Rewrite as two complete sentences, rephrasing if needed
  1. Check the arguments above:
  • If empty: Copy as plain text
  • If contains "slack" (case-insensitive): Copy as rich text for Slack

Default Mode

Copy the content using pbcopy with a heredoc. Use the exact text without any code block indentation or markdown fences.

cat << 'EOF' | pbcopy
[content here]
EOF

Slack Mode

Convert the content to HTML and copy using AppleScript so Slack's default editor accepts it:

  1. Convert markdown to HTML:
  • # Heading or ## Heading<b>Heading</b><br>
  • **text**<b>text</b>
  • - item or • item → list items, wrap in <ul><li>...</li></ul>, add single <br> after </ul> or </ol>
  • Blank lines immediately after headings → ignore (heading already has <br>)
  • Blank lines after a paragraph → <br><br>
  • Other blank lines → <br>
  1. Copy using this exact pattern:

``bash
html='[your HTML here]'
hex=$(echo -n "$html" | hexdump -ve '1/1 "%.2x"')
osascript -e "set the clipboard to {text:\" \", «class HTML»:«data HTML${hex}»}"
``

---

After copying, say "Done" - nothing more.

````

joelev · 6 months ago

While waiting for native support, I made a minimal skill that adds /yank for clipboard access:

https://github.com/joelev/yank

Install:

mkdir -p ~/.claude/skills/yank
curl -o ~/.claude/skills/yank/SKILL.md \
  https://raw.githubusercontent.com/joelev/yank/main/SKILL.md

It's ~40 lines, cross-platform (macOS, Linux, Windows via WSL), and uses the platform info Claude Code already provides in context—no detection scripts. Claude will also offer to yank when generating prompts or snippets.

Not a replacement for a proper native command, but scratches the itch for now.

MarkusJLechner · 5 months ago

i am using

ccopy() {
  local Q="$1"
  local OUT

  OUT=$(
    find ~/.claude/projects \
      -type f \
      -name '*.jsonl' \
      ! -path '*/subagents/*' \
      -print0 \
      | xargs -0 ls -t 2>/dev/null \
      | while read -r f; do
          tail -r "$f" \
            | rg -i -N "$Q" \
            | jq -r -j '
                .message.content[]?
                | select(.type=="text" and (.text | test("\\S")))
                | .text, "\^@"
              ' \
            | { IFS= read -r -d "" out && printf "%s" "$out"; } \
            && break
        done
  )

  [ -n "$OUT" ] || return 1

  printf "%s\n" "$OUT" | pbcopy
  printf "%s\n" "$OUT"
}

find session id with /status, then e.g. ccopy 01ef658c-19ec-42b9-83f5-89a8c5c2f524
or with text ccopy ref type

takes the latest project and latest message and increases to later answers if not found

not perfect in any away, but improving.

Btw, I would also like to have just /copy n to copy the last responses...

Shadetail · 5 months ago

+1 on this. I primarily use Claude Code through the VS Code extension, and the lack of any copy/share mechanism is a real pain point.

The web UI (claude.ai) has a simple copy button on each message - I'd love to see the same in the VS Code panel. Right-click → "Copy as Markdown" on individual messages, or a way to select a range of messages and copy them. The /copy N command proposed here would also work great in VS Code's input.

karlb · 4 months ago

There's support for /copy since https://github.com/anthropics/claude-code/releases/tag/v2.1.20, but no parameters yet, AFAICS.

prebhdevsingh · 4 months ago

I can confirm that /copy is now supported from the latest version.

simon-liebehenschel · 4 months ago

Yes, it was implemented. However, it has bugs: the /copy command sometimes hangs indefinitely, requiring you to press Ctrl + C to abort it and then retry. This might deserve a separate issue.

timdmackey · 4 months ago
I can confirm that /copy is now supported from the latest version.

Nice! Up until now i've just been telling Claude "pbcopy the last message". This consistently worked, but fewer characters to type makes me happy :).

mhavo · 4 months ago

Building on @pokey's one-liner and @MarkusJLechner's ccopy, here's a version I've been using that adds a few things:

  • Cross-platform clipboard (pbcopy / xclip / wl-copy)
  • fzf picker when multiple messages match (optional, falls back to first match)
  • -p project filter with substring matching — e.g. -p dotfiles matches the project dir -Users-me--dotfiles
  • Works in both bash and zsh

Requires: rg, jq. Optional: fzf.

# Search Claude Code conversations and copy the matching message to clipboard.
# Two-pass: rg (fixed string) narrows files, jq decodes the matching message text.
# Usage: ccopy [-p project-or-session-id] <query>
ccopy() {
  local project="" q=""
  [[ "$1" == "-p" ]] && { project="$2"; shift 2; }
  q="$1"
  [[ -z "$q" ]] && { echo "Usage: ccopy [-p project-or-session-id] <query>" >&2; return 1; }

  _ccopy_clip() {
    if command -v pbcopy &>/dev/null; then pbcopy
    elif command -v xclip &>/dev/null; then xclip -selection clipboard
    elif command -v wl-copy &>/dev/null; then wl-copy
    else cat >/dev/null; echo "No clipboard command (pbcopy/xclip/wl-copy)" >&2; return 1
    fi
  }

  local base=~/.claude/projects
  local files=()

  if [[ -n "$project" ]]; then
    if [[ -d "$base/$project" ]]; then
      while IFS= read -r line; do files+=("$line"); done < <(
        rg -Fil --sortr modified --glob '*.jsonl' --glob '!**/subagents/**' -- "$q" "$base/$project" 2>/dev/null)
    else
      local -a dirs
      while IFS= read -r line; do dirs+=("$line"); done < <(
        find "$base" -maxdepth 1 -type d -name "*${project}*" 2>/dev/null)
      if (( ${#dirs[@]} )); then
        while IFS= read -r line; do files+=("$line"); done < <(
          rg -Fil --sortr modified --glob '*.jsonl' --glob '!**/subagents/**' -- "$q" "${dirs[@]}" 2>/dev/null)
      else
        local found
        found=$(find "$base" -type f -name "${project}.jsonl" ! -path '*/subagents/*' 2>/dev/null | head -1)
        [[ -n "$found" ]] || { echo "Not found: $project" >&2; return 1; }
        files=("$found")
      fi
    fi
  else
    while IFS= read -r line; do files+=("$line"); done < <(
      rg -Fil --sortr modified --glob '*.jsonl' --glob '!**/subagents/**' -- "$q" "$base" 2>/dev/null)
  fi

  local matches=()
  for f in "${files[@]}"; do
    [[ -f "$f" ]] || continue
    while IFS= read -r -d '' chunk; do
      [[ -n "$chunk" ]] && matches+=("$chunk")
    done < <(
      rg -FiN -- "$q" "$f" 2>/dev/null \
        | jq -r -j --arg q "$q" '
            .message.content[]?
            | select(.type=="text" and (.text | ascii_downcase | contains($q | ascii_downcase)))
            | .text, "\^@"
          ' 2>/dev/null
    )
  done

  (( ${#matches[@]} )) || { echo "No match: $q" >&2; return 1; }

  local out
  if (( ${#matches[@]} == 1 )); then
    out="${matches[@]:0:1}"
  elif command -v fzf &>/dev/null; then
    out=$(printf '%s\0' "${matches[@]}" | fzf --read0 --height=50% --preview-window=wrap)
    [[ -z "$out" ]] && return 0
  else
    out="${matches[@]:0:1}"
  fi

  printf '%s\n' "$out" | _ccopy_clip
  printf '%s\n' "$out"
}

Examples:

ccopy "some unique phrase"              # search all projects
ccopy -p dotfiles "search query"        # substring match on project name
ccopy -p e056a06c-8425-4cdc "query"     # session UUID also works

For browsing and searching sessions visually, claude-history is excellent — and there's a PR pending to add message copying there too. Even so, this function stays useful as a standalone alternative with no external dependencies beyond rg and jq.

Complements /copy nicely — /copy grabs from the current session, ccopy searches across all past conversations.

onne · 4 months ago

+1 on this. This is especially painful when claude streams responses from subagents and main thread intermittently, in response to a single prompt. In that case, you only get the last section of the response from /copy. Using esc + esc allows you to roll back between prompts, so it doesn't help to granulate the copy action.

Example prompt
1. use a subagent to search the web for "A fluffy blue creature roamed the verdant forest" and explain the context
2. use the `_env.yaml` pattern for setting `MODE` in env according to build environment
Outcome for /copy

Whichever activity completes first, the search or the edit, prints. When the next completes, it prints too.
/copy only pasteboard-copies the latter.

MarkusJLechner · 4 months ago

can we say that this is fixed with https://github.com/anthropics/claude-code/releases/tag/v2.1.77 ?

/copy now accepts an optional index: /copy N copies the Nth-latest assistant response
robross0606 · 3 months ago

Why do I not see this available in the Claude extension panel if it is supported?

<img width="616" height="125" alt="Image" src="https://github.com/user-attachments/assets/953cddde-328a-4f4e-8ad4-75787e1aacf6" />

I also don't see it in the available slash commands menu:

<img width="616" height="398" alt="Image" src="https://github.com/user-attachments/assets/38046a73-fc44-46ef-884d-487d9e1b26f6" />

yurukusa · 3 months ago

While native /copy isn't available yet, you can get similar functionality with a custom skill + Bash:

1. Create a /copy skill:

<!-- .claude/skills/copy.md -->
---
description: Copy the last response to clipboard
---
Copy your most recent response to the system clipboard using the appropriate command for the current OS.

For macOS: pipe to `pbcopy`
For Linux/WSL: pipe to `xclip -selection clipboard` or `clip.exe` (WSL)
For Windows: pipe to `clip.exe`

Write the content to a temp file first, then copy it.

2. Or use a PostToolUse hook for automatic copying:

Save as ~/.claude/hooks/auto-copy-response.sh:

#!/bin/bash
# Auto-copy the last assistant message to clipboard
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)

# Only trigger after Write tool (captures generated code)
[ "$TOOL" != "Write" ] && exit 0

FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
[ -z "$FILE" ] && exit 0

# Copy file content to clipboard (adjust for your OS)
if command -v pbcopy &>/dev/null; then
  cat "$FILE" | pbcopy  # macOS
elif command -v clip.exe &>/dev/null; then
  cat "$FILE" | clip.exe  # WSL/Windows
elif command -v xclip &>/dev/null; then
  cat "$FILE" | xclip -selection clipboard  # Linux
fi
exit 0

3. Quickest workaround — just ask Claude:
Tell Claude: "Copy your last response to my clipboard" — it will use the Bash tool to echo "..." | pbcopy (or equivalent).

Not as clean as a native /copy N command, but functional today.

KernelBypass · 1 month ago

They added it, but it works like crap: only copies one line in a multi-tool call agentic response.
Frustratingly bad experience. All other harnesses allow you to copy messages easily.

iwan-uschka · 1 month ago

In VS Code you can use the following workaround, as long as /copy is not available in claude code extension:

  1. Open terminal in VS Code (same window as claude code extension is running in).
  2. /resume in terminal and choose the session from claude code extension
  3. /copy in terminal
kyrylo-kochubyey · 1 month ago

The core ask is simple: use soft wrapping for code block content only — emit long lines without inserting hard \n
characters, and let the terminal emulator handle visual wrapping. The TUI chrome (spinners, separators, progress
bars) can keep using terminal width — only code block text needs to change.

Why this matters:

  • Hard \n in code blocks makes every long command broken on paste — you get a multi-line mess with indentation

artifacts instead of a runnable one-liner

  • There is no workaround that doesn't destroy something else — setting stty cols 300 breaks the entire TUI layout;

/copy works but requires an extra step every single time

  • tmux users, iTerm2 users, and terminal users on every platform are all hitting this — see also #42296 (closed as

not planned, same root cause)

The fix is surgical: don't call the hard-wrap renderer for fenced code block content. Emit the raw line and let the
terminal wrap it visually. This is standard behavior for every other terminal tool that outputs code.

KernelBypass · 1 month ago
In VS Code you can use the following workaround, as long as /copy is not available in claude code extension: 1. Open terminal in VS Code (same window as claude code extension is running in). 2. /resume in terminal and choose the session from claude code extension 3. /copy in terminal

It's very tedious and also if you forget to exit the resumed conversation from CLI and send a message in chat within the extension, you might corrupt your chat history and/or make it unfork'able/unrewind'able.

I don't understand how they still don't have copy UI when every single harness does. GHCP even allows to copy the entire conversation, even a very long one, in one click.