This is the short version. The longer one lives next to the code it describes, in Thor-Extensions/CONTRIBUTING.md.

Read the extensions policy first if you have not: it covers what an extension is allowed to do, and what a user is agreeing to when they install one.

Where things live

  • Templatethor-extension-template. A starter project on the current contract: the compileOnly dependency, the manifest meta-data and a config activity in its own process. Start here.
  • Extensions repoThor-Extensions. Source for every published extension, the catalog, and the release CI.
  • The contractcom.trinadhthatakula:thor-extension-api on Maven Central.
  • Thor itselfThor, GPL-3.0-or-later.

Depend on the contract

Add the API as a compileOnly dependency:

build.gradle.kts (your extension)

dependencies {
    compileOnly("com.trinadhthatakula:thor-extension-api:VERSION")
}

VERSION is 3.0.0, the version of the contract Thor is currently built against.

Implement one of:

  • ThorExtension — metadata only: name, description, version, author. Thor loads this class in its own process to list your extension, so keep it trivial. No Compose, no Asgard, no privileged calls in here.
  • DebloatExtension : ThorExtension — a manufacturer debloat list. Pure data.

Declare your class in the manifest, and start your package id with com.valhalla.thor.ext. — that prefix is what Thor scans for, and a package outside it is never looked at.

AndroidManifest.xml

<meta-data android:name="thor.extension.class"
    android:value="com.valhalla.thor.ext.example.ExampleExtension" />

That is the only meta-data key Thor reads. ExtensionManager looks up thor.extension.class and nothing else, so any other key you declare under that namespace is inert. Your API level is fixed by the version of thor-extension-api you compile against, not by anything in the manifest.

Your configuration UI runs in your own process

Ship an exported activity with an intent filter for com.valhalla.thor.extension.action.CONFIGURE. Thor launches it when the user taps Configure.

AndroidManifest.xml

<activity android:name=".ConfigActivity" android:exported="true"
    android:theme="@android:style/Theme.Translucent.NoTitleBar">
    <intent-filter>
        <action android:name="com.valhalla.thor.extension.action.CONFIGURE" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

Because it is your process, bundle your own Compose and UI libraries — implementation this time, not compileOnly — and set android.enableR8.fullMode=false in your own gradle.properties, since R8’s full mode mis-optimises Compose default arguments. Thor passes its current theme as intent extras, so you can match its look without guessing. Persist your settings in your own storage, not Thor’s.

Submit it

Open a pull request against Thor-Extensions adding your source under unverified/<name>/, with a LICENSE in your folder. That is the only entry point — verified/ is not a tier you submit to.

Every pull request is reviewed by AI review agents and by the maintainer. Both have to be satisfied.

Once it is reviewed and accepted, the maintainer moves it to verified/, builds it, and signs it with the project key — at which point it appears in the in-app catalog and loads on a stock Thor. Nothing can be self-signed into that tier; that is the whole meaning of the badge.

Until then your extension carries no pinned signature, and Thor relaxes its signature check on debug builds only. So it loads on a Thor built from source in debug, and users have to build it themselves.

What each tier means is set out in the extensions policy.

Asking Thor to do privileged things

Extensions do not run privileged code inside Thor. To act on a package, call Thor’s operations provider over a content URI; the call cold-starts Thor if it is not already running.

  • URIcontent://<thorPackage>.extensionops. Try com.valhalla.thor, then com.valhalla.thor.debug.
  • Methodsfreeze, unfreeze, toggle.
  • Extras — a Bundle with a packages string array.
  • Returns — a Bundle with ok: Boolean and count: Int.

Thor verifies the caller is a pinned-signer extension before doing anything, and refuses to operate on its own package or on yours. A debug Thor relaxes that to any package under the extension prefix, which is how you test yours before it is verified. Being trusted buys the right to ask, not the right to freeze anything: a freeze still goes through the same safety-tier check the app itself uses, so a package rated Unsafe is refused no matter who asked. Extension-driven freezes always disable and enable — Thor’s Suspend mode does not apply to them.

Call it off the main thread: ContentResolver.call is a synchronous IPC. You need no special permission, only package visibility.

AndroidManifest.xml

<queries>
    <package android:name="com.valhalla.thor" />
    <package android:name="com.valhalla.thor.debug" />
</queries>

The verified thor-automation-extension in the extensions repo is a working reference client.

Checklist

  • compileOnly on com.trinadhthatakula:thor-extension-api, version 3.0.0
  • package id starts with com.valhalla.thor.ext.
  • thor.extension.class meta-data declared
  • config UI in your own exported activity, not in Thor
  • android.enableR8.fullMode=false
  • a LICENSE in your folder
  • the pull request adds your source under unverified/<name>/