# i-have-adhd Hook System Architecture: How hooks.json and always-on.mjs Work Together

> Explore the i-have-adhd hook system architecture. Learn how hooks.json and always-on.mjs automate ADHD skill rule injection at Claude session start for a smoother experience.

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

---

**The i-have-adhd repository implements a lightweight, opt-in hook system using a declarative JSON registry ([`hooks.json`](https://github.com/ayghri/i-have-adhd/blob/main/hooks.json)) that triggers a Node.js bootstrap script (`always-on.mjs`) to automatically inject ADHD-friendly skill rules at the start of every Claude session when users create a specific flag file.**

The `ayghri/i-have-adhd` project extends Claude Code with persistent ADHD assistance through a clean separation of concerns: declarative event mapping and imperative execution logic. The architecture relies on two core files—[`hooks/hooks.json`](https://github.com/ayghri/i-have-adhd/blob/main/hooks/hooks.json) defining when to run, and `hooks/always-on.mjs` determining what to inject—creating a robust, cross-platform mechanism for modifying system behavior.

## The Declarative Registry: hooks.json

The [`hooks/hooks.json`](https://github.com/ayghri/i-have-adhd/blob/main/hooks/hooks.json) file serves as the central **hook registry**, mapping session lifecycle events to executable commands. According to the source code, it currently declares a single `SessionStart` event that matches four distinct session state transitions via the regex pattern `"startup|resume|clear|compact"`.

For each matched event, the registry specifies a `command` type hook that spawns Node.js with a dynamic import one-liner. This command resolves the plugin installation directory from the `CLAUDE_PLUGIN_ROOT` or `PLUGIN_ROOT` environment variable, constructs a file URL pointing to `hooks/always-on.mjs`, and executes the module. The configuration enforces a **30-second timeout** and displays the status message `"Checking i-have-adhd always-on flag..."` during session initialization.

```json
// https://github.com/ayghri/i-have-adhd/blob/main/hooks/hooks.json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear|compact",
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"(async()=>{const root=process.env.CLAUDE_PLUGIN_ROOT||process.env.PLUGIN_ROOT;if(root)await import(require('node:url').pathToFileURL(require('node:path').join(root,'hooks','always-on.mjs')).href)})().catch(()=>{})\"",
            "timeout": 30,
            "statusMessage": "Checking i-have-adhd always-on flag..."
          }
        ]
      }
    ]
  }
}

```

## The Imperative Engine: always-on.mjs

The `hooks/always-on.mjs` script contains the runtime logic for conditionally injecting the ADHD skill rules. Running in a **Node.js environment** for macOS, Linux, and Windows compatibility, the script first checks for the existence of an opt-in flag file at `$CLAUDE_CONFIG_DIR/.i-have-adhd-always`, defaulting to `~/.claude/.i-have-adhd-always` when the environment variable is unset.

If the flag file is absent, the script exits silently with code `0` to avoid blocking session startup. When the flag exists, the script resolves the path to [`skills/i-have-adhd/SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md) relative to its own directory using `fileURLToPath(import.meta.url)`, reads the markdown content, and strips any leading **YAML frontmatter** using the regex `/^---[^\S\r\n]*\r?\n[\s\S]*?\r?\n---[^\S\r\n]*(?:\r?\n|$)/`. The cleaned ruleset is written to **stdout** prefixed with an activation notice explaining how to disable the mode.

```javascript
// https://github.com/ayghri/i-have-adhd/blob/main/hooks/always-on.mjs
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

try {
  const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
  const flagPath = path.join(claudeDir, ".i-have-adhd-always");

  // Only fire when the user has opted in.
  if (!fs.existsSync(flagPath)) process.exit(0);

  // Resolve SKILL.md relative to this script's own location.
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
  const skillPath = path.join(scriptDir, "..", "skills", "i-have-adhd", "SKILL.md");
  if (!fs.existsSync(skillPath)) process.exit(0);

  // Strip a leading YAML front-matter block.
  const body = fs
    .readFileSync(skillPath, "utf8")
    .replace(/^---[^\S\r\n]*\r?\n[\s\S]*?\r?\n---[^\S\r\n]*(?:\r?\n|$)/, "")
    .replace(/(?:\r?\n)+$/, "");

  process.stdout.write(
    "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.\n\n${body}\n`,
  );
} catch {
  // Never block session start.
  process.exit(0);
}

```

## Execution Flow and Error Handling

When a Claude session initializes, the runtime executes the following sequence:

1. Parses [`hooks.json`](https://github.com/ayghri/i-have-adhd/blob/main/hooks.json) and matches the `SessionStart` event against the current transition state
2. Spawns the Node.js command defined in the registry with a 30-second execution budget
3. Executes `always-on.mjs`, which checks for the `.i-have-adhd-always` flag file
4. If opted in, streams the processed skill content to stdout; otherwise exits immediately with code `0`
5. Injects the output into the system prompt context, shaping every subsequent model response until the user explicitly disables the mode

This design ensures **graceful degradation**—the `try-catch` block wrapping the entire script guarantees that any runtime errors result in `process.exit(0)`, ensuring that hook failures never prevent session initialization.

## Managing Always-On Mode

Users control the hook behavior through filesystem operations rather than dynamic configuration.

To enable persistent ADHD mode across all sessions:

```bash
mkdir -p ~/.claude && touch ~/.claude/.i-have-adhd-always

```

To disable the mode temporarily for the current session, send the command:

```

stop adhd mode

```

To permanently uninstall the always-on behavior:

```bash
rm ~/.claude/.i-have-adhd-always

```

## Summary

- The architecture separates **declarative event mapping** ([`hooks.json`](https://github.com/ayghri/i-have-adhd/blob/main/hooks.json)) from **imperative execution logic** (`always-on.mjs`), allowing independent updates to either component.
- The system uses a **filesystem flag** (`.i-have-adhd-always`) for opt-in consent, ensuring users explicitly choose to enable the behavior.
- **Cross-platform compatibility** is achieved through Node.js built-in modules (`node:fs`, `node:path`, `node:url`) avoiding shell-specific dependencies.
- All execution paths terminate with exit code `0`, implementing a "fail-safe" philosophy that prioritizes session availability over hook functionality.
- The script performs **runtime content processing**, stripping YAML frontmatter from [`SKILL.md`](https://github.com/ayghri/i-have-adhd/blob/main/SKILL.md) before injection to ensure clean system prompt integration.

## Frequently Asked Questions

### What triggers the i-have-adhd hook to run?

The hook triggers on four specific session lifecycle events defined in the `SessionStart` matcher: `startup`, `resume`, `clear`, and `compact`. These correspond to initiating a new conversation, returning to an existing one, clearing the message context, or compacting the conversation history to save tokens.

### Where does the always-on flag file need to be located?

By default, the script looks for `.i-have-adhd-always` in the directory specified by the `CLAUDE_CONFIG_DIR` environment variable, falling back to `~/.claude/` if that variable is unset. The path is constructed using `path.join(os.homedir(), ".claude")` when resolved on standard systems.

### Why does always-on.mjs strip YAML frontmatter from SKILL.md?

The script removes YAML frontmatter—content delimited by triple dashes at the start of the file—to prevent metadata headers from polluting the system prompt. The regex `/^---[^\S\r\n]*\r?\n[\s\S]*?\r?\n---[^\S\r\n]*(?:\r?\n|$)/` ensures only the actual markdown ruleset is injected, maintaining clean instructions for the model.

### Can the hook system be extended to handle other session events?

Yes. The JSON registry structure supports adding new event types alongside `SessionStart`, such as `MessageReceived` or `SessionEnd`. Each new entry requires a `matcher` pattern, a `command` string, a `timeout` value in seconds, and an optional `statusMessage`. New imperative scripts can be created in the `hooks/` directory and referenced via the same Node.js dynamic import pattern used by `always-on.mjs`.