Source plugins

Install from GitLab, Gitea or an internal artifact server. A plugin is one executable that answers in JSON — the whole protocol, with a working example.

On this page

GitHub is built in. Anything else — GitLab, Gitea, an internal artifact server — can be added as a plugin, without recompiling ketch and without a ketch release.

A plugin is an executable named ketch-source-<scheme>. ketch runs it with a subcommand and reads one JSON document from its stdout. That is the whole contract, so a plugin can be written in any language.

Discovery

ketch looks in ~/.ketch/plugins first, then every directory on PATH. The first ketch-source-<name> found wins, so a copy in the plugins directory shadows one on PATH. ketch plugin list shows what was found and ketch plugin dir prints the directory.

Every discovered plugin is asked for its capabilities on every ketch command, so capabilities must be fast and must not touch the network.

Subcommands

capabilities              -> {"protocol":1,"scheme":"gitlab","download":false,"search":true}
describe <id>             -> a SourceInfo object, or null
releases <id> [--limit N] [--prerelease]
                          -> [ Release, ... ]
search <query> --limit N  -> [ SourceInfo, ... ]
download <url> <dest>     -> only when capabilities.download is true

KETCH_PROTOCOL_VERSION is set in the environment for each call.

Exit 0 with the JSON document on stdout. Anything else is a failure, and whatever the plugin wrote to stderr is shown to the user.

capabilities

{ "protocol": 1, "scheme": "gitlab", "download": false, "search": true }
FieldRequiredMeaning
protocolyesMust equal ketch’s PROTOCOL_VERSION, currently 1. A mismatch is reported and the plugin is ignored.
schemeyesThe prefix users type: gitlab:group/project. ASCII letters, digits, - and _ only.
downloadnotrue if the plugin fetches assets itself.
searchnotrue if search is implemented.

releases

Newest first. Drafts must be excluded and prereleases included only when --prerelease is passed — ketch re-applies both filters anyway, so a plugin that ignores its flags cannot change what gets installed.

[
  {
    "version": "1.2.3",
    "tag": "v1.2.3",
    "prerelease": false,
    "draft": false,
    "published_at": "2026-01-15T10:00:00Z",
    "notes": "release notes",
    "assets": [
      {
        "name": "tool-1.2.3-aarch64-apple-darwin.tar.gz",
        "url": "https://example.invalid/download/tool.tar.gz",
        "size": 2400000,
        "content_type": "application/gzip",
        "digest": { "algo": "sha256", "hex": "e3b0c442..." },
        "headers": { "PRIVATE-TOKEN": "..." }
      }
    ]
  }
]

Only version, tag and each asset’s name and url are required; every other field defaults.

Asset names matter more than anything else here: ketch picks which asset to install by scoring the name against the host platform. Report the names the project actually publishes.

digest is what lets ketch verify the download without extra requests. Supply it whenever the source knows it. Only sha256 is compared directly against the download; other algo values are ignored and ketch falls back to checksum sidecars or aggregate files on the release.

Both return SourceInfo:

{
  "id": "group/project",
  "name": "project",
  "description": "one line",
  "homepage": "https://example.invalid",
  "stars": 1200,
  "license": "MIT",
  "archived": false
}

describe returns one object or null. search returns an array, empty if the plugin does not search.

download

Only called when capabilities.download is true. Write the asset to <dest> and exit 0.

When download is false, ketch fetches asset.url itself, sending asset.headers and no ketch credentials — a plugin’s URLs never receive the user’s GitHub token. Put whatever credentials your assets need in headers.

Either way ketch hashes the file that actually lands on disk. A plugin does not get to assert the checksum of its own download.

What ketch enforces

A plugin is a third-party executable run on the user’s behalf, so:

  • stdin is closed. A plugin that tries to prompt gets EOF rather than the user’s terminal.
  • output is capped at 8 MiB per stream.
  • there is a 30-second deadline per subcommand, after which the process is killed.
  • a broken plugin is a warning, not a failure. Discovery reports it and every other source keeps working.

A minimal example

#!/bin/sh
# ketch-source-example — put in ~/.ketch/plugins and chmod +x
set -eu
case "$1" in
  capabilities)
    printf '{"protocol":1,"scheme":"example","search":false}\n'
    ;;
  describe)
    printf '{"id":"%s","name":"%s"}\n' "$2" "${2##*/}"
    ;;
  releases)
    printf '[{"version":"1.0.0","tag":"v1.0.0","assets":[{"name":"tool-1.0.0-aarch64-apple-darwin.tar.gz","url":"https://example.invalid/tool.tar.gz"}]}]\n'
    ;;
  *)
    echo "unsupported subcommand: $1" >&2
    exit 1
    ;;
esac

Then:

ketch plugin list
ketch install example:some/project