Runtimes & APIs

Shell & Python Scripts

Subprocess execution with 30-second watchdog, environment variable inputs, and structured JSON outputs.

On this page (9 sections)

The executable script action runtime (ScriptAction) allows OpenClip to execute external standalone script files (.sh, .zsh, .py, .rb, .node) located within extension packages or ~/.openclip/extensions/.


Process Execution Lifecycle#

Selection Context ---> Process Instance ---> Inject Environment & write stdin
 |
 v
 Script Execution (Zsh/Python)
 |
 v
 Read stdout & JSON / Text Output
 |
 v
 ActionResult (.paste / .copy / .openURL)
  1. Executable Check: ScriptAction checks FileManager.default.isExecutableFile(atPath:).
  2. Environment Injection:
  • OPENCLIP_TEXT: Selected text string.
  • OPENCLIP_MATCHED, OPENCLIP_CAPTURE_N, OPENCLIP_BUNDLE_ID, OPENCLIP_ACTION_ID (see the matrix below).
  1. Pipes Setup: Sets up standardInput, standardOutput, and standardError using Pipe().
  2. Standard Input Delivery: Selected text is written to stdin asynchronously.
  3. Timeout Watchdog (opt-in): A GCD timer terminates the process once the invocation's explicit timeout elapses. With no timeout (the default for loading tasks) there is no watchdog — the process runs until it exits, and users can abort it anytime by clicking the loading toast. When armed, the runner signals the direct child, the process group when the child is the leader, and remaining descendants, then sends SIGKILL after a short delay if the process identifier is unchanged. Descendants come from a proc_listchildpids walk of the subtree, snapshotted before the child is terminated; a probe that reports no children is still read once, so a spurious zero cannot drop a descendant. Any new action that spawns a subprocess must route through the same runner.
  4. Process Exit Evaluation: Ensures termination status is 0. Non-zero exit status throws NSError containing stderr text.

Environment Variables Matrix#

Environment VariableDescription
OPENCLIP_TEXTFull text selected by the user.
OPENCLIP_HTMLHTML markup of the selection (if captured).
OPENCLIP_RTFRTF markup of the selection (if captured).
OPENCLIP_MATCHEDText matched by the action's regex (falls back to the full selection).
OPENCLIP_CAPTURE_NRegex capture group N (1-based), one per group.
OPENCLIP_BUNDLE_IDBundle identifier of the frontmost/source app.
OPENCLIP_ACTION_IDThe action's identifier.
OPENCLIP_OPTION_<IDENTIFIER>Resolved configuration option value; uppercase the identifier and replace hyphens with underscores.

The selected text is also written to the subprocess's stdin. File-backed shell/script actions expose manifest-level and per-action options in Preferences → Extensions → the action. Values are resolved for the action ID on every invocation through the injected option store, including secret options. Unset values use the declared default, or an empty string when absent. For example, api-key becomes OPENCLIP_OPTION_API_KEY; read it as "$OPENCLIP_OPTION_API_KEY" in shell or os.environ["OPENCLIP_OPTION_API_KEY"] in Python. Boolean values are strings ("true" / "false"). Choose identifiers whose normalized names are unique. Option values are passed as environment data, without shell interpolation.

Output Processing: JSON vs Plain Text#

ScriptAction evaluates output from stdout via ShellResultMapper using a dual-mode parser:

Mode 1: Structured JSON Output (ScriptJSONOutput)#

Scripts can return a JSON payload to specify explicit platform actions:

json
{
  "type": "paste",
  "value": "Transformed text output"
}

Supported type values:

  • "paste" → ActionResult.paste(value) (replaces selection in target application).
  • "copy" → ActionResult.copy(value) (copies text to system clipboard).
  • "pasteContent" / "paste-content" → ActionResult.pasteContent (rich multi-type paste; fields: value = plain text, html, rtf).
  • "copyContent" / "copy-content" → ActionResult.copyContent (rich multi-type copy; same fields as "pasteContent").
  • "cut" → ActionResult.cut(value) (copies text and sends backspace delete event).
  • "openURL" / "url" → ActionResult.openURL(URL) (opens URL in default browser; URL parsed from value).
  • "keyPress" / "keypress" → ActionResult.keyPress(KeyPressSpec) (key, modifiers: ["command", "shift", ...]).
  • "runShortcut" / "shortcut" → ActionResult.runShortcut(name:input:) (name/shortcutName, input/value).
  • "notify" / "notification" → ActionResult.notify(title:body:) (title, body/message).
  • "shareService" / "share" → ActionResult.shareService(identifier:text:) (identifier required, value/input).
  • "sequence" → ActionResult.sequence([ActionResult]) (actions: [ScriptJSONOutput]).
  • "fail" / "failure" / "error" → surfaces error toast (message/reason/value).
  • "toast" → ActionResult.toast (message, style: "success"/"error"/"info", keepVisible optional, default false).
  • "configure" → ActionResult.openConfiguration (reason, missing: [optionID]).
  • "file" → ActionResult.file(FileOutputPayload) (renders native file preview card; fields: path to existing file or data containing base64 data safely saved to ~/.openclip/cache/outputs/, optional filename, mimeType, and optional action: "copy"/"save" to bypass preview).
  • "copyFile" / "copy-file" → ActionResult.copyFile(URL) (copies file at path to pasteboard).
  • "saveFile" / "save-file" → ActionResult.saveFile(URL) (saves file at path to user's configured save location).

"showContent" is not accepted — a decoded-but-unknown type maps to .success.

Mode 2: Plain Text Output Fallback#

If stdout contains non-JSON plain text:

  1. File Detection: If the action does not replace selection (replaceSelection: false), OpenClip checks if the trimmed output corresponds to an existing regular file path or file:// URL on disk. If so, it returns ActionResult.file(FileOutputPayload) to render the native file card.
  2. Text Fallback: Otherwise, ScriptAction treats the raw output as implicitly returned text and returns ActionResult.text(stdoutString) — delivered per the user's per-click preference (preview/paste/copy).

Practical Examples#

Python Script Generating a File (generate_chart.py)#

python
#!/usr/bin/env python3
import sys, json

out_path = "/tmp/chart.png"
# ... create chart file at out_path ...

# Either output the path directly on stdout for auto-detection:
# sys.stdout.write(out_path)

# Or emit explicit JSON:
print(json.dumps({
    "type": "file",
    "path": out_path,
    "filename": "chart.png",
    "mimeType": "image/png"
}))

Python Script Example (clean_markdown.py)#

python
#!/usr/bin/env python3
import sys, os, re

# Read input from stdin or environment
text = sys.stdin.read() or os.environ.get("OPENCLIP_TEXT", "")

# Remove markdown links, leaving plain text
clean_text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)

# Print result to stdout
sys.stdout.write(clean_text)

Zsh Shell Script Example (uppercase.sh)#

bash
#!/bin/zsh
echo "$OPENCLIP_TEXT" | tr '[:lower:]' '[:upper:]'