How Commands Are Registered with the OpenCode Plugin: Inside the `i-have-adhd` Skill System
TLDR: Commands are registered with the OpenCode plugin by the plugin's config hook, which appends the repository's skills directory to config.skills.paths — OpenCode then automatically indexes every SKILL.md file it finds and creates a matching slash command, such as /i-have-adhd.
In the ayghri/i-have-adhd repository, a lightweight OpenCode plugin handles command registration entirely through the standard plugin contract defined in opencode.json. The mechanism is elegant: instead of explicitly declaring commands, the plugin merely surfaces a skill directory, and OpenCode's skill-discovery engine does the rest. This article walks through the exact code path, explains why the /i-have-adhd command appears in chat sessions, and shows how the plugin extends this behavior with an optional always-on mode.
The Plugin Structure: Where Command Registration Happens
OpenCode loads every plugin referenced in opencode.json at startup. Each plugin is an async function that returns an object whose keys are hook names — lifecycle callbacks that OpenCode invokes at specific moments. For command registration, the critical hook is config.
The plugin file lives at .opencode/plugins/i-have-adhd.mjs and its config hook looks like this:
// .opencode/plugins/i-have-adhd.mjs (excerpt)
export default async () => {
return {
// Make the skill discoverable (so the `skill` tool and the /i-have-adhd
// command can load it).
config: async (config) => {
// Ensure the `skills` section exists.
config.skills = config.skills || {};
config.skills.paths = config.skills.paths || [];
// Add the repository’s `skills` folder to the list of paths that Open Code
// scans for skills.
if (!config.skills.paths.includes(skillsDir)) {
config.skills.paths.push(skillsDir);
}
},
// … (always-on system-prompt transform omitted)
};
};
The hook receives the current OpenCode config object and mutates it in place. Three things happen here:
- It ensures
config.skillsexists — OpenCode expects askillsobject containing apathsarray. - It dedupes the paths array — the
includescheck prevents duplicate entries if the plugin is loaded multiple times. - It pushes the repository's
skillsdirectory (../../skills) onto that array.
Once this config hook executes, OpenCode scans the directory and registers any skill files it finds.
How the config.skills.paths Mechanism Creates Slash Commands
Here is the core insight: OpenCode does not let plugins register commands directly. Instead, it subscribes to skills — and every discovered skill automatically becomes a slash-command.
The skill definition resides at skills/i-have-adhd/SKILL.md. When the plugin appends the skills folder to config.skills.paths, OpenCode's indexer scans the directory and recognizes the SKILL.md file as a valid skill. A command is then registered chain-based: OpenCode derives the command name from the skill's directory name, producing /i-have-adhd.
To invoke the command, the user simply types it inside a chat session:
opencode chat
# then inside the session:
/i-have-adhd
On first invocation, OpenCode loads SKILL.md and applies the ADHD-friendly response ruleset to all subsequent replies in that session. If the user triggers the command again later, the ruleset is reapplied — the skill is session-scoped by default.
Why This Design Matters: Commands as a Side Effect of Skills
The registration flow in i-have-adhd demonstrates a deliberate architectural choice in OpenCode plugins:
- Skills are the source of truth. The ruleset itself — the actual behavior change — lives in
SKILL.md, not in the plugin code. - Command creation is implicit. OpenCode's skill engine registers the command for you; the plugin only needs to make the skill discoverable.
- The plugin stays tiny. The entire registration logic is about ten lines of JavaScript. This makes the plugin easier to maintain and test, and it means there is no duplicated command logic between plugin and skill.
If you ever need to register a different command, the procedure is: create a new directory inside skills/, add a SKILL.md with the desired ruleset, and OpenCode will automatically generate a /your-command-name shorthand. The plugin does not need to be modified.
Always-On Variation: How the Plugin Augments the Command
While the command itself is session-scoped, the i-have-adhd plugin also provides an opt-in always-on mode through a separate hook — experimental.chat.system.transform. This hook injects the same ruleset from SKILL.md into every system prompt, making the skill apply permanently rather than only after /i-have-adhd is typed.
Here's how the user enables it:
# Opt-in so the ruleset is added to every system prompt
touch ~/.config/opencode/.i-have-adhd-always
After the flag file exists, the transform hook prepends the ruleset to every system prompt. To disable:
# Disable permanent injection
rm ~/.config/opencode/.i-have-adhd-always
This does not alter the command registration process at all — it's a separate runtime augmentation that complements the skill-based mechanism.
Key Files for Command Registration
The following files work together to implement the command registration described above:
| File | Purpose |
|---|---|
.opencode/plugins/i-have-adhd.mjs |
Registers the skills directory, enabling the /i-have-adhd command and provides the always-on system-prompt hook. |
skills/i-have-adhd/SKILL.md |
The canonical skill definition; the source of truth for the ruleset that the command loads. |
opencode.json |
Root OpenCode configuration that references the plugin file. |
tests/test_opencode_plugin.py |
Test suite that verifies the plugin correctly registers the skill and the /i-have-adhd command. |
INSTALL.md |
Documentation on how to install the plugin and enable the command/always-on flag. |
Summary
- Command registration in OpenCode is skill-driven. The plugin's
confighook appendsskillsDirtoconfig.skills.paths, and OpenCode auto-generates the slash-command from theSKILL.mdfile it finds. - The plugin code is minimal. All command logic is just three lines of array manipulation — OpenCode handles the rest.
- Session-scoped vs always-on. The
/i-have-adhdcommand applies the ruleset to a session; theexperimental.chat.system.transformhook enables permanent injection with the flag file. - The design is extensible. A user can add new skills to the
skills/folder and get new commands without touching the plugin at all.
Frequently Asked Questions
Where exactly is the command listed in the plugin code?
There isn't an explicit command declaration — the command is created by OpenCode after the config hook runs. In .opencode/plugins/i-have-adhd.mjs, the config hook appends ../../skills to config.skills.paths. OpenCode then discovers skills/i-have-adhd/SKILL.md and registers the /i-have-adhd command automatically.
What is the role of opencode.json in the registration process?
opencode.json is the root configuration that lists the plugin as enabled. When OpenCode starts, it reads this file and loads every plugin referenced there. The plugin's exported async function then runs and its config hook mutates the OpenCode configuration, adding the skill path.
Can I register a command that isn't tied to a skill?
No, not in the current i-have-adhd architecture. The plugin registers commands indirectly — via skill discovery. To add a new command, you add a new SKILL.md under the skills/ directory. OpenCode's skill engine then handles the command name derivation (the folder name becomes the command slug).
How does the experimental.chat.system.transform hook relate to command registration?
It doesn't affect registration at all. That hook is a separate runtime mechanism for always-on behavior. It checks for the existence of ~/.config/opencode/.i-have-adhd-always and, if present, injects the ruleset from SKILL.md into every system prompt. The command itself is only loaded when explicitly invoked.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →