Permissions
The host API gives a plugin nothing by default. Its manifest lists the permissions it needs; Settings ▸ Plugins shows them, with an explanation on hover, and enabling the plugin is the user's consent to exactly that list. Every API call checks the list first and throws PermissionDenied otherwise. The consent is kept in a file beside the Plugins folder that no plugin can write; a plugin disabled after errors loses its consent and runs again only when the user enables it.
| Permission | What it grants |
|---|---|
selection.read | The selected paths and names in the command context. Implied for commands with minSelection of 1 or more; needed explicitly otherwise. |
process.spawn:<tool> | Running exactly that tool, by name, from /usr/bin, /bin, /usr/sbin, /sbin, /usr/local/bin or /opt/homebrew/bin, in that order; never from PATH. One entry per tool. Settings marks tools that run any program (sh, python3, osascript, find…), so the consent says what it really grants. |
file.readText | Reading UTF-8 text files within the current selection, up to 1 MB each. |
openURL | Handing an http, https or mailto URL of at most 2 048 characters to the default browser or mail app. There is no fetch; the cap keeps a plugin without network from carrying a file out as query parameters. |
app.open:<bundle id> | ocelot.openInApp with that app: opens the shown folder or a selected folder in it. Folders only, never files or links, since a file handed to Terminal would be run. |
network.client | Outbound network connections from the processes the plugin runs: its process-plugin worker, or the tools a script spawns. Without it every socket is denied, Unix-domain sockets included. Listening is never allowed. |
ocelot.storage needs no permission: it is the plugin's own data.
The sandbox
Nothing a plugin causes runs unconfined. A process plugin's worker and every tool a script spawns start under a sandbox profile written for that plugin; whatever they start inherits it. If the sandbox launcher is missing, the process does not run at all.
The sandbox limits writes, the network and system services. Reading stays open apart from the Read row below, and the permissions above do not narrow it: a process plugin can read what the app can without asking for anything.
| Allowed | Denied | |
|---|---|---|
| Write | $OCELOT_PLUGIN_DATA (the plugin's persistent folder) and $TMPDIR (a folder of its own under the user's temporary folder); /dev/null | everything else: the plugin's own code folder, /tmp, the user's temporary folder around $TMPDIR |
| Read | everything the app itself may read | other plugins' folders; ~/.ssh, ~/.gnupg, ~/.aws, ~/.docker, ~/.kube, ~/.netrc; keychains, cookies, mail, messages and browser profiles under ~/Library |
| Network | outbound connections with network.client | every socket without it; listening always |
| Services | writing preferences, the pasteboard, the keychain service, Apple Events, launchd, privacy prompts in the app's name, opening other apps through Launch Services, sysctl writes | |
| Processes | signals to itself and its own children | signals to any other process |
Rules are by service, never by executable path: a tool copied into the data folder hits the same wall. A plugin uses the folder access the app already has (Desktop, Documents, Downloads once granted) but can never raise a privacy prompt; a folder the app was not granted is refused, not asked for.
Consequences worth knowing before you choose a tool: a program that writes through /tmp regardless of $TMPDIR fails to write (shell here-documents do that); perl cannot start under the profile; sips saves through the user's temporary folder and fails. Convert images in a process plugin of your own instead.
The Mac App Store edition, when it ships, is sandboxed itself and runs no process plugins and no tools.
Choosing permissions
Ask for what the plugin uses and nothing more; the consent screen is where users decide whether to trust it. A tool that runs any program (sh, python3) is a broad grant and is marked as such. If a command needs the selection only sometimes, it still needs selection.read.