app.keychain - Secure Credential Storage API

Securely store sensitive data (API keys, tokens, passwords, etc.) using macOS Keychain.

Permission: Requires keychain declared in the manifest (L1 standard) Isolation: Each plugin’s credentials are isolated from each other; plugins cannot access other plugins’ credentials.

Methods

app.keychain.set(key, value)

Store a credential. Updates if the key already exists.

Parameters:

  • key (string) - Credential name
  • value (string) - Credential value

Returns: boolean, error

local ok, err = app.keychain.set("api_key", "sk-xxxxxxxxxxxx")
if not ok then
    app.log.error("Storage failed: " .. err)
end

app.keychain.get(key)

Retrieve a credential.

Parameters:

  • key (string) - Credential name

Returns: string|nil, error - Credential value or nil (returns nil without error when not found)

local token, err = app.keychain.get("api_key")
if token then
    app.log.info("Token length: " .. #token)
elseif err then
    app.log.error("Read failed: " .. err)
else
    app.log.info("Credential does not exist")
end

app.keychain.delete(key)

Delete a credential. Deleting a non-existent key also returns success.

Parameters:

  • key (string) - Credential name

Returns: boolean, error

local ok, err = app.keychain.delete("api_key")

app.keychain.has(key)

Check if a credential exists.

Parameters:

  • key (string) - Credential name

Returns: boolean

if app.keychain.has("api_key") then
    local key = app.keychain.get("api_key")
    -- Use key...
end

Examples

API Key Management

function MyPlugin:handleSetup(context)
    -- Check if already configured
    if app.keychain.has("github_token") then
        local result = app.dialog.alert({
            title = "Already Configured",
            message = "GitHub Token already exists. Update it?",
            buttons = {"Update", "Cancel"}
        })
        if result ~= 1 then return end
    end

    -- Collect credentials via form
    local result = app.dialog.form({
        title = "Configure GitHub Token",
        fields = {
            {type = "text", id = "token", label = "Personal Access Token", default = ""}
        }
    })

    if result and result.token ~= "" then
        local ok, err = app.keychain.set("github_token", result.token)
        if ok then
            app.notification.show("Configuration Successful", "GitHub Token has been securely stored")
        else
            app.dialog.alert({title = "Error", message = "Storage failed: " .. err})
        end
    end
end

function MyPlugin:handleFetch(context)
    local token = app.keychain.get("github_token")
    if not token then
        app.dialog.alert({title = "Not Configured", message = "Please run 'Configure Token' first"})
        return
    end

    local resp = app.http.get("https://api.github.com/user", {
        ["Authorization"] = "Bearer " .. token,
        ["Accept"] = "application/vnd.github.v3+json"
    })

    if resp and resp.status == 200 then
        local user = app.json.parse(resp.body)
        app.notification.show("GitHub", "Logged in as: " .. user.login)
    end
end

Multi-Credential Management

function MyPlugin:handleManageCredentials(context)
    local keys = {"api_key", "secret", "webhook_url"}
    local items = {}

    for _, key in ipairs(keys) do
        local exists = app.keychain.has(key)
        table.insert(items, key .. ": " .. (exists and "Configured" or "Not configured"))
    end

    local selected = app.dialog.choose(items, {
        title = "Credential Management",
        message = "Select a credential to manage"
    })

    if selected then
        local key = keys[1]  -- Determine key from selected item
        for i, item in ipairs(items) do
            if item == selected then key = keys[i] end
        end

        local result = app.dialog.alert({
            title = key,
            message = app.keychain.has(key) and "Currently configured" or "Currently not configured",
            buttons = {"Set", "Delete", "Cancel"}
        })

        if result == 1 then
            local input = app.dialog.form({
                title = "Set " .. key,
                fields = {{type = "text", id = "value", label = "Value"}}
            })
            if input then
                app.keychain.set(key, input.value)
            end
        elseif result == 2 then
            app.keychain.delete(key)
            app.notification.show("Deleted", key)
        end
    end
end

Notes

  1. Plugin Isolation: Each plugin’s credentials are isolated; different plugins cannot read each other’s credentials
  2. Storage Security: Uses macOS Keychain (kSecClassGenericPassword) with encrypted storage
  3. String Only: value must be a string type; serialize complex data to JSON first if needed
  4. Persistence: Credentials are stored in the system Keychain and are not automatically removed when a plugin is uninstalled
  5. Access Level: Uses kSecAttrAccessibleAfterFirstUnlock, accessible after the device is first unlocked
Developer Documentation
User Guide
Getting Started Script Menus FAQ
Script Development
Development Guide
Plugin Development
Quick Start Development Guide Example Plugins
API Reference
Overview API Query Plugin Info Logging Finder Context Plugin Settings Internationalization
UI & Interaction
Dialog Progress Notification Chooser WebView Status Bar Dock
Files & Paths
File Operations Path Utilities Finder Actions Trash Extended Attributes Metadata File Watcher
Data Formats
JSON Plist CSV XML PDF Image
Text & Encoding
String Regex Date & Time Color Crypto
System
Shell Commands Process Application System Info AppleScript Shortcuts
System Info
Network Power/Battery Screen/Appearance Audio Bluetooth Location
Network
HTTP WebSocket URL
Input & Clipboard
Keyboard Mouse Hotkey Clipboard Window
Storage
SQLite Keychain UserDefaults
Media
OCR QR Code
Utilities
Archive UTI Share Timer Wake Lock Thread