What a repository is
Clink reads packs from public GitHub repositories. You add one by name, Clink reads its latest release, and every file is verified before it is installed. Nothing is uploaded, and nothing of yours runs on a server.
Enter owner/repository, github.com/owner/repository, or the full HTTPS URL. Clink verifies every release manifest and file before anything can be downloaded.
Pick a kind
Each kind of pack has its own file extension and its own collection name inside the manifest, and Clink holds each to its own size limit. Pick the row you are publishing and keep to it: a theme filed under panels is simply not read.
| Kind | File | Collection | Largest file, in bytes | Official repository |
|---|---|---|---|---|
| Themes | .clinktheme | themes | 128,000 | anti-ltd/clink-themes |
| Layouts | .clinklayout | layouts | 512,000 | anti-ltd/clink-layouts |
| Profiles | .clinkprofile | profiles | 256,000 | anti-ltd/clink-profiles |
| Sounds | .clinkpack | sounds | 512,000 | anti-ltd/clink-sounds |
| Fonts | .otf .ttf | fonts | 32,000,000 | anti-ltd/clink-fonts |
| Panels | .clinkpanel | panels | 50,000 | anti-ltd/clink-panels |
| Actions | .clinkext | actions | 48,000 | anti-ltd/clink-actions |
| Plugins | .clinkplugin | plugins | 66,000 | anti-ltd/clink-plugins |
Language packs are the exception. A pack there is a set of lexicon and model files rather than one document, and the official repository carries the toolchain that builds them. Start from its README instead of this page.
Start from the official repository
The quickest route is a fork. It arrives with the pack folder, the manifest builder and the release workflow already wired together, plus working examples to copy.
# Everything a repository needs is already wired together in the official one.
gh repo fork anti-ltd/clink-panels --clone
cd clink-panels
# Your packs replace the examples. Keep tools/ and .github/.
rm Panels/*.clinkpanel
cp ~/Downloads/my-panel.clinkpanel Panels/kaomoji-plus.clinkpanelKeep tools/ and .github/workflows/. Those two are what turns a push into a release Clink can read, and a fork without them publishes nothing.
Or build one from three files
Nothing about a repository is special. It needs a folder of packs, a script that writes the manifest, and a workflow that cuts the release.
my-panels/
├── Panels/
│ └── kaomoji.clinkpanel # one file per pack, the name is the id
├── tools/
│ └── build-manifest.py # writes manifest.json from that folder
└── .github/workflows/
└── release.yml # turns a push into a releaseThe workflow does three things: rebuild the manifest, delete the previous release, and publish a new one tagged latest with the pack files and the manifest attached. This is the whole of it.
name: Release panels
on:
push:
branches: [main]
paths: ["Panels/**", "tools/**", ".github/workflows/release.yml"]
permissions: { contents: write }
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 tools/build-manifest.py
- run: gh release delete latest --yes || true
env: { GH_TOKEN: "${{ github.token }}" }
- run: gh release create latest Panels/*.clinkpanel manifest.json --title "Latest panels" --latest
env: { GH_TOKEN: "${{ github.token }}" }The builder walks the folder and writes one entry per file. It is the only place the hash and the byte count come from, which is why it runs in the workflow rather than by hand.
#!/usr/bin/env python3
import hashlib, json, os, pathlib
root = pathlib.Path(__file__).resolve().parents[1]
repository = os.environ.get("GITHUB_REPOSITORY", "<owner>/<repository>")
packs = []
for path in sorted((root / "Panels").glob("*.clinkpanel")):
data = path.read_bytes()
pack = json.loads(data)
packs.append({
"id": path.stem,
"name": pack["name"],
"version": "latest",
"asset": {
"path": path.name,
"url": f"https://github.com/{repository}/releases/download/latest/{path.name}",
"sha256": hashlib.sha256(data).hexdigest(),
"byteCount": len(data),
},
})
(root / "manifest.json").write_text(json.dumps({"version": "latest", "panels": packs}, indent=2))The manifest
Each release publishes manifest.json beside the pack files, and Clink reads it from the latest release at https://github.com/<owner>/<repository>/releases/latest/download/manifest.json. Every entry names the file, its address inside that same release, its SHA-256 hash and its exact byte count.
{
"version": "latest",
"panels": [
{
"id": "kaomoji",
"name": "Kaomoji",
"version": "latest",
"asset": {
"path": "kaomoji.clinkpanel",
"url": "https://github.com/<owner>/<repository>/releases/download/latest/kaomoji.clinkpanel",
"sha256": "8f14e45fce…",
"byteCount": 2048
}
}
]
}The collection name has to match the kind. Panels go under panels, themes under themes, and so on down the table above.
Try it before you publish
A pack in a repository is the same file you can open by hand, so test it the short way first: AirDrop it to your phone, or put it in Files and open it with Clink. Then check the manifest your script writes.
python3 tools/build-manifest.py
cat manifest.json- Run the builder. It rewrites manifest.json from whatever is in the folder right now.
- Read what it wrote. Every entry should name a file that is really there, and a byte count that matches it.
- Open the pack file itself in Clink and use it. A panel or an action lands in its editor, where you can run it.
Publishing
git add Panels manifest.json
git commit -m "Add my first panel"
git pushPush to main and the workflow takes over. It replaces the latest release rather than adding to it, so the newest push is always what people get.
Open this tab after the repository has published a release.
Add it in Clink
- Open General, then Repositories, and add owner/repository.
- Open the tab for your kind and pull to refresh. Your packs are listed with whatever names the manifest gave them.
- Tap one to download. Clink verifies it, then installs it like any other pack.
Adding a repository is enough for data packs: themes, layouts, profiles, sounds, fonts and languages. Panels, actions and plugins carry logic, so each repository needs a switch of its own before Clink will install any of them, and the app asks the first time.
Panels contain constrained interactive logic. Only allow repositories you trust.
What Clink checks
- The address is HTTPS on github.com, under the releases of the repository you added.
- The download is exactly the byte count in the manifest, and its 64 character SHA-256 hash matches.
- The file carries the right extension and decodes as the kind of pack it claims to be.
- Panels, actions and plugins pass the source policy before they are stored.
When a download is refused, it is one of these five. The message in the app is deliberately short, so check them in order.
| Check | Refused when |
|---|---|
github.com | The address is not HTTPS on github.com, under the releases of the repository you added. A manifest cannot point somewhere else, not even to another repository of yours. |
byteCount | The file is not exactly the size the manifest claims, or it is over the limit for its kind. Rebuilding the manifest after every edit fixes most of these. |
sha256 | The hash does not match the bytes. Usually the manifest was written before the last change to the file. |
path | The file name does not end in the extension the kind requires, or it tries to climb out of the release with two dots. |
source | A panel, an action or a plugin did not pass the source policy: too long, or carrying a fragment the policy refuses. |
Updating, and taking something back
The id is the file name without its extension, and it is what makes an update an update. Keep the name and a new release replaces the copy people already have. Rename the file and you have published a second pack beside the first.
Removing a pack from the repository stops new downloads. It does not reach the phones that already have it, so treat a bad release as something to replace rather than to delete.
Official repositories
Fork any of these to start. Each one builds its own manifest and publishes itself with GitHub Actions.