Skip to content

flamerged/sshshot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

sshshot

npm version npm downloads license CI provenance

Take a screenshot locally → it auto-uploads via SSH → the remote file path lands on your clipboard. Built for pasting screenshots into Claude Code, OpenAI Codex, Aider, or any other AI agent running over SSH on a remote box, where you can't drag-and-drop images. macOS, Linux (X11 & Wayland), Windows, WSL.

npm install -g @flamerged/sshshot

Fork of HendrikYtt/clipshot with macOS support added (cherry-picked from upstream PR #1, with both Codex-flagged P1 bugs fixed) and modern dev tooling (Yarn 4, ESLint 9, Husky, lint-staged, branch protection). Original credit and license preserved.

Demo on Windows / PowerShell Upstream demo on Windows. macOS and Linux X11 demos are on the Roadmap.

The problem

You SSH'd into a remote dev box to use Claude Code, Codex, Aider, or any other CLI AI agent. You want to ask it about a screenshot. But you can't paste an image into a remote terminal — there's no drag-and-drop, no clipboard sharing, and uploading via scp then typing the path manually breaks your flow.

The fix

sshshot runs as a tiny background daemon on your local machine. The moment you take a screenshot:

  1. Locally — sshshot detects the new screenshot (clipboard or screenshot folder).
  2. Uploads it — pipes the image bytes through SSH to ~/sshshot-screenshots/screenshot-<timestamp>.png on your remote.
  3. Pastes back — replaces your clipboard with the remote absolute path to that file.
  4. You paste the path into Claude Code / Codex / wherever — the AI's tool layer reads the file and sees your screenshot.

No manual scp. No typing paths. Just Cmd+Shift+4, then Cmd+V into your AI prompt.

Quick start

# install globally
npm install -g @flamerged/sshshot

# first run — interactive setup, auto-detects hosts from ~/.ssh/config
sshshot

# later
sshshot start    # start daemon (pick a remote)
sshshot stop     # stop daemon
sshshot status   # is it running, and against which remote
sshshot config   # change remotes

Per-OS setup

macOS

Two screenshot flows are supported:

  • Cmd+Shift+3 / 4 / 5 — saves a .png file to your Desktop (or wherever defaults read com.apple.screencapture location points). Zero dependencies. This is what most people use.

  • Cmd+Ctrl+Shift+3 / 4 / 5 — image goes straight to clipboard, no file. Requires pngpaste:

    brew install pngpaste

If pngpaste isn't installed, the daemon logs a warning and falls back silently to the file-watcher path.

Linux

Both X11 and Wayland are supported. The daemon auto-detects which session you're in (XDG_SESSION_TYPE / WAYLAND_DISPLAY) and uses the right tool. Install one of:

# Wayland (default on modern Ubuntu/Fedora/etc.)
sudo apt install wl-clipboard   # Debian/Ubuntu
sudo dnf install wl-clipboard   # Fedora
sudo pacman -S wl-clipboard     # Arch

# X11 (older sessions, X-only environments)
sudo apt install xclip          # Debian/Ubuntu
sudo dnf install xclip          # Fedora
sudo pacman -S xclip            # Arch

If the wrong tool is installed for your session type, the daemon will print a clear "install X" message at startup and clipboard reads/writes will silently no-op until you fix it.

Windows / WSL

Works out of the box — uses PowerShell's System.Windows.Forms.Clipboard. WSL is auto-detected via /proc/version.

Use cases

  • Claude Code over SSH — paste the path; Claude reads the file via its Read tool.
  • OpenAI Codex CLI — same flow; Codex reads files referenced in prompts.
  • Aider, Continue, any AI agent that can read local files on the remote — works.
  • Plain ssh sessions where you want to ship a screenshot to a teammate's box — also works; you just paste the path into a chat.

Features

  • Auto-detects SSH remotes from ~/.ssh/config (does not scan shell history — by design, see Security)
  • Local mode for when you don't need a remote — saves to ~/sshshot-screenshots/, copies the local path
  • Remote mode uploads via your existing SSH config (ControlMaster connection reuse honored if you've set it)
  • Per-source MD5 deduping — Mac file-saved and clipboard screenshots can't collide and re-upload each other (a real bug Codex caught in upstream PR #1)
  • Background daemon — start once, take screenshots all day
  • WSL support (reads Windows clipboard from inside Linux)

Commands

sshshot                  first-time setup, then start the daemon
sshshot start            start daemon (prompts for which remote)
sshshot stop             stop the running daemon
sshshot status           show running PID + target remote
sshshot target           show current active target + available targets
sshshot target <name>    switch active target without restarting the daemon
sshshot pause            keep daemon alive but stop touching the clipboard
sshshot resume           resume processing screenshots
sshshot toggle           flip pause/resume in one command
sshshot config           add/remove SSH remotes
sshshot uninstall        stop daemon + remove ~/.config/sshshot

Pausing without losing your target

When you want to take a screenshot for Slack/GitHub/iMessage and don't want sshshot replacing your clipboard with the remote path, don't stop the daemon — pause it:

sshshot pause     # daemon stays alive, target stays selected, clipboard untouched
# … take your local screenshot, paste it wherever …
sshshot resume    # back to processing screenshots into the remote
# or:
sshshot toggle    # flips between the two

sshshot status shows [paused] next to the running PID so you know which mode you're in. This is intentionally lighter than stop/start — no process restart, no re-prompting for which remote, instant flip.

Switching targets at runtime

If you have multiple remotes configured and want to change which one screenshots ship to without stopping/starting the daemon, use sshshot target:

sshshot target myhost     # next screenshot goes to "myhost"
sshshot target prod       # ...now "prod"
sshshot target local      # local-mode (saves to ~/sshshot-screenshots)
sshshot target            # show current active target

The daemon picks up the change on its next poll cycle (~200 ms). Works from any terminal, including Zed/VSCode integrated terminals — it's just a CLI command, no window-focus magic, no extra permissions.

Logs land in ~/.config/sshshot/logs/sshshot-<timestamp>.log (rotated hourly).

How it works (technical)

  • TypeScript CLI. Single binary entry (dist/index.js) that double-purposes as both the foreground configurator and the daemon (when invoked with --daemon <remote>).
  • Background daemon spawned with spawn(..., {detached: true}) (no shell, no nohup) so closing your terminal doesn't kill it.
  • 200 ms poll loop reads the clipboard and (on macOS) the screenshot folder, MD5-hashes new bytes, dedupes per source, then uploads.
  • Upload mechanic: ssh <remote> 'mkdir -p ~/sshshot-screenshots && cat > ~/sshshot-screenshots/<filename>' with the image piped to stdin. No temp files, no scp.
  • All OS-specific work is shell-outs (xclip, pbcopy / pngpaste, PowerShell Clipboard.GetImage()). No native modules, no Electron, no large runtime cost — the daemon idles around 30–80 MB resident.

FAQ

Does this need any privileges? Sudo? Mac Screen Recording permission? No. sshshot doesn't capture the screen — it forwards screenshots you took with your OS's built-in keystrokes. macOS pbcopy / pngpaste and reading ~/Desktop don't require TCC entitlements.

Will Claude Code / Codex actually see my screenshot, or just receive a string? They receive the path as a string. Both tools (and most AI agents) are happy to call their built-in file-read on a .png you reference, decode the image, and use it as input. You don't have to do anything — just paste the path in your prompt and ask your question.

What about Cmd+Shift+5's video capture / screen recording feature? sshshot only ships screenshot images (.png), not screen recordings (.mov). Recordings are out of scope.

Does this expose my screenshots to anyone else? Only to the remote you select. Transport is plain SSH, so authentication and encryption are whatever your ~/.ssh/config already provides. The screenshot is uploaded under your remote user's home directory.

Why not use ShareX / Flameshot / Maccy / etc.? Those tools either capture and host externally (privacy: your screenshot lives on a 3rd-party server) or are clipboard managers without an SSH-paste path. sshshot's whole point is the remote box you already use becomes the destination.

Why not a browser extension? You'd still need a server to receive uploads, and AI agents running over SSH don't see your browser's clipboard. SSH-side delivery cuts out the middleman.

Roadmap

Reliability fixes

These are user-visible bugs surfaced by audit reviews — none silent, all observable in real workflows.

  • SSH upload uses BatchMode=yes + an absolute upload timeout (via AbortController/kill) + a stdin EPIPE handler, so a hung remote doesn't permanently occupy an upload slot
  • sshshot stop honors the daemon's 5 s in-flight-upload grace before SIGKILL — currently SIGTERM is sent and the process is killed before the daemon can finish a mid-flight upload
  • Decouple the poll loop from upload completion so a slow SSH upload can't cause the daemon to miss the next clipboard / file screenshot
  • Process all stable macOS screenshots newer than lastSeenScreenshotMtime in mtime order — currently a burst of multiple rapid screenshots only uploads the most recent one
  • Editing remotes via sshshot config preserves activeTarget and paused (currently both are silently dropped)
  • Typo'd CLI commands like sshshot stat print a clear error and exit 1 — currently they fall through every known-command branch and run first-run setup, which is confusing
  • Fatal CLI errors set a non-zero exit code so scripts and CI detect failures (currently main().catch(console.error) exits 0)
  • loadConfig runtime-validates the parsed JSON shape (so a hand-edited { "remotes": "host" } doesn't break the daemon); saveConfig writes via temp-file + atomic rename so a crash mid-write can't truncate the file
  • Cache clipboard-tool availability at startup — currently a missing pngpaste/xclip/wl-paste is re-spawned every 200 ms poll
  • ~/.ssh/config parsing handles unreadable files (try/catch on read), dedupes auto-detected aliases, and filters ? wildcards in addition to *
  • removePidFile only unlinks when the file's contents still match process.pid, so a stop-and-restart race can't delete the new daemon's pid file
  • mtime-vs-daemon-start gate so a Screenshot-named file copied into the watch dir (restored from backup, cp'd in, etc.) isn't mistakenly uploaded as a fresh screenshot

Features and ergonomics

  • Context-aware clipboard routing (macOS first) — replace clipboard with the remote path only when the foreground app is a terminal-ish thing (Terminal/iTerm2/Warp/Ghostty/Zed/VS Code/etc.). For Slack/GitHub/iMessage screenshots the image bytes stay on the clipboard. pause/resume is the short-term workaround.
  • Retry on SSH failure with exponential backoff — currently a failed upload silently drops the screenshot
  • System notification on upload success / failure (osascript -e 'display notification' on macOS, notify-send on Linux) so the daemon's status is visible without tailing logs
  • sshshot doctor — single command that reports Node version, platform/session detection, OS tool availability (pngpaste/xclip/wl-clipboard/pbcopy/PowerShell), config shape, daemon PID sanity, target reachability, SSH auth, and remote directory writability. Goes from "is it broken?" to "exactly this is missing"
  • Non-interactive CLI surface: sshshot start <target> shorthand, sshshot start --local, sshshot status --json, sshshot config add/remove/list, sshshot uninstall --yes, --foreground flag for debugging without detaching. Makes scripts, dotfiles, and launch agents painless
  • Start in local mode without any SSH remotes configured — first-run UX should offer "local only" as a valid path; currently sshshot start exits when remotes.length === 0 even though local is always a valid target
  • sshshot last / sshshot history — show the most recent N uploads with timestamps + remote paths
  • sshshot open — open the most recent screenshot in the system image viewer
  • Optional pngquant / optipng compression passthrough before upload — 80–90 % size reduction for similar visual quality, big win on slow links
  • Configurable remote upload directory (currently hardcoded to ~/sshshot-screenshots/); same for the local mode directory
  • Record macOS demo gif (current demo-windows.gif is the upstream Windows/PowerShell flow)
  • Record Linux X11 / Wayland demo gif
  • Optional inline image paste (instead of path) for tools that accept multipart pastes — future ergonomic win
  • File Socket.dev false-positive review request once Supply Chain score plateaus

Already shipped (kept for context)

  • ✅ macOS support (clipboard via pngpaste + screenshot-folder file watcher)
  • ✅ Linux Wayland clipboard via wl-clipboard (X11 still supported via xclip)
  • ✅ Localized macOS screenshot filenames (de, fr, es, pt, it, nl, sv, no, da, ja, ko, ru)
  • fs.watch event-driven detection (replaced 200ms polling on macOS, polling kept as backstop)
  • spawnSync migration for ssh -G and clipboard-write paths (no shell-interpolation surface)
  • ✅ Vitest test suite + CI (filename matching, hash dedupe, ssh-config parsing, safe-filename guard)
  • ✅ npm Trusted Publishing via OIDC + Sigstore provenance attestations
  • ✅ Conventional Commits + commitlint + branch-name enforcement (Husky)
  • ✅ Auto-publishing via semantic-release (fix: → patch, feat: → minor, breaking is manual)

Security

sshshot is intentionally narrow about what it touches:

  • Reads ~/.ssh/config — to enumerate hostnames you've already configured. Required for the auto-detect prompt during setup.
  • Reads/writes the clipboard — the whole point.
  • Reads the macOS screenshot folder (file-watcher mode) and clipboard via pngpaste — to capture the image you just took.
  • Pipes the image to a remote you selected via ssh user@host — the upload mechanic.

What sshshot does not do, by deliberate design:

  • Does not read your shell history (~/.bash_history, ~/.zsh_history). Reading shell history is the #1 behavioral signal of credential-stealing malware (info-stealers do exactly that to find AWS/SSH credentials), and Socket.dev's AI scanner correctly flags packages that do it. Earlier upstream versions scanned history; this fork removed it. Users without ~/.ssh/config entries can add hosts manually via the setup prompt.
  • Does not capture the screen. The OS captures; sshshot only forwards what you produced via Cmd+Shift / Cmd+Ctrl+Shift / Print Screen.
  • Does not phone home. No telemetry, analytics, or auto-update checks.
  • Does not require root, sudo, or any TCC entitlement.

Contributing

PRs welcome. The repo enforces:

  • master is protected — CI must pass, conversation must resolve, signed commits required.
  • Squash-merge only (single commit lands on master per PR).
  • Pre-commit hook runs Prettier and ESLint on staged files via lint-staged.
# clone and set up
git clone https://github.com/flamerged/sshshot.git
cd sshshot
corepack enable           # activates the pinned Yarn 4
yarn install              # generates yarn.lock the first time
yarn build                # tsc → dist/
yarn typecheck            # tsc --noEmit
yarn lint                 # eslint
yarn format               # prettier --write

Acknowledgements

Built on clipshot by Hendrik Ytt. macOS support originally proposed in clipshot#1 by amoghbanta — the architecture (concurrent clipboard via pngpaste + screenshot-folder file watcher) is theirs; sshshot fixed two P1 bugs flagged in OpenAI Codex's review of that PR.

License

MIT — original copyright Hendrik Ytt, fork copyright Mersad Ajanovic.

About

Take a screenshot locally → it auto-uploads via SSH → the remote path lands on your clipboard. Paste screenshots into Claude Code, OpenAI Codex, Aider, or any AI agent running over SSH. macOS, Linux X11, Windows, WSL.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors