A macOS clipboard manager in the spirit of Ditto.
macOS keeps exactly one clipboard entry, so everything you copy overwrites what came before. mcditto records every copy — text, rich text, images, files — into a local SQLite database, and gives you back any of it with ⌘⇧V: a resizable popup opens at the mouse pointer, you pick an entry with the arrow keys or by typing, press Return, and it lands at the cursor in whatever app you were just using.
Everything stays on your Mac. Nothing is uploaded anywhere.
That download link always resolves to the most recent release, so it never needs updating — see Releases.
The app is signed ad-hoc, not notarized, so the first launch needs right-click → Open to get past Gatekeeper.
There is no Xcode project — the app is a Swift Package plus a script that assembles the bundle, so the Command Line Tools are enough.
make appThat produces build/Ditto.app. To run it:
make runTo keep it around:
make installOther targets: make selftest (store and capture checks), make icon (regenerates
Resources/AppIcon.icns), make clean.
Accessibility — required only for pasting. mcditto pastes by putting the entry on the clipboard, switching back to your previous app and sending a ⌘V keystroke, and macOS gates synthetic keystrokes behind Accessibility. Grant it in System Settings → Privacy & Security → Accessibility.
Without it, everything else still works: the history is recorded, the popup opens, and picking an entry copies it to the clipboard so you can press ⌘V yourself.
The ⌘⇧V hotkey itself needs no permission — it is registered through Carbon, not an event tap.
macOS ties the grant to the app's code signature. make app signs ad-hoc with a stable
identifier, which normally survives rebuilds, but if it does not, sign with a self-signed
certificate instead:
- Keychain Access → Certificate Assistant → Create a Certificate…
- Name it e.g.
mcditto-dev, type "Code Signing", self-signed. - Build with it:
CODESIGN_IDENTITY=mcditto-dev make app| Key | Action |
|---|---|
| ⌘⇧V | Open the history popup at the mouse pointer |
| type | Filter entries |
| ↑ ↓ | Move the selection |
| Page ↑ / ↓, Home, End | Jump |
| Return, double-click | Paste the selected entry |
| ⌘1 … ⌘9 | Paste the Nth visible entry |
| ⌘⌫ | Delete the selected entry |
| Esc | Close |
Digits type into the search box as normal — only ⌘-digit is a shortcut, so searching for numbers still works.
The popup remembers the size you drag it to and always reopens at the pointer. The preview panel on the right can be turned off in Settings → General for a narrower, list-only popup. The menu bar icon covers everything else: pause recording, clear history, settings, quit.
Every flavor of a copy is kept, so pasting styled text into Pages keeps its formatting while pasting into a terminal gives plain text. Copying the same thing twice does not create a second entry — the existing one moves to the top and its use count goes up.
Payloads under 128 KB live inline in the database; larger ones (images, mostly) go to
files under blobs/, referenced from the row.
Not recorded: anything a password manager marks with the standard
org.nspasteboard.ConcealedType flag, and anything copied from an app you list in
Settings → Privacy.
Two rules run at launch and then hourly:
- entries not used in the last 365 days are deleted
- beyond 10 000 entries, the least recently used are deleted
Both numbers are adjustable in Settings → History. Blob files whose rows are gone are cleaned up in the same pass.
~/Library/Application Support/mcditto/
├── clips.db SQLite database (WAL mode, FTS5 index on previews)
└── blobs/ payloads too large to store inline
To look at the history yourself:
sqlite3 ~/Library/Application\ Support/mcditto/clips.db "SELECT id,kind,use_count,source_app,substr(preview,1,60) FROM entries ORDER BY last_used_at DESC LIMIT 20;"Sources/mcditto/
├── main.swift agent bootstrap (--selftest lives here too)
├── AppDelegate.swift wires everything together, schedules pruning
├── SelfTest.swift store, retention and capture checks
├── Store/ SQLite wrapper, schema, blobs, retention
├── Capture/ pasteboard polling and flavor extraction
├── Paste/ hotkey, permissions, paste injection
├── UI/ popup panel, list, menu bar, settings
└── Support/ paths and settings
The git tag is the only place a version is written down. Nothing in the Swift
source or in Info.plist holds a version number — Info.plist ships placeholders that
scripts/make-app.sh fills in at build time.
Pushing a v* tag is what cuts a release. Commits on main build nothing on their own:
git tag -a v0.1.5 -m "v0.1.5"
git push origin v0.1.5.github/workflows/build.yml then compiles the release
binary, assembles the bundle, verifies Info.plist really carries the tag's version,
runs make selftest, and publishes Ditto.app.zip as a GitHub release with generated
notes. Tag v0.1.5 therefore ships with CFBundleShortVersionString 0.1.5.
Each release is marked latest, which is what keeps the download button above pointing
at the newest one. That URL follows whichever release GitHub considers latest; it does
not need a tag named latest.
A local make app with no VERSION set describes itself from the tags instead
(0.1.4-dirty, say), so a development build is never mistaken for a release. You can
always override explicitly with VERSION=1.2.3 make app.
The project page under docs/ is served at
https://bertyhell.github.io/mcditto/, deployed by
.github/workflows/pages.yml on the same version tags —
or on demand from the Actions tab when the page needs updating between releases.
Set Settings → Pages → Build and deployment → Source to GitHub Actions for that
workflow to take over; the "Deploy from a branch" option runs a GitHub-managed workflow
instead, which cannot be edited and still pins deprecated action versions.
- Not sandboxed, and not notarized. A global hotkey and synthetic keystrokes both rule out the App Store; this is an app you build and run yourself.
- While a password field has focus anywhere on the system, macOS blocks simulated keystrokes. mcditto detects this and tells you to press ⌘V yourself rather than failing silently.
- If another app already owns ⌘⇧V, registration fails and you get a warning at launch; the menu bar icon still opens the popup.
