Keepit

Keepit security

A clipboard manager sees everything you copy, so it has to be clear about what it keeps, where, and who can get at it. This page is the complete picture for Keepit. The code is open (github.com/andrey-insarov/Keepit), so every claim here can be checked against it.

In one paragraph

Keepit stores your clipboard history and templates in one folder on your Mac and nowhere else — plus, if you switch it on, encrypted copies in a folder you choose (iCloud Drive, Dropbox, a disk); the provider behind that folder sees the ciphertext, the file size and a short unencrypted header (listed below), nothing of the content. There is no account, no Keepit cloud, no analytics, and the app makes exactly one kind of network request — the daily update check against GitHub — which you can switch off. The backup folder is an option, off until you turn it on; the update check is on in the direct-download edition and can be turned off; the password is an option too. None of them is required for the design to hold — with no backup folder Keepit itself never writes outside the Mac, and the rest of this page stays true — but they are not equivalent: without a password the history and templates are plain files in your own Library folder, readable by anything that runs as you, and there is nothing to lock. Turning an option on adds exactly what it says and no more. Clipboard data that the copying app marks as concealed is not recorded, and copies from known password managers are excluded by app — a best-effort heuristic, explained below. If you set a password, everything on disk is AES-256-GCM encrypted with a key derived from that password; the key exists only in memory, is wiped when Keepit locks, and can optionally be released by Touch ID from the Secure Enclave. A 24-word recovery phrase is the only way back in if you forget the password — Keepit itself cannot reset it.

What is stored, and where

Everything lives in ~/Library/Application Support/Keepit/ (App Store edition: the app's container under ~/Library/Containers/com.it.keep/):

File Contents Encrypted with a password?
history.json The clipboard history: text, RTF, HTML, PNG images, file lists, with date and source app Yes
templates.json The template tree, including per-template hot keys Yes
settings.json Settings: hot keys, limits, ignore list — no clipboard data No
vault.json Encryption metadata: KDF name and round count, PBKDF2 salt, a sealed verifier, the wrapped recovery material —
restore-journal/, protection-journal/ Only during a restore or a password change: the previous bytes of the files above, so an interrupted commit is undone at the next launch As the originals

The data folder is created with mode 0700 and every file Keepit creates (the data files, vault.json, the journals, the copies it writes) with 0600, from the first byte — the temporary file of an atomic write is created 0600 before anything lands in it. A folder or file that already existed keeps the mode it had, and a rename (a .corrupt-… copy set aside) moves the file with its mode: Keepit does not change permissions you set. The history is written when it changes and read on launch. With Save history on quit, restore on start off, history.json is removed the moment the setting is turned off, and again at every lock and at quit; history then lives only in memory until Keepit quits. If the file cannot be removed (permissions, a locked flag, a file Keepit could not read and therefore keeps), Keepit says so rather than assuming it is gone. Without a password the JSON is plain text on disk, protected only by your macOS account and FileVault. Settings → Advanced → Open Data Folder shows the folder.

Keepit also writes to the macOS keychain — only the Touch ID entry described below — and registers itself as a login item if you let it start at login.

What is not recorded

Three layers, in decreasing order of certainty. It matters which one you are relying on.

  1. Marked by the source — reliable. Anything the copying app marks as concealed or transient (org.nspasteboard.ConcealedType, org.nspasteboard.TransientType, 1Password's own type) is skipped on the strength of the mark alone; Keepit does not need to know which app wrote it. 1Password, Bitwarden, Apple Passwords, Keychain Access and many terminals set these marks. (The check can be turned off on Settings → History; leave it on.)
  2. Known by app — best effort. Copies from a password manager that sets no mark are excluded by its bundle identifier: KeePassXC, MacPass, Strongbox, KeeWeb, LastPass, Dashlane, Enpass, Keeper, NordPass, RoboForm (and, as a second line, the apps in layer 1) are built in and always active; add your own apps on Settings → History. The limit here is macOS: the pasteboard does not record which app wrote it, so Keepit works from which apps were in front since it last looked (every 300 ms by default). If any of them is on the list, the change is skipped — including when you copy and switch to another app before Keepit notices. This errs towards skipping. It cannot cover a copy made by a background process or a script, and it cannot tell a password from any other text in an app that is not on the list.
  3. By your choice. Anything copied while recording is paused (Pause Recording for 10 Minutes, ⌥P), apps on your ignore list, and formats other than plain text, RTF, HTML, PNG and file lists.

Keepit does not inspect text for things that look like passwords or one-time codes. If an app that is not on the list puts a secret on the clipboard without marking it, it is recorded like anything else: use the pause or the ignore list for such apps, and delete the item from the history.

Encryption

Turn on Require password at launch and the history and templates files are encrypted:

The unlock dialog with the on-screen keyboard open. Photographed with a phone: on the author's Mac a screenshot of this scene shows the dialog alone
The unlock dialog with the on-screen keyboard open. Photographed with a phone: on the author's Mac a screenshot of this scene shows the dialog alone

Changing the password re-wraps the recovery material; the data files are re-encrypted with the new key.

Touch ID

With Unlock with Touch ID on, Keepit stores a copy of the data key in the keychain under an access control of kSecAttrAccessibleWhenUnlockedThisDeviceOnly + biometryCurrentSet. That means:

The password keeps working as a fallback and is still required to change the password itself unless you allow Touch ID for that too. Turning Touch ID off deletes the keychain entry.

Recovery phrase

When you set a password Keepit generates 32 bytes of entropy and shows them once as a 24-word BIP-39-style phrase (2048-word English list, 8-bit checksum to catch typos). A key derived from that entropy (HKDF) wraps the data key, and the entropy is in turn wrapped by the data key, so:

The phrase itself is never written to disk. Print it, or keep it in a password manager. Without the password, Touch ID on the same Mac, or the phrase, the data is unrecoverable by design — there is no server-side reset and Keepit's authors cannot help.

Backups

A backup is one .keepit file holding the history, the templates and (optionally) the preferences, compressed with LZFSE and sealed with AES-256-GCM. Two ways to make one, same file format:

What a file looks like inside. A magic string and a format version, then a small JSON header, then the ciphertext. The header is authenticated as GCM additional data — it cannot be altered without the file failing to open — but it is not encrypted, because the reader needs it to know how to derive the key. Field by field: mode (auto or manual), kdf and rounds, salt, created, app (the Keepit version), minReader, sourceID (a random installation identifier, not tied to hardware), backupID (random, per file), recovery (the vault's recovery block: the data key wrapped under the phrase-derived key, plus the phrase's entropy wrapped under the data key — both useless without the phrase or the key), and label — your Mac's name, present only if you switched it on. That, and the file's size, is what a storage provider or anyone who finds the file can learn: that it is a Keepit backup, when it was made, from which installation, and roughly how much data it holds. Not what is in it, not the password. The complete specification is in docs/FORMAT.md in the repository, with reference files produced by an independent implementation; backups written by Keepit 2.x (format 1) still open.

Restore verifies the GCM tag before anything else, decompresses with a hard size limit, and then checks every item against the same limits the app applies when recording — a backup is untrusted input even when it is your own. Replace first writes a verified copy of the current data; the swap itself is written to disk as one transaction with a journal, so a crash in the middle is put right at the next launch. Trust settings (Touch ID, URL automation and its trusted apps, the backup folder) are never taken from a backup.

Where the copies go is up to you, and so is who else can reach that folder. The ciphertext is only as safe as the password behind it: the same advice as for the vault applies, and for Save to Archive… files, which carry their own password, a short one is the weak point. Deleting a copy from the folder removes it there; what cannot be taken back is anything the provider keeps as an earlier version or in its trash, or a copy that was downloaded elsewhere — treat a file that has left the folder as permanent, and a weak password on it as the real exposure. On the App Store edition, access to the folder is a security-scoped bookmark granted when you choose it; Keepit never scans iCloud Drive or anything else on its own.

Network volumes. Keepit writes each copy to a temporary name, flushes it, reads it back and compares a hash before renaming it into place, and it never deletes an old copy before a new one has passed that check. How atomic and durable those steps are on a NAS depends on the file system and the protocol (SMB, NFS) and on the server's own caching — Keepit cannot see through them. A local folder or a folder synced by iCloud Drive or Dropbox is the recommended target; on a NAS, verify once that a copy opens with Restore… from another machine before relying on it.

Permissions Keepit asks for

Permission Why What happens without it
Accessibility To send ⌘V to the app you were working in, and to find the text cursor so the menu opens there Items are copied to the clipboard; you press ⌘V yourself; the menu opens at the pointer
Touch ID / keychain Only if you enable Touch ID unlock Password only
Login item Launch at login (on by default, switchable on General) Start Keepit by hand

Keepit does not ask for Full Disk Access, Screen Recording, Input Monitoring, Contacts, Calendars, Location or the camera, and does not use any of them. The Accessibility permission is powerful — it is the same one window managers and automation tools need — which is why the app is signed and notarized and the source is public. What Keepit does with it is narrow: one ⌘V (and, for a template with {cursor}, a few left arrows) addressed to the process of the app you were in, never to "whatever is in front". Immediately before each of those key events Keepit checks that it is still unlocked (and was not locked and unlocked in between), that the target app is still running and is the one in front, that nothing else has written the clipboard since, and that no modifier key is held; if any of that fails, nothing is typed — the item stays on the clipboard for a manual ⌘V. Without a known target app (a keepit:// command with no app in front, the Viewer's own copy) no key is sent at all.

Clipboard access as macOS sees it. Keepit reads the clipboard by polling (macOS sends no notification when it changes), so from macOS 15.4 on, System Settings → Privacy & Security lists Keepit among the apps that read the pasteboard, and the system log records those reads. That is the history being recorded — it is what a clipboard manager does — not a sign of anything else going on.

Automation (keepit:// URLs)

The keepit:// URL scheme (paste item N, paste a template, add text, pause) is off by default and switched on in Settings → Advanced → Automation. Any process or web page can ask macOS to open a URL — browsers do confirm the first time for a custom scheme, scripts do not — and add and paste change the clipboard or type into the front application without a prompt. With the option off, Keepit ignores such URLs with a beep and logs that it did.

With it on, Keepit checks the sender. macOS delivers the URL as an Apple Event that carries the kernel's audit token of the process behind it; from that token Keepit reads the process's code-signing identity — its signing identifier and Team ID — and checks that the running code is valid against its own signature. Three kinds of sender:

Commands that only open Keepit's own windows (history, templates, viewer, settings) never ask. A URL with a repeated parameter name is refused. The list of trusted apps is this installation's: it is not written into backups and a restore never brings one in. Before 3.0.7 the list held bundle identifiers, which any local program can claim; that list is ignored and emptied at the first launch of 3.0.7, so each trusted app is asked once more.

System log

Keepit writes to the unified system log (Console.app, log show, sysdiagnose) under the subsystem com.it.keep, and only about errors and unexpected states: a file that could not be saved or decoded, a hot key the system refused to register, Touch ID or the login item failing, the updater not starting. It never logs a clipboard item, a template, a password, a key, the contents of a file, or whether a particular copy was recorded or skipped. Every value inserted into a message (a file name, an error, a key combination) is marked private, so Console shows <private> in its place unless an administrator explicitly turns private data on for the subsystem. There is no log file of Keepit's own, and nothing is sent anywhere.

Network

The direct-download edition makes one request: once a day (and when you choose Check for Updates…) it fetches the Sparkle appcast from GitHub Releases over HTTPS. Updates are verified with an EdDSA (Ed25519) signature embedded in the app before they are installed; a download that does not match is rejected. The App Store edition is updated by the App Store and makes no requests at all.

That one request can be switched off: Settings → General → Updates → Check for updates automatically. Off, the updater is not started at all — no scheduled check, no check on demand — and Check for Updates… leaves the menus; the setting is kept in settings.json (checkForUpdates). You are then responsible for updating by hand (keep.it.com, GitHub Releases or Homebrew); security fixes reach you only when you look.

No clipboard content, no usage data and no identifiers are ever sent anywhere. There is no crash reporter and no analytics library in the binary.

If you want certainty rather than a promise. The App Store edition carries no network entitlement at all — neither com.apple.security.network.client nor .server — and under the App Sandbox the system refuses every connection to an app without one, whatever the app would try. Check it yourself:

codesign -d --entitlements - /Applications/Keepit.app

— no network key in the output. For the direct download, which needs the network for its update check, you can have the same certainty from the outside: switch update checks off and deny Keepit outgoing connections in an outbound firewall — LuLu (free, open source), Little Snitch or another; macOS's own firewall covers incoming connections only. You will see Keepit never ask.

Code signing and verifying a download

Every release is built on GitHub Actions from a tagged commit, signed with the Developer ID of UK Intis Telecom LTD, notarized by Apple and stapled. The release page carries, for each DMG, a .dmg.sig (Sparkle EdDSA signature) and a SHA256SUMS file. To verify a download by hand:

shasum -a 256 -c SHA256SUMS --ignore-missing
spctl -a -vv -t install Keepit-2.11.0.dmg      # "accepted, source=Notarized Developer ID"

Homebrew installs the same DMG from the same release.

The signing keys never leave GitHub's release environment: only v* tags can deploy to it, the build-and-test job that runs on every push and pull request has no access to secrets, and every third-party action in the workflow is pinned to a commit hash rather than a movable tag.

Uninstalling and data removal

Settings → Advanced → Reset and uninstall → Uninstall… removes the app bundle, the data folder, the keychain entry, the login item and the Accessibility grant, after asking for the password or Touch ID and a typed confirmation. Deleting the app by hand leaves the data folder in place — remove ~/Library/Application Support/Keepit/ yourself if you want it gone.

Input limits

Whatever lands on the clipboard is checked before it is stored, so a hostile or runaway copy cannot exhaust memory or disk:

What Limit Where
Text 2 MB of UTF-8 (rich text / HTML up to 4× that); larger copies are not recorded Settings → History
Images 10 MB and 40 megapixels; the pixel count is read from the image header, the image is never decoded first Settings → History
File lists 1 000 paths Settings → History
History 1 000 text / 30 image items, 256 MB in total; the oldest go first Settings → History
Templates 5 000 items, 16 levels deep, names of 200 characters fixed
Paste queue 500 items per session fixed
Backup files 512 MB fixed
Password 256 characters; a prompt answer ({input:}) 10 000 fixed

The same validator stands between every source and the stores — the clipboard, a backup, a file chosen in the Viewer, a keepit:// command, an edit in the Viewer, and Keepit's own data files when they are loaded. Besides the sizes above it checks that a format is what it claims: plain text is UTF-8, RTF starts with {\rtf, HTML is UTF-8 or UTF-16 with a byte-order mark, an image is a PNG by its bytes, a file list holds absolute paths; a format Keepit does not know is kept as bounded opaque bytes. A rich format that fails is left out of its item and the text kept; an item whose primary content fails is refused. A data file that fails on load is repaired by the same rules (and a copy of it, as it was, is kept next to it), so a damaged history.json or templates.json does not take the rest of the data with it.

Limitations worth knowing

Reporting a vulnerability

Please use GitHub's private vulnerability reporting (Security → Report a vulnerability on the repository) or write to admin@keep.it.com rather than opening a public issue. You will get an answer within a few days, and a fix is released as soon as it is ready; credit is given if you want it.