Batch screenshots into a single Claude Code paste
Project description
paste-shots
A utility that lets you batch multiple screenshots and paste them into any app in one action — terminal AI tools like Claude Code and OpenCode, chat apps like Teams and Slack, issue trackers, email, and anywhere else that accepts image paste. The niche it fills: sending several screenshots in a single action, which the system clipboard alone can't do.
Runs as a system-tray icon with an optional floating draggable widget (via a small bundled GNOME Shell extension). No cloud, no background scanning.
Supported versions
Targets Ubuntu 22.04 LTS through 26.04 LTS (GNOME 42–50). Everything
install.sh does is apt-based; other distributions (Fedora, Arch,
Debian-non-Ubuntu) aren't supported out of the box, though the Python/JS
code itself is distro-neutral if you install the dependencies by hand.
| Ubuntu | GNOME | Default session | Extension build used |
|---|---|---|---|
| 22.04 LTS (Jammy) | 42 | Wayland (X11 available) | gnome-extension/legacy (imports.*) |
| 22.10, 23.04 | 43, 44 | Wayland (X11 available) | gnome-extension/legacy |
| 23.10, 24.04 LTS, 24.10 | 45, 46, 47 | Wayland (X11 available) | gnome-extension/modern (ESM) |
| 25.04 | 48 | Wayland (X11 available) | gnome-extension/modern |
| 25.10, 26.04 LTS | 49, 50 | Wayland-only (GNOME-on-Xorg removed) | gnome-extension/modern |
Ubuntu 25.10 dropped GNOME-on-Xorg, and 26.04 LTS removed X11 from GDM entirely — on those releases the GNOME session is Wayland under all circumstances. paste-shots prefers the Wayland code path on those systems automatically; the X11 fallback paths still fire for legacy applications running under XWayland.
install.sh detects the GNOME Shell version and copies the correct
extension build to ~/.local/share/gnome-shell/extensions/. Non-GNOME
desktops (KDE, XFCE) can use the core tray and paste pipeline but don't
get the focus-raise DBus service or the floating widget.
Other distributions
install.sh only knows apt, but the runtime has no Ubuntu-specific
dependencies. To run on a non-Ubuntu system, install these packages by
hand (names vary by distro):
| Component | Ubuntu (apt) | Fedora (dnf) | Arch (pacman / AUR) |
|---|---|---|---|
| GTK + GObject Introspection | python3-gi, gir1.2-gtk-3.0 |
python3-gobject, gtk3 |
python-gobject, gtk3 |
| Tray icon | gir1.2-ayatanaappindicator3-0.1 |
libayatana-appindicator-gtk3 |
libayatana-appindicator (AUR) |
| Tray host inside GNOME 41+ | gnome-shell-extension-appindicator |
gnome-shell-extension-appindicator |
gnome-shell-extension-appindicator |
| Wayland clipboard | wl-clipboard |
wl-clipboard |
wl-clipboard |
| X11 clipboard fallback | xclip |
xclip |
xclip |
| Wayland keystrokes | ydotool |
ydotool |
ydotool |
| X11 keystrokes | xdotool |
xdotool |
xdotool |
| Notifications | libnotify-bin |
libnotify |
libnotify |
After installing the deps, run paste-shots directly from a checkout —
./scripts/paste-shots-tray and ./scripts/paste-shots work on any
distro. The ydotoold systemd user service that install.sh sets up on
Ubuntu also needs to be enabled for Wayland keystroke injection to work.
Installation
git clone https://github.com/Mir-Zairan/paste-shots.git
cd paste-shots
./install.sh
The installer takes care of:
- apt dependencies (clipboard, keystroke, GTK tray)
- CLI scripts into
~/.local/bin ydotooldsystemd user service +uinputudev rule (required for Ctrl+V injection on Wayland)- GNOME Shell extension (
paste-shots@zaiarn) - Autostart for the tray
If you're on Wayland, you MUST log out and log back in after the first
install so the input group membership and the Shell extension load.
Usage
Tray menu
| Item | Behavior |
|---|---|
| Paste new screenshots | Everything taken since the last successful paste |
| Paste last N… | Dialog — pick how many recent shots |
| Pick screenshots… | GTK thumbnail picker with All / Last 3 / Last 5 shortcuts |
| Open screenshots folder | xdg-opens the watch folder |
| Settings… | See below |
The tray icon shows a live count of new (since last paste) screenshots,
updated via inotify.
Floating widget
Optional — enable it in Settings. On GNOME Wayland the widget is drawn by the Shell extension on the chrome layer: always above other windows, draggable, position persists across sessions. On X11 a plain GTK always-on-top window is used as a fallback. Click the widget to run "paste new"; drag it to reposition.
Command line
Paste actions (the same three bound by Settings → Keyboard Shortcuts):
paste-shots # paste everything since last paste
paste-shots 3 # paste the last 3
paste-shots --pick # thumbnail picker
Configuration from the shell — useful for scripting, dotfile management, or keybindings outside GNOME:
paste-shots --get # print whole config as JSON
paste-shots --get tray_icon # print one value
paste-shots --set tray_icon=false # write one value (parses JSON: true/false/numbers/strings)
paste-shots --set paste_delay=0.4
paste-shots --set 'custom_paste_targets=["jetbrains-phpstorm","helix"]'
paste-shots --settings # open the settings dialog standalone
--set writes to ~/.config/paste-shots/settings.json and signals the
running tray to hot-reload — no restart required. Settable keys are derived
from the defaults: watch_dir, tray_icon, expanded_icons,
floating_widget, paste_delay, paste_mode, notifications,
autostart, custom_paste_targets, floating_pos.
Diagnostics & lifecycle:
paste-shots --focused-class # print the wm_class of the currently focused window
paste-shots --quit # gracefully shut down the running tray
paste-shots --help # full usage
--focused-class is the recommended way to discover the wm_class for an
unsupported app: focus that app, run the command from another terminal,
copy the output into Settings → Custom paste targets.
Settings
| Setting | Default | Notes |
|---|---|---|
| Watch folder | ~/Pictures/Screenshots |
|
| Tray icon | on | |
| Floating widget | off | GNOME Shell extension preferred; GTK fallback on X11 |
| Paste delay | 0.6 s | Interval between multi-image pastes |
| Desktop notifications | on | |
| Launch at login | on | |
| Paste target | Terminals only | Controls which focused windows accept a paste. Terminals only (default) — paste only into terminal emulators. Anywhere — no focus validation, Ctrl+V fires into whatever has focus. To paste into a non-terminal app (IDE, chat, browser) either switch to Anywhere or list the app's WM_CLASS in Custom paste targets below. |
| Custom paste targets | (empty) | Extends the built-in terminal allowlist with your own WM_CLASS substrings. Use this for apps not recognised by default — IDE terminals, chat apps with image-paste support, anything else. Run paste-shots --focused-class while the target app is focused to find its WM_CLASS. |
IDEs and other non-terminal apps
To paste into an IDE (VS Code, JetBrains, Cursor, Zed, Sublime, …), a chat app, or anything else that isn't a terminal emulator, either:
- Switch Paste target to Anywhere, or
- Add the app's WM_CLASS to Custom paste targets (run
paste-shots --focused-classwhile the app is focused to read it).
Either way, make sure the cursor / terminal pane / message field that should receive the paste has keyboard focus before triggering paste-shots. The tool sends Ctrl+V to whatever currently has focus inside the target window — it has no way to navigate to a specific pane.
Environment override: PASTE_SHOTS_WATCH_DIR takes precedence over the
settings-file value.
Paste targets (where can it paste?)
paste-shots only sends Ctrl+V into windows on its paste-target allowlist.
Many apps silently drop image-clipboard paste (gedit, file managers, browsers
on certain pages, the desktop) and ydotool would falsely report success —
so the allowlist exists as a silent-fail guard. The mode controls how
aggressive the guard is:
paste_mode |
Accepts | Use case |
|---|---|---|
terminal_only (default) |
Standalone terminal emulators only. Other apps are accepted only if their WM_CLASS is in Custom paste targets. | The intended path: paste screenshots into Claude Code, OpenCode, or any other terminal-driven assistant. |
any |
Anything that has keyboard focus, including IDEs, chat apps, browsers, image-aware text fields, document editors. | Unlocks paste-shots for general use beyond terminal AI tools — see below. |
"Anywhere" mode — pasting outside terminal AI tools
Set paste_mode = any (Settings → Paste target → "Anywhere", or
paste-shots --set paste_mode=any) to bypass the allowlist entirely. Any
focused window receives Ctrl+V. With this turned on, the same batch and
picker flows work in:
- Chat apps that accept image paste — Discord, Slack, Element, Telegram Desktop, Signal, Matrix clients, Zulip, Mattermost, Rocket.Chat.
- Issue trackers / docs in a browser — GitHub/GitLab issue & PR comment fields, Linear, Jira, Notion, Confluence, Google Docs, Outline, HackMD.
- Email composers — Thunderbird, Geary, web Gmail, Outlook web.
- Note-taking apps — Obsidian, Logseq, Joplin, Standard Notes.
- Anywhere a normal Ctrl+V on an image would work, plus the things that the focus-allowlist would otherwise block.
The "batch multiple screenshots into one turn" workflow that motivates the tool extends straight to multi-image messages on those services — drop three before/after shots into a single Slack thread or GitHub comment in one action, with the same focus-lock and paste-delay behaviour.
Note: the allowlist exists for a reason. With any you may occasionally
fire Ctrl+V into a window that silently swallows image paste; the marker
will still advance because ydotool reports success. If you find a class
of app where this happens, switch back to terminal_only and add a
custom_paste_targets entry instead.
How pasting is verified
Each paste step is checked end-to-end:
- Clipboard copy runs (
wl-copy/xclip) — non-zero exit = failure. - Clipboard is then polled with
wl-paste --list-typesto confirm an image mime actually landed. - The target window is re-raised before each keystroke (see Focus-lock below).
ydotool/xdotoolsends Ctrl+V — non-zero exit = failure.
The "last paste" marker only advances when every file in the batch succeeded. If anything failed, the marker stays put so the next "Paste new" run re-picks the failed files.
Focus-lock
Before sending each Ctrl+V, paste-shots re-raises the window that had focus when the paste was triggered, so a stray click partway through a multi-image batch can't redirect the paste mid-flight. Always-on; not configurable.
| Session | Mechanism |
|---|---|
| X11 / XWayland | xdotool windowactivate --sync |
| Wayland + GNOME + extension | DBus into org.pasteshots.Shell.RaiseWindow |
| Wayland + GNOME (no extension) | Countdown notification, user switches manually |
| Wayland + sway/Hyprland | Countdown fallback (plug-in points exist for native protocols) |
"New since last paste" logic
Each successful batch touches ~/.local/share/paste-shots/last-paste. The
next run includes only files with mtime > marker. On first ever run,
everything from the past 10 minutes is eligible. Files that failed their
previous paste stay eligible until they succeed.
GNOME Shell extension
Two parallel builds live in gnome-extension/:
legacy/paste-shots@zaiarn/— GNOME 42–44 (imports.*module system)modern/paste-shots@zaiarn/— GNOME 45+ (ESM withimport/export)
install.sh runs gnome-shell --version and copies the matching package
to ~/.local/share/gnome-shell/extensions/paste-shots@zaiarn/. GNOME 45
broke the extension module system with no single-codebase backport, so the
two builds have to be maintained in parallel — keep them in sync when
changing behaviour.
The extension registers its DBus object on GNOME Shell's existing
org.gnome.Shell bus name (standard pattern for extensions) at object
path /org/pasteshots/Shell, interface org.pasteshots.Shell:
Ping() -> boolSnapshotFocused() -> stringRaiseWindow(wid: string) -> boolRaiseLastTerminal() -> boolDescribeWindow(wid: string) -> stringShowFloatingWidget(bool) -> boolUpdateBadge(uint) -> bool
The tool works without the extension — focus-lock falls back to a countdown notification, and the floating widget falls back to the GTK window.
Supported formats
PNG, JPG, JPEG.
Performance & footprint
Numbers from scripts/bench on Ubuntu 22.04 / Wayland / Python 3.10,
4-thread tray idle for ~4.5 hours:
| Metric | Value |
|---|---|
| Resident memory (RSS) | 45 MB (peak = current — no growth) |
| Proportional set size (PSS) | 21 MB (the more meaningful "private + share of shared") |
| Anonymous (Python heap) | 18 MB |
| Swap used | 0 KB |
| Threads | 4 — Python main, GLib mainloop, GDBus worker, dconf worker |
| Open file descriptors | 17 |
| Idle CPU | 0.0% averaged over a 5-second sample |
Microbench timings (median of 200 runs unless noted):
| Hot path | n = 1 | n = 100 | n = 1000 |
|---|---|---|---|
screenshots_in (dir scan) |
9 µs | 440 µs | 4.7 ms |
find_since_marker (no marker) |
24 µs | 800 µs | 8.6 ms |
find_since_marker (with marker) |
21 µs | 625 µs | 6.9 ms |
find_last_n(3) |
18 µs | 635 µs | 8.4 ms |
| Hot path | Median |
|---|---|
is_paste_target("alacritty") |
1.5 µs |
is_paste_target("firefox") |
3.9 µs |
is_paste_target × 8 mixed classes |
21 µs |
Cold import of paste_shots.cli (CLI startup) |
2 ms |
Real clipboard round-trip (1.2 MB PNG, wl-copy + verify):
| Step | Median | Mean |
|---|---|---|
copy_to_clipboard end-to-end |
113 ms | 115 ms |
clipboard_has_image (verify only) |
81 ms | 81 ms |
So the per-file paste budget is roughly: ~115 ms clipboard + ~5 ms focus
check + ~10 ms keystroke + the user-configurable paste_delay (default
600 ms, dwarfing everything else). Lowering paste_delay to 0.2–0.3 s
keeps multi-image pastes snappy on most receiving apps; the floor is
whatever the receiving terminal needs to consume one Ctrl+V before the
next.
Reproduce these numbers locally:
scripts/bench # full report
scripts/bench --no-clipboard # skip the wl-copy round-trip
The harness uses an isolated tmp directory so it doesn't disturb your
live tray, real screenshots folder, or settings.json.
Troubleshooting
"no terminal/editor focused" on every paste
The paste-target allowlist is rejecting whatever window has focus — this
is the silent-fail guard that stops Ctrl+V firing into apps that ignore
image clipboard (gedit, browsers, file managers, the desktop). Click into
a real terminal or editor and try again. If you're using an unsupported
IDE, run paste-shots --focused-class while it's focused and paste the
result into Settings → Custom paste targets.
Paste does nothing on Wayland
Most likely ydotoold isn't running, your user isn't in the input
group, or the uinput device isn't accessible. Run install.sh once,
then log out and log back in — the input group membership only
takes effect on a new session. To verify:
systemctl --user status ydotoold # should be 'active (running)'
groups | tr ' ' '\n' | grep -x input # should print 'input'
ls -l /dev/uinput # should show a non-error stat
"clipboard does not report image mime after copy"
The clipboard tool succeeded but the image never landed on the
selection. Confirm the right tool for your session is installed —
wl-clipboard for Wayland, xclip for X11. On Wayland, multiple
clipboard managers (e.g. clipman, cliphist) sometimes consume the
selection before paste-shots can verify it; quit the manager
temporarily to test.
Paste lands in the wrong window
Focus-lock re-raises the window that had focus the moment you triggered
paste-shots — so the fix is to focus the right window first, then click
the tray icon (or run the CLI). If a stray window is winning the focus
race, run paste-shots --focused-class while you've focused the intended
target and confirm it returns the right WM_CLASS. If paste-shots is
rejecting your target, add its WM_CLASS to Settings → Custom paste
targets or switch Paste target to Anywhere.
Tray icon doesn't appear on GNOME
GNOME 41+ removed legacy SystemTray support. The tray relies on the
gnome-shell-extension-appindicator extension, which install.sh
installs via apt. Verify it's present and enabled:
gnome-extensions list --enabled | grep appindicator
If missing, install it (sudo apt install gnome-shell-extension-appindicator
on Ubuntu, equivalent on other distros) and log out / log back in.
Floating widget doesn't stay on top under Wayland
Under GNOME the widget should be drawn by the bundled Shell extension, not GTK. Verify:
gnome-extensions list --enabled | grep paste-shots
If the extension isn't enabled, install.sh either failed to copy it
or you didn't log out and back in afterwards. The GTK fallback (X11)
honors keep_above, but Mutter intentionally ignores keep_above for
regular client windows on Wayland — that's why the extension exists.
"tray already running; exiting"
A second tray instance was rejected by the singleton lock at
$XDG_RUNTIME_DIR/paste-shots.lock. If no tray is actually running
(crash, reboot quirk on a networked filesystem), run paste-shots --quit to clean up; if that says "no tray was running," remove the
lock file manually.
Marker keeps re-picking the same files
The "since last paste" marker only advances when every file in a
batch succeeded. If even one paste failed, the next "Paste new" run
re-picks the failed files so you can retry. To force-advance manually,
touch ~/.local/share/paste-shots/last-paste.
How to confirm a paste really succeeded
paste-shots checks each step end-to-end:
- Clipboard tool exit code (non-zero = failure)
- Clipboard MIME via
wl-paste --list-types/xclip -t TARGETS - Target window re-raise (when focus-lock is on)
- Keystroke tool exit code
If any step fails, the marker stays put and a notification surfaces the
specific error. Step-level logging isn't on by default — running the
tray from a terminal (paste-shots-tray) prints any exceptions to
stderr.
Development
python3 -m pytest tests/
Tests cover the pure logic (finders, marker-advance rules, config load/save). Clipboard and keystroke paths require a live display and are tested manually.
Layout:
paste-shots/
├── install.sh
├── pyproject.toml # package metadata + console_scripts
├── scripts/
│ ├── paste-shots # bash shim → python3 -m paste_shots.cli
│ └── paste-shots-tray # bash shim → python3 -m paste_shots.tray_app
├── src/paste_shots/
│ ├── config.py # settings.json + paths
│ ├── finders.py # screenshot listing, marker rules (pure)
│ ├── clipboard.py # wl-copy / xclip
│ ├── keys.py # ydotool / xdotool keystroke injection
│ ├── pipeline.py # focus → copy → raise → keystroke orchestration
│ ├── errors.py # PasteError
│ ├── core.py # back-compat re-export shim
│ ├── picker.py # GTK thumbnail picker
│ ├── watcher.py # inotify for the badge
│ ├── window.py # focus-lock + DBus into the Shell extension
│ ├── floating.py # GTK fallback widget
│ ├── shortcuts.py # GNOME custom keybindings
│ ├── settings_dialog.py
│ ├── notify.py
│ ├── tray_app.py # AppIndicator tray
│ ├── tray_ipc.py # PID-file IPC between CLI and tray
│ └── cli.py # CLI entrypoint
├── gnome-extension/paste-shots@zaiarn/
└── tests/
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paste_shots-0.1.0.tar.gz.
File metadata
- Download URL: paste_shots-0.1.0.tar.gz
- Upload date:
- Size: 44.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41d133fe9784c5a8f1bddb89359e98fb37859b82a6f4067422dbed712d2a3f63
|
|
| MD5 |
d77724bd13c6ebe454b7cad369a6dd33
|
|
| BLAKE2b-256 |
37f9728bc3438e50770bd2fdbdb90d138ee4c7ff1af360f396f9e2a8c62373b7
|
Provenance
The following attestation bundles were made for paste_shots-0.1.0.tar.gz:
Publisher:
publish.yml on Mir-Zairan/paste-shots
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paste_shots-0.1.0.tar.gz -
Subject digest:
41d133fe9784c5a8f1bddb89359e98fb37859b82a6f4067422dbed712d2a3f63 - Sigstore transparency entry: 1397695776
- Sigstore integration time:
-
Permalink:
Mir-Zairan/paste-shots@c0a5356a2c6963eb26bd6313057c971e56df5390 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Mir-Zairan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c0a5356a2c6963eb26bd6313057c971e56df5390 -
Trigger Event:
release
-
Statement type:
File details
Details for the file paste_shots-0.1.0-py3-none-any.whl.
File metadata
- Download URL: paste_shots-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7fd4ec418fdc3facb6825a426964752d6aac94f58dedf6102f5babd781fdcf41
|
|
| MD5 |
e156c56e9d45b81ac2184f2c7391b25c
|
|
| BLAKE2b-256 |
e5fdf6cf5aad713c33d26cbc2cbbd846b75c6b60ceb8d7a75246a26da49101d2
|
Provenance
The following attestation bundles were made for paste_shots-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Mir-Zairan/paste-shots
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paste_shots-0.1.0-py3-none-any.whl -
Subject digest:
7fd4ec418fdc3facb6825a426964752d6aac94f58dedf6102f5babd781fdcf41 - Sigstore transparency entry: 1397695787
- Sigstore integration time:
-
Permalink:
Mir-Zairan/paste-shots@c0a5356a2c6963eb26bd6313057c971e56df5390 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Mir-Zairan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c0a5356a2c6963eb26bd6313057c971e56df5390 -
Trigger Event:
release
-
Statement type: