ClipStack is a lightweight Windows clipboard-history utility. It remembers the latest copied items and lets you paste any of them with a global shortcut.
It is a native C# / WPF / .NET 10 desktop app (not Electron, not Chromium, not a web UI). The goal is low idle CPU, low memory, and fast popup opening.
- Event-driven clipboard capture (
AddClipboardFormatListener) — no polling - Capture work runs off the UI thread, so large images and files never freeze the popup
- Honours password-manager clipboard opt-out formats
- History encrypted at rest with DPAPI under your Windows account
- Pin clips so they are never evicted
- Paste as plain text, stripping HTML/RTF
- Default history of 50 items (configurable 1–50)
- Global shortcut default: Ctrl + Shift + S
- Compact floating popup near the cursor, with a filter box (
/) - System tray app (no taskbar window during normal use)
- Local-only storage under
%LocalAppData%\ClipStack - Velopack installer + automatic GitHub update support
| Kind | Behavior |
|---|---|
| Text | Unicode / plain text, including large text, URLs, multiline |
| Rich text | Text + HTML + RTF stored as one history item |
| Images | Bitmap clipboard images stored as PNG + thumbnail |
| Files/folders | File-drop path lists only (files are not copied into ClipStack) |
ClipStack only reads the formats above; any other app-specific format is left alone.
Clipboard history is stored only on this device. ClipStack does not upload clipboard contents, does not include clipboard data in logs, and does not sync between devices.
Update checks only request public ClipStack release metadata from GitHub. Clipboard contents are never included.
Before reading any content, ClipStack checks the clipboard for the opt-out formats that password managers, browsers, and banking apps use to tell history tools to skip a clip:
| Format | Meaning |
|---|---|
ExcludeClipboardContentFromMonitorProcessing |
Presence alone excludes the clip |
Clipboard Viewer Ignore |
Presence alone excludes the clip |
ClipboardViewerIgnore |
Presence alone excludes the clip |
CanIncludeInClipboardHistory |
DWORD; 0 excludes the clip |
An excluded clip is never hashed, previewed, or written to disk — only the format name that triggered the skip reaches the log. A marker that is present but whose value cannot be parsed is treated as exclusion.
CanUploadToCloudClipboard is deliberately not honoured as a local opt-out: it
governs cross-device sync only, and Windows itself still keeps those clips in local
history, so treating it as a storage opt-out would silently drop clips you expect to keep.
This depends on the source application setting one of these formats. An app that copies a password without marking it — including most web pages — cannot be detected. Encryption at rest is what covers those clips.
Payload files are encrypted with DPAPI under your Windows account (Encrypt history on disk, on by default). The key derives from your Windows credentials and is never stored by ClipStack, so another account on the same machine cannot read the history even with full file access.
What this does not protect against: code already running as you, which can decrypt exactly as ClipStack does. It raises the cost of offline disk access, not of local malware.
Each encrypted file carries a short header and reads dispatch on that header, not on the index. So:
- History written before this version stays readable and is never rewritten.
- Encryption applies from the next capture onward — upgrading is a no-op.
- Turning the setting off later leaves earlier encrypted clips readable, and vice versa.
- A clip that cannot be decrypted (encrypted under a different account, or after a credential reset) is reported rather than returned as garbage.
If a clip cannot be encrypted it is stored in the clear rather than lost, and you are notified — a clip silently stored unencrypted while the setting claims otherwise would be the worst outcome.
DPAPI costs roughly 50 ms per megabyte. Both encryption and decryption run on background threads, so even a 50 MB image (~2.5 s to encrypt, ~3.4 s to decrypt) never blocks the popup or the hotkey.
Open history: Ctrl + Shift + S
In the popup:
| Key | Action |
|---|---|
| ↑ / ↓ | Change selection |
| Enter | Paste selected |
1–9 |
Paste items 1–9 |
0 |
Paste item 10 |
| Delete | Remove selected item |
| Shift + Enter | Paste selected as plain text |
Shift + 1-9 / 0 |
Paste that item as plain text |
| Ctrl + P | Pin / unpin selected item |
/ or Ctrl + F |
Open the filter box |
| Esc | Hide popup |
Mouse click also pastes; Shift+click pastes as plain text. Window deactivation hides the popup.
Pasting normally offers whatever the clip carries, so text copied from a browser or Word brings its HTML and RTF styling with it. Paste as plain text in Settings makes text-only the default, and Shift inverts whichever way that setting is set — so the other behaviour is always one modifier away.
Clips with no text at all — images and file drops — restore normally even when plain text is requested. There is no useful "plain" form of those, and pasting nothing would be worse.
Ctrl + P pins the selected clip. Pinned clips sort to the top, show a marker, and are
never evicted: the history limit counts unpinned clips only, so the visible list can
exceed it by however many clips you have pinned. Pinning every slot cannot stop capture
— new clips still arrive and push each other out. Unpinning returns a clip to its normal
position by recency.
Pin state lives in index.json. A history rebuilt from folders after a corrupt index
loses it, along with the timestamps and hashes that recovery already cannot restore.
The list keeps focus when the popup opens, so every shortcut above works immediately.
Press / (or Ctrl + F, for layouts where / is not on the OemQuestion key) to reveal
the filter box.
| Key | Action while the filter box has focus |
|---|---|
| Any character | Types into the filter |
| ↑ / ↓ | Change selection without leaving the box |
| Enter | Paste selected |
| Esc | Return focus to the list, keeping the filter applied |
Terms are separated by spaces and all must match (AND). Matching is case-insensitive
and runs against each clip's stored preview text, its file paths, and its kind — so
image or files narrows by type. The header shows matched / total while a filter is
active, and the numeric shortcuts renumber to the visible rows.
Filtering reads only the in-memory index, never payload files on disk, so it stays instant across a full history. This does mean it matches the stored preview (the first ~240 characters of a clip), not the full body of a long text clip.
The filter resets every time the popup closes.
- Show Clipboard History
- Pause / Resume Clipboard Capture
- Clear History
- Settings
- Check for Updates
- Start with Windows
- Exit ClipStack
Double-click the tray icon to show history.
%LocalAppData%\ClipStack\
index.json
settings.json
release-config.json
logs\
items\<guid>\...
Clear local data:
.\tools\clean-local-data.ps1- Windows 10 or Windows 11
- .NET 10 SDK
cd C:\Users\souna\Projects\ClipStack
dotnet restore .\ClipStack.sln
dotnet build .\ClipStack.sln -c Release
dotnet test .\ClipStack.sln -c Release
# Run (tray app)
dotnet run --project .\src\ClipStack.App\ClipStack.App.csproj -c Release
# or
.\src\ClipStack.App\bin\Release\net10.0-windows\ClipStack.exeApproximate idle memory (does not kill an existing instance):
.\tools\measure-memory.ps1Pinned package/tool version: Velopack / vpk 1.2.0
.\tools\build-release.ps1 -Version 1.0.3This will:
- Restore tools and packages
- Run tests
- Publish self-contained
win-x64(not single-file) - Package with
vpk pack - Write installer/update files under
artifacts\releases
Public releases use the GitHub repository configured in release-config.json:
https://github.com/sounak1125/ClipStack
When Download updates automatically is enabled, ClipStack checks the public GitHub release feed shortly after every installed app launch, then in the background while the app remains open. A newer version is downloaded silently, then a non-modal notification lets the user choose Restart & update or Later. Manual checks remain available from Settings and the tray menu.
Push a semantic-version tag to run the release workflow:
git tag v1.0.3
git push origin v1.0.3The GitHub Actions workflow downloads the previous Velopack release metadata, runs tests, builds the self-contained application, creates full/delta packages, and publishes the installer and update assets to GitHub Releases.
Installed copies check the win release channel and preserve clipboard history
under %LocalAppData%\ClipStack across updates.
Sign the final public installer/update packages with your own certificate. Do not commit secrets or signing passwords to the repository.
Installed builds default to enabling per-user startup via:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run value ClipStack
Debug/unpackaged runs do not auto-enable startup. The tray checkbox and Settings stay synchronized with the registry.
- Auto-paste uses
SendInputand cannot paste into elevated windows unless ClipStack itself is elevated (ClipStack intentionally runsasInvokerand will not request admin). If auto-paste fails, content remains on the clipboard — pressCtrl+V. - Some apps use private clipboard formats that ClipStack cannot reconstruct.
- Extremely large clipboard payloads can be skipped when over the configured size limit (default 200 MB).
Change values in one place:
src/ClipStack.Core/AppIdentity.cs
Also update Directory.Build.props, release-config packaging IDs in tools/build-release.ps1, and the tray/menu strings if you customize further.
Replace these generated project assets:
src/ClipStack.App/Assets/clipstack-icon.png— transparent 1024px mastersrc/ClipStack.App/Assets/clipstack.ico— Windows multi-resolution icon
The .ico is embedded into the executable and used by application windows,
the system tray, shortcuts, portable builds, and installer packages.
- Copy one line / multiple lines / emoji / non-English / large document
- Paste into Notepad and a browser
- Confirm duplicates do not create duplicate rows
- Copy from browser / Word
- Paste into a rich editor and Notepad
- Confirm plain-text fallback
- Copy screenshot / browser image
- Confirm popup shows thumbnail only
- Paste original; restart app; confirm persistence
- Copy file(s) / folder; paste into Explorer via ClipStack
- Delete a source file; confirm missing-path handling
- Copy a password from KeePass / Bitwarden / 1Password
- Confirm no new row appears and no folder is written under
items\ - Confirm
logs\clipstack.logrecords only the format name, never the value - Copy ordinary text from the same app; confirm it still captures
/and Ctrl + F both open the filter box- Type to narrow; confirm header shows
matched / total - Digits and Delete edit the query instead of pasting/deleting while the box has focus
- ↑ / ↓ and Enter still work from inside the box
- Esc returns to the list with the filter applied;
1–9then act on visible rows - Close and reopen; confirm the filter is cleared
- Copy a large image or a ~100 MB file
- Confirm the popup still opens instantly and the hotkey responds during capture
- Paste that large image back; confirm the popup does not freeze while it decrypts
- Copy styled text from a browser; paste with Enter into WordPad (styling kept)
- Paste with Shift+Enter (styling stripped)
- Enable Paste as plain text; confirm Enter and Shift+Enter swap behaviour
- Shift+click and Shift+
1behave the same as Shift+Enter - Confirm an image clip still pastes when plain text is requested
Ctrl + Ppins the selected clip; it moves to the top with a marker- Copy past the history limit; confirm the pinned clip survives
- Unpin; confirm it returns to its position by recency
- Restart; confirm pins persist
- With Encrypt history on disk on, copy text, then open
items\<guid>\text.txt - Confirm the file is not readable and the clip still pastes correctly
- Turn the setting off, copy again, and confirm both old and new clips still paste
- Mouse, arrows, Enter,
1–0, Esc, Delete - Multi-monitor and mixed DPI
- Change shortcut; conflicting shortcut; restart
- Pause/resume; clear; startup toggle; exit; second instance
- Unpackaged updater fails safely; installed update path when feed configured
ClipStack.Core— models, hashing, search predicate, atomic JSON storage (no WPF)ClipStack.App— tray, WPF popup/settings, Win32 interop, Velopack- Hidden
HwndSourcewindow owns clipboard listener + hotkey - Self-copy suppression avoids restoring history items as new duplicates
Capture runs in two phases, split at ClipboardSnapshot:
- Read (
ClipboardFormatReader.ReadSnapshotAsync) — UI/STA thread. Checks the exclusion formats, then marshals clipboard data into strings, byte arrays, and a frozenBitmapSource. STA is required here; the work is just marshalling. - Build (
ClipboardFormatReader.BuildItemData) plusHistoryStore.AddOrPromote— thread pool, viaTask.Run. Reads original image files, generates thumbnails, encodes PNGs, hashes with SHA-256, and writes payloads to disk.
Everything crossing that boundary is immutable or frozen, which is what makes phase 2 safe off-thread. Freezing the clipboard bitmap in phase 1 is load-bearing — a bitmap that cannot be frozen is dropped rather than passed across threads.
Restore is split the same way at RestorePayloads: read and decrypt on the thread pool,
then assemble and publish the DataObject on the STA thread. Without that split, DPAPI
decryption of a large image would stall the dispatcher for seconds during paste.