i-have-adhd Hook System Architecture: How hooks.json and always-on.mjs Work Together
The i-have-adhd repository implements a lightweight, opt-in hook system using a declarative JSON registry (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 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 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.
// 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 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.
// 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:
- Parses
hooks.jsonand matches theSessionStartevent against the current transition state - Spawns the Node.js command defined in the registry with a 30-second execution budget
- Executes
always-on.mjs, which checks for the.i-have-adhd-alwaysflag file - If opted in, streams the processed skill content to stdout; otherwise exits immediately with code
0 - 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:
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:
rm ~/.claude/.i-have-adhd-always
Summary
- The architecture separates declarative event mapping (
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.mdbefore 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.
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 →