A native desktop app that quietly cleans copied text and links using rules you control
3.7 kB · 163 lines
Markdown
at main

Trusted shell rules #

Shell rules execute native processes with the user's authority. They are disabled by default, unavailable in portable browser/WASM hosts, and should be used only for code you trust.

Enable them explicitly:

config:
  shell:
    enabled: true

Shell #

shell exchanges the selected UTF-8 value through stdin/stdout:

rules:
  - type: shell
    id: uppercase
    shell: bash
    timeout: 5s
    run: |
      set -euo pipefail
      tr '[:lower:]' '[:upper:]'

Exactly one of run or script_path is required. Relative script paths are resolved from the config or imported file declaring the rule.

Exit codes:

Code Result
0 Replace with UTF-8 stdout. Empty output is valid.
3 No match.
other Keep the clipboard unchanged and report a rule error.

The process is non-interactive and receives CT_CONFIG_DIR, CT_STATE_DIR, CT_CACHE_DIR, CT_RULE_ID, CT_INPUT_FORMAT, and best-effort source-app metadata. Rule-local env values may add variables but cannot override reserved CT_* or PWD.

Item shell #

item-shell is the advanced full-item form. Instead of receiving only selected text, it exchanges the complete clipboard item through JSON files named by CT_INPUT_ITEM and CT_OUTPUT_ITEM. Output actions are:

  • no-match;
  • replace-text;
  • replace-item.

Use formats: ["*"] only when the script genuinely needs every native representation; it increases clipboard reads and may expose sensitive payloads to the script.

Input item #

The input is a versioned exchange document rather than the application's private persisted shape:

{
  "version": 1,
  "platform": "macos",
  "source_app": {
    "bundle_id": "com.apple.Safari",
    "name": "Safari"
  },
  "semantics": {
    "text": {
      "value": "https://example.com/",
      "authored": false,
      "derived_from": ["public.utf8-plain-text"]
    }
  },
  "representations": [
    {
      "kind": "public.utf8-plain-text",
      "encoding": "utf8",
      "data": "https://example.com/"
    },
    {
      "kind": "public.png",
      "encoding": "file",
      "path": "/absolute/temp/path/input/representations/001.bin"
    }
  ]
}

Portable semantics are JSON strings. Valid UTF-8 native bytes use encoding: "utf8"; non-UTF-8 input uses an absolute temporary file path.

Output actions #

No match:

{ "action": "no-match" }

Portable text replacement:

{
  "action": "replace-text",
  "text": "new text",
  "message": "Optional notification"
}

Atomic full-item replacement:

{
  "action": "replace-item",
  "item": {
    "version": 1,
    "platform": "portable",
    "semantics": {
      "text": {
        "value": "new text",
        "authored": true
      },
      "html": {
        "value": "<strong>new text</strong>",
        "authored": true
      }
    },
    "representations": []
  }
}

Output native representations may use utf8, file, or base64. Referenced files must be regular non-symlink files inside the invocation's output directory. The host rejects traversal, symlink escapes, outside paths, and oversized payloads, then removes the temporary exchange directory.

Imported shell rules #

Local imported shell rules are allowed by default once shell support is enabled. Remote imports are denied unless all of these are explicit:

config:
  shell:
    enabled: true
    remote_imports: true

rules:
  - import:
      source: https://example.com/trusted-rules.yaml
      permissions:
        shell: true
      sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

The digest pins the downloaded bytes. A mismatch fails closed.