Contributing a mod
Writing it with a coding agent? Install the openmods skill (npx skills add shouryamaanjain/openmods --skill openmods -g): it carries this guide and a map of each harness's source.
1. Make the change in the harness
git clone https://github.com/anomalyco/opencode && cd opencode
git checkout v2.0.18
Pick the latest release tag, not the default branch. Mods are pinned to releases because that is what users have installed and what CI can reproduce.
Use the Bun version the release pins in its package.json packageManager field; openmods does this for you when it builds, but for your own dev runs install it with curl -fsSL https://bun.sh/install | bash -s bun-v1.4.2. Change whatever you want. Commit as you go; each commit becomes one patch, so keep them meaningful (feat(tui): add vim keys to the session list). Keep the harness's own tests and typecheck passing:
cd packages/tui && bun run typecheck
2. Try it
From your OpenCode clone, once:
openmods dev
From then on opencode runs your clone straight from source, in whatever project you open it. Edit, then start opencode again to see the change: nothing to commit, pack or build. openmods dev --stop switches back to your modded build or your stock OpenCode.
Before you publish, try it the way users will get it, built and alongside your other mods:
openmods install . # builds OpenCode with your commits, as the local mod <you>/<branch name>
It packs your commits (commit first) as a local mod in ~/.openmods/local, which is never published. --name sets the mod's name when the branch name will not do.
3. Pack it into your fork of the registry
Fork this repository and clone your fork next to your OpenCode clone, then:
openmods pack . --name my-mod --registry ../openmods
A mod is named <owner>/<name>, where the owner is your GitHub handle, in lowercase. pack reads it from git config github.user, else from the GitHub CLI; pass --owner to set it.
--local writes the mod under ~/.openmods/local, where it is installable but not part of the registry. Without it, pack finds the release tag below your commits, runs git format-patch, and writes mods/<owner>/my-mod/ with a mod.json, a README stub, and an opencode/ folder holding support.json and a folder of patches named after the release, such as v2.0.18/. Fill in the description, tags, and license. Describe what the mod changes and why in the README; that page is what people read before they build it.
The same mod can support more than one harness. Make the change in a Codex checkout too and run openmods pack ../codex --name my-mod --registry ../openmods: it adds codex/ next to opencode/ and keeps the shared files as you edited them. Users pick the harness with --opencode or --codex, or from a list when they leave the flag out.
mod.json fields, shared by every harness:
| field | meaning |
|---|---|
owner |
your GitHub handle; lowercase, digits, hyphens |
name |
lowercase, digits, hyphens; unique within the owner |
description |
one line for the listing, up to 200 characters; pack leaves a TODO to replace |
license, tags |
what the listing shows |
author |
your name and GitHub handle, filled in by pack |
maintainers |
GitHub handles the bot mentions when a new release breaks the mod. Defaults to the owner. |
The registry check rejects fields it does not know, so a misspelled one is caught.
<harness>/support.json fields:
| field | meaning |
|---|---|
versions |
the mod's versions on this harness, newest first, one per release it supports. Each has ref and commit (the harness release and its exact commit), patches (the ordered list of its patch files, in a folder named after the release tag, generated by pack), update (which update of the mod it holds) and an optional note on what that update changed, up to 200 characters. There is one version per release, and every patch file in the folder is listed. Patches must carry full blob ids (git format-patch --full-index, which pack does); the linter rejects short ones. CI adds a version when the newest one still applies and typechecks on the harness's newest release, and its rebased patches change the same lines as before. Older versions stay, so users whose other mods are still on an older release can build yours with them. |
conflicts |
optional; other mods, as owner/name, that break this one on this harness without touching the same lines, such as two mods that bind the same key in different files. Mods that change the same lines are found automatically; you do not list those. |
<harness>/status.json is written by CI only. It records the last release the mod was tested against there and whether it passed.
4. Check it
openmods check mods/<you>/my-mod/opencode --build
This clones the harness into a folder in your temp directory, checks out the release, applies your patches as users build them, and builds. Run it before opening a PR: CI does not build mods, it runs the quick version, --typecheck, on each version a pull request changes and against each new harness release.
5. Open a pull request
Fork this repository, commit the mod's folder, and open an ordinary pull request. The template asks what the mod does; add a screenshot or recording if it changes what you see.
The standards
A check called standards enforces these, alongside Greptile's review:
- It changes one mod, and nothing outside that mod's folder.
- You own the mod. A new mod lives under your GitHub handle,
mods/<you>/<mod>/. To change an existing mod you must be its owner or be listed in itsmaintainersbefore your pull request; a pull request cannot add its own author. Registry maintainers can change any mod. - The README has a
## Permissionssection that says what the mod does with the network, files, commands and the agent's instructions. Write "none" or "unchanged" where it does nothing.openmods packwrites the section for you to fill in; no TODO may be left in it. - Every new update says what changed:
openmods pack --note "...". - The patches are readable source: no binary files, no line over 1,000 characters (how minified or generated code shows), and no patch over 1 MB.
- The addresses git puts in a patch's header, which is published, are GitHub's private ones:
openmods packputs the private address for your handle (<you>@users.noreply.github.com) in theFrom:line and in trailers such asSigned-off-by:andCo-authored-by:. A patch with any other address there is refused until it is packed again. The rest of your commit message is published as you wrote it. - The mod's folder holds only
mod.json,README.md, and per harnesssupport.jsonand its patches.status.jsonis written by CI. - Only registry maintainers change
revoked.json.
Changes to the harness's dependencies or build files are allowed, but the check lists them so reviewers read them first.
The reviews
- Apply and typecheck. CI applies each version your pull request changes to the release it names and typechecks the result. A failure blocks the merge.
- Greptile reviews the code your patches add, for correctness and fit with the harness, and for security: anything malicious, and anything the README's Permissions section does not disclose.
- For an update, a comment shows what it changes compared with the published update, as plain code.
- A maintainer reads the diff, the reviews and the README, and merges. Every pull request needs this approval; the bots help the maintainer, they do not replace them. The release bot's own commits, which only add versions CI has checked on a new release, go straight to main.
An update to your mod goes through the same steps as a new mod. Labels say what kind of change a pull request is: mod: new, mod: update, and build files when the patches change the harness's dependencies or build scripts.
A mod changes the harness itself. That is the whole point: it reaches what plugins, skills and MCP servers cannot. A submission that only adds a skill, a plugin or an MCP server belongs in that system's own channel, such as npm, skills.sh or the harness's config, and will be pointed there. If your mod adds a new tool or behavior the agent needs to know about, describe it in the harness's own source, where its built-in tools are described, rather than shipping a skill beside it.
Maintainers also look for a license compatible with the harness.
Updating a mod for a new release
When a new release breaks your mod, CI opens an issue that mentions you, labelled conflict, with these steps filled in. They work on any machine, because the registry keeps your mod's commits, messages included:
cd opencode && git fetch --tags
git checkout -b my-mod v2.0.18 # the last release the mod supports
git am ../openmods/mods/<you>/my-mod/opencode/v2.0.18/*.patch # the mod as it last worked
git rebase --onto v2.1.0 v2.0.18 my-mod # resolve conflicts, then git rebase --continue
openmods install . --owner <you> # try it on 2.1.0
openmods pack . --name my-mod --owner <you> --registry ../openmods --note "works on OpenCode 2.1.0"
pack adds a version for 2.1.0 and keeps the older ones. The issue closes itself once that version is merged. If the code is the same and only rebased, it stays the same update; if you changed the code, it becomes the next update. Open a PR. Mods have no version number of their own: a mod is "for OpenCode 2.1.0", and CI adds that version automatically when the newest one still applies and typechecks on the new release, without changing different lines. CI checks each harness's newest release, so a release that is skipped over gets no version of its own. You only need to do this by hand when CI opens an issue saying the mod no longer supports a release.
To ship an update, change your commits and pack again, with --force if that release already has a version, and say what changed with --note. pack numbers the update itself: one more than the latest when the changed lines differ, the same number when they do not. Users of your mod are asked once whether to rebuild with it, and see your note. openmods check <mod-folder> --at <tag> checks an older version.
Proposing a harness
OpenMods supports open-source harnesses: any harness whose source is public on GitHub and can be built from a release. To propose one, open a pull request that adds a single file, harnesses/<id>.json, following schema/harness.schema.json and the list below. It is labelled harness.
The definition needs:
id, the file's name, andname, as users see it.repo: the public GitHub repository, andlicense.binary: the command users run.installer: the one-line install command from the harness's own docs, and the folders it puts the binary in (paths), so OpenMods can find a stock install a new shell has not picked up yet. OpenMods offers it to users who install a mod for a harness they do not have.install,typecheckandbuild: the commands that prepare, check and build a release, andartifact, the path of the built executable.buildInstall, optional: the install a build needs, when it is less thaninstall, such as only the packages the build uses. Builds run it; a typecheck andopenmods devruninstall.recipe: the files, or lines of files, those commands depend on, such as the build script and toolchain pin. The release watch compares them on every release and holds the harness's mods when they change.releaseTagPattern: which tags are releases.stockMarker, optional: a pattern found in the stock harness's program and not in other programs of the same name (fx's, say, shares its name with a JSON viewer), so OpenMods never takes the wrong program for the stock harness.- Toolchains: a release that pins Bun (
packageManagerinpackage.json) or Zig (minimum_zig_versioninbuild.zig.zon) is built with exactly that version, which OpenMods fetches into~/.openmods/toolchains. Anything else the build needs goes inrequirements.
The standards check makes sure each of these is there, except installer.paths, which is optional. Greptile and a maintainer then read every command in it, because they run on users' machines and in CI. A maintainer runs the harness build workflow to prove it builds before any mod ships. If a modded build and the stock one would clash side by side, the fix is the harness's own switches in args, argsUnless, stockWhen or sameRelease, never a patch: OpenMods does not change a harness's code. A harness that is already supported is changed only by maintainers; open an issue to suggest a change.
Releasing OpenMods
Users only ever run a release of OpenMods, a vX.Y.Z tag: the installer and openmods update take the newest one, so a change merged into cli/ reaches nobody until it is released. To release, raise the version in cli/package.json in a pull request. Once it is merged and the tests pass on main, the release workflow tags that commit and publishes a GitHub release with its notes. v* tags are protected: only maintainers and the release workflow, which pushes the tag with the release bot's deploy key, can make them. A change that the registry relies on, such as a new field in a harness definition, raises the version in the same pull request, so its release goes out with it. Users' copies read the registry as soon as it changes, and ignore what they do not know until they update.