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)- Executable Check:
ScriptActionchecksFileManager.default.isExecutableFile(atPath:). - Environment Injection:
OPENCLIP_TEXT: Selected text string.OPENCLIP_MATCHED,OPENCLIP_CAPTURE_N,OPENCLIP_BUNDLE_ID,OPENCLIP_ACTION_ID(see the matrix below).
- Pipes Setup: Sets up
standardInput,standardOutput, andstandardErrorusingPipe(). - Standard Input Delivery: Selected text is written to
stdinasynchronously. - Timeout Watchdog (opt-in): A GCD timer terminates the process once the invocation's explicit
timeoutelapses. 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 aproc_listchildpidswalk 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. - Process Exit Evaluation: Ensures termination status is
0. Non-zero exit status throwsNSErrorcontainingstderrtext.
Environment Variables Matrix#
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:
{
"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 fromvalue)."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:)(identifierrequired,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",keepVisibleoptional, defaultfalse)."configure"→ActionResult.openConfiguration(reason,missing: [optionID])."file"→ActionResult.file(FileOutputPayload)(renders native file preview card; fields:pathto existing file ordatacontaining base64 data safely saved to~/.openclip/cache/outputs/, optionalfilename,mimeType, and optionalaction:"copy"/"save"to bypass preview)."copyFile"/"copy-file"→ActionResult.copyFile(URL)(copies file atpathto pasteboard)."saveFile"/"save-file"→ActionResult.saveFile(URL)(saves file atpathto 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:
- File Detection: If the action does not replace selection (
replaceSelection: false), OpenClip checks if the trimmed output corresponds to an existing regular file path orfile://URL on disk. If so, it returnsActionResult.file(FileOutputPayload)to render the native file card. - Text Fallback: Otherwise,
ScriptActiontreats the raw output as implicitly returned text and returnsActionResult.text(stdoutString)— delivered per the user's per-click preference (preview/paste/copy).
Practical Examples#
Python Script Generating a File (generate_chart.py)#
#!/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)#
#!/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)#
#!/bin/zsh
echo "$OPENCLIP_TEXT" | tr '[:lower:]' '[:upper:]'