Skip to content
cccskills
FreePublished on GitHub

macos-clipboard-pasteboard

Use when reading, writing, or watching the macOS clipboard — NSPasteboard types and multiple representations, the changeCount polling pattern, the transient/concealed pasteboard conventions clipboard tools must respect, and the Sequoia paste-privacy alerts.

The English summary is being prepared. Showing the original description.

Run score Not tested

See how to install

Files included (1)

  • SKILL.md8.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

macOS Clipboard & Pasteboard

Work with NSPasteboard correctly: write rich content with plain-text fallbacks, watch for changes the only way macOS allows (polling changeCount), and — if you're building anything clipboard-manager-shaped — respect the concealed/transient conventions that keep passwords out of your history.

When to use

Use this skill when the user says:

  • clipboard / pasteboard / NSPasteboard
  • copy / paste programmatically
  • clipboard history / clipboard manager
  • watch the clipboard / detect copy
  • paste as plain text
  • custom pasteboard type / drag-and-drop data type
  • clipboard privacy / "app pasted from" alert

Do not use this skill for iOS (UIPasteboard differs in important ways), or for drag-and-drop UI mechanics (onDrag/Transferable view wiring) beyond the data-type layer.

Core rule

Write every flavor the receiver might want, richest first.
Read by asking for the best type you can handle.
Never clobber, log, or upload the user's clipboard —
and never store what a password manager marked concealed.

Writing

Always clearContents() first — it claims ownership and bumps changeCount. Then declare the richest set of representations you have:

import AppKit

// Simple string
let pb = NSPasteboard.general
pb.clearContents()
pb.setString("hello", forType: .string)

// Rich content with fallbacks: receivers pick the best they support.
// One NSPasteboardItem = one logical thing with multiple flavors.
let item = NSPasteboardItem()
item.setString(htmlString, forType: .html)
item.setString(plainString, forType: .string)     // the fallback that
pb.clearContents()                                //  makes TextEdit,
pb.writeObjects([item])                           //  terminals etc. work

// Files
pb.clearContents()
pb.writeObjects([fileURL as NSURL])

// Multiple files = multiple items, not one item with many flavors
pb.writeObjects(urls as [NSURL])

The classic bug is writing only HTML or only a custom type: paste then silently does nothing in half the apps on the system. Plain .string rides along with almost everything.

Custom types

Namespace them like a UTI and add them alongside standard flavors:

extension NSPasteboard.PasteboardType {
    static let taskItem = NSPasteboard.PasteboardType("com.example.myapp.task")
}

let item = NSPasteboardItem()
item.setData(try JSONEncoder().encode(task), forType: .taskItem)  // full fidelity
item.setString(task.title, forType: .string)                      // world-readable

Your own app pastes with full fidelity; everyone else gets sensible text.

Reading

let pb = NSPasteboard.general

// Best-type reading: order = your preference
if let types = pb.types {
    if types.contains(.fileURL),
       let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL] {
        handle(files: urls)
    } else if let s = pb.string(forType: .string) {
        handle(text: s)
    }
}

// Cheap capability check before enabling a Paste button:
let canPaste = pb.canReadObject(forClasses: [NSURL.self, NSString.self],
                                options: nil)

Read the pasteboard only on user intent — a paste command, a button, a drop. Reading on timers or at launch is what triggers the privacy alerts below, and it's the behavior they were designed to shame.

Watching for changes — changeCount polling

There is no notification for pasteboard changes on macOS. The sanctioned pattern is polling changeCount — an integer that increments on every ownership change:

@MainActor
final class PasteboardWatcher {
    private var timer: Timer?
    private var lastCount = NSPasteboard.general.changeCount
    private let onChange: (NSPasteboard) -> Void

    init(onChange: @escaping (NSPasteboard) -> Void) {
        self.onChange = onChange
    }

    func start(interval: TimeInterval = 0.5) {
        stop()
        timer = Timer.scheduledTimer(withTimeInterval: interval,
                                     repeats: true) { [weak self] _ in
            guard let self else { return }
            let pb = NSPasteboard.general
            guard pb.changeCount != self.lastCount else { return }
            self.lastCount = pb.changeCount
            self.onChange(pb)
        }
        timer?.tolerance = interval / 2   // let the system coalesce wakeups
    }

    func stop() {
        timer?.invalidate()
        timer = nil
    }
}
  • 0.5s is plenty; nobody notices half a second of clipboard-history lag, and tolerance keeps it off the battery's back.
  • Compare changeCount only — don't read contents to detect change.
  • Stop the watcher when the feature is off. An always-on reader is exactly what the OS now warns users about.

The clipboard-manager conventions

Any tool that stores clipboard contents must honor the de-facto org.nspasteboard marker types (password managers write them; well-behaved history tools read them):

extension NSPasteboard.PasteboardType {
    /// Passwords, secrets — never store, never display
    static let concealed = NSPasteboard.PasteboardType("org.nspasteboard.ConcealedType")
    /// Ephemeral by intent (e.g. a "paste once" item) — don't keep
    static let transient = NSPasteboard.PasteboardType("org.nspasteboard.TransientType")
    /// Machine-generated (linters, formatters) — usually skip in history
    static let autoGenerated = NSPasteboard.PasteboardType("org.nspasteboard.AutoGeneratedType")
}

func shouldStore(_ pb: NSPasteboard) -> Bool {
    guard let types = pb.types else { return false }
    return !types.contains(.concealed) && !types.contains(.transient)
}

And the mirror image: if your app copies something secret (a generated token, a recovery code), mark it concealed so other tools skip it:

let item = NSPasteboardItem()
item.setString(secret, forType: .string)
item.setString("", forType: .concealed)   // presence is the signal
pb.clearContents()
pb.writeObjects([item])

Sequoia paste privacy

Since macOS 15.x, reading NSPasteboard.general without user intent can surface a system alert — "AppName pasted from OtherApp" — and a per-app clipboard setting exists under Privacy & Security. Consequences:

  • Read only inside user-intent code paths (paste command, drop handler, explicit button). Never probe contents at launch "to see what's there".
  • Use canReadObject(forClasses:) / inspect types for UI enablement — checking types is not reading contents.
  • A clipboard-history feature is by definition intent-less reading: make it opt-in, explain it at enable time ("reads everything you copy, stores it only on this Mac"), and expect the system to tell the user you're doing it.

Don't clobber the user's clipboard

"Copy to clipboard" features are fine. But automation that pastes through the clipboard (type-for-the-user tools) must save and restore:

// Snapshot → do your programmatic paste → restore
let saved = pb.pasteboardItems?.map { old -> NSPasteboardItem in
    let copy = NSPasteboardItem()
    for type in old.types {
        if let data = old.data(forType: type) {
            copy.setData(data, forType: type)
        }
    }
    return copy
} ?? []
// … write payload, send ⌘V …
pb.clearContents()
pb.writeObjects(saved)

Restore after a short delay (the receiving app must read the payload first). Losing what the user had copied is one of the fastest ways to make them delete your app.

Checklist

[ ] Writes include a plain .string fallback next to rich/custom flavors
[ ] clearContents() before every write
[ ] Reads happen on user intent only (paste/drop/button), never on timers
[ ] Watching uses changeCount polling with tolerance, and stops when off
[ ] History/storage honors ConcealedType and TransientType
[ ] Own secrets are written with the concealed marker
[ ] Programmatic paste-through saves and restores the user's clipboard
[ ] Nothing from the clipboard is logged, synced, or uploaded

Manual test

  1. Copy from your app → paste into TextEdit, a terminal, and Mail — all three produce something sensible.
  2. Copy a password from a password manager → your history feature does not record it.
  3. Enable watching → copy elsewhere → change picked up within ~1s; disable → polling stops (check with a breakpoint).
  4. On macOS 15+: confirm normal use never triggers the "pasted from" alert outside genuine paste actions.
  5. Paste-through automation: copy something first, run the feature, press ⌘V afterwards → your original copy is back.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when creating a new project folder, starter pack, app scaffold, or agent-ready workspace — AGENTS.md, README, TASKS, DECISIONS, PROMPTS, an idempotent bootstrap script, and starter-pack modes.

The English summary is being prepared. Showing the original description.

aka-kika/akakika-skills11Updated Sep 12, 2026

Use when designing or implementing states for AI agent tasks, background jobs, local model runs, queues, and notifications — one shared set of states, only legal transitions, and a recovery path out of every failure.

The English summary is being prepared. Showing the original description.

aka-kika/akakika-skills11Updated Sep 12, 2026

Use when exposing app features to App Intents, Shortcuts, Siri, Spotlight, widgets, controls, or system actions — small safe AppIntents, AppShortcutsProvider, AppEnum/AppEntity, parameters, and open-vs-background behavior.

The English summary is being prepared. Showing the original description.

aka-kika/akakika-skills11Updated Sep 12, 2026

Use when designing, reviewing, or implementing a command-palette / ⌘K layer for an Apple-platform app — keyboard-first global search, ranked and grouped results, native floating panel, and protected destructive commands.

The English summary is being prepared. Showing the original description.

aka-kika/akakika-skills11Updated Sep 12, 2026

Use when designing or implementing empty, first-run, no-result, offline, error, or waiting states in Apple-platform apps — every empty state explains what is missing, why it matters, and what to do next.

The English summary is being prepared. Showing the original description.

aka-kika/akakika-skills11Updated Sep 12, 2026

Use when designing or implementing feedback, status, progress, alerts, errors, success states, and live activity indicators in Apple-platform apps — inline status, banners, alerts, notifications, and loading states.

The English summary is being prepared. Showing the original description.

aka-kika/akakika-skills11Updated Sep 12, 2026

See all skills by aka-kika

Report a problem with this skill