Docs
View as markdown

relate extension

The relate extension command, which scaffolds, checks, installs, lists and removes extension packages, with flags, output, trust tiers and exit codes.

cliextensions

relate extension manages extension A package that adds an action to the CLI, run as relate run <name> and installed with relate extension install. Read more in the glossary: packages that add an action to the CLI, run as relate run <name>. It has five actions. init scaffolds a new package, check tests a package directory without installing it, install puts a package on the machine from one of four sources, list prints what is installed, and remove uninstalls one. Only install and list --available use the network; the other actions read and write local files, and none of them opens a store A relate database: a directory, a single .rl file, or :memory:, opened with --database. Read more in the glossary.

Synopsis

relate extension init <name> [--lang sh|py|ts|rust]
relate extension install <name> [--github REPO@TAG|@SHA] [--url URL] [--from DIR] [--version V] [--force] [--re-approve | --approve SHA]
relate extension list [--available | --trust]
relate extension remove <name> [--forget-pin]
relate extension check <dir>
ActionDoesNetwork
init <name>Creates the package directory ./<name>/None
check <dir>Runs a package directory through the stages a mount needsNone
install <name>Verifies a package and installs it under the extensions directoryExcept with --from
listPrints the installed packages; --trust prints the recorded GitHub pinsNone
list --availablePrints the first-party channel's catalogRequired
remove <name>Deletes an installed packageNone

Options

FlagArgumentDefaultScopeSinceDescription
--langsh|py|ts|rustpyrelate extension—init: the template language.
--githubREPO@TAG|@SHAunsetrelate extension—install: a GitHub release pinned to a commit; the tag's commit and the asset's SHA-256 are recorded.
--urlURLunsetrelate extension—install: the tarball at an https URL (third party).
--fromDIRunsetrelate extension—install: an unpacked package directory (local; no network, no signature).
--versionVthe newest listedrelate extension—install: the channel version.
--force—offrelate extension—install: clobber a target directory without the install marker.
--re-approve—offrelate extension—install --github: re-approve whatever the pin resolves to now.
--approveSHAunsetrelate extension—install --github: re-approve only this commit.
--available—offrelate extension—list: the channel's catalog (the signed extensions.json index); fails closed offline.
--trust—offrelate extension—list: the recorded GitHub pins.
--forget-pin—offrelate extension—remove: also clear the name's recorded GitHub pin.

A — under Since: the data does not record the release that added the flag.

Each flag belongs to one action, named in its row; a flag given to another action is a usage error. Of the install sources, --github, --url and --from name where the package comes from, and two of them together are a usage error; with none, the source is the first-party channel.

Where packages live

PathHolds
$XDG_CONFIG_HOME/relate/extensions/<name>/One installed package: extension.json, its executable, and the provenance marker .relate-provenance.json
$XDG_CONFIG_HOME/relate/extensions-trust.jsonThe recorded GitHub pins, one per extension name

When XDG_CONFIG_HOME is unset, empty or a relative path, ~/.config takes its place. This directory is the only place relate looks for extensions: never the working directory, so a cloned repository cannot add an action to the CLI.

The manifest, extension.json, declares the package: kind (command), name (the action's name after relate run), version, protocol (the Model Context Protocol revision it speaks, 2025-06-18), the flags table, commit (per-batch or whole-import) and executable, a path inside the package directory, and the optional positional, requires-fresh-store and hidden. The relate run reference covers the flags table, and Write an extension the rest.

init

relate extension init <name> creates ./<name>/ with three items (four for --lang rust) and prints them, with the two commands that install and run the package:

FileContent
extension.jsonA manifest with version 0.1.0, the protocol revision this binary serves, one sample flag --name (default there), and commit: per-batch
The executable<name>.sh, <name>.py (the default) or <name>.ts; for --lang rust, a Cargo project (Cargo.toml, src/main.rs) whose built binary, copied into the package directory, is the executable the manifest names
README.mdThe package's quickstart

The sh, py and ts executables hold a generated protocol client inline, so they depend on no client library; the Rust project depends on the relate-wire crate instead. Each has a walk() function to edit. The sample writes one :Scaffold node and reports a greeting built from --name. An existing directory of that name is refused (exit code 1); a name with a path separator, a reserved command word as the name, or an unknown --lang is a usage error (exit code 2).

check

relate extension check <dir> runs the package through six stages, in the order a mount uses them, printing one line per stage and stopping at the first failure:

StagePasses whenExit code on failure
manifestextension.json decodes, its executable stays inside the directory, its name is not a reserved command word, and its protocol is the one this binary serves2
executableThe file exists, is a regular file, and is executable2
spawnThe executable starts and takes its flag defaults on stdin1
initializeIts first request is initialize, for the protocol this binary serves1
notifications/initializedIt confirms the handshake1
tools/listAny tool it calls is one of the five the mount serves1

A package that passes every stage gets a seventh line about its GitHub pin (pin: approved, pin: drifted, pin: not from a GitHub source (<tier>) or pin: unknown), which never changes the exit code. check opens no store, installs nothing and fetches nothing, but the spawn stage starts the package's executable, so any code the package runs when it starts runs with the user's permissions; it is not a sandbox. Each stage waits at most ten seconds. It exits 0 when the package would mount.

install

relate extension install <name> takes the package from one source. The source is the trust decision, and the marker records it as the package's tier:

TierSource flagWhat is checked before anything lands
first-partyNoneThe channel index, extensions.json, verifies against the release signing key relate upgrade uses, and the package's SHA-256 matches its signed row; --version V picks a listed version, default the newest
github-pin--github OWNER/REPO@TAG or @<commit>The tag's commit and the asset's SHA-256 are recorded on the first install; a later install whose tag moved or whose asset changed is refused before any download unless --re-approve or --approve <commit> accepts it
third-party--url URLNothing beyond the transport: https only, or plain http to the loopback interface; any other URL is refused before any fetch
local--from DIRNothing beyond the package rules: the directory is the trust act, and its executable must resolve inside it

Every tier then runs the same package rules (the manifest decodes, the executable stays inside the package, the protocol matches, the name collides with no command and no installed extension) in a staging directory beside the destination, writes the marker, and renames the package into place. A failed install leaves nothing behind. On success it prints one line naming the tier, the name, the version and the source, such as installed local extension team-import 0.1.0 from ./team-import.

CaseBehavior
The name is installed from the same tierReinstalled
The name is installed from another tierRefused, naming the installed tier and source; the refusal names relate extension remove <name>
The manifest's name differs from <name>Refused
A target directory without a markerRefused unless --force

The extension install guide walks through each tier, and the security reference covers the signing key.

list

FormPrints, one line per entryNetwork
list<name> <version> <tier> <source>; a package placed by hand, without a marker, prints <name> <version> unmarked; nothing when none is installedNone
list --trustThe recorded GitHub pins: name, repository, tag, commit and asset SHA-256None
list --availableThe channel's catalog: name, newest version, protocol, and whether it is installed; a package marked hidden is left outRequired; offline, it fails naming the index URL

--available and --trust together are a usage error.

remove

relate extension remove <name> deletes the package directory and prints removed extension <name> (<path>). It requires the provenance marker: a directory without one, such as a package copied by hand, is refused with a message naming the path, and remove has no --force. A GitHub pin outlives the package, so a reinstall is still compared with it; --forget-pin clears the pin too.

Environment

VariableEffectDefaultUsed by
GITHUB_TOKENOptional bearer token for release-asset downloads; required while the repository is internal.—relate upgrade, relate extension install
RELATE_EXTENSIONS_JSON_URLOverride the extension channel-index URL (dev and test).https://relatedb.ai/extensions.jsonrelate extension install, relate extension list --available
RELATE_RELEASE_DOWNLOAD_URLOverride the base of the direct release-download route (dev and test).https://github.com/relatedb/relate/releases/downloadrelate upgrade, relate extension install
RELATE_GITHUB_API_URLOverride the base of the GitHub API asset route (dev and test).https://api.github.comrelate upgrade, relate extension install
XDG_CONFIG_HOMEWhen set, non-empty and absolute, the config directory is $XDG_CONFIG_HOME/relate/.—usage metrics, relate extension, relate skill
HOMEThe fallback config directory ~/.config/relate/, and the base of the agent directories relate skill install writes to.—usage metrics, relate extension, relate skill

Errors and exit codes

Every action ends with one of three exit code The number a relate run returns to the shell; each nonzero value names a class of failure, such as 2 for usage, 5 for conflict, 6 for a rejected statement. Read more in the glossary.

Exit codeWhen
0The action completed; for check, the package would mount
1A runtime failure: an existing init directory, a source that cannot be read or fetched, a failed verification, a tier or name conflict, a missing marker on remove, a name that is not installed, a check stage that fails when the package runs
2A usage error, or a check failure in the manifest or the executable. A usage error is followed by the five usage lines and Run 'relate extension --help' for more information.
Message on stderrExit code
Error: extension requires an action (init, install, list, remove or check)2
Error: extension init: <dir>/<name> already exists (init writes a new package directory)1
Error: extension init: '<name>' is not a valid package name (a single directory name, no path separators)2
Error: extension init: unknown --lang value '<value>' (expected sh, py, ts or rust)2
Error: extension name '<name>' collides with the reserved command word '<word>'2
Error: extension install: --url accepts only https URLs (or plain http to the loopback interface); '<url>' is neither2
Error: extension install: --github '<value>' needs a pin: owner/repo@tag or owner/repo@<full-commit-sha> (an untagged repository is a moving target)2
Error: extension install: --url and --from are mutually exclusive (they both name the install source)2
Error: extension install: --github is mutually exclusive with --url and --from (each names the install source)2
Error: extension install: --re-approve and --approve <commit-sha> only apply to --github installs2
Error: extension install --approve requires a full 40-hex commit sha2
Error: extension install: the package's manifest names '<manifest name>' but the install was for '<name>'1
Error: extension install: extension '<name>' is installed <tier> (source: <source>); refusing a third-party install over it — remove it first: relate extension remove <name>1
Error: extension remove: no installed extension '<name>' (relate extension list prints the installed set)1
Error: extension remove: refusing to remove <path>: no .relate-provenance.json marker present (the directory was not installed by relate; remove it manually)1
Error: extension list: --trust and --available ask different questions (recorded pins vs the channel catalog)2

When the channel index cannot be read, the message names its URL. This run had RELATE_EXTENSIONS_JSON_URL=http://127.0.0.1:8874/extensions.json with nothing listening; the text after io: comes from the operating system, and this recording is from macOS:

Error: extension list: could not read the extension channel index from http://127.0.0.1:8874/extensions.json: GET http://127.0.0.1:8874/extensions.json: io: Connection refused (os error 61)

Examples

The examples run in a fresh directory, written <dir>, with a fresh home, written <home>, and no XDG_CONFIG_HOME. Nothing is installed yet, so list prints nothing and exits 0:

relate extension list
echo "exit=$?"
exit=0

init scaffolds a package with a shell executable:

relate extension init team-import --lang sh
created sh extension 'team-import' in <dir>/team-import
  extension.json  the manifest (mount name, MCP revision, flag table)
  team-import.sh  the executable: generated glue plus your walk()
  README.md       the quickstart for this package

next:
  relate extension install team-import --from <dir>/team-import
  relate run team-import -d ./store

check runs it through the six stages without installing it. The initialize line names the binary's version:

relate extension check team-import
manifest: ok (team-import 0.1.0, protocol 2025-06-18)
executable: ok (team-import.sh)
spawn: ok
initialize: ok (MCP 2025-06-18, server relate 0.1.2)
notifications/initialized: ok
tools/list: ok (the shared tool table: 5 tools; the extension calls 'write_cypher')
pin: not from a GitHub source (unmarked)

install --from installs it as a local package, and list shows the tier and the source:

relate extension install team-import --from ./team-import
relate extension list
installed local extension team-import 0.1.0 from ./team-import
team-import 0.1.0 local ./team-import

A plain http URL to another host is refused before any fetch:

relate extension install other-import --url http://example.com/other-import.tar.gz
echo "exit=$?"
Error: extension install: --url accepts only https URLs (or plain http to the loopback interface); 'http://example.com/other-import.tar.gz' is neither
exit=2

remove deletes the installed package:

relate extension remove team-import
removed extension team-import (<home>/.config/relate/extensions/team-import)

See also