Skip to content

Instantly share code, notes, and snippets.

@1v
Last active January 8, 2026 17:27
Show Gist options
  • Select an option

  • Save 1v/3a6a4b893d406dfc1a6fd6211f86d0f3 to your computer and use it in GitHub Desktop.

Select an option

Save 1v/3a6a4b893d406dfc1a6fd6211f86d0f3 to your computer and use it in GitHub Desktop.
Paste screenshots into Claude Code terminal (Windows/WSL) - AutoHotkey v2 clipboard image watcher for Cursor

Clipboard Image to Path for Claude Code (Windows/WSL)

AutoHotkey v2 script that lets you paste screenshots directly into Claude Code terminal on Windows.

Problem

Claude Code terminal accepts image paths, but when you take a screenshot (Win+Shift+S), the clipboard contains raw image data - not a file path. This script bridges that gap.

How It Works

  1. Take a screenshot with Win+Shift+S
  2. Script automatically saves it as PNG and puts the file path in clipboard
  3. Paste into Claude Code terminal - it receives the path and displays the image

Features

  • Works with both WSL and Windows Cursor/VS Code workspaces
  • Auto-detects WSL vs Windows mode from window title ([WSL: ...])
  • Stores images in WSL filesystem (/tmp/clipboard_images/)
  • Uses CF_HDROP clipboard format - works in Chrome and other apps too
  • Auto-cleanup: keeps max 5 files, removes files older than 60 minutes
  • System tray icon with menu

Requirements

  • Windows 10/11
  • AutoHotkey v2
  • WSL with Ubuntu (or change WslDistro variable)

Installation

  1. Install AutoHotkey v2
  2. Start WSL (wsl in terminal)
  3. Double-click clipboard_image_to_path.ahk

Usage

  1. Take a screenshot (Win+Shift+S, select area)
  2. Switch to Cursor/VS Code terminal with Claude Code
  3. Press Ctrl+V - the image path is pasted

The script detects:

  • WSL Cursor (window title contains [WSL:): pastes /tmp/clipboard_images/...
  • Windows Cursor: pastes \\wsl$\Ubuntu\tmp\clipboard_images\...
  • Other apps (Chrome, etc.): passes through file drop format

Configuration

Edit these variables at the top of the script:

global WslDistro := "Ubuntu"           ; Your WSL distro name
global MaxFiles := 5                   ; Max images to keep
global MaxAgeMinutes := 60             ; Delete images older than this

Tray Menu

Right-click the system tray icon:

  • Open Cache Folder - View saved images
  • Cleanup Now - Delete old images
  • Exit - Stop the script

Credits

Inspired by clipboard-image-watcher (C# version).

; Clipboard Image Watcher - AutoHotkey v2 Version
; Monitors clipboard for images and converts them to file paths for terminal pasting.
;
; Requirements: AutoHotkey v2 (https://www.autohotkey.com/)
;
; How to use:
; 1. Install AutoHotkey v2
; 2. Double-click this script to run
; 3. Take a screenshot (Win+Shift+S)
; 4. The image is automatically saved and clipboard becomes the file path
; 5. Paste into Claude Code terminal
;
; Right-click system tray icon to exit
#Requires AutoHotkey v2.0
#SingleInstance Force
Persistent
; Configuration
global WslDistro := "Ubuntu"
global CacheDir := "\\wsl$\" . WslDistro . "\tmp\clipboard_images"
global WslPathPrefix := "/tmp/clipboard_images/"
global MaxFiles := 5
global MaxAgeMinutes := 60
global LastImageHash := ""
global IsProcessing := false
; Load GDI+ library at startup
global hGdiplus := DllCall("LoadLibrary", "Str", "gdiplus", "Ptr")
global pGdipToken := 0
; Initialize GDI+
InitGDIPlus()
; Check if WSL is accessible and create cache directory
wslRoot := "\\wsl$\" . WslDistro
if !DirExist(wslRoot) {
MsgBox("WSL (" . WslDistro . ") is not running or not accessible.`n`nPlease start WSL first, then restart this script.", "Clipboard Image Watcher - Error", 48)
ExitApp()
}
if !DirExist(CacheDir)
DirCreate(CacheDir)
; Create tray menu
A_TrayMenu.Delete()
A_TrayMenu.Add("Open Cache Folder", (*) => Run(CacheDir))
A_TrayMenu.Add("Cleanup Now", (*) => CleanupOldFiles())
A_TrayMenu.Add()
A_TrayMenu.Add("Exit", (*) => ExitScript())
; Set tray tip
A_IconTip := "Clipboard Image Watcher`nWSL storage: " . WslDistro . "`nWaiting for screenshots..."
; Show startup notification
TrayTip("Running (WSL: " . WslDistro . ")`nAuto-detects WSL/Windows mode from active window.", "Clipboard Image Watcher", 1)
; Monitor clipboard changes
OnClipboardChange(ClipboardChanged)
; Cleanup timer (every 5 minutes)
SetTimer(CleanupOldFiles, 300000)
; Initial cleanup
CleanupOldFiles()
return
; Ctrl+V hotkey - convert path for Cursor terminals
$^v:: {
global WslPathPrefix, CacheDir
title := WinGetTitle("A")
; Only intercept in Cursor when clipboard has our cached file
if InStr(title, "- Cursor") {
clipContent := A_Clipboard
; Check if clipboard contains a path from our cache
if InStr(clipContent, CacheDir) {
SplitPath(clipContent, &filename)
if InStr(title, "[WSL:") {
; WSL Cursor - use Linux path
A_Clipboard := WslPathPrefix . filename
} else {
; Windows Cursor - convert CF_HDROP to text path
A_Clipboard := clipContent
}
Send("{Ctrl down}v{Ctrl up}")
; Restore file drop list after paste
Sleep(100)
Run("powershell -NoProfile -Command `"Set-Clipboard -Path '" . clipContent . "'`"",, "Hide")
return
}
}
; For everything else, just paste normally
Send("{Ctrl down}v{Ctrl up}")
}
ExitScript() {
ShutdownGDIPlus()
ExitApp()
}
InitGDIPlus() {
global pGdipToken
si := Buffer(24, 0)
NumPut("UInt", 1, si, 0)
DllCall("gdiplus\GdiplusStartup", "Ptr*", &pGdipToken, "Ptr", si, "Ptr", 0)
}
ShutdownGDIPlus() {
global pGdipToken
if pGdipToken
DllCall("gdiplus\GdiplusShutdown", "Ptr", pGdipToken)
}
ClipboardChanged(DataType) {
global IsProcessing, LastImageHash, CacheDir
; Prevent re-entry
if IsProcessing
return
; Only process if clipboard contains image data
if (DataType != 2)
return
IsProcessing := true
try {
; Small delay to let clipboard settle
Sleep(50)
; Create a unique filename
timestamp := FormatTime(, "yyyyMMdd_HHmmss")
filename := "clipboard_" . timestamp . "_" . Random(1000, 9999) . ".png"
filepath := CacheDir "\" filename
; Save clipboard image to file
if SaveClipboardImage(filepath) {
; Check file was created and has content
if FileExist(filepath) {
fileSize := FileGetSize(filepath)
; Skip if same as last image (by size - simple hash)
if (fileSize = LastImageHash) {
try FileDelete(filepath)
IsProcessing := false
return
}
LastImageHash := fileSize
; Set clipboard as file drop list
RunWait("powershell -NoProfile -Command `"Set-Clipboard -Path '" . filepath . "'`"",, "Hide")
; Update tray tip
A_IconTip := "Clipboard Image Watcher`nReady to paste: " . filename
; Cleanup old files
SetTimer(() => CleanupOldFiles(), -1000)
}
}
} catch as e {
; Silently fail
}
IsProcessing := false
}
SaveClipboardImage(filepath) {
global pGdipToken
if !pGdipToken
return false
result := false
hBitmap := 0
pBitmap := 0
clipboardOpened := false
try {
; Open clipboard
loop 10 {
if DllCall("OpenClipboard", "Ptr", 0) {
clipboardOpened := true
break
}
Sleep(10)
}
if !clipboardOpened
return false
; Try to get bitmap handle (CF_BITMAP = 2)
hBitmap := DllCall("GetClipboardData", "UInt", 2, "Ptr")
if !hBitmap {
; Try CF_DIB (8) and convert
hDib := DllCall("GetClipboardData", "UInt", 8, "Ptr")
if hDib {
hBitmap := DibToHBitmap(hDib)
}
}
DllCall("CloseClipboard")
clipboardOpened := false
if !hBitmap
return false
; Create GDI+ bitmap from HBITMAP
DllCall("gdiplus\GdipCreateBitmapFromHBITMAP", "Ptr", hBitmap, "Ptr", 0, "Ptr*", &pBitmap)
if !pBitmap {
DllCall("DeleteObject", "Ptr", hBitmap)
return false
}
; Get PNG encoder CLSID
encoderCLSID := Buffer(16, 0)
GetEncoderCLSID("image/png", encoderCLSID)
; Save to file
status := DllCall("gdiplus\GdipSaveImageToFile"
, "Ptr", pBitmap
, "WStr", filepath
, "Ptr", encoderCLSID
, "Ptr", 0)
result := (status = 0)
} catch as e {
; Error occurred
}
; Cleanup
if clipboardOpened
DllCall("CloseClipboard")
if pBitmap
DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmap)
if hBitmap
DllCall("DeleteObject", "Ptr", hBitmap)
return result
}
DibToHBitmap(hDib) {
pDib := DllCall("GlobalLock", "Ptr", hDib, "Ptr")
if !pDib
return 0
; Parse BITMAPINFOHEADER
headerSize := NumGet(pDib, 0, "UInt")
width := NumGet(pDib, 4, "Int")
height := NumGet(pDib, 8, "Int")
bitCount := NumGet(pDib, 14, "UShort")
; Calculate pixel data offset
colorTableSize := 0
if (bitCount <= 8)
colorTableSize := (1 << bitCount) * 4
pixelOffset := headerSize + colorTableSize
; Create device context and bitmap
hDC := DllCall("GetDC", "Ptr", 0, "Ptr")
hBitmap := DllCall("CreateCompatibleBitmap", "Ptr", hDC, "Int", width, "Int", Abs(height), "Ptr")
if hBitmap {
hMemDC := DllCall("CreateCompatibleDC", "Ptr", hDC, "Ptr")
hOldBmp := DllCall("SelectObject", "Ptr", hMemDC, "Ptr", hBitmap, "Ptr")
DllCall("SetDIBitsToDevice"
, "Ptr", hMemDC
, "Int", 0, "Int", 0
, "UInt", width, "UInt", Abs(height)
, "Int", 0, "Int", 0
, "UInt", 0, "UInt", Abs(height)
, "Ptr", pDib + pixelOffset
, "Ptr", pDib
, "UInt", 0)
DllCall("SelectObject", "Ptr", hMemDC, "Ptr", hOldBmp)
DllCall("DeleteDC", "Ptr", hMemDC)
}
DllCall("ReleaseDC", "Ptr", 0, "Ptr", hDC)
DllCall("GlobalUnlock", "Ptr", hDib)
return hBitmap
}
GetEncoderCLSID(mimeType, clsidBuffer) {
numEncoders := 0
size := 0
DllCall("gdiplus\GdipGetImageEncodersSize", "UInt*", &numEncoders, "UInt*", &size)
if !size || !numEncoders
return false
encoders := Buffer(size, 0)
DllCall("gdiplus\GdipGetImageEncoders", "UInt", numEncoders, "UInt", size, "Ptr", encoders)
; ImageCodecInfo structure size varies by pointer size
stride := 48 + 7 * A_PtrSize
Loop numEncoders {
offset := (A_Index - 1) * stride
pMimeType := NumGet(encoders, offset + 32 + 4 * A_PtrSize, "Ptr")
if pMimeType && (StrGet(pMimeType, "UTF-16") = mimeType) {
DllCall("RtlMoveMemory", "Ptr", clsidBuffer, "Ptr", encoders.Ptr + offset, "UInt", 16)
return true
}
}
return false
}
CleanupOldFiles() {
global CacheDir, MaxFiles, MaxAgeMinutes
files := []
try {
Loop Files, CacheDir "\clipboard_*.png" {
files.Push({path: A_LoopFilePath, time: A_LoopFileTimeModified})
}
} catch {
return
}
if !files.Length
return
; Sort by time (newest first) using simple bubble sort
n := files.Length
Loop n - 1 {
i := A_Index
Loop n - i {
j := A_Index
if (files[j].time < files[j + 1].time) {
temp := files[j]
files[j] := files[j + 1]
files[j + 1] := temp
}
}
}
now := A_Now
; Remove old files and keep only MaxFiles
for i, file in files {
try {
fileAge := DateDiff(now, file.time, "Minutes")
if (fileAge > MaxAgeMinutes) || (i > MaxFiles) {
FileDelete(file.path)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment