Paste Images in Claude Code on WSL2: Clipboard Bridge Fix
Fix image pasting in Claude Code on WSL2 with a custom hook. PowerShell clipboard bridge saves screenshots automatically, no extensions needed.
In This Guide click to collapse
TL;DR
Claude Code can’t paste images from the Windows clipboard when running inside WSL2. I built a UserPromptSubmit hook that calls PowerShell to save the clipboard image directly to a file via wslpath -w, then injects the file path as context. No extensions, no third-party tools. Just a bash script and a hook config. Type “paste” in your message and the screenshot appears automatically.
The Problem: No Image Paste in WSL2
If you use Claude Code inside WSL2 on Windows, you’ve hit this wall: you take a screenshot with Win+Shift+S, try to paste it into the conversation, and nothing happens. No image, no error, just silence.
This isn’t a Claude Code bug. It’s a WSL2 limitation. The Linux environment running inside WSL2 has no access to the Windows clipboard’s image data. Text works fine (through clip.exe and powershell.exe Get-Clipboard), but binary image data doesn’t cross the boundary.
It’s a well-known pain point. There are multiple open GitHub issues about it, VS Code extensions designed to work around it, and even standalone helper tools. But I wanted something that works natively inside Claude Code’s terminal, with no IDE required and no extra tools to install.
The answer turned out to be Claude Code’s hooks system. A UserPromptSubmit hook can intercept your message before it reaches Claude, run a script, and inject additional context. If that script can grab the clipboard image and save it to a file, Claude can read it.
Simple in theory. Getting it to actually work took four failed attempts.
What Didn’t Work (4 Failed Approaches)
Before I share the solution, here’s what I tried and why each approach failed. If you’re troubleshooting WSL2 clipboard issues, this might save you hours.
Attempt 1: Piping Base64 Through stdout
The obvious first approach: have PowerShell convert the clipboard image to base64 and pipe it back through stdout.
B64=$(powershell.exe -NoProfile -NonInteractive -Command '
Add-Type -AssemblyName System.Drawing
$img = Get-Clipboard -Format Image
if ($img) {
$ms = New-Object System.IO.MemoryStream
$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
[Convert]::ToBase64String($ms.ToArray())
$ms.Dispose()
$img.Dispose()
}
' 2>/dev/null | tr -d '\r\n')
Result: $B64 was always empty. The base64 string for a typical screenshot is 50 to 100KB+, and WSL2’s stdout pipe from powershell.exe silently drops large outputs. No error, no truncation warning. The variable is simply empty.
WSL2 Stdout Pitfall
When calling powershell.exe from WSL2, large stdout payloads (50KB+) are silently dropped. No error is raised. This affects any approach that pipes binary data or large strings back through the WSL2/Windows interop layer.
Attempt 2: Get-Clipboard Text Format (No Output)
I tried simplifying to just read text from the clipboard:
powershell.exe -Command 'Get-Clipboard'
powershell.exe -Command 'Get-Clipboard -Format Text'
Both returned nothing, not even text clipboard contents that were definitely there. This appears to be a WSL2 session context issue where some PowerShell cmdlets don’t produce stdout when called non-interactively from the Linux side.
Attempt 3: System.Windows.Forms.Clipboard (STA Failure)
Maybe the issue was the cmdlet. I tried the .NET clipboard API directly, with the required -sta flag for COM access:
powershell.exe -NoProfile -sta -Command '
Add-Type -AssemblyName System.Windows.Forms
$cb = [System.Windows.Forms.Clipboard]::ContainsImage()
Write-Output "ContainsImage: $cb"
'
Returned absolutely nothing. Even with -sta (Single Threaded Apartment, required for clipboard COM access), the Forms-based clipboard API produced zero stdout in WSL2 context.
Attempt 4: Wrong Hook JSON Output Format
At this point I had a working file-save approach (spoiler for the next section), but the hook wasn’t injecting context into Claude. My first JSON output format was:
{"additionalContext": "Image found at /tmp/clipboard-paste.png"}
The hook reported success, but Claude never received the context. The additionalContext field was silently ignored because it wasn’t nested in the correct structure.
Failure Summary
| Approach | Root Cause | Symptom |
|---|---|---|
| Base64 stdout pipe | WSL2 drops large stdout from powershell.exe | Empty variable, no error |
| Get-Clipboard text | WSL2 session context blocks cmdlet output | No output at all |
| System.Windows.Forms | COM apartment threading fails in WSL2 | No output even with -sta |
| Flat JSON output | Missing hookSpecificOutput wrapper |
Hook “succeeds” but context ignored |
The Working Solution
Prerequisite: jq
The hook script parses JSON with jq. Install it first: sudo apt install jq (Ubuntu/Debian) or sudo dnf install jq (Fedora/RHEL). Without jq the hook will silently fail.
The fix came from flipping the approach: instead of bringing data from PowerShell to WSL through stdout, have PowerShell write directly to a file that both sides can access.
PowerShell Saves Directly via wslpath
The key insight is wslpath -w, which converts a Linux path to a Windows UNC path:
# Linux path
/tmp/clipboard-paste-123.png
# Converted by wslpath -w
\\wsl$\Ubuntu\tmp\clipboard-paste-123.png
PowerShell on the Windows side can write to that UNC path. Only the tiny string “SAVED” needs to travel through stdout, well within WSL2’s limits:
SAVE_PATH="/tmp/clipboard-paste-$(date +%s%N).png"
WIN_PATH=$(wslpath -w "$SAVE_PATH")
RESULT=$(powershell.exe -NoProfile -NonInteractive -Command "
Add-Type -AssemblyName System.Drawing
\$img = Get-Clipboard -Format Image
if (\$img) {
\$img.Save('$WIN_PATH', [System.Drawing.Imaging.ImageFormat]::Png)
\$img.Dispose()
Write-Output 'SAVED'
}
" 2>/dev/null | tr -d '\r\n')
Why This Works
Get-Clipboard -Format Image correctly detects clipboard images from WSL2. The detection works fine. The problem was only with getting the image data out through stdout. By saving directly to a shared filesystem path, we bypass the stdout bottleneck entirely.
Correct Hook JSON Output Format
Claude Code hooks require a specific nested JSON structure to inject context. The correct format for UserPromptSubmit hooks:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Image found in Windows clipboard and saved to /tmp/clipboard-paste-123.png. Please use the Read tool to view this image."
}
}
Three required fields:
hookSpecificOutput: the wrapper object (this is what I was missing)hookEventName: must match"UserPromptSubmit"additionalContext: the text Claude receives alongside your message
Keyword Trigger for Zero Overhead
PowerShell startup takes ~0.4 seconds. That’s noticeable if it runs on every message. The hook reads the user’s prompt from stdin and only fires when it contains “paste”:
INPUT=$(cat)
if ! echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null | grep -qi 'paste'; then
exit 0
fi
No “paste” keyword? The hook exits immediately with zero overhead.
MD5 Deduplication
If you type “paste” in consecutive messages, the same clipboard image would be injected multiple times. A simple hash check prevents this:
HASH_FILE="/tmp/.clipboard-paste-last-hash"
HASH=$(md5sum "$SAVE_PATH" | cut -d' ' -f1)
if [[ -f "$HASH_FILE" ]] && [[ "$(cat "$HASH_FILE")" == "$HASH" ]]; then
rm -f "$SAVE_PATH"
exit 0 # Same image, skip
fi
printf '%s' "$HASH" > "$HASH_FILE"
Complete Script & Configuration
Installation (2 Steps)
- Save the script below to
~/.claude/hooks/clipboard-image-bridge.sh - Add the hook config to
~/.claude/settings.json
Step 1: The Script (~/.claude/hooks/clipboard-image-bridge.sh):
#!/usr/bin/env bash
# WSL2 Clipboard Image Bridge for Claude Code
# Intercepts "paste" keyword, grabs Windows clipboard image, injects as context
INPUT=$(cat)
# Only trigger on messages containing "paste"
if ! echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null | grep -qi 'paste'; then
exit 0
fi
# Save clipboard image to temp file via PowerShell
SAVE_PATH="/tmp/clipboard-paste-$(date +%s%N).png"
WIN_PATH=$(wslpath -w "$SAVE_PATH")
RESULT=$(powershell.exe -NoProfile -NonInteractive -Command "
Add-Type -AssemblyName System.Drawing
\$img = Get-Clipboard -Format Image
if (\$img) {
\$img.Save('$WIN_PATH', [System.Drawing.Imaging.ImageFormat]::Png)
\$img.Dispose()
Write-Output 'SAVED'
}
" 2>/dev/null | tr -d '\r\n')
# No image in clipboard? Exit silently
if [[ "$RESULT" != "SAVED" ]] || [[ ! -f "$SAVE_PATH" ]]; then
rm -f "$SAVE_PATH" 2>/dev/null
exit 0
fi
# Deduplicate: skip if same image as last paste
HASH_FILE="/tmp/.clipboard-paste-last-hash"
HASH=$(md5sum "$SAVE_PATH" | cut -d' ' -f1)
if [[ -f "$HASH_FILE" ]] && [[ "$(cat "$HASH_FILE")" == "$HASH" ]]; then
rm -f "$SAVE_PATH"
exit 0
fi
printf '%s' "$HASH" > "$HASH_FILE"
# Inject context for Claude
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Image found in Windows clipboard and saved to ${SAVE_PATH}. Please use the Read tool to view this image."
}
}
EOF
Step 2: Hook Config (add to ~/.claude/settings.json):
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/clipboard-image-bridge.sh",
"timeout": 10
}
]
}
]
}
}
Make the script executable:
chmod +x ~/.claude/hooks/clipboard-image-bridge.sh
That’s it. Take a screenshot with Win+Shift+S, type a message with the word “paste” anywhere in it, and Claude will automatically read the image.
How It Works, Step by Step
Here’s the full flow from screenshot to Claude seeing the image:
| Step | What Happens | Where | Time |
|---|---|---|---|
| 1 | User types message containing “paste” | Claude Code CLI | |
| 2 | Hook reads stdin, detects “paste” keyword via jq + grep |
WSL2 (bash) | <10ms |
| 3 | Script calls wslpath -w to convert temp path |
WSL2 | <5ms |
| 4 | PowerShell grabs clipboard image and saves to UNC path | Windows (via WSL2 interop) | ~400ms |
| 5 | Script checks md5sum against last paste hash | WSL2 (bash) | <10ms |
| 6 | Script outputs hookSpecificOutput JSON to stdout |
WSL2 (bash) | <1ms |
| 7 | Claude Code receives additional context with file path | Claude Code | |
| 8 | Claude uses Read tool to view the PNG image | Claude Code |
Total overhead: ~0.4 seconds, only when your message contains “paste”. All other messages have zero added latency.
Key Takeaways for WSL2 Developers
This project exposed several WSL2 interop behaviors that aren’t well-documented. If you’re building anything that crosses the WSL2/Windows boundary, keep these in mind:
| Problem | Root Cause | Solution |
|---|---|---|
| Large stdout from powershell.exe is empty | WSL2 silently drops payloads over ~50KB | Write to a file via wslpath -w UNC paths |
| Get-Clipboard returns nothing for text | WSL2 session context blocks some cmdlet output | Use -Format Image with System.Drawing (works) |
| System.Windows.Forms produces no output | COM apartment threading broken in WSL2 context | Use System.Drawing assembly instead |
| Hook context silently ignored | Missing hookSpecificOutput wrapper |
Nest under hookSpecificOutput.hookEventName |
| PowerShell adds ~0.4s to every message | PowerShell startup overhead | Keyword trigger: only call PS when needed |
| Same image injected repeatedly | Clipboard persists between messages | md5sum hash deduplication |
The Golden Rule of WSL2 Interop
Never pipe large data through stdout between Windows and WSL2. Use the shared filesystem instead. wslpath -w gives you UNC paths that both sides can read and write to. Keep stdout for small control signals (“SAVED”, “OK”, “ERROR”) and let the filesystem handle the payload.
FAQ
Why can’t I paste images in Claude Code on WSL2?
WSL2 runs a real Linux kernel that doesn’t have access to the Windows clipboard’s image data. Text clipboard works through utilities like clip.exe, but binary image data doesn’t cross the WSL2/Windows boundary. Claude Code’s native Ctrl+V image paste relies on terminal clipboard support, which WSL2 terminals don’t provide for images.
How do I access the Windows clipboard from WSL2?
You can call powershell.exe directly from WSL2 to access Windows clipboard functions. For images, use Get-Clipboard -Format Image with the System.Drawing assembly. The key limitation is that you can’t pipe large image data back through stdout. Instead, save the image directly to a file using a wslpath -w converted path that PowerShell can write to.
How do Claude Code hooks work with UserPromptSubmit?
A UserPromptSubmit hook runs a command every time you send a message. The hook receives your prompt as JSON on stdin and can output a JSON response with hookSpecificOutput containing an additionalContext field. This context gets appended to your message before Claude processes it, which makes it perfect for injecting file paths, metadata, or other information.
Can I use PowerShell from WSL2 to get clipboard images?
Yes, but with caveats. Get-Clipboard -Format Image correctly detects images in the Windows clipboard when called from WSL2. However, you cannot pipe the image data back through stdout (it silently drops large payloads). The workaround is to have PowerShell save the image directly to a file at a wslpath -w converted path, then read that file from the Linux side.
What is wslpath and how does it convert paths between Windows and WSL?
wslpath is a built-in WSL utility that converts paths between Linux and Windows formats. wslpath -w /tmp/file.png returns \\wsl$\Ubuntu\tmp\file.png (a UNC path). This UNC path lets Windows programs (like PowerShell) read and write files inside the WSL2 filesystem. It’s the bridge that makes file-based data exchange between the two environments possible.
How do I set up a Claude Code hook for automatic clipboard image pasting?
Create a bash script at ~/.claude/hooks/clipboard-image-bridge.sh that reads stdin for the “paste” keyword, calls PowerShell to save the clipboard image to /tmp/, and outputs the correct hookSpecificOutput JSON. Then register it in ~/.claude/settings.json under the UserPromptSubmit hook event with a 10-second timeout. Full script and config are provided in the Complete Script section above.
When should I use a VS Code extension instead of this hook?
Since this article was first published, VS Code extensions for clipboard image paste in Claude Code have appeared on the marketplace. If you already work in VS Code (using the Remote-WSL extension or running Claude Code in the integrated terminal), an extension is the faster install path. Use this hook-based approach when you work outside VS Code (wezterm, Alacritty, tmux, Windows Terminal) or when you want the solution to be part of your dotfiles (no IDE dependency). Both approaches solve the same underlying problem: WSL2 can’t directly read Windows clipboard image data, so some helper has to save the image to a file first.
