# How Commands Are Registered with the OpenCode Plugin: Inside the `i-have-adhd` Skill System

> Discover how the OpenCode plugin registers commands by appending skills paths. Learn how SKILL.md files automatically create slash commands like /i-have-adhd.

- Repository: [Ayoub Ghriss/i-have-adhd](https://github.com/ayghri/i-have-adhd)
- Tags: internals
- Published: 2026-08-22

---

**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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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:

```javascript
// .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:

1. **It ensures `config.skills` exists** — OpenCode expects a `skills` object containing a `paths` array.
2. **It dedupes the paths array** — the `includes` check prevents duplicate entries if the plugin is loaded multiple times.
3. **It pushes the repository's `skills` directory** (`../../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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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:

```bash
opencode chat

# then inside the session:

/i-have-adhd

```

On first invocation, OpenCode loads [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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:

```bash

# 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:

```bash

# 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`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md) | The canonical skill definition; the source of truth for the ruleset that the command loads. |
| [`opencode.json`](https://github.com/ayghri/i-have-adhd/blob/main/opencode.json) | Root OpenCode configuration that references the plugin file. |
| [`tests/test_opencode_plugin.py`](https://github.com/ayghri/i-have-adhd/blob/main/tests/test_opencode_plugin.py) | Test suite that verifies the plugin correctly registers the skill and the `/i-have-adhd` command. |
| [`INSTALL.md`](https://github.com/ayghri/i-have-adhd/blob/main/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 `config` hook appends `skillsDir` to `config.skills.paths`, and OpenCode auto-generates the slash-command from the [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md) file 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-adhd` command applies the ruleset to a session; the `experimental.chat.system.transform` hook 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`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md) and registers the `/i-have-adhd` command automatically.

### What is the role of [`opencode.json`](https://github.com/ayghri/i-have-adhd/blob/main/opencode.json) in the registration process?

[`opencode.json`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/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`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md) into every system prompt. The command itself is only loaded when explicitly invoked.