Runtimes & APIs

Integration & Automation API

The inbound openclip:// contract: routes, curated settings, callbacks, and what is not yet exposed.

On this page (13 sections)

Create Custom Actions

Add search engines or inline scripts in seconds directly inside OpenClip.

OpenClip exposes an inbound openclip:// URL scheme so other apps and scripts can drive a small, curated part of its configuration. This document is the current-state contract: what exists today, exactly as implemented. It is intentionally not a roadmap — anything not listed under "Available now" does not exist.

Status: first iteration. The surface is deliberately small (power, appearance, AI on/off). There is no discovery endpoint and no CLI yet; see Not available yet.


Availability#

The settings, command, and write routes are always available.

The install route is the existing extension-store flow and keeps its own source allow-list and confirmation dialog.


Routes#

URLPurpose
openclip://install?id=<id>&url=<https-url>[&name=<name>]Install a store extension (allow-listed + dialog)
openclip://settings[?callback=<url>]Read the curated settings
openclip://set?<key>=<value>[&…][&callback=<url>]Write curated settings
openclip://command/<name>[?callback=<url>]Run an app-level command

The scheme is openclip (registered in Info.plist). The host selects the route; matching is case-insensitive. Routing and parsing live in Sources/Core/Integration/OpenClipDeepLink.swift; the app performs the effects in Sources/OpenClip/Platform/DeepLinkRouter.swift.

openclip://install#

openclip://install?id=com.example.myext&name=My%20Extension&url=https%3A%2F%2Fopenclip.app%2Fmyext.zip

Downloads and installs a signed extension package. The download host must be on the allow-list:

github.com, release-assets.githubusercontent.com, objects.githubusercontent.com,
raw.githubusercontent.com, getopenclip.vercel.app, getopenclip.app,
www.getopenclip.app, openclip.app

A confirmation dialog is shown before installation. Malformed URLs (missing id/url) are ignored.

openclip://settings (read)#

Returns the current value of every curated key.

openclip://settings?callback=myapp%3A%2F%2Fopenclip%2Freply

The reply is delivered by opening the callback URL with an added result query item carrying a JSON object keyed by setting name:

myapp://openclip/reply?result={"isAppEnabled":true,"popupTheme":"glass","popupScale":3,...}

If no callback is supplied the route still runs but has no reply target; there is currently no other read channel.

openclip://set (write)#

Sets one or more curated keys in a single request.

openclip://set?popupTheme=glass&popupScale=3&isAppEnabled=false

Every query item other than callback/x-success is treated as name=value. The reply:

myapp://openclip/reply?result={"ok":true,"applied":3,"skipped":[]}
  • ok — true when nothing was skipped.
  • applied — how many writes succeeded.
  • skipped — names that are unknown to OpenClip or whose value did not decode to the key's type. A skipped name is left untouched (a rejected write never clobbers the existing value).

openclip://command/<name> (commands)#

App-level verbs that are not a single settings write.

openclip://command/open-settings
openclip://command/pause
openclip://command/resume
openclip://command/reset-appearance
CommandEffect
open-settingsBring OpenClip's Settings window to the front
pausePause the popup for one hour (same as the menu bar's Pause)
resumeClear a temporary pause
reset-appearanceRestore the popup appearance keys to their defaults

Reply: {"ok":true,"command":"<name>"}.


Value encoding#

Query values may be either a JSON fragment or a bare token:

  • isAppEnabled=false → JSON boolean false
  • popupScale=3 → JSON number 3
  • popupTheme=glass → JSON string "glass" (bare token, JSON-encoded for you)
  • popupTheme="glass" → JSON string "glass" (already-encoded fragment, used as-is)

Rule: if the value parses as JSON (including true, 3, "x", arrays, objects, null), it is used as-is; otherwise it is JSON-encoded as a string. On read, values come back as plain JSON, so popupTheme reads as "glass" and popupScale as 3 — never wrapped.


Callbacks#

Reads and command/write results are returned by opening a caller-supplied URL (the x-callback-url pattern).

  • Parameter: callback — x-success is accepted as an alias.
  • Reply query item: result (JSON).
  • Only non-web custom schemes are accepted. http, https, file, javascript, data, about, blob, ws, wss, and ftp callbacks are dropped, so a web page cannot use the read route to collect settings. The reply is delivered with NSWorkspace.open.

Curated settings#

The allow-list is defined in Sources/OpenClip/Settings/IntegrationSettings.swift. Only these keys are readable or writable; anything else is reported in skipped.

KeyTypeDefaultValues
isAppEnabledBooltrue"Appear Automatically"
isMouseHoldEnabledBooltrue
showMenuBarIconBooltrue
startAtLoginBoolfalse
isAIEnabledBooltrueAI on/off only — see below
popupThemeString"classic"classic | glass
popupThemeColorString"system"system | light | dark
popupAlignmentString"left"left | center | right
popupVerticalPositionString"auto"auto | above | below
popupScaleInt31–5
popupBarWidthInt31–5

Deliberately never exposed#

  • AI provider configuration — service, endpoint, model, and especially the API key (stored in ~/.openclip/secrets.json via SecretStore, never UserDefaults). Only the isAIEnabled on/off switch is reachable.
  • Extension store / arbitrary installs — only the allow-listed install route exists.
  • Update channel, hotkeys, Action Results, per-action enable toggles.

Change broadcast#

After every successful write or command, OpenClip posts a distributed notification so a client can refresh its mirror without polling:

  • Name: com.openclip.integration.settingsDidChange
  • userInfo["keys"]: sorted array of setting names that changed (empty for commands that changed none)
swift
DistributedNotificationCenter.default().addObserver(
    forName: NSNotification.Name("com.openclip.integration.settingsDidChange"),
    object: nil, queue: .main
) { note in
    let names = note.userInfo?["keys"] as? [String] ?? []
}

Not available yet#

So this is a settings surface, not a general automation API. The following do not exist and are candidates for a later iteration:

  • Discovery / capabilities endpoint (openclip://capabilities) — a self-describing JSON schema of settable keys, types, and available commands, so a client can render the surface without hardcoding this table.
  • CLI (openclip get|set|capabilities, or extending the existing --dump-settings / --dump-logs modes) — synchronous reads on stdout with exit codes.
  • Running actions (openclip://run?action=<id>&text=…) — headless invocation of any action with a returned result. This is the largest missing piece and the basis for real text automation.
  • App Intents / Shortcuts integration.
  • Per-action enable toggles and Action Results keys (a set-membership write, not a single key).

Implementation map#

ConcernLocation
URL grammar + reply constructionSources/Core/Integration/OpenClipDeepLink.swift
Curated read/write + value normalizationSources/Core/Integration/SettingsBridge.swift
Dispatch, broadcast, installSources/OpenClip/Platform/DeepLinkRouter.swift
Allow-list + side effectsSources/OpenClip/Settings/IntegrationSettings.swift
Entry pointAppDelegate.application(_:open:) → DeepLinkRouter.shared.handle(_:)
TestsTests/OpenClipTests/DeepLinkTests.swift, IntegrationSettingsBridgeTests.swift