The history
Copying
What PasteDaemon keeps when you copy something — text, formatting, files, images and where each came from — what it deliberately does not keep, and how long any of it stays.
Each time something is copied, PasteDaemon stores it as a new entry. Text, images and files copied in Finder are all kept, and the history persists across launches.
An entry is a value plus the history of what that value used to be. Each past value is kept with the time it was set and what set it — a trigger, an action, a variable, or you — and an entry keeps its 20 most recent values.
Entries come in two kinds, which differ only in whether they carry a name. Unnamed entries are the clipboard history. Named ones are variables — which is why a variable can be searched and pasted from the overlay exactly as a copy can.
Copying something the history already holds is a use, not a change: the entry moves to the top without gaining a version, and its timestamp updates. So something you reach for daily never ages out from under your hand.
That means the history holds each value once. However far down the list the entry sits, and whether or not it is pinned, copying that text again brings the same entry back to the top rather than adding a second row beside it — with its pin, its past values and everything else it carries. What is compared is what an entry holds now: a row you have typed over is a different thing from the value it used to hold, so copying that old value starts a new entry.
The same goes for a row that comes to say it some other way. Where a trigger rewrites a copy into text an older row already held — a rule turning every PROJ-1 into the same link — the older row is absorbed just as a copy would have absorbed it, and the row just written is the one that stands.
Appending to the copy before it
Hold ⌘ and tap C twice and what you have selected is added to the end of the copy before it, instead of being kept as a copy of its own. Three pieces from around a page — a name here, an order number there, a line of an address — become one item and one paste, joined by a newline. Both go on the clipboard together, so the next ⌘V pastes the lot.
It is off until you switch it on, under Copying in General preferences. It changes what the most worn chord on your Mac does, in every app, so nothing turns it on for you — and noticing the second tap needs the same Accessibility permission automatic pasting needs, which the pane will tell you about if it is missing.
The gesture is read narrowly, because every miss is a copy quietly joined to the last one. ⌘ has to stay down between the two taps — letting go of it is exactly what you do when you copy two separate things in a row — the taps have to be within half a second, and nothing may be pressed in between. ⌥⌘C and ⇧⌘C are other chords and are left alone, holding C down is one press however long you hold it, and a third tap starts a fresh pair rather than appending again.
What you append is not kept as an item of its own: it is half of one copy, so there is no second row for it, nothing is gathered, and no trigger fires on it — which also means a forget rule cannot protect text you append by hand. The item it grows comes to the top of the history holding both, with what it said before kept among its past values, so an append you did not mean is one step from being undone. The joined text is plain: two copies' formatting cannot be joined into one, and a file list with a sentence added to it is no longer the paths of a file.
An image cannot be appended to, and neither can a copy with nothing in front of it. Both say so and keep the copy as an ordinary one.
What is not kept
- Concealed copies. Pasteboard contents marked
org.nspasteboard.ConcealedType— the convention password managers use — are ignored. - Anything copied while an excluded app is in front. No entry, no past value, no trigger.
- Anything at all, while recording is paused.
- Anything a
forgettrigger catches. A trigger that recognises the shape of a thing that should never be kept — an API token, a one-time code — drops each such copy the instant it is made, and leaves the text on the clipboard to be pasted where it was going.
Pausing
⌘T in the overlay, or Pause Recording in the menu bar, stops the app keeping anything. The system clipboard works as it always does and everything already in the history can still be used; nothing new is recorded and no trigger runs.
Resuming starts from whatever is on the clipboard then. What was copied while paused is passed over rather than arriving all at once, which is what pausing was for.
Because a paused history looks exactly like a working one, it says so in three places: the menu bar icon dims, the menu item reads Resume Recording, and the overlay carries a banner across the top. It is deliberately not saved — a pause is for the next few minutes, and one that survived a restart would go on quietly losing copies long after it was wanted. Every launch starts recording.
Where a copy came from
Each copy remembers the app it was made in, and the overlay shows it: the app's icon with its name beside it, under the row's text. Which window a snippet came out of is often the fastest way to recognise it — three similar-looking URLs are told apart at a glance by the browser, the terminal and the chat app they were copied from.
Whoever is in front when the pasteboard changes is taken to be who copied, which is the only guess available: the pasteboard says nothing about where its contents came from. The poll is a quarter of a second behind at worst.
Like formatting and files, the source belongs to the value rather than to the entry, so the version list says where each past value came from. A copy from Safari that a trigger rewrote is still a copy from Safari and still shows Safari; values the app itself produced name the action instead, since "copied in PasteDaemon" is not news. Text typed into a variable was copied nowhere and shows nothing.
What is stored is the bundle identifier and the name. The icon is looked up fresh, so it follows the app being updated or moved, and an app that is no longer installed leaves the row showing the name it was copied under.
Formatting
Text copied with formatting — bold, links, colour — keeps how it looked, so using it again from the history pastes it as it was copied. Rich text (with and without attachments) and HTML are stored alongside the plain text every other part of the app works in, and using an entry offers them back richest first: an app that can take one does, and an app that cannot still gets the text.
Formatting describes one exact run of characters and goes no further. Every action and trigger reads and writes plain text, so a value one of them produced has none — a triggered rewrite is pasted plain, and the formatted original stays behind it in that entry's history where undo and the version list can reach it. Bringing a past value back brings its formatting with it. A single formatted shape larger than 256 KB is left behind; the plain text is kept either way.
Two shipped actions say which shape should go on the clipboard, instead of all of them:
unfmt— the text alone. No bold, no links, no colour, no fonts.html— the HTML of the copy in place of the richer shapes beside it. A web copy carries the page's own markup; a copy from a word processor carries rich text, which is converted.
Neither changes a character of the text, so either can sit anywhere in a chain, and where both are named the last one wins.
Run by hand — from the overlay, the menu bar, or a command like $2 | unfmt — the choice is about this paste: the entry keeps how it was copied, so pressing ↩ on that row later pastes it dressed as it always was. Named from a trigger, the choice is about the copy, because a trigger fires when something is copied and there is no paste yet to decide about. That is how "anything copied out of that app is plain text" gets said.
A shape is only honoured while the text is still the text that was copied. $1 | upper | html has no HTML for what it produced and says so, rather than pasting the old copy's markup over the new text. unfmt can always be honoured.
Two things make formatting rather than choosing among what a copy already had: a script action that declares its output, and md-rich, which reads the text as Markdown and puts what it renders to on the clipboard. Both are held to the rule above from that point on.
Files
Copying files in Finder stores an entry whose text is their paths, one to a line, and which remembers that those paths are files. Using it again puts the files back on the clipboard, so pasting into Finder copies them as it would have at the time; the paths go on as text beside them, so pasting into anything else gives you the paths.
Unlike an image, a file list is not inert. The paths are the entry's value, so filters read them, $1 names them, actions run on them and a search matches on them — a folder name finds files that are not shown by name in the row.
The shape goes exactly as far as the text it describes: text a filter has rewritten is paths no longer, so it pastes as text, and the copy as it was made stays behind it in that entry's history.
Files are looked for before text, because a copy in Finder puts a string on the pasteboard too — the bare file names — which is the least useful thing on offer. Only a real file copy answers as one: text that merely spells a path, or a file:// URL, is text.
Nothing is copied to a folder of the app's own. An entry holds paths, which are a promise about somewhere else, so what is still there is checked when the row is drawn — a row says how many of its files have gone — and again when it is used, where only the survivors go back on the clipboard. A list whose files have all gone still pastes as the paths it always was.
Images
An image copied to the clipboard becomes an entry like any other, and using it puts it back on the clipboard as it was copied. Where an app offers a picture and a string together — the address of an image on a web page, the name of a file — the text is what is kept: it is what the copy was about.
Every image is read. On its way into the history the picture is handed to Vision's accurate text recogniser and the words it finds are stored beside it, so a screenshot is searchable by what it says. The reading happens off the main thread, so the entry is on screen before it comes back; a very large screenshot can take a moment. The same picture copied twice is the entry that already holds it, and is not read again.
An image answers to what it is and what it says: a query matches against PNG image · 1024×768 and against the words that were read, so typing an invoice number finds the screenshot of the invoice.
Otherwise an image is inert. Every other action and trigger reads and writes text, and an image is not text:
- No trigger fires on an image.
- No action is offered, in the overlay or the menu bar, while the newest item is an image.
- A filter that names one says so rather than reading it as empty.
- An image cannot be stored in a variable.
- An image entry never gains a second value, so it has no version history.
ocr is the one filter that reads the picture rather than text, which makes it the one command an image entry can be the start of — $1 | ocr. What it hands on is text, so the rest of the library follows it: $1 | ocr | trim > $address. It is typed rather than picked from a list, since the list of actions only appears while the newest item is text.
The bytes live in files beside the JSON rather than in it, named for a hash of their contents, so the same image copied twice is stored once. Images are held to limits of their own, so that a run of screenshots cannot push out the text that was in the history first: the 50 most recent images, and 200 MB of them together. A single image larger than 32 MB is not recorded at all, and the overlay says so at the time.
Retention
How long is the only question the history asks. There is no cap on how many entries it holds. A count is the wrong question — a clipboard fills at a hundred copies one day and three the next, so the same number is a fortnight or an afternoon depending on the week.
Retention is answered once per kind of thing: text, images and file lists each get one of 24 hours, 7 days, 1 month, 3 months or unlimited, set in General preferences. Anything older is dropped; anything newer is kept, however much of it there is.
Which kind an entry counts as is worked out from what it holds now, not from how it arrived. An image is an image; an entry that remembers its text is paths is a file list; everything else is text. So a file list a filter has rewritten is text from then on, and is kept for as long as text is.
Age is counted from when the entry last took its value, which is what the row already shows. A pin never expires — pinning is how an entry steps off the belt, and time is the belt.
The history is swept three times over, because each catches what the others cannot: at launch, before anything can be shown; when something is copied, alongside the image budget; and every five minutes while the app sits idle, which is the machine a retention is most for. Shortening a period sweeps immediately, so a change visibly takes effect while you are still looking at the pane.
Variables are never pruned at all. If something is worth keeping past its kind's retention, it wants either a pin or a name.
Where all this is kept
Everything is in ~/Library/Application Support/PasteDaemon:
| File | What is in it |
|---|---|
config.json | Settings, actions, triggers, abbreviations and variables. Laid out prettily with its keys in order — it is meant to be readable and editable by hand |
history.json | The clipboard history, pins included. Written compactly and off the main thread, since it is rewritten after every copy |
images/ | The bytes of copied images, one file each, named for a hash of their contents |
shell-history.json | The search bar's last 100 commands |
vault.key, vault.pub | The secrets vault's key pair. Deleting these makes every stored secret permanently unreadable |
A secret is written as ciphertext and its plain value is never on disk. The API key is not in config.json either — it is in the login keychain, because a credential in a plain-text file that every process running as you can read is a password written on the door.