How CI Validation Works with `plugin-load-check.yml` in the i-have-adhd Repository

TLDR: The repository uses a dedicated GitHub Actions workflow at .github/workflows/plugin-load-check.yml that splits validation into two jobs — hook-parity for unit-testing native hook implementations and load for end-to-end plugin loading via the Claude Code CLI — to catch both static schema and runtime errors before any pull request merges.

The ayghri/i-have-adhd repository contains a Claude Code plugin that ships always-on hooks and skill definitions. Maintaining such a plugin requires more than schema validation — hooks can fail to load at runtime even when their JSON declarations are syntactically valid. That's why the maintainers built a two-pronged CI validation pipeline using plugin-load-check.yml, a GitHub Actions workflow that verifies both hook parity across operating systems and actual plugin installation through the Claude Code CLI. This article breaks down exactly how that workflow is configured, what each job validates, and how you can reproduce the checks locally.

What Triggers the CI Validation Workflow

The plugin-load-check.yml workflow is defined in .github/workflows/plugin-load-check.yml and runs on two event types, as specified at lines L6-L15 of the workflow file:

  • pull_request — fires only when specific paths change: .claude-plugin/**, hooks/**, skills/**, the test file tests/test_always_on_hooks.py, or the workflow file itself. This path filtering keeps CI fast by avoiding unnecessary runs on unrelated changes.
  • push — runs for every commit delivered to the main branch, guaranteeing that the deployed plugin is always validated.

Inside the Workflow: Two CI Jobs, Two Validation Layers

The workflow defines two jobs that complement each other. According to the source at plugin-load-check.yml#L18-L30 and #L31-L48, these are the hook-parity job and the load job.

The hook-parity Job: Cross-Platform Unit Testing

The hook-parity job runs on a matrix of both Ubuntu and Windows runners. Its purpose is to verify that the native hook implementations load correctly across platforms — catching platform-specific bugs before they reach users.

The job executes the following steps:

  1. Check out the repository with actions/checkout@v4.
  2. Set up Python 3.x using actions/setup-python@v5.
  3. Run the unit test suite with this command:
python -m unittest tests.test_always_on_hooks -v

This command exercises the hook implementations against the test suite defined in tests/test_always_on_hooks.py. If any hook fails to load or behave as expected, the job fails, blocking the pull request from merging.

The load Job: End-to-End Plugin Installation

The load job runs on Ubuntu only and simulates a real user environment by installing the plugin through the official Claude Code CLI. It performs these steps:

  1. Check out the repository.
  2. Install the Claude Code CLI globally:
npm install -g @anthropic-ai/claude-code
  1. Create a temporary, sandboxed config directory:
export CLAUDE_CONFIG_DIR="$(mktemp -d)"
mkdir -p "$CLAUDE_CONFIG_DIR"
  1. Add the current checkout to the Claude marketplace:
claude plugin marketplace add "$GITHUB_WORKSPACE"
  1. Install the plugin itself:
claude plugin install i-have-adhd@i-have-adhd
  1. List installed plugins and grep for the enabled marker:
claude plugin list | grep "✔ enabled"

If the ✔ enabled marker is absent after installation, the step fails with an explicit error message, causing the entire workflow to fail. This step catches dynamic loading issues that static schema validation would miss — for example, duplicate hook declarations that Claude Code's loader rejects at runtime.

Why You Need Both Static and Dynamic Validation

Schecking schema validation alone cannot catch runtime load errors. A .claude-plugin/plugin.json or hooks/hooks.json can be perfectly valid JSON yet still fail when the CLI attempts to instantiate the hooks. The hook-parity job covers static and behavioral correctness through the Python unit tests, while the load job covers dynamic loading end-to-end. Together they provide a comprehensive safety net that no single check can deliver.

The key files involved in this validation pipeline:

File Role in CI Validation
.github/workflows/plugin-load-check.yml The workflow that orchestrates both jobs
tests/test_always_on_hooks.py Unit test suite used by the hook-parity job
.claude-plugin/plugin.json Plugin manifest parsed by the Claude Code CLI
hooks/hooks.json Central declaration of always-on hook definitions
skills/i-have-adhd/agents/openai.yaml Example skill definition that affects plugin behavior

Running the CI Validation Locally

You can replicate both CI checks on your own machine. For the hook parity test:

python -m unittest tests.test_always_on_hooks -v

For the end-to-end plugin load check:


# Install Claude Code CLI (once)

npm install -g @anthropic-ai/claude-code

# Create a temporary config directory

export CLAUDE_CONFIG_DIR="$(mktemp -d)"
mkdir -p "$CLAUDE_CONFIG_DIR"

# Add the current repository to the marketplace and install

claude plugin marketplace add .
claude plugin install i-have-adhd@i-have-adhd

# Confirm the plugin is enabled

claude plugin list | grep "✔ enabled"

Summary

  • plugin-load-check.yml is the single CI workflow that validates every plugin change in the ayghri/i-have-adhd repository.
  • The hook-parity job unit-tests native hooks on both Ubuntu and Windows via python -m unittest tests.test_always_on_hooks -v.
  • The load job performs a real YAML-free end-to-end install using the Claude Code CLI, verifying that the plugin reports itself as ✔ enabled.
  • Combined, these jobs catch both schema-level and runtime load errors — duplicate hook declarations, invalid manifests, or platform-specific hook failures — before they ever reach a merged commit.

Frequently Asked Questions

What does the hook-parity job actually test?

The hook-parity job runs the Python unit tests in tests/test_always_on_hooks.py against the native hook implementations on both Ubuntu and Windows. It verifies that the hooks load and behave identically across operating systems, which protects against platform-specific runtime failures.

Why does the load job use a temporary CLAUDE_CONFIG_DIR?

The load job sets CLAUDE_CONFIG_DIR to a temporary directory created with mktemp -d. This ensures the plugin installation happens in a sandboxed, isolated configuration space so the CI test doesn't interfere with a developer's real Claude Code configuration or the runner's settings.

What happens if the plugin does not display the enabled marker?

If the claude plugin list | grep "✔ enabled" step does not find the marker, the step exits with a non-zero code and an explicit error message. This fails the load job, which blocks the pull request from merging and signals the developer to investigate the plugin's loading issue.

Why is the workflow triggered only for specific file paths on pull requests?

The pull_request trigger filters by paths like .claude-plugin/**, hooks/**, and skills/**. That path whitelist means CI only runs when changes actually affect the plugin's loading behavior, saving runner time and avoiding noisy workflow executions for unrelated code changes.

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 →