Skip to content

Commit fb019cf

Browse files
committed
v1.2.3: fix auto-paste (drop sandbox + yield activation), migrate history
Auto-paste was silently broken since the project flipped on ENABLE_APP_SANDBOX (1.0.0): a sandboxed app on macOS 14+ cannot use NSRunningApplication.activate() to bring a foreign app forward, so the synthesized ⌘V landed on us instead of the target. Two coupled fixes: 1. Drop ENABLE_APP_SANDBOX and set com.apple.security.app-sandbox = false in the checked-in entitlements file. Hardened runtime stays on; no network linkage; AES-GCM at rest; Keychain key. Every shipping clipboard manager (Maccy, Paste, Alfred, Raycast) runs unsandboxed for the same reason. 2. NSApp.yieldActivation(to:) before app.activate() so macOS 14+ accepts the foreign-app activation. Moved off the deprecated activate(options:) API. History from pre-1.2.3 sandbox containers is migrated to the unsandboxed Application Support location on first launch. Migration runs before KeyManager so a re-signed-binary keychain re-auth prompt can't block it. CHANGELOG, SECURITY.md, README updated to reflect the unsandboxed posture and the trade-off rationale.
1 parent 4ca2110 commit fb019cf

7 files changed

Lines changed: 139 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22

33
All notable changes to this project are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/).
44

5+
## [1.2.3] — 2026-05-31
6+
7+
### Fixed
8+
9+
- **Auto-paste now actually works.** Two compounding bugs were silently swallowing the synthesized ⌘V:
10+
1. The App Sandbox was enabled (since v1.0.0, via `ENABLE_APP_SANDBOX = YES` in the project). A sandboxed app on macOS 14+ cannot use `NSRunningApplication.activate()` to bring a foreign app to the foreground, so the previously-focused app never actually came forward and the keystroke landed on Clip-Board (or nowhere).
11+
2. Even unsandboxed, macOS 14+ requires the currently-active app to explicitly **yield activation** before another app can take focus. We now call `NSApp.yieldActivation(to:)` before activating the target, then post ⌘V once it's truly frontmost.
12+
13+
### Changed
14+
15+
- **App is no longer sandboxed.** Every shipping clipboard manager that does cross-app paste injection (Maccy, Paste, Alfred, Raycast) runs unsandboxed for exactly this reason — the sandbox is fundamentally incompatible with "activate any other app and synthesize a paste into it." See `SECURITY.md` for the full rationale and what hardening we *do* still apply (hardened runtime, no network linkage, AES-GCM at rest, Keychain key, file mode 0600 / dir 0700).
16+
- Updated `NSRunningApplication.activate(options:)` (deprecated in macOS 14) → `activate()`.
17+
- Bumped auto-paste activation budget from 500 ms → 600 ms (the yield handoff costs a frame or two).
18+
19+
### Security / hygiene
20+
21+
- Entitlements file now explicitly sets `com.apple.security.app-sandbox = false` with an inline comment explaining the trade-off, so reviewers can diff source against the signed binary and see the unsandboxed posture is intentional, not an oversight.
22+
23+
### Migration
24+
25+
- **Pre-1.2.3 history is automatically carried over.** Upgrading from 1.2.2 or earlier: on first launch, the old sandbox-container history (`~/Library/Containers/Siddharth.Sangwa.ClipBoard/Data/Library/Application Support/ClipboardManager/`) is copied to the new unsandboxed location (`~/Library/Application Support/ClipboardManager/`). The legacy container is left in place untouched, so downgrading still works.
26+
27+
[1.2.3]: https://github.com/Light-House-Group/Clip-Board/releases/tag/v1.2.3
28+
529
## [1.2.2] — 2026-05-30
630

731
### Fixed

Clip Board.xcodeproj/project.pbxproj

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,11 @@
254254
CODE_SIGN_ENTITLEMENTS = "Clip Board/Clip Board.entitlements";
255255
CODE_SIGN_STYLE = Automatic;
256256
COMBINE_HIDPI_IMAGES = YES;
257-
CURRENT_PROJECT_VERSION = 5;
257+
CURRENT_PROJECT_VERSION = 6;
258258
DEVELOPMENT_TEAM = 474XD43PW6;
259-
ENABLE_APP_SANDBOX = YES;
259+
ENABLE_APP_SANDBOX = NO;
260260
ENABLE_HARDENED_RUNTIME = YES;
261261
ENABLE_PREVIEWS = YES;
262-
ENABLE_USER_SELECTED_FILES = readonly;
263262
GENERATE_INFOPLIST_FILE = YES;
264263
INFOPLIST_FILE = "Clip-Board-Info.plist";
265264
INFOPLIST_KEY_CFBundleDisplayName = "Clip Board";
@@ -271,7 +270,7 @@
271270
"@executable_path/../Frameworks",
272271
);
273272
MACOSX_DEPLOYMENT_TARGET = 14;
274-
MARKETING_VERSION = 1.2.2;
273+
MARKETING_VERSION = 1.2.3;
275274
PRODUCT_BUNDLE_IDENTIFIER = Siddharth.Sangwa.ClipBoard;
276275
PRODUCT_NAME = "$(TARGET_NAME)";
277276
REGISTER_APP_GROUPS = YES;
@@ -294,12 +293,11 @@
294293
CODE_SIGN_ENTITLEMENTS = "Clip Board/Clip Board.entitlements";
295294
CODE_SIGN_STYLE = Automatic;
296295
COMBINE_HIDPI_IMAGES = YES;
297-
CURRENT_PROJECT_VERSION = 5;
296+
CURRENT_PROJECT_VERSION = 6;
298297
DEVELOPMENT_TEAM = 474XD43PW6;
299-
ENABLE_APP_SANDBOX = YES;
298+
ENABLE_APP_SANDBOX = NO;
300299
ENABLE_HARDENED_RUNTIME = YES;
301300
ENABLE_PREVIEWS = YES;
302-
ENABLE_USER_SELECTED_FILES = readonly;
303301
GENERATE_INFOPLIST_FILE = YES;
304302
INFOPLIST_FILE = "Clip-Board-Info.plist";
305303
INFOPLIST_KEY_CFBundleDisplayName = "Clip Board";
@@ -311,7 +309,7 @@
311309
"@executable_path/../Frameworks",
312310
);
313311
MACOSX_DEPLOYMENT_TARGET = 14;
314-
MARKETING_VERSION = 1.2.2;
312+
MARKETING_VERSION = 1.2.3;
315313
PRODUCT_BUNDLE_IDENTIFIER = Siddharth.Sangwa.ClipBoard;
316314
PRODUCT_NAME = "$(TARGET_NAME)";
317315
REGISTER_APP_GROUPS = YES;

Clip Board/AppAndCrypto.swift

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,10 @@ final class KeyManager {
192192

193193
/// Centralized on-disk locations, each created with tight permissions once at first use.
194194
///
195-
/// Under the app sandbox, `applicationSupportDirectory` resolves to the container path
196-
/// (`~/Library/Containers/<bundle-id>/Data/Library/Application Support/ClipboardManager`),
197-
/// not the global `~/Library/Application Support`. Either way, it's per-user and
198-
/// inaccessible to other apps without the user explicitly granting access.
195+
/// Resolves to `~/Library/Application Support/ClipboardManager`. The directory is created
196+
/// 0700 on first access so other users on the machine can't read it. (Releases 1.0.0–1.2.2
197+
/// ran sandboxed and stored under `~/Library/Containers/<bundle-id>/Data/...`; 1.2.3+ is
198+
/// unsandboxed and migrates the old container payload via `migrateLegacyContainerIfNeeded()`.)
199199
enum AppPaths {
200200
/// `…/Application Support/ClipboardManager` (0700). Cached — created once.
201201
static let base: URL = {
@@ -214,6 +214,53 @@ enum AppPaths {
214214
return folder
215215
}()
216216

217+
/// One-time migration of pre-1.2.3 sandbox-container history into the unsandboxed
218+
/// Application Support location. Idempotent and safe: only runs when the destination
219+
/// has no encrypted history yet and a legacy container payload exists. Never overwrites
220+
/// newer data; never deletes the source (so a downgrade still finds the old store).
221+
/// Call once at launch, BEFORE the persistence manager loads.
222+
static func migrateLegacyContainerIfNeeded() {
223+
let fm = FileManager.default
224+
let historyName = "history.json.enc"
225+
let destHistory = base.appendingPathComponent(historyName)
226+
if fm.fileExists(atPath: destHistory.path) { return }
227+
228+
// Reconstruct the old sandbox container path manually — it's a regular path in
229+
// the user's home directory; we just hard-code the bundle ID component.
230+
// NOTE: appendingPathComponent percent-encodes embedded slashes, so chain one
231+
// component per call (passing "Library/Containers" as one arg is wrong).
232+
guard let bundleID = Bundle.main.bundleIdentifier else { return }
233+
guard let home = ProcessInfo.processInfo.environment["HOME"].map(URL.init(fileURLWithPath:)) else { return }
234+
let legacy = home
235+
.appendingPathComponent("Library", isDirectory: true)
236+
.appendingPathComponent("Containers", isDirectory: true)
237+
.appendingPathComponent(bundleID, isDirectory: true)
238+
.appendingPathComponent("Data", isDirectory: true)
239+
.appendingPathComponent("Library", isDirectory: true)
240+
.appendingPathComponent("Application Support", isDirectory: true)
241+
.appendingPathComponent("ClipboardManager", isDirectory: true)
242+
let legacyHistory = legacy.appendingPathComponent(historyName)
243+
guard fm.fileExists(atPath: legacyHistory.path) else { return }
244+
245+
Log.app.notice("Migrating legacy sandbox-container history into Application Support.")
246+
do {
247+
try fm.copyItem(at: legacyHistory, to: destHistory)
248+
} catch {
249+
Log.app.error("Legacy history copy failed: \(error.localizedDescription, privacy: .private)")
250+
return
251+
}
252+
// Carry images over too if present.
253+
let legacyImages = legacy.appendingPathComponent("images", isDirectory: true)
254+
if let names = try? fm.contentsOfDirectory(atPath: legacyImages.path) {
255+
for name in names {
256+
let src = legacyImages.appendingPathComponent(name)
257+
let dst = imagesFolder.appendingPathComponent(name)
258+
if fm.fileExists(atPath: dst.path) { continue }
259+
try? fm.copyItem(at: src, to: dst)
260+
}
261+
}
262+
}
263+
217264
private static func ensureDirectory(_ url: URL) {
218265
let fm = FileManager.default
219266
guard !fm.fileExists(atPath: url.path) else { return }
@@ -447,6 +494,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
447494
private var menuBarController: MenuBarController?
448495

449496
func applicationDidFinishLaunching(_ notification: Notification) {
497+
// 0. One-time migration of pre-1.2.3 sandbox-container history into the
498+
// unsandboxed Application Support directory. No-op after first run.
499+
// Runs BEFORE KeyManager: a re-signed binary can trigger a modal Keychain
500+
// re-authorization prompt that blocks ensureKeyExists for the first few
501+
// seconds, and we don't want migration gated on the user dismissing it.
502+
AppPaths.migrateLegacyContainerIfNeeded()
503+
450504
// 1. Encryption key must exist before persistence load.
451505
do { try KeyManager.shared.ensureKeyExists() }
452506
catch { Log.crypto.error("ensureKeyExists failed: \(error.localizedDescription, privacy: .private)") }

Clip Board/Clip Board.entitlements

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<!--
6+
Clip-Board is NOT sandboxed. A sandboxed app cannot use
7+
NSRunningApplication.activate() to bring a foreign app to the foreground,
8+
which makes the "synthesize ⌘V into the previously focused app" auto-paste
9+
flow silently fail. Every shipping clipboard manager (Maccy, Paste, Alfred,
10+
Raycast) runs unsandboxed for the same reason.
11+
12+
What we still do for hardening:
13+
• Hardened runtime ON (see Xcode build settings)
14+
• No network entitlements requested or linked (verify: otool -L)
15+
• History encrypted at rest with AES-GCM-256 (key in Keychain,
16+
WhenUnlockedThisDeviceOnly, non-syncable)
17+
• Files 0600, directory 0700, atomic writes
18+
19+
Reviewers can verify the running binary's entitlements with:
20+
codesign -d --entitlements - "Clip Board.app"
21+
-->
522
<key>com.apple.security.app-sandbox</key>
6-
<true/>
23+
<false/>
724
<key>com.apple.security.network.client</key>
825
<false/>
926
<key>com.apple.security.network.server</key>
@@ -18,7 +35,5 @@
1835
<false/>
1936
<key>com.apple.security.personal-information.calendars</key>
2037
<false/>
21-
<key>com.apple.security.files.user-selected.read-only</key>
22-
<true/>
2338
</dict>
2439
</plist>

Clip Board/History.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,11 +460,20 @@ final class AutoPaster {
460460
return
461461
}
462462
Log.autopaste.info("Activating target: \(app.localizedName ?? "?", privacy: .public) (pid=\(app.processIdentifier))")
463-
app.activate(options: [])
463+
464+
// macOS 14+ requires the currently-active app to explicitly yield activation
465+
// rights before another app can take focus. Without this, our LSUIElement
466+
// retains "active app" status after the panel orders out, and the synthesized
467+
// ⌘V either lands on us or nowhere. yieldActivation(to:) is the documented
468+
// replacement for the legacy `.activateIgnoringOtherApps` option.
469+
if #available(macOS 14.0, *) {
470+
NSApp.yieldActivation(to: app)
471+
}
472+
app.activate()
464473

465474
// Wait for the target app to actually be frontmost before injecting the keystroke.
466-
// 500ms total budget, polled every 20ms.
467-
let deadline = Date().addingTimeInterval(0.5)
475+
// 600ms total budget, polled every 20ms.
476+
let deadline = Date().addingTimeInterval(0.6)
468477
waitUntilFrontmost(app: app, deadline: deadline)
469478
}
470479

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,12 @@ Two verification commands you can run on any downloaded release:
142142
# 1. Linked libraries — should contain only Apple system frameworks.
143143
otool -L "Clip Board.app/Contents/MacOS/Clip Board"
144144

145-
# 2. Sandbox entitlements — should show network.client and network.server as <false/>.
145+
# 2. Entitlements — should show network.client and network.server as <false/>,
146+
# and app-sandbox as <false/> (see SECURITY.md for why we're unsandboxed).
146147
codesign -d --entitlements - "Clip Board.app"
147148
```
148149

149-
The entitlements file is checked into the repo at [`Clip Board/Clip Board.entitlements`](Clip%20Board/Clip%20Board.entitlements) — the source of truth a reviewer can diff against the signed binary.
150+
The entitlements file is checked into the repo at [`Clip Board/Clip Board.entitlements`](Clip%20Board/Clip%20Board.entitlements) — the source of truth a reviewer can diff against the signed binary. **Note:** Clip-Board is not sandboxed, because the macOS App Sandbox blocks `NSRunningApplication.activate()` on a foreign app — which silently breaks auto-paste. Every shipping clipboard manager (Maccy, Paste, Alfred, Raycast) runs unsandboxed for the same reason. See [SECURITY.md](SECURITY.md#sandbox--entitlements) for the full rationale and the hardening we apply in lieu of the sandbox (hardened runtime, no network linkage, AES-GCM at rest, Keychain key).
150151

151152
### Auto-paste and the Accessibility permission
152153

@@ -182,7 +183,7 @@ The floating panel and the menu-bar window share the same SwiftUI root (`SharedH
182183
1. You copy text → macOS pasteboard updates → `changeCount` increments.
183184
2. `ClipboardWatcher` polls every 500 ms, detects the change, **skips transient/concealed types**.
184185
3. Trimmed text → `ItemsViewModel.addItem` → dedupe (move-to-top on exact match) or insert.
185-
4. After a 300 ms debounce, the items array is snapshotted on the main thread, then handed to the IO queue: **encode → AES-GCM encrypt → atomic write** to the app's sandboxed Application Support directory (`~/Library/Containers/Siddharth.Sangwa.ClipBoard/Data/Library/Application Support/ClipboardManager/history.json.enc`sandboxed apps can't reach the global `~/Library/Application Support`), file mode `0600`, directory mode `0700`.
186+
4. After a 300 ms debounce, the items array is snapshotted on the main thread, then handed to the IO queue: **encode → AES-GCM encrypt → atomic write** to `~/Library/Application Support/ClipboardManager/history.json.enc`, file mode `0600`, directory mode `0700`. (Releases ≤ 1.2.2 ran sandboxed and stored under `~/Library/Containers/Siddharth.Sangwa.ClipBoard/Data/Library/Application Support/ClipboardManager/`1.2.3 migrates that payload on first launch.)
186187
5. On launch: load file → decrypt → decode. If anything fails, **quarantine** to `history.broken-<timestamp>` and start fresh.
187188

188189
History is wrapped as `{"version": 1, "items": [...]}` for forward compatibility. The legacy unversioned format is migrated transparently.

SECURITY.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,22 @@ When you select an item, Clip-Board writes it to `NSPasteboard.general` (the sys
5959

6060
## Sandbox & entitlements
6161

62-
The app is sandboxed (`com.apple.security.app-sandbox = true`) with **network entitlements explicitly disabled** (`com.apple.security.network.client = false`, `com.apple.security.network.server = false`). The entitlements file is checked into the repo at [`Clip Board/Clip Board.entitlements`](Clip%20Board/Clip%20Board.entitlements); reviewers can verify the signed binary against it with:
63-
64-
```bash
65-
codesign -d --entitlements - "Clip Board.app"
66-
```
62+
**Clip-Board is not sandboxed**, and this is intentional. The macOS App Sandbox blocks `NSRunningApplication.activate()` on a foreign app, which makes "bring your last-focused app back to front, then synthesize ⌘V into it" silently fail. Every shipping clipboard manager that does cross-app paste injection (Maccy, Paste, Alfred, Raycast) runs unsandboxed for the same reason. Releases prior to 1.2.3 shipped with `app-sandbox = true` set in the project; that was a packaging carry-over from the Xcode template, not a security decision, and it broke auto-paste. As of 1.2.3 the entitlements file explicitly sets it to `false` with an in-file comment explaining why.
63+
64+
What we still do for hardening, in lieu of the sandbox:
65+
66+
- **Hardened runtime ON** (`ENABLE_HARDENED_RUNTIME = YES`) — Library Validation is enforced.
67+
- **No network entitlements requested or used.** The binary does not link `Network.framework`, `CFNetwork` (except via system Foundation, which is unavoidable but unused for outbound), or third-party HTTP libraries. Verify yourself:
68+
```bash
69+
otool -L "Clip Board.app/Contents/MacOS/Clip Board"
70+
```
71+
Only Apple system frameworks should be listed.
72+
- **Network client/server entitlements explicitly `false`** in the checked-in entitlements file at [`Clip Board/Clip Board.entitlements`](Clip%20Board/Clip%20Board.entitlements). These are defaults, but stating them explicitly means a reviewer can diff source vs. signed binary and confirm intent. Verify the signed binary's entitlements with:
73+
```bash
74+
codesign -d --entitlements - "Clip Board.app"
75+
```
76+
- History encrypted at rest with **AES-GCM-256**, key in **Keychain** (`WhenUnlockedThisDeviceOnly`, non-syncable). See the *Cryptographic details* section above.
77+
- History file mode `0600`, directory mode `0700`, **atomic writes** (no partial-file exposure on crash).
78+
- History storage lives under `~/Library/Application Support/ClipboardManager/` (the conventional unsandboxed path; previously inside the sandbox container, now plain Application Support).
6779

6880
If you have suggestions for hardening any of the above — particularly key rotation, in-memory protection, or schema integrity — open an issue (for design discussions) or follow the reporting process (for vulnerabilities).

0 commit comments

Comments
 (0)