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

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.

// 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. It strips the YAML frontmatter (the metadata block between --- delimiters at the top of the file) and extracts only the instructional content.

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

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.

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:


# 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, 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 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 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 within the repository, eliminating network-based attack vectors. However, users should ensure the 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →