Copy Claude's last reply, or its last code block, to the clipboard as clean Markdown with /copy, with no terminal indentation or trailing spaces.

Copy Claude's last reply, or its last code block, to the clipboard as clean Markdown with /copy. Nothing is selected in the terminal, so there's no gutter indentation, no wrapped lines, and no trailing spaces.
The pain is one of the ten most-reacted open issues on the Claude Code tracker: copy and paste from the terminal includes unwanted indentation and trailing spaces. This reads the reply's source from the transcript instead.
/plugin marketplace add MDmubarak786/claude-mods
/plugin install copy-last@modhub
Try it for one session without installing:
claude --plugin-dir ./mods/copy-last
| Command | What it copies |
|---|---|
/copy | The last reply, as Markdown source. |
/copy code | The last fenced code block in the last reply, without the fence. Looks back through the last five replies if the latest has none. |
/copy code 2 | The second-to-last code block. |
The command prints what it copied, as a character and line count.
From claude plugin validate ./mods/copy-last:
hooks: session.start, command.run{command=copy}
calls: $.command.register, $.session.messages (via lastReplies), $.ui.copy, $.ui.log
It reads the transcript through the mods API and writes to the clipboard through Claude Code. No files, processes, or network. Nothing is saved.
claude plugin validate --strict and claude plugin test pass. /copy answered from a live claude -p session.bold is copied as written. That's the point for pasting into an issue or a doc; for plain prose, copy from the terminal.MIT, see the repository root.
hooks/register.ts 70 lines1// copy-last: copy Claude's last reply, or its last code block, to the clipboard.
2//
3// /copy the last reply, as Markdown source
4// /copy code the last fenced code block in the last reply
5// /copy code 2 the second-to-last code block
6//
7// Copying from the terminal picks up gutter indentation and trailing spaces.
8// This reads the reply from the transcript and copies the source text.
9
10const FENCE = /```[^\n]*\n([\s\S]*?)```/g
11
12function codeBlocks(text: string): string[] {
13 const blocks: string[] = []
14 for (const match of text.matchAll(FENCE)) blocks.push(match[1].replace(/\n$/, ''))
15 return blocks
16}
17
18async function lastReplies($, count: number): Promise<string[]> {
19 const messages = await $.session.messages()
20 const replies: string[] = []
21 for (let i = messages.length - 1; i >= 0 && replies.length < count; i--) {
22 const m = messages[i]
23 if (m.role === 'assistant' && typeof m.text === 'string' && m.text.trim()) replies.push(m.text)
24 }
25 return replies
26}
27
28function describe(text: string): string {
29 const lines = text.split('\n').length
30 return text.length + ' characters, ' + lines + (lines === 1 ? ' line' : ' lines')
31}
32
33export function register(on) {
34 on('session.start', async ($, e, next) => {
35 try {
36 await $.command.register({
37 name: 'copy',
38 description: 'Copy the last reply, or its last code block, as clean text',
39 argumentHint: '[code [N]]',
40 })
41 } catch (error) {
42 $.ui.log('could not register /copy: ' + error)
43 }
44 return next(e)
45 })
46
47 on('command.run', { command: 'copy' }, async ($, e) => {
48 const args = e.args.trim().split(/\s+/).filter(Boolean)
49 const wantCode = args[0] === 'code'
50 const nth = wantCode && args[1] ? Number(args[1]) : 1
51 if (wantCode && (!Number.isInteger(nth) || nth < 1)) return { text: 'Usage: /copy, /copy code, or /copy code N' }
52
53 // Look back through a few replies so a block in the previous reply is still reachable.
54 const replies = await lastReplies($, wantCode ? 5 : 1)
55 if (replies.length === 0) return { text: 'Nothing to copy yet.' }
56
57 let text = replies[0]
58 if (wantCode) {
59 const blocks = replies.flatMap((reply) => codeBlocks(reply).reverse())
60 if (blocks.length === 0) return { text: 'No code block in the last replies.' }
61 if (nth > blocks.length) return { text: 'Only ' + blocks.length + ' code block(s) found.' }
62 text = blocks[nth - 1]
63 }
64
65 const copied = await $.ui.copy({ text })
66 if (!copied.isCopied) return { text: 'Could not copy to the clipboard.' }
67 return { text: 'Copied ' + (wantCode ? 'code block: ' : 'reply: ') + describe(text) }
68 }).catch(async () => ({ text: 'copy-last: the command failed, so nothing was copied.' }))
69}
70