Skip to content

Instantly share code, notes, and snippets.

@cowwoc
Last active February 1, 2026 03:30
Show Gist options
  • Select an option

  • Save cowwoc/be4893de4bed8f3d819f3bc91a889a7a to your computer and use it in GitHub Desktop.

Select an option

Save cowwoc/be4893de4bed8f3d819f3bc91a889a7a to your computer and use it in GitHub Desktop.
Claude Code clipboard sync installer
<#
.SYNOPSIS
Installs Claude Code clipboard sync for Windows Terminal + WSL2
.DESCRIPTION
This script:
1. Creates the clipboard watcher PowerShell script
2. Creates a VBS wrapper for hidden execution
3. Creates a startup shortcut
4. Configures Windows Terminal keybindings
.NOTES
Run as: powershell -ExecutionPolicy Bypass -File install-claude-clipboard-sync.ps1
#>
param(
[string]$WslDistro = "Ubuntu",
[string]$WslUser = $env:USERNAME.ToLower(),
[string]$ClaudeClipboardPath = "/home/node/clipboard/screenshot.png",
[switch]$Uninstall
)
$ErrorActionPreference = "Stop"
# Paths
$scriptPath = "$env:USERPROFILE\Documents\claude-sync-clipboard.ps1"
$vbsPath = "$env:USERPROFILE\Documents\claude-sync-clipboard.vbs"
$startupLink = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\claude-sync-clipboard.lnk"
$wtSettingsPath = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
$wtPreviewSettingsPath = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json"
# Determine WSL destination path
# This writes to ~/.claude/clipboard/ on WSL host, which Docker maps to /home/node/.claude/clipboard/ inside container
$wslDestPath = "\\wsl$\$WslDistro\home\$WslUser\.claude\clipboard\screenshot.png"
function Write-Status {
param([string]$Message, [string]$Type = "Info")
$color = switch ($Type) {
"Success" { "Green" }
"Warning" { "Yellow" }
"Error" { "Red" }
default { "Cyan" }
}
Write-Host "[$Type] " -ForegroundColor $color -NoNewline
Write-Host $Message
}
function Stop-ClipboardWatcher {
$processes = Get-Process -Name powershell, pwsh -ErrorAction SilentlyContinue |
Where-Object { $_.MainWindowTitle -eq "" } |
ForEach-Object {
try {
$cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($_.Id)").CommandLine
if ($cmdLine -match "claude-sync-clipboard") { $_ }
} catch { }
}
foreach ($proc in $processes) {
Write-Status "Stopping existing clipboard watcher (PID: $($proc.Id))..." "Warning"
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
}
function Install-ClipboardWatcher {
Write-Status "Creating clipboard watcher script..."
$clipboardWatcherScript = @"
Add-Type -WarningAction SilentlyContinue -TypeDefinition @`"
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Security.Cryptography;
public class ClipboardWatcher : Form {
[DllImport("user32.dll", SetLastError = true)]
private static extern bool AddClipboardFormatListener(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RemoveClipboardFormatListener(IntPtr hwnd);
private const int WM_CLIPBOARDUPDATE = 0x031D;
private string destPath;
private string lastHash = "";
public ClipboardWatcher(string dest) {
destPath = dest;
this.ShowInTaskbar = false;
this.WindowState = FormWindowState.Minimized;
this.FormBorderStyle = FormBorderStyle.None;
this.Load += (s, e) => { this.Visible = false; };
AddClipboardFormatListener(this.Handle);
}
protected override void WndProc(ref Message m) {
if (m.Msg == WM_CLIPBOARDUPDATE) {
ProcessClipboard();
}
base.WndProc(ref m);
}
private void ProcessClipboard() {
try {
if (Clipboard.ContainsImage()) {
Image img = Clipboard.GetImage();
if (img != null) {
using (MemoryStream ms = new MemoryStream()) {
img.Save(ms, ImageFormat.Png);
byte[] bytes = ms.ToArray();
string hash = BitConverter.ToString(MD5.Create().ComputeHash(bytes));
if (hash != lastHash) {
string dir = Path.GetDirectoryName(destPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
File.WriteAllBytes(destPath, bytes);
lastHash = hash;
}
}
img.Dispose();
}
} else {
if (File.Exists(destPath)) File.Delete(destPath);
lastHash = "";
}
} catch { }
}
protected override void OnFormClosing(FormClosingEventArgs e) {
RemoveClipboardFormatListener(this.Handle);
base.OnFormClosing(e);
}
}
`"@ -ReferencedAssemblies System.Windows.Forms, System.Drawing
`$dest = "$wslDestPath"
`$watcher = New-Object ClipboardWatcher(`$dest)
[System.Windows.Forms.Application]::Run(`$watcher)
"@
$clipboardWatcherScript | Set-Content $scriptPath -Encoding UTF8
Write-Status "Created: $scriptPath" "Success"
}
function Install-VbsWrapper {
Write-Status "Creating VBS wrapper for hidden execution..."
$vbsContent = @"
CreateObject("WScript.Shell").Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""$scriptPath""", 0, False
"@
$vbsContent | Set-Content $vbsPath -Encoding ASCII
Write-Status "Created: $vbsPath" "Success"
}
function Install-StartupShortcut {
Write-Status "Creating startup shortcut..."
$ws = New-Object -ComObject WScript.Shell
$shortcut = $ws.CreateShortcut($startupLink)
$shortcut.TargetPath = "wscript.exe"
$shortcut.Arguments = "`"$vbsPath`""
$shortcut.Save()
Write-Status "Created: $startupLink" "Success"
}
function Update-WindowsTerminalSettings {
param([string]$settingsPath)
if (-not (Test-Path $settingsPath)) {
Write-Status "Windows Terminal settings not found at: $settingsPath" "Warning"
return $false
}
Write-Status "Updating Windows Terminal settings: $settingsPath"
# Backup
$backupPath = "$settingsPath.backup.$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Copy-Item $settingsPath $backupPath
Write-Status "Backup created: $backupPath"
# Read and parse JSON
$content = Get-Content $settingsPath -Raw
# Remove comments for parsing (Windows Terminal allows comments)
$jsonContent = $content -replace '(?m)^\s*//.*$', '' -replace '/\*[\s\S]*?\*/', ''
try {
$settings = $jsonContent | ConvertFrom-Json
} catch {
Write-Status "Failed to parse settings.json: $_" "Error"
return $false
}
# Define our action
$actionId = "User.SendClaudeClipboardPath"
$esc = [char]0x1b # Actual ESC character - ConvertTo-Json will escape it as \u001b
$newAction = @{
command = @{
action = "sendInput"
input = "${esc}[200~$ClaudeClipboardPath${esc}[201~"
}
id = $actionId
}
$newKeybinding = @{
id = $actionId
keys = "alt+v"
}
# Handle actions array
if (-not $settings.PSObject.Properties['actions']) {
$settings | Add-Member -NotePropertyName 'actions' -NotePropertyValue @()
}
# Remove existing action with same id
$settings.actions = @($settings.actions | Where-Object { $_.id -ne $actionId })
$settings.actions += $newAction
# Handle keybindings array
if (-not $settings.PSObject.Properties['keybindings']) {
$settings | Add-Member -NotePropertyName 'keybindings' -NotePropertyValue @()
}
# Remove existing keybinding with same id
$settings.keybindings = @($settings.keybindings | Where-Object { $_.id -ne $actionId })
$settings.keybindings += $newKeybinding
# Write back
$settings | ConvertTo-Json -Depth 100 | Set-Content $settingsPath -Encoding UTF8
Write-Status "Windows Terminal settings updated" "Success"
return $true
}
function Uninstall-All {
Write-Status "Uninstalling Claude clipboard sync..." "Warning"
Stop-ClipboardWatcher
if (Test-Path $scriptPath) { Remove-Item $scriptPath -Force; Write-Status "Removed: $scriptPath" }
if (Test-Path $vbsPath) { Remove-Item $vbsPath -Force; Write-Status "Removed: $vbsPath" }
if (Test-Path $startupLink) { Remove-Item $startupLink -Force; Write-Status "Removed: $startupLink" }
Write-Status "Uninstall complete. Windows Terminal settings were not modified." "Success"
Write-Status "Manually remove the 'User.SendClaudeClipboardPath' action and keybinding if desired." "Info"
}
function Show-Summary {
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Installation Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Configuration:" -ForegroundColor Yellow
Write-Host " WSL Distro: $WslDistro"
Write-Host " WSL User: $WslUser"
Write-Host " WSL host path: \\wsl`$\$WslDistro\home\$WslUser\.claude\clipboard\screenshot.png"
Write-Host " Docker path: $ClaudeClipboardPath (via volume mount)"
Write-Host ""
Write-Host "Usage:" -ForegroundColor Yellow
Write-Host " 1. Copy a screenshot to clipboard (Win+Shift+S, then select area)"
Write-Host " 2. In Windows Terminal with Claude Code, press Alt+V"
Write-Host " 3. The path to the screenshot will be pasted"
Write-Host ""
Write-Host "To start now without reboot:" -ForegroundColor Yellow
Write-Host " wscript.exe `"$vbsPath`""
Write-Host ""
Write-Host "To uninstall:" -ForegroundColor Yellow
Write-Host " powershell -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Uninstall"
Write-Host ""
}
# Main
Write-Host ""
Write-Host "Claude Code Clipboard Sync Installer" -ForegroundColor Cyan
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host ""
if ($Uninstall) {
Uninstall-All
exit 0
}
# Stop any existing watcher
Stop-ClipboardWatcher
# Install components
Install-ClipboardWatcher
Install-VbsWrapper
Install-StartupShortcut
# Update Windows Terminal settings
$wtUpdated = $false
if (Test-Path $wtSettingsPath) {
$wtUpdated = Update-WindowsTerminalSettings -settingsPath $wtSettingsPath
}
if (Test-Path $wtPreviewSettingsPath) {
$wtUpdated = (Update-WindowsTerminalSettings -settingsPath $wtPreviewSettingsPath) -or $wtUpdated
}
if (-not $wtUpdated) {
Write-Status "Could not find Windows Terminal settings. Manual configuration required." "Warning"
Write-Host ""
Write-Host "Add to your Windows Terminal settings.json:" -ForegroundColor Yellow
Write-Host @"
"actions": [
{
"command": {
"action": "sendInput",
"input": "\u001b[200~$ClaudeClipboardPath\u001b[201~"
},
"id": "User.SendClaudeClipboardPath"
}
],
"keybindings": [
{
"id": "User.SendClaudeClipboardPath",
"keys": "alt+v"
}
]
"@
}
# Start the watcher
Write-Status "Starting clipboard watcher..."
Start-Process wscript.exe -ArgumentList "`"$vbsPath`""
Write-Status "Clipboard watcher started" "Success"
Show-Summary
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment