Vibecode Paste
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Paste, store a local encrypted clipboard history with search, pinboards, and expiration. The hard boundary is apple-platform polish, icloud sync, rich previews, and universal capture, plus os integration polish and maintenance.
You are building a lean indie version of Paste. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Paste · indie build A background agent that captures everything you copy into an encrypted local store, skips items password managers mark as concealed, keeps rich previews for images and files, organises pins into boards, expires old entries on a schedule, and pastes back into the application you were in. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | the pasteboard, the keychain and text insertion are all platform surfaces | | App type | A background agent with LSUIElement true | it watches; it appears on a hotkey | | Capture | Polling NSPasteboard.changeCount | there is no change notification of any kind, so polling is the supported approach rather than a workaround | | Storage | SQLite encrypted at rest, with the key in the keychain | a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep | | Insertion | Pasteboard plus a synthetic Cmd-V with restore | per-character posting drops characters and mangles anything outside ASCII | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you own one - Why: The pasteboard, keychain and insertion APIs are all platform surfaces. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds and signs the agent. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **A password manager that marks concealed items** · free with an open source manager - Why: Phase 1's most important check cannot be tested without one, and it is the check that keeps this from becoming a password log. - Get it: Any password manager that sets the concealed pasteboard type. Test by copying a password and confirming nothing is captured. - Verify: A copied password does not appear in the history - [ ] **Willingness to grant Accessibility for paste-back** · free - Why: Sending Cmd-V into another application needs it. Capture and search work without it, so it can be requested late. - Get it: System Settings, then Privacy and Security, then Accessibility, when the paste-back feature is first used. - Verify: Selecting a clip pastes into the previous application - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it, no notarization. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. - Apple-platform polish, iCloud sync, rich previews, and universal capture - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Paste, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Paste The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an encrypted clipboard history with search, pinboards and expiry, that refuses to store what should not be stored to replace Paste. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · the pasteboard, the keychain and text insertion are all platform surfaces - App type: A background agent with LSUIElement true · it watches; it appears on a hotkey - Capture: Polling NSPasteboard.changeCount · there is no change notification of any kind, so polling is the supported approach rather than a workaround - Storage: SQLite encrypted at rest, with the key in the keychain · a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep - Insertion: Pasteboard plus a synthetic Cmd-V with restore · per-character posting drops characters and mangles anything outside ASCII ### Data model (create this before Phase 1) - clips(id, kind, hash, bytes, preview_text, source_bundle_id, copied_at, pinned, board_id, expires_at) · kind is text, image, file or rich - boards(id, name, position) · pinboards for things you paste repeatedly - clip_data(clip_id, encrypted_blob) · the actual content, encrypted at rest, separate from the searchable metadata - excluded_apps(bundle_id, reason) · applications whose copies are never captured - The searchable metadata deliberately holds a preview rather than the content, so search does not require decrypting everything. ### Phase 1 · Capture, and what not to capture Build: Poll the pasteboard change count on a timer. On a change, inspect the item types before reading contents. Skip anything marked concealed, transient or automatically generated, and skip copies from applications on an exclusion list. Trap: Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. Done when: Copying three things produces three entries, newest first. A password copied from a password manager does not appear, verified by a test with a synthetic concealed item. A copy from an excluded application is not captured. Copying the same text twice updates a timestamp rather than adding a row. Do not build yet: No encryption, no search, no pasting back. ### Phase 2 · Encryption at rest Build: Generate a key on first launch, store it in the keychain, and encrypt clip contents before writing. Keep searchable metadata separate and limited to a short preview. Verify by inspecting the database file directly. Trap: Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. Done when: Opening the database file with a database tool shows no readable clip content. A short preview is present and is deliberately truncated. Deleting the keychain entry makes existing clips unreadable rather than crashing the application. The key never appears in any file on disk. Do not build yet: No search or pasting yet. ### Phase 3 · Search and pasting back Build: A hotkey opening a panel over the frontmost application. Search across previews and metadata. Selecting an entry decrypts it, writes it to the pasteboard, sends Cmd-V and restores the previous pasteboard contents afterwards. Trap: Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. Done when: The hotkey opens the panel from any application and Escape returns focus. Typing filters entries as you type across a thousand clips without a visible pause. Selecting an entry pastes it into the previously focused application. The pasteboard afterwards holds what it held before. Do not build yet: No boards or expiry yet. ### Phase 4 · Boards and rich content Build: Pinboards holding clips you keep. Rich handling per type: image thumbnails, file references that survive the file moving or warn when it does not, and formatted text that can be pasted as plain or formatted. Trap: Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. Done when: An image clip shows a thumbnail and pastes as an image. A file clip whose file has moved is marked as unavailable in the list rather than failing on paste. Formatted text can be pasted as plain text with a modifier. Pinned clips on a board survive expiry. Do not build yet: No expiry or distribution yet. ### Phase 5 · Expiry, which is a feature Build: Expiry by age and by total size, applied on a schedule, never touching pinned clips. Per-type expiry, because a copied password fragment deserves a shorter life than a code snippet. Secure deletion of expired content. Trap: A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. Done when: Clips older than the configured age are removed on schedule and pinned ones are not. Total size stays under the configured cap by removing oldest first. Expired content is removed from the database file rather than left as a deleted row, verified by inspecting the file. Per-type retention is configurable. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly what is captured, what is excluded, how long it is kept and that it is encrypted at rest. Trap: Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The README states what is captured, what is refused, the retention defaults and the encryption. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync. A clipboard history synced between machines is a substantially larger security question than a local one, and it is why the paid application charges. - Universal capture across every application, including the ones that use private pasteboards. - The rich preview quality of the commercial product, which handles a long tail of content types. - Compatibility across macOS releases without maintenance. ### README must contain - That concealed pasteboard items are refused, and what that means for password managers - That content is encrypted at rest with the key in the keychain, and that previews are not - The retention defaults per type, and that expiry is a feature rather than a limitation - That expired content is removed from the file rather than left as a deleted row - Which applications are excluded from capture by default ===== AGENTS.md ===== # Agent instructions · Paste indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, Polling NSPasteboard.changeCount, SQLite encrypted at rest, with the key in the keychain, Pasteboard plus a synthetic Cmd-V with restore. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ===== BUILD_PLAN.md ===== # Build plan · Paste A background agent that captures everything you copy into an encrypted local store, skips items password managers mark as concealed, keeps rich previews for images and files, organises pins into boards, expires old entries on a schedule, and pastes back into the application you were in. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture, and what not to capture Everything you copy is stored, except the things that must never be. ### Steps 1. Poll the change count A timer comparing changeCount. Only read the pasteboard when it has moved. Files: `Watcher.swift` 2. Inspect types before reading Skip concealed, transient and auto-generated markers. Check before reading, since reading is what creates the exposure. Files: `Watcher.swift` 3. Deduplicate by hash Same content updates the timestamp. Otherwise a repeated copy fills the history. Files: `Store.swift` ### Done when - [ ] Three copies produce three entries, newest first - [ ] A concealed item is refused, proved by test - [ ] An excluded application's copies are not captured - [ ] A repeated copy updates rather than duplicating ### Watch out - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. ## Phase 2 · Encryption at rest The history on disk is unreadable without the key, and the key is in the keychain. ### Steps 1. Generate and store a key On first launch, into the keychain with an appropriate access policy. Never a file. Files: `Crypto.swift` 2. Encrypt content, truncate previews Content encrypted, preview short and treated as readable. Files: `Store.swift` 3. Verify by inspection Open the database file with a tool and look. If you can read a clip, the encryption is not doing what you think. ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` ### Done when - [ ] The database file shows no readable clip content - [ ] Previews are present and truncated - [ ] A missing keychain key degrades rather than crashes - [ ] No key material appears in any file ### Watch out - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. ## Phase 3 · Search and pasting back A hotkey opens the history, typing filters it, and Return pastes into where you were. ### Steps 1. Add the hotkey and panel A non-activating panel so the previous application keeps its focus context. Files: `Panel.swift` 2. Search previews and metadata Without decrypting everything, which is why previews exist. Files: `Search.swift` 3. Paste and restore carefully Save the pasteboard, write, send Cmd-V, restore after a delay, and only if your value is still there. Files: `Paste.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] Search filters a thousand clips without a visible pause - [ ] Selecting pastes into the previous application - [ ] The pasteboard is restored to its prior contents ### Watch out - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. ## Phase 4 · Boards and rich content Things you paste repeatedly live somewhere, and images and files behave like themselves. ### Steps 1. Handle each type properly Images, files and rich text each have their own capture, preview and paste behaviour. Files: `Types/` 2. Check file availability when shown Mark missing files in the list rather than failing at paste time. Files: `Types/FileClip.swift` 3. Add boards Named collections of pinned clips, reorderable, exempt from expiry. Files: `Boards.swift` ### Done when - [ ] An image clip shows a thumbnail and pastes as an image - [ ] A moved file is marked unavailable rather than failing at paste - [ ] Formatted text can be pasted as plain with a modifier - [ ] Pinned clips survive expiry ### Watch out - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. ## Phase 5 · Expiry, which is a feature The history does not keep everything you have ever copied, forever. ### Steps 1. Expire by age and size Both, on a schedule and after each capture. Pinned clips exempt. Files: `Expiry.swift` 2. Make deletion actually delete Vacuum after removal so the content is not recoverable from the file. Verify by inspection. Files: `Store.swift` 3. Per-type retention Different lifetimes per kind, with the setting prominent rather than buried. Files: `Settings.swift` ### Done when - [ ] Aged clips are removed and pinned ones are not - [ ] Total size stays under the cap - [ ] Expired content is not recoverable from the database file - [ ] Per-type retention is configurable ### Watch out - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ## Not in this build - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. ## After v1, if you want it - A merge action combining several clips into one paste - Per-application default boards, so a code snippet board opens in an editor - Transform actions on paste, such as stripping formatting or lowercasing ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Encrypted clip store. The key lives in the keychain, not here. HISTORY_DB_PATH=~/Library/Application Support/Clips/history.db # Required. How often the change count is checked. Lower feels instant and costs battery. POLL_INTERVAL_MS=200 # Required. How long text clips are kept. Finite by default and deliberately so. RETENTION_DAYS_TEXT=30 # Required. Images are large and rarely wanted a month later. RETENTION_DAYS_IMAGE=7 # Required. Total size cap. Oldest unpinned clips are removed first. MAX_HISTORY_MB=500 # Required. Applications never captured from, in addition to the concealed type check. EXCLUDED_BUNDLE_IDS=com.example.passwordmanager
You are building a lean indie version of Paste. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Paste · indie build A background agent that captures everything you copy into an encrypted local store, skips items password managers mark as concealed, keeps rich previews for images and files, organises pins into boards, expires old entries on a schedule, and pastes back into the application you were in. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | the pasteboard, the keychain and text insertion are all platform surfaces | | App type | A background agent with LSUIElement true | it watches; it appears on a hotkey | | Capture | Polling NSPasteboard.changeCount | there is no change notification of any kind, so polling is the supported approach rather than a workaround | | Storage | SQLite encrypted at rest, with the key in the keychain | a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep | | Insertion | Pasteboard plus a synthetic Cmd-V with restore | per-character posting drops characters and mangles anything outside ASCII | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you own one - Why: The pasteboard, keychain and insertion APIs are all platform surfaces. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds and signs the agent. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **A password manager that marks concealed items** · free with an open source manager - Why: Phase 1's most important check cannot be tested without one, and it is the check that keeps this from becoming a password log. - Get it: Any password manager that sets the concealed pasteboard type. Test by copying a password and confirming nothing is captured. - Verify: A copied password does not appear in the history - [ ] **Willingness to grant Accessibility for paste-back** · free - Why: Sending Cmd-V into another application needs it. Capture and search work without it, so it can be requested late. - Get it: System Settings, then Privacy and Security, then Accessibility, when the paste-back feature is first used. - Verify: Selecting a clip pastes into the previous application - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it, no notarization. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. - Apple-platform polish, iCloud sync, rich previews, and universal capture - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Paste, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Paste The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an encrypted clipboard history with search, pinboards and expiry, that refuses to store what should not be stored to replace Paste. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · the pasteboard, the keychain and text insertion are all platform surfaces - App type: A background agent with LSUIElement true · it watches; it appears on a hotkey - Capture: Polling NSPasteboard.changeCount · there is no change notification of any kind, so polling is the supported approach rather than a workaround - Storage: SQLite encrypted at rest, with the key in the keychain · a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep - Insertion: Pasteboard plus a synthetic Cmd-V with restore · per-character posting drops characters and mangles anything outside ASCII ### Data model (create this before Phase 1) - clips(id, kind, hash, bytes, preview_text, source_bundle_id, copied_at, pinned, board_id, expires_at) · kind is text, image, file or rich - boards(id, name, position) · pinboards for things you paste repeatedly - clip_data(clip_id, encrypted_blob) · the actual content, encrypted at rest, separate from the searchable metadata - excluded_apps(bundle_id, reason) · applications whose copies are never captured - The searchable metadata deliberately holds a preview rather than the content, so search does not require decrypting everything. ### Phase 1 · Capture, and what not to capture Build: Poll the pasteboard change count on a timer. On a change, inspect the item types before reading contents. Skip anything marked concealed, transient or automatically generated, and skip copies from applications on an exclusion list. Trap: Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. Done when: Copying three things produces three entries, newest first. A password copied from a password manager does not appear, verified by a test with a synthetic concealed item. A copy from an excluded application is not captured. Copying the same text twice updates a timestamp rather than adding a row. Do not build yet: No encryption, no search, no pasting back. ### Phase 2 · Encryption at rest Build: Generate a key on first launch, store it in the keychain, and encrypt clip contents before writing. Keep searchable metadata separate and limited to a short preview. Verify by inspecting the database file directly. Trap: Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. Done when: Opening the database file with a database tool shows no readable clip content. A short preview is present and is deliberately truncated. Deleting the keychain entry makes existing clips unreadable rather than crashing the application. The key never appears in any file on disk. Do not build yet: No search or pasting yet. ### Phase 3 · Search and pasting back Build: A hotkey opening a panel over the frontmost application. Search across previews and metadata. Selecting an entry decrypts it, writes it to the pasteboard, sends Cmd-V and restores the previous pasteboard contents afterwards. Trap: Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. Done when: The hotkey opens the panel from any application and Escape returns focus. Typing filters entries as you type across a thousand clips without a visible pause. Selecting an entry pastes it into the previously focused application. The pasteboard afterwards holds what it held before. Do not build yet: No boards or expiry yet. ### Phase 4 · Boards and rich content Build: Pinboards holding clips you keep. Rich handling per type: image thumbnails, file references that survive the file moving or warn when it does not, and formatted text that can be pasted as plain or formatted. Trap: Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. Done when: An image clip shows a thumbnail and pastes as an image. A file clip whose file has moved is marked as unavailable in the list rather than failing on paste. Formatted text can be pasted as plain text with a modifier. Pinned clips on a board survive expiry. Do not build yet: No expiry or distribution yet. ### Phase 5 · Expiry, which is a feature Build: Expiry by age and by total size, applied on a schedule, never touching pinned clips. Per-type expiry, because a copied password fragment deserves a shorter life than a code snippet. Secure deletion of expired content. Trap: A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. Done when: Clips older than the configured age are removed on schedule and pinned ones are not. Total size stays under the configured cap by removing oldest first. Expired content is removed from the database file rather than left as a deleted row, verified by inspecting the file. Per-type retention is configurable. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly what is captured, what is excluded, how long it is kept and that it is encrypted at rest. Trap: Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The README states what is captured, what is refused, the retention defaults and the encryption. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync. A clipboard history synced between machines is a substantially larger security question than a local one, and it is why the paid application charges. - Universal capture across every application, including the ones that use private pasteboards. - The rich preview quality of the commercial product, which handles a long tail of content types. - Compatibility across macOS releases without maintenance. ### README must contain - That concealed pasteboard items are refused, and what that means for password managers - That content is encrypted at rest with the key in the keychain, and that previews are not - The retention defaults per type, and that expiry is a feature rather than a limitation - That expired content is removed from the file rather than left as a deleted row - Which applications are excluded from capture by default ===== AGENTS.md ===== # Agent instructions · Paste indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, Polling NSPasteboard.changeCount, SQLite encrypted at rest, with the key in the keychain, Pasteboard plus a synthetic Cmd-V with restore. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ===== BUILD_PLAN.md ===== # Build plan · Paste A background agent that captures everything you copy into an encrypted local store, skips items password managers mark as concealed, keeps rich previews for images and files, organises pins into boards, expires old entries on a schedule, and pastes back into the application you were in. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture, and what not to capture Everything you copy is stored, except the things that must never be. ### Steps 1. Poll the change count A timer comparing changeCount. Only read the pasteboard when it has moved. Files: `Watcher.swift` 2. Inspect types before reading Skip concealed, transient and auto-generated markers. Check before reading, since reading is what creates the exposure. Files: `Watcher.swift` 3. Deduplicate by hash Same content updates the timestamp. Otherwise a repeated copy fills the history. Files: `Store.swift` ### Done when - [ ] Three copies produce three entries, newest first - [ ] A concealed item is refused, proved by test - [ ] An excluded application's copies are not captured - [ ] A repeated copy updates rather than duplicating ### Watch out - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. ## Phase 2 · Encryption at rest The history on disk is unreadable without the key, and the key is in the keychain. ### Steps 1. Generate and store a key On first launch, into the keychain with an appropriate access policy. Never a file. Files: `Crypto.swift` 2. Encrypt content, truncate previews Content encrypted, preview short and treated as readable. Files: `Store.swift` 3. Verify by inspection Open the database file with a tool and look. If you can read a clip, the encryption is not doing what you think. ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` ### Done when - [ ] The database file shows no readable clip content - [ ] Previews are present and truncated - [ ] A missing keychain key degrades rather than crashes - [ ] No key material appears in any file ### Watch out - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. ## Phase 3 · Search and pasting back A hotkey opens the history, typing filters it, and Return pastes into where you were. ### Steps 1. Add the hotkey and panel A non-activating panel so the previous application keeps its focus context. Files: `Panel.swift` 2. Search previews and metadata Without decrypting everything, which is why previews exist. Files: `Search.swift` 3. Paste and restore carefully Save the pasteboard, write, send Cmd-V, restore after a delay, and only if your value is still there. Files: `Paste.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] Search filters a thousand clips without a visible pause - [ ] Selecting pastes into the previous application - [ ] The pasteboard is restored to its prior contents ### Watch out - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. ## Phase 4 · Boards and rich content Things you paste repeatedly live somewhere, and images and files behave like themselves. ### Steps 1. Handle each type properly Images, files and rich text each have their own capture, preview and paste behaviour. Files: `Types/` 2. Check file availability when shown Mark missing files in the list rather than failing at paste time. Files: `Types/FileClip.swift` 3. Add boards Named collections of pinned clips, reorderable, exempt from expiry. Files: `Boards.swift` ### Done when - [ ] An image clip shows a thumbnail and pastes as an image - [ ] A moved file is marked unavailable rather than failing at paste - [ ] Formatted text can be pasted as plain with a modifier - [ ] Pinned clips survive expiry ### Watch out - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. ## Phase 5 · Expiry, which is a feature The history does not keep everything you have ever copied, forever. ### Steps 1. Expire by age and size Both, on a schedule and after each capture. Pinned clips exempt. Files: `Expiry.swift` 2. Make deletion actually delete Vacuum after removal so the content is not recoverable from the file. Verify by inspection. Files: `Store.swift` 3. Per-type retention Different lifetimes per kind, with the setting prominent rather than buried. Files: `Settings.swift` ### Done when - [ ] Aged clips are removed and pinned ones are not - [ ] Total size stays under the cap - [ ] Expired content is not recoverable from the database file - [ ] Per-type retention is configurable ### Watch out - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ## Not in this build - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. ## After v1, if you want it - A merge action combining several clips into one paste - Per-application default boards, so a code snippet board opens in an editor - Transform actions on paste, such as stripping formatting or lowercasing ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Encrypted clip store. The key lives in the keychain, not here. HISTORY_DB_PATH=~/Library/Application Support/Clips/history.db # Required. How often the change count is checked. Lower feels instant and costs battery. POLL_INTERVAL_MS=200 # Required. How long text clips are kept. Finite by default and deliberately so. RETENTION_DAYS_TEXT=30 # Required. Images are large and rarely wanted a month later. RETENTION_DAYS_IMAGE=7 # Required. Total size cap. Oldest unpinned clips are removed first. MAX_HISTORY_MB=500 # Required. Applications never captured from, in addition to the concealed type check. EXCLUDED_BUNDLE_IDS=com.example.passwordmanager
You are building a production product version of Paste. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Paste · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Paste, store a local encrypted clipboard history with search, pinboards, and expiration. The hard boundary is apple-platform polish, icloud sync, rich previews, and universal capture, plus os integration polish and maintenance. ## Product outcome A clipboard history that is encrypted at rest, refuses to store what password managers mark as concealed, forgets on a schedule you set, and pastes back without losing what you had copied. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode - only the permissions required by the selected feature ## Explicit non-goals for v1 - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. - Apple-platform polish, iCloud sync, rich previews, and universal capture - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes ## Success criteria - The concealed item test still refuses to store - The database file is confirmed to contain no readable clip content - The pasteboard is confirmed restored after every paste - Expired content is confirmed unrecoverable from the file - Idle processor use is profiled and acceptable ===== BRIEF.md ===== # Build brief · Paste The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an encrypted clipboard history with search, pinboards and expiry, that refuses to store what should not be stored to replace Paste. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · the pasteboard, the keychain and text insertion are all platform surfaces - App type: A background agent with LSUIElement true · it watches; it appears on a hotkey - Capture: Polling NSPasteboard.changeCount · there is no change notification of any kind, so polling is the supported approach rather than a workaround - Storage: SQLite encrypted at rest, with the key in the keychain · a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep - Insertion: Pasteboard plus a synthetic Cmd-V with restore · per-character posting drops characters and mangles anything outside ASCII ### Data model (create this before Phase 1) - clips(id, kind, hash, bytes, preview_text, source_bundle_id, copied_at, pinned, board_id, expires_at) · kind is text, image, file or rich - boards(id, name, position) · pinboards for things you paste repeatedly - clip_data(clip_id, encrypted_blob) · the actual content, encrypted at rest, separate from the searchable metadata - excluded_apps(bundle_id, reason) · applications whose copies are never captured - The searchable metadata deliberately holds a preview rather than the content, so search does not require decrypting everything. ### Phase 1 · Capture, and what not to capture Build: Poll the pasteboard change count on a timer. On a change, inspect the item types before reading contents. Skip anything marked concealed, transient or automatically generated, and skip copies from applications on an exclusion list. Trap: Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. Done when: Copying three things produces three entries, newest first. A password copied from a password manager does not appear, verified by a test with a synthetic concealed item. A copy from an excluded application is not captured. Copying the same text twice updates a timestamp rather than adding a row. Do not build yet: No encryption, no search, no pasting back. ### Phase 2 · Encryption at rest Build: Generate a key on first launch, store it in the keychain, and encrypt clip contents before writing. Keep searchable metadata separate and limited to a short preview. Verify by inspecting the database file directly. Trap: Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. Done when: Opening the database file with a database tool shows no readable clip content. A short preview is present and is deliberately truncated. Deleting the keychain entry makes existing clips unreadable rather than crashing the application. The key never appears in any file on disk. Do not build yet: No search or pasting yet. ### Phase 3 · Search and pasting back Build: A hotkey opening a panel over the frontmost application. Search across previews and metadata. Selecting an entry decrypts it, writes it to the pasteboard, sends Cmd-V and restores the previous pasteboard contents afterwards. Trap: Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. Done when: The hotkey opens the panel from any application and Escape returns focus. Typing filters entries as you type across a thousand clips without a visible pause. Selecting an entry pastes it into the previously focused application. The pasteboard afterwards holds what it held before. Do not build yet: No boards or expiry yet. ### Phase 4 · Boards and rich content Build: Pinboards holding clips you keep. Rich handling per type: image thumbnails, file references that survive the file moving or warn when it does not, and formatted text that can be pasted as plain or formatted. Trap: Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. Done when: An image clip shows a thumbnail and pastes as an image. A file clip whose file has moved is marked as unavailable in the list rather than failing on paste. Formatted text can be pasted as plain text with a modifier. Pinned clips on a board survive expiry. Do not build yet: No expiry or distribution yet. ### Phase 5 · Expiry, which is a feature Build: Expiry by age and by total size, applied on a schedule, never touching pinned clips. Per-type expiry, because a copied password fragment deserves a shorter life than a code snippet. Secure deletion of expired content. Trap: A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. Done when: Clips older than the configured age are removed on schedule and pinned ones are not. Total size stays under the configured cap by removing oldest first. Expired content is removed from the database file rather than left as a deleted row, verified by inspecting the file. Per-type retention is configurable. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly what is captured, what is excluded, how long it is kept and that it is encrypted at rest. Trap: Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The README states what is captured, what is refused, the retention defaults and the encryption. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync. A clipboard history synced between machines is a substantially larger security question than a local one, and it is why the paid application charges. - Universal capture across every application, including the ones that use private pasteboards. - The rich preview quality of the commercial product, which handles a long tail of content types. - Compatibility across macOS releases without maintenance. ### README must contain - That concealed pasteboard items are refused, and what that means for password managers - That content is encrypted at rest with the key in the keychain, and that previews are not - The retention defaults per type, and that expiry is a feature rather than a limitation - That expired content is removed from the file rather than left as a deleted row - Which applications are excluded from capture by default ===== ARCHITECTURE.md ===== # Architecture · Paste ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | the pasteboard, the keychain and text insertion are all platform surfaces | | App type | A background agent with LSUIElement true | it watches; it appears on a hotkey | | Capture | Polling NSPasteboard.changeCount | there is no change notification of any kind, so polling is the supported approach rather than a workaround | | Storage | SQLite encrypted at rest, with the key in the keychain | a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep | | Insertion | Pasteboard plus a synthetic Cmd-V with restore | per-character posting drops characters and mangles anything outside ASCII | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Watcher | Polling and the exclusion rules. | Polling is the supported approach and is not swappable. The type check before reading is the safety property. | | Crypto | Key management and encryption at rest. | Any modern authenticated cipher. Keeping the key in the keychain rather than a file is the part that matters. | | Store | Clips, previews and deletion. | The separation of encrypted content from searchable preview is what makes search fast without decrypting everything. | | Paste | Insertion and pasteboard restoration. | Not swappable in practice; per-character insertion is the alternative and is worse. | | Expiry | Forgetting on schedule. | Any implementation, provided expired content leaves the file rather than remaining as a deleted row. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `HISTORY_DB_PATH` · required · Encrypted clip store. The key lives in the keychain, not here. - `POLL_INTERVAL_MS` · required · How often the change count is checked. Lower feels instant and costs battery. - `RETENTION_DAYS_TEXT` · required · How long text clips are kept. Finite by default and deliberately so. - `RETENTION_DAYS_IMAGE` · required · Images are large and rarely wanted a month later. - `MAX_HISTORY_MB` · required · Total size cap. Oldest unpinned clips are removed first. - `EXCLUDED_BUNDLE_IDS` · required · Applications never captured from, in addition to the concealed type check. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Paste product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, Polling NSPasteboard.changeCount, SQLite encrypted at rest, with the key in the keychain, Pasteboard plus a synthetic Cmd-V with restore. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. - Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. ===== MILESTONES.md ===== # Delivery milestones · Paste Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture, and what not to capture Everything you copy is stored, except the things that must never be. ### Steps 1. Poll the change count A timer comparing changeCount. Only read the pasteboard when it has moved. Files: `Watcher.swift` 2. Inspect types before reading Skip concealed, transient and auto-generated markers. Check before reading, since reading is what creates the exposure. Files: `Watcher.swift` 3. Deduplicate by hash Same content updates the timestamp. Otherwise a repeated copy fills the history. Files: `Store.swift` ### Done when - [ ] Three copies produce three entries, newest first - [ ] A concealed item is refused, proved by test - [ ] An excluded application's copies are not captured - [ ] A repeated copy updates rather than duplicating ### Watch out - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. ## M2 · Encryption at rest The history on disk is unreadable without the key, and the key is in the keychain. ### Steps 1. Generate and store a key On first launch, into the keychain with an appropriate access policy. Never a file. Files: `Crypto.swift` 2. Encrypt content, truncate previews Content encrypted, preview short and treated as readable. Files: `Store.swift` 3. Verify by inspection Open the database file with a tool and look. If you can read a clip, the encryption is not doing what you think. ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` ### Done when - [ ] The database file shows no readable clip content - [ ] Previews are present and truncated - [ ] A missing keychain key degrades rather than crashes - [ ] No key material appears in any file ### Watch out - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. ## M3 · Search and pasting back A hotkey opens the history, typing filters it, and Return pastes into where you were. ### Steps 1. Add the hotkey and panel A non-activating panel so the previous application keeps its focus context. Files: `Panel.swift` 2. Search previews and metadata Without decrypting everything, which is why previews exist. Files: `Search.swift` 3. Paste and restore carefully Save the pasteboard, write, send Cmd-V, restore after a delay, and only if your value is still there. Files: `Paste.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] Search filters a thousand clips without a visible pause - [ ] Selecting pastes into the previous application - [ ] The pasteboard is restored to its prior contents ### Watch out - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. ## M4 · Boards and rich content Things you paste repeatedly live somewhere, and images and files behave like themselves. ### Steps 1. Handle each type properly Images, files and rich text each have their own capture, preview and paste behaviour. Files: `Types/` 2. Check file availability when shown Mark missing files in the list rather than failing at paste time. Files: `Types/FileClip.swift` 3. Add boards Named collections of pinned clips, reorderable, exempt from expiry. Files: `Boards.swift` ### Done when - [ ] An image clip shows a thumbnail and pastes as an image - [ ] A moved file is marked unavailable rather than failing at paste - [ ] Formatted text can be pasted as plain with a modifier - [ ] Pinned clips survive expiry ### Watch out - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. ## M5 · Expiry, which is a feature The history does not keep everything you have ever copied, forever. ### Steps 1. Expire by age and size Both, on a schedule and after each capture. Pinned clips exempt. Files: `Expiry.swift` 2. Make deletion actually delete Vacuum after removal so the content is not recoverable from the file. Verify by inspection. Files: `Store.swift` 3. Per-type retention Different lifetimes per kind, with the setting prominent rather than buried. Files: `Settings.swift` ### Done when - [ ] Aged clips are removed and pinned ones are not - [ ] Total size stays under the cap - [ ] Expired content is not recoverable from the database file - [ ] Per-type retention is configurable ### Watch out - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ## M6 · Signing, notarization and updates (production only) Others can install it and understand what it keeps. ### Steps 1. Sign and notarize Hardened runtime, Developer ID, notarytool, stapler. ```sh xcrun notarytool submit Clips.zip --keychain-profile AC --wait ``` 2. Ship Sparkle updates EdDSA key pair with the private half outside the repository. Files: `appcast.xml` 3. Document what is kept Captured, refused, retained and encrypted. Four short paragraphs. Files: `README.md` ### Done when - [ ] spctl reports the notarized build accepted - [ ] The README states capture, exclusion, retention and encryption - [ ] An appcast update installs and relaunches - [ ] A fresh Mac opens it with no warning ### Watch out - Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. ===== OPERATIONS.md ===== # Operations · Paste ## Backup The history is encrypted and the key is in the keychain, so a backup of the database without the keychain is unreadable. That is usually the desired property, but it means a restore needs both. ## Restore Restore the database and the keychain entry. Without the key the history is gone, which is a reasonable default for this kind of data and should be stated rather than discovered. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Idle processor use from the poll loop, and database size against the cap. Battery impact is the thing users notice and the thing worth profiling before release. ## Incident checklist If something sensitive was captured, deletion must remove it from the file rather than marking a row deleted, which is why the vacuum step exists. Verify by inspecting the file rather than trusting the delete. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The concealed item test still refuses to store - [ ] The database file is confirmed to contain no readable clip content - [ ] The pasteboard is confirmed restored after every paste - [ ] Expired content is confirmed unrecoverable from the file - [ ] Idle processor use is profiled and acceptable ## Launch constraint Do not market omitted Paste capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Encrypted clip store. The key lives in the keychain, not here. HISTORY_DB_PATH=~/Library/Application Support/Clips/history.db # Required. How often the change count is checked. Lower feels instant and costs battery. POLL_INTERVAL_MS=200 # Required. How long text clips are kept. Finite by default and deliberately so. RETENTION_DAYS_TEXT=30 # Required. Images are large and rarely wanted a month later. RETENTION_DAYS_IMAGE=7 # Required. Total size cap. Oldest unpinned clips are removed first. MAX_HISTORY_MB=500 # Required. Applications never captured from, in addition to the concealed type check. EXCLUDED_BUNDLE_IDS=com.example.passwordmanager
# Paste · indie build A background agent that captures everything you copy into an encrypted local store, skips items password managers mark as concealed, keeps rich previews for images and files, organises pins into boards, expires old entries on a schedule, and pastes back into the application you were in. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | the pasteboard, the keychain and text insertion are all platform surfaces | | App type | A background agent with LSUIElement true | it watches; it appears on a hotkey | | Capture | Polling NSPasteboard.changeCount | there is no change notification of any kind, so polling is the supported approach rather than a workaround | | Storage | SQLite encrypted at rest, with the key in the keychain | a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep | | Insertion | Pasteboard plus a synthetic Cmd-V with restore | per-character posting drops characters and mangles anything outside ASCII | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you own one - Why: The pasteboard, keychain and insertion APIs are all platform surfaces. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds and signs the agent. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **A password manager that marks concealed items** · free with an open source manager - Why: Phase 1's most important check cannot be tested without one, and it is the check that keeps this from becoming a password log. - Get it: Any password manager that sets the concealed pasteboard type. Test by copying a password and confirming nothing is captured. - Verify: A copied password does not appear in the history - [ ] **Willingness to grant Accessibility for paste-back** · free - Why: Sending Cmd-V into another application needs it. Capture and search work without it, so it can be requested late. - Get it: System Settings, then Privacy and Security, then Accessibility, when the paste-back feature is first used. - Verify: Selecting a clip pastes into the previous application - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it, no notarization. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. - Apple-platform polish, iCloud sync, rich previews, and universal capture - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Paste, and the README should say so rather than pretend.
# Build brief · Paste The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an encrypted clipboard history with search, pinboards and expiry, that refuses to store what should not be stored to replace Paste. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · the pasteboard, the keychain and text insertion are all platform surfaces - App type: A background agent with LSUIElement true · it watches; it appears on a hotkey - Capture: Polling NSPasteboard.changeCount · there is no change notification of any kind, so polling is the supported approach rather than a workaround - Storage: SQLite encrypted at rest, with the key in the keychain · a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep - Insertion: Pasteboard plus a synthetic Cmd-V with restore · per-character posting drops characters and mangles anything outside ASCII ### Data model (create this before Phase 1) - clips(id, kind, hash, bytes, preview_text, source_bundle_id, copied_at, pinned, board_id, expires_at) · kind is text, image, file or rich - boards(id, name, position) · pinboards for things you paste repeatedly - clip_data(clip_id, encrypted_blob) · the actual content, encrypted at rest, separate from the searchable metadata - excluded_apps(bundle_id, reason) · applications whose copies are never captured - The searchable metadata deliberately holds a preview rather than the content, so search does not require decrypting everything. ### Phase 1 · Capture, and what not to capture Build: Poll the pasteboard change count on a timer. On a change, inspect the item types before reading contents. Skip anything marked concealed, transient or automatically generated, and skip copies from applications on an exclusion list. Trap: Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. Done when: Copying three things produces three entries, newest first. A password copied from a password manager does not appear, verified by a test with a synthetic concealed item. A copy from an excluded application is not captured. Copying the same text twice updates a timestamp rather than adding a row. Do not build yet: No encryption, no search, no pasting back. ### Phase 2 · Encryption at rest Build: Generate a key on first launch, store it in the keychain, and encrypt clip contents before writing. Keep searchable metadata separate and limited to a short preview. Verify by inspecting the database file directly. Trap: Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. Done when: Opening the database file with a database tool shows no readable clip content. A short preview is present and is deliberately truncated. Deleting the keychain entry makes existing clips unreadable rather than crashing the application. The key never appears in any file on disk. Do not build yet: No search or pasting yet. ### Phase 3 · Search and pasting back Build: A hotkey opening a panel over the frontmost application. Search across previews and metadata. Selecting an entry decrypts it, writes it to the pasteboard, sends Cmd-V and restores the previous pasteboard contents afterwards. Trap: Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. Done when: The hotkey opens the panel from any application and Escape returns focus. Typing filters entries as you type across a thousand clips without a visible pause. Selecting an entry pastes it into the previously focused application. The pasteboard afterwards holds what it held before. Do not build yet: No boards or expiry yet. ### Phase 4 · Boards and rich content Build: Pinboards holding clips you keep. Rich handling per type: image thumbnails, file references that survive the file moving or warn when it does not, and formatted text that can be pasted as plain or formatted. Trap: Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. Done when: An image clip shows a thumbnail and pastes as an image. A file clip whose file has moved is marked as unavailable in the list rather than failing on paste. Formatted text can be pasted as plain text with a modifier. Pinned clips on a board survive expiry. Do not build yet: No expiry or distribution yet. ### Phase 5 · Expiry, which is a feature Build: Expiry by age and by total size, applied on a schedule, never touching pinned clips. Per-type expiry, because a copied password fragment deserves a shorter life than a code snippet. Secure deletion of expired content. Trap: A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. Done when: Clips older than the configured age are removed on schedule and pinned ones are not. Total size stays under the configured cap by removing oldest first. Expired content is removed from the database file rather than left as a deleted row, verified by inspecting the file. Per-type retention is configurable. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly what is captured, what is excluded, how long it is kept and that it is encrypted at rest. Trap: Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The README states what is captured, what is refused, the retention defaults and the encryption. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync. A clipboard history synced between machines is a substantially larger security question than a local one, and it is why the paid application charges. - Universal capture across every application, including the ones that use private pasteboards. - The rich preview quality of the commercial product, which handles a long tail of content types. - Compatibility across macOS releases without maintenance. ### README must contain - That concealed pasteboard items are refused, and what that means for password managers - That content is encrypted at rest with the key in the keychain, and that previews are not - The retention defaults per type, and that expiry is a feature rather than a limitation - That expired content is removed from the file rather than left as a deleted row - Which applications are excluded from capture by default
# Agent instructions · Paste indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, Polling NSPasteboard.changeCount, SQLite encrypted at rest, with the key in the keychain, Pasteboard plus a synthetic Cmd-V with restore. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried.
# Build plan · Paste A background agent that captures everything you copy into an encrypted local store, skips items password managers mark as concealed, keeps rich previews for images and files, organises pins into boards, expires old entries on a schedule, and pastes back into the application you were in. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture, and what not to capture Everything you copy is stored, except the things that must never be. ### Steps 1. Poll the change count A timer comparing changeCount. Only read the pasteboard when it has moved. Files: `Watcher.swift` 2. Inspect types before reading Skip concealed, transient and auto-generated markers. Check before reading, since reading is what creates the exposure. Files: `Watcher.swift` 3. Deduplicate by hash Same content updates the timestamp. Otherwise a repeated copy fills the history. Files: `Store.swift` ### Done when - [ ] Three copies produce three entries, newest first - [ ] A concealed item is refused, proved by test - [ ] An excluded application's copies are not captured - [ ] A repeated copy updates rather than duplicating ### Watch out - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. ## Phase 2 · Encryption at rest The history on disk is unreadable without the key, and the key is in the keychain. ### Steps 1. Generate and store a key On first launch, into the keychain with an appropriate access policy. Never a file. Files: `Crypto.swift` 2. Encrypt content, truncate previews Content encrypted, preview short and treated as readable. Files: `Store.swift` 3. Verify by inspection Open the database file with a tool and look. If you can read a clip, the encryption is not doing what you think. ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` ### Done when - [ ] The database file shows no readable clip content - [ ] Previews are present and truncated - [ ] A missing keychain key degrades rather than crashes - [ ] No key material appears in any file ### Watch out - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. ## Phase 3 · Search and pasting back A hotkey opens the history, typing filters it, and Return pastes into where you were. ### Steps 1. Add the hotkey and panel A non-activating panel so the previous application keeps its focus context. Files: `Panel.swift` 2. Search previews and metadata Without decrypting everything, which is why previews exist. Files: `Search.swift` 3. Paste and restore carefully Save the pasteboard, write, send Cmd-V, restore after a delay, and only if your value is still there. Files: `Paste.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] Search filters a thousand clips without a visible pause - [ ] Selecting pastes into the previous application - [ ] The pasteboard is restored to its prior contents ### Watch out - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. ## Phase 4 · Boards and rich content Things you paste repeatedly live somewhere, and images and files behave like themselves. ### Steps 1. Handle each type properly Images, files and rich text each have their own capture, preview and paste behaviour. Files: `Types/` 2. Check file availability when shown Mark missing files in the list rather than failing at paste time. Files: `Types/FileClip.swift` 3. Add boards Named collections of pinned clips, reorderable, exempt from expiry. Files: `Boards.swift` ### Done when - [ ] An image clip shows a thumbnail and pastes as an image - [ ] A moved file is marked unavailable rather than failing at paste - [ ] Formatted text can be pasted as plain with a modifier - [ ] Pinned clips survive expiry ### Watch out - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. ## Phase 5 · Expiry, which is a feature The history does not keep everything you have ever copied, forever. ### Steps 1. Expire by age and size Both, on a schedule and after each capture. Pinned clips exempt. Files: `Expiry.swift` 2. Make deletion actually delete Vacuum after removal so the content is not recoverable from the file. Verify by inspection. Files: `Store.swift` 3. Per-type retention Different lifetimes per kind, with the setting prominent rather than buried. Files: `Settings.swift` ### Done when - [ ] Aged clips are removed and pinned ones are not - [ ] Total size stays under the cap - [ ] Expired content is not recoverable from the database file - [ ] Per-type retention is configurable ### Watch out - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ## Not in this build - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. ## After v1, if you want it - A merge action combining several clips into one paste - Per-application default boards, so a code snippet board opens in an editor - Transform actions on paste, such as stripping formatting or lowercasing
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Encrypted clip store. The key lives in the keychain, not here. HISTORY_DB_PATH=~/Library/Application Support/Clips/history.db # Required. How often the change count is checked. Lower feels instant and costs battery. POLL_INTERVAL_MS=200 # Required. How long text clips are kept. Finite by default and deliberately so. RETENTION_DAYS_TEXT=30 # Required. Images are large and rarely wanted a month later. RETENTION_DAYS_IMAGE=7 # Required. Total size cap. Oldest unpinned clips are removed first. MAX_HISTORY_MB=500 # Required. Applications never captured from, in addition to the concealed type check. EXCLUDED_BUNDLE_IDS=com.example.passwordmanager
# Paste · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Paste, store a local encrypted clipboard history with search, pinboards, and expiration. The hard boundary is apple-platform polish, icloud sync, rich previews, and universal capture, plus os integration polish and maintenance. ## Product outcome A clipboard history that is encrypted at rest, refuses to store what password managers mark as concealed, forgets on a schedule you set, and pastes back without losing what you had copied. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode - only the permissions required by the selected feature ## Explicit non-goals for v1 - Cross-device sync of clipboard history. - Universal capture including private pasteboards. - The rich preview quality of the commercial product. - Guaranteed compatibility across macOS releases. - Apple-platform polish, iCloud sync, rich previews, and universal capture - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes ## Success criteria - The concealed item test still refuses to store - The database file is confirmed to contain no readable clip content - The pasteboard is confirmed restored after every paste - Expired content is confirmed unrecoverable from the file - Idle processor use is profiled and acceptable
# Build brief · Paste The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an encrypted clipboard history with search, pinboards and expiry, that refuses to store what should not be stored to replace Paste. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · the pasteboard, the keychain and text insertion are all platform surfaces - App type: A background agent with LSUIElement true · it watches; it appears on a hotkey - Capture: Polling NSPasteboard.changeCount · there is no change notification of any kind, so polling is the supported approach rather than a workaround - Storage: SQLite encrypted at rest, with the key in the keychain · a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep - Insertion: Pasteboard plus a synthetic Cmd-V with restore · per-character posting drops characters and mangles anything outside ASCII ### Data model (create this before Phase 1) - clips(id, kind, hash, bytes, preview_text, source_bundle_id, copied_at, pinned, board_id, expires_at) · kind is text, image, file or rich - boards(id, name, position) · pinboards for things you paste repeatedly - clip_data(clip_id, encrypted_blob) · the actual content, encrypted at rest, separate from the searchable metadata - excluded_apps(bundle_id, reason) · applications whose copies are never captured - The searchable metadata deliberately holds a preview rather than the content, so search does not require decrypting everything. ### Phase 1 · Capture, and what not to capture Build: Poll the pasteboard change count on a timer. On a change, inspect the item types before reading contents. Skip anything marked concealed, transient or automatically generated, and skip copies from applications on an exclusion list. Trap: Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. Done when: Copying three things produces three entries, newest first. A password copied from a password manager does not appear, verified by a test with a synthetic concealed item. A copy from an excluded application is not captured. Copying the same text twice updates a timestamp rather than adding a row. Do not build yet: No encryption, no search, no pasting back. ### Phase 2 · Encryption at rest Build: Generate a key on first launch, store it in the keychain, and encrypt clip contents before writing. Keep searchable metadata separate and limited to a short preview. Verify by inspecting the database file directly. Trap: Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. Done when: Opening the database file with a database tool shows no readable clip content. A short preview is present and is deliberately truncated. Deleting the keychain entry makes existing clips unreadable rather than crashing the application. The key never appears in any file on disk. Do not build yet: No search or pasting yet. ### Phase 3 · Search and pasting back Build: A hotkey opening a panel over the frontmost application. Search across previews and metadata. Selecting an entry decrypts it, writes it to the pasteboard, sends Cmd-V and restores the previous pasteboard contents afterwards. Trap: Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. Done when: The hotkey opens the panel from any application and Escape returns focus. Typing filters entries as you type across a thousand clips without a visible pause. Selecting an entry pastes it into the previously focused application. The pasteboard afterwards holds what it held before. Do not build yet: No boards or expiry yet. ### Phase 4 · Boards and rich content Build: Pinboards holding clips you keep. Rich handling per type: image thumbnails, file references that survive the file moving or warn when it does not, and formatted text that can be pasted as plain or formatted. Trap: Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. Done when: An image clip shows a thumbnail and pastes as an image. A file clip whose file has moved is marked as unavailable in the list rather than failing on paste. Formatted text can be pasted as plain text with a modifier. Pinned clips on a board survive expiry. Do not build yet: No expiry or distribution yet. ### Phase 5 · Expiry, which is a feature Build: Expiry by age and by total size, applied on a schedule, never touching pinned clips. Per-type expiry, because a copied password fragment deserves a shorter life than a code snippet. Secure deletion of expired content. Trap: A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. Done when: Clips older than the configured age are removed on schedule and pinned ones are not. Total size stays under the configured cap by removing oldest first. Expired content is removed from the database file rather than left as a deleted row, verified by inspecting the file. Per-type retention is configurable. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly what is captured, what is excluded, how long it is kept and that it is encrypted at rest. Trap: Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The README states what is captured, what is refused, the retention defaults and the encryption. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - Cross-device sync. A clipboard history synced between machines is a substantially larger security question than a local one, and it is why the paid application charges. - Universal capture across every application, including the ones that use private pasteboards. - The rich preview quality of the commercial product, which handles a long tail of content types. - Compatibility across macOS releases without maintenance. ### README must contain - That concealed pasteboard items are refused, and what that means for password managers - That content is encrypted at rest with the key in the keychain, and that previews are not - The retention defaults per type, and that expiry is a feature rather than a limitation - That expired content is removed from the file rather than left as a deleted row - Which applications are excluded from capture by default
# Architecture · Paste ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | the pasteboard, the keychain and text insertion are all platform surfaces | | App type | A background agent with LSUIElement true | it watches; it appears on a hotkey | | Capture | Polling NSPasteboard.changeCount | there is no change notification of any kind, so polling is the supported approach rather than a workaround | | Storage | SQLite encrypted at rest, with the key in the keychain | a clipboard history is a plaintext record of everything you copied, which includes things you would not choose to keep | | Insertion | Pasteboard plus a synthetic Cmd-V with restore | per-character posting drops characters and mangles anything outside ASCII | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Watcher | Polling and the exclusion rules. | Polling is the supported approach and is not swappable. The type check before reading is the safety property. | | Crypto | Key management and encryption at rest. | Any modern authenticated cipher. Keeping the key in the keychain rather than a file is the part that matters. | | Store | Clips, previews and deletion. | The separation of encrypted content from searchable preview is what makes search fast without decrypting everything. | | Paste | Insertion and pasteboard restoration. | Not swappable in practice; per-character insertion is the alternative and is worse. | | Expiry | Forgetting on schedule. | Any implementation, provided expired content leaves the file rather than remaining as a deleted row. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `HISTORY_DB_PATH` · required · Encrypted clip store. The key lives in the keychain, not here. - `POLL_INTERVAL_MS` · required · How often the change count is checked. Lower feels instant and costs battery. - `RETENTION_DAYS_TEXT` · required · How long text clips are kept. Finite by default and deliberately so. - `RETENTION_DAYS_IMAGE` · required · Images are large and rarely wanted a month later. - `MAX_HISTORY_MB` · required · Total size cap. Oldest unpinned clips are removed first. - `EXCLUDED_BUNDLE_IDS` · required · Applications never captured from, in addition to the concealed type check. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Paste product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, Polling NSPasteboard.changeCount, SQLite encrypted at rest, with the key in the keychain, Pasteboard plus a synthetic Cmd-V with restore. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. - Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes.
# Delivery milestones · Paste Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture, and what not to capture Everything you copy is stored, except the things that must never be. ### Steps 1. Poll the change count A timer comparing changeCount. Only read the pasteboard when it has moved. Files: `Watcher.swift` 2. Inspect types before reading Skip concealed, transient and auto-generated markers. Check before reading, since reading is what creates the exposure. Files: `Watcher.swift` 3. Deduplicate by hash Same content updates the timestamp. Otherwise a repeated copy fills the history. Files: `Store.swift` ### Done when - [ ] Three copies produce three entries, newest first - [ ] A concealed item is refused, proved by test - [ ] An excluded application's copies are not captured - [ ] A repeated copy updates rather than duplicating ### Watch out - Password managers mark their pasteboard items with a concealed type, and a history that ignores those markers becomes a plaintext log of every password you copied. Check types before reading contents, not after, and write a test that feeds a synthetic concealed item and asserts nothing was stored. ## M2 · Encryption at rest The history on disk is unreadable without the key, and the key is in the keychain. ### Steps 1. Generate and store a key On first launch, into the keychain with an appropriate access policy. Never a file. Files: `Crypto.swift` 2. Encrypt content, truncate previews Content encrypted, preview short and treated as readable. Files: `Store.swift` 3. Verify by inspection Open the database file with a tool and look. If you can read a clip, the encryption is not doing what you think. ```sh sqlite3 history.db "select * from clip_data limit 1;" ``` ### Done when - [ ] The database file shows no readable clip content - [ ] Previews are present and truncated - [ ] A missing keychain key degrades rather than crashes - [ ] No key material appears in any file ### Watch out - Encrypting the content but storing a full plaintext preview alongside it defeats the purpose, because the preview usually contains the sensitive part. Keep the preview short and truncated, and treat it as something that could be read. ## M3 · Search and pasting back A hotkey opens the history, typing filters it, and Return pastes into where you were. ### Steps 1. Add the hotkey and panel A non-activating panel so the previous application keeps its focus context. Files: `Panel.swift` 2. Search previews and metadata Without decrypting everything, which is why previews exist. Files: `Search.swift` 3. Paste and restore carefully Save the pasteboard, write, send Cmd-V, restore after a delay, and only if your value is still there. Files: `Paste.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] Search filters a thousand clips without a visible pause - [ ] Selecting pastes into the previous application - [ ] The pasteboard is restored to its prior contents ### Watch out - Restoring the previous pasteboard too quickly races the paste and the target application receives the restored value instead of the clip. Restore after a delay, and only if the pasteboard still holds what you wrote. ## M4 · Boards and rich content Things you paste repeatedly live somewhere, and images and files behave like themselves. ### Steps 1. Handle each type properly Images, files and rich text each have their own capture, preview and paste behaviour. Files: `Types/` 2. Check file availability when shown Mark missing files in the list rather than failing at paste time. Files: `Types/FileClip.swift` 3. Add boards Named collections of pinned clips, reorderable, exempt from expiry. Files: `Boards.swift` ### Done when - [ ] An image clip shows a thumbnail and pastes as an image - [ ] A moved file is marked unavailable rather than failing at paste - [ ] Formatted text can be pasted as plain with a modifier - [ ] Pinned clips survive expiry ### Watch out - Storing a copied file as a path means the clip breaks silently when the file moves or is deleted, and it will look fine in the list until you paste it. Either store the file's contents for small files, or check existence when the clip is shown and mark it clearly. ## M5 · Expiry, which is a feature The history does not keep everything you have ever copied, forever. ### Steps 1. Expire by age and size Both, on a schedule and after each capture. Pinned clips exempt. Files: `Expiry.swift` 2. Make deletion actually delete Vacuum after removal so the content is not recoverable from the file. Verify by inspection. Files: `Store.swift` 3. Per-type retention Different lifetimes per kind, with the setting prominent rather than buried. Files: `Settings.swift` ### Done when - [ ] Aged clips are removed and pinned ones are not - [ ] Total size stays under the cap - [ ] Expired content is not recoverable from the database file - [ ] Per-type retention is configurable ### Watch out - A clipboard history that keeps everything forever is a comprehensive record of everything you have handled, and it is the kind of thing people forget they are carrying. Default to a finite window and make the retention setting prominent rather than buried. ## M6 · Signing, notarization and updates (production only) Others can install it and understand what it keeps. ### Steps 1. Sign and notarize Hardened runtime, Developer ID, notarytool, stapler. ```sh xcrun notarytool submit Clips.zip --keychain-profile AC --wait ``` 2. Ship Sparkle updates EdDSA key pair with the private half outside the repository. Files: `appcast.xml` 3. Document what is kept Captured, refused, retained and encrypted. Four short paragraphs. Files: `README.md` ### Done when - [ ] spctl reports the notarized build accepted - [ ] The README states capture, exclusion, retention and encryption - [ ] An appcast update installs and relaunches - [ ] A fresh Mac opens it with no warning ### Watch out - Signing invalidates any Accessibility grant testers gave the unsigned build, and macOS will not prompt again. Put the removal steps in the release notes.
# Operations · Paste ## Backup The history is encrypted and the key is in the keychain, so a backup of the database without the keychain is unreadable. That is usually the desired property, but it means a restore needs both. ## Restore Restore the database and the keychain entry. Without the key the history is gone, which is a reasonable default for this kind of data and should be stated rather than discovered. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Idle processor use from the poll loop, and database size against the cap. Battery impact is the thing users notice and the thing worth profiling before release. ## Incident checklist If something sensitive was captured, deletion must remove it from the file rather than marking a row deleted, which is why the vacuum step exists. Verify by inspecting the file rather than trusting the delete. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The concealed item test still refuses to store - [ ] The database file is confirmed to contain no readable clip content - [ ] The pasteboard is confirmed restored after every paste - [ ] Expired content is confirmed unrecoverable from the file - [ ] Idle processor use is profiled and acceptable ## Launch constraint Do not market omitted Paste capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Encrypted clip store. The key lives in the keychain, not here. HISTORY_DB_PATH=~/Library/Application Support/Clips/history.db # Required. How often the change count is checked. Lower feels instant and costs battery. POLL_INTERVAL_MS=200 # Required. How long text clips are kept. Finite by default and deliberately so. RETENTION_DAYS_TEXT=30 # Required. Images are large and rarely wanted a month later. RETENTION_DAYS_IMAGE=7 # Required. Total size cap. Oldest unpinned clips are removed first. MAX_HISTORY_MB=500 # Required. Applications never captured from, in addition to the concealed type check. EXCLUDED_BUNDLE_IDS=com.example.passwordmanager
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Paste because small utilities are technically reproducible, but customers pay for polish and immediate fixes when an OS update breaks a daily habit. The recurring cost buys permissions, accessibility APIs, sandboxing, signing, notarization, OS regressions, updates, backups, and support, not just the visible interface.
xApple-platform polish, iCloud sync, rich previews, and universal capture
xyears of OS-version edge cases
xlarge extension ecosystem
xcross-device sync
xcommercial support and rapid compatibility fixes
Don't feel like building it? These folks already made it free.
CCopyQA clipboard filing cabinet with tabs, filters, rules, and built-in encryption.open source↗no votes, no pay-to-list · just what's real
Paste pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| personal | — | $2.50/user | Use on all personal devices; no published device cap; includes updates. |
| personal lifetime | custom | — | Lifetime Personal access on all personal devices with updates. |
| family | custom | — | Family-sharing plan sold through Apple's App Store; public numeric member cap and price were not exposed on Paste's page. |
| team | custom | — | Team clipboard and administration plan; public numeric price and limits were not exposed in the crawlable page. |
free tierno free tier
billingmonthly + annual + lifetime; 7-day trial on paid plans
hidden costsPrices exclude tax and can vary by region. Family billing is App Store-only, and the current monthly, lifetime, and Team checkout amounts were not exposed in the public rendered page.
verified 2026-08-14 · source ↗
Vibecode Paste
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Paste replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Paste cost?
Paste costs about $2.5/month (Personal, checked 2026-08-14), which is $30 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Paste?
Honestly: Apple-platform polish, iCloud sync, rich previews, and universal capture; years of OS-version edge cases; large extension ecosystem; cross-device sync; commercial support and rapid compatibility fixes. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Paste?
Yes: CopyQ (A clipboard filing cabinet with tabs, filters, rules, and built-in encryption.) The prompt is for when you want it exactly your way.