# How OpenCode's Plugin Architecture Injects the i-have-adhd Ruleset Per Turn

> Discover how OpenCode's plugin architecture injects the i-have-adhd ruleset per turn using the experimental.chat.system.transform hook and an opt-in flag for customized model responses.

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

---

**OpenCode's plugin architecture injects the i-have-adhd ruleset on every turn by implementing the `experimental.chat.system.transform` hook in `.opencode/plugins/i-have-adhd.mjs`, which checks for an opt-in flag file and appends the processed ruleset to the system prompt before each model response.**

The `ayghri/i-have-adhd` repository provides an OpenCode plugin that ensures ADHD-friendly formatting rules persist across conversation turns without manual invocation. This deep integration leverages OpenCode's experimental hook system to modify system prompts dynamically based on user preferences.

## Skill Discovery and Command Registration

Before handling per-turn injection, the plugin registers the skill with OpenCode's runtime through the `config` hook defined in `.opencode/plugins/i-have-adhd.mjs`. This hook performs two critical setup tasks:

- **Path Registration**: It adds the `skills` directory to OpenCode's search path, enabling the runtime to locate the `i-have-adhd` skill definition.
- **Command Binding**: It registers the `/i-have-adhd` slash command, allowing users to manually invoke the skill on demand when the always-on mode is disabled.

## Per-Turn Injection via experimental.chat.system.transform

The core mechanism relies on the `experimental.chat.system.transform` hook, which OpenCode calls each time it generates a turn. This hook receives the current system prompt as `output.system` and conditionally modifies it.

### The Opt-In Flag File Mechanism

The plugin first checks for user consent by verifying the existence of a flag file at `~/.config/opencode/.i-have-adhd-always` (respecting `$XDG_CONFIG_HOME` if set). If this file does not exist, the hook returns immediately without modifying the prompt, ensuring zero overhead for users who have not opted in.

```javascript
// From .opencode/plugins/i-have-adhd.mjs
'experimental.chat.system.transform': async (_input, output) => {
  const flagPath = path.join(
    process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
    'opencode',
    '.i-have-adhd-always'
  );
  
  if (!fs.existsSync(flagPath)) return;  // Silent exit if not opted in
  
  // ... ruleset processing logic
}

```

### Processing the Ruleset Source

When the flag file exists, the plugin reads the canonical ruleset from [`skills/i-have-adhd/SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md). It strips the YAML frontmatter (the metadata block between `---` delimiters at the top of the file) and extracts only the instructional content.

```javascript
const rulesetBody = () => {
  const skillPath = path.join(__dirname, '..', '..', 'skills', 'i-have-adhd', 'SKILL.md');
  const content = fs.readFileSync(skillPath, 'utf-8');
  // Strip YAML frontmatter
  return content.replace(/^---\n[\s\S]*?\n---\n/, '').trim();
};

```

### Appending to the System Prompt

The plugin constructs an injection string containing:
1. A header announcing "ADHD MODE ACTIVE (always-on)" with instructions for disabling it
2. The cleaned ruleset content from [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md)

This content is appended to the final system prompt block in the `output.system` array, ensuring the model receives the formatting constraints before generating each response.

```javascript
const body = rulesetBody();
const header = `ADHD MODE ACTIVE (always-on). The ruleset below applies to every response. 
  "stop adhd mode" turns it off for this session; delete ${flagPath} to turn always-on off for good.`;
const injected = `${header}\n\n${body}`;

// Append to existing system prompt or create new entry
if (output.system.length) {
  output.system[output.system.length - 1] += `\n\n${injected}`;
} else {
  output.system.push(injected);
}

```

## Enabling and Disabling Always-On Mode

Users control this behavior through filesystem operations and chat commands:

```bash

# Enable always-on injection for every turn

touch ~/.config/opencode/.i-have-adhd-always

# Disable for current session only (model-recognized command)

# Type in chat: "stop adhd mode"

# Permanently disable always-on mode

rm ~/.config/opencode/.i-have-adhd-always

```

## Claude Code Parity

The repository maintains feature parity with Claude Code through `hooks/always-on.mjs`, which implements an identical injection mechanism for Claude's ecosystem. The Claude version checks `~/.claude/.i-have-adhd-always` and appends the same ruleset to Claude's system prompts. By using distinct configuration directories (`~/.config/opencode/` vs `~/.claude/`), the two implementations remain independent, allowing users to enable the feature selectively for each tool.

## Summary

- OpenCode loads the i-have-adhd plugin from `.opencode/plugins/i-have-adhd.mjs`, which registers the skill and the `/i-have-adhd` command.
- The `experimental.chat.system.transform` hook executes on every turn, checking for the opt-in flag at `~/.config/opencode/.i-have-adhd-always`.
- When enabled, the hook reads [`skills/i-have-adhd/SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md), strips YAML frontmatter, and appends the ruleset to the system prompt with an activation header.
- The implementation mirrors Claude Code's `hooks/always-on.mjs` but uses OpenCode-specific configuration paths to maintain isolation between tools.
- Users disable the feature either temporally via the "stop adhd mode" chat command or permanently by deleting the flag file.

## Frequently Asked Questions

### How does the plugin avoid impacting performance when disabled?

The `experimental.chat.system.transform` hook returns early with a synchronous `fs.existsSync` check against the flag file path. If the file does not exist at `~/.config/opencode/.i-have-adhd-always`, the function exits before performing any disk reads or string processing, ensuring zero per-turn overhead for non-users.

### Can the ruleset be modified without restarting OpenCode?

Yes. Because the plugin reads [`skills/i-have-adhd/SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md) during each invocation of the transform hook, changes to the ruleset file take effect immediately on the next turn. There is no caching mechanism that requires a restart of the OpenCode runtime.

### What distinguishes the OpenCode implementation from the Claude Code version?

While both use the same [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md) source file, the OpenCode version in `.opencode/plugins/i-have-adhd.mjs` utilizes the `experimental.chat.system.transform` hook specific to OpenCode's architecture, whereas Claude Code uses `hooks/always-on.mjs` with Claude's native hook system. The implementations also use separate flag file locations (`~/.config/opencode/` vs `~/.claude/`) to allow independent toggling of the feature per tool.

### Is the ruleset injection secure against prompt injection attacks?

The plugin reads only from the local filesystem path [`skills/i-have-adhd/SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md) within the repository, eliminating network-based attack vectors. However, users should ensure the [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md) file remains unmodified by unauthorized processes, as the plugin executes with the same privileges as the OpenCode runtime and appends file contents directly into the system prompt chain.