Skip to content

feat: add missing CI/CD pipeline files#97

Merged
affaan-m merged 2 commits intoaffaan-m:mainfrom
roeiba:main
Jan 29, 2026
Merged

feat: add missing CI/CD pipeline files#97
affaan-m merged 2 commits intoaffaan-m:mainfrom
roeiba:main

Conversation

@roeiba
Copy link
Copy Markdown
Contributor

@roeiba roeiba commented Jan 27, 2026

Summary

This PR adds the complete CI/CD pipeline files that were promised in #72 but weren't included due to a force-push.

What's Included

GitHub Actions Workflows

  • ci.yml — Multi-platform matrix (Ubuntu, macOS, Windows) × Node 18/20/22 × npm/pnpm/yarn/bun
  • maintenance.yml — Scheduled dependency/security checks
  • release.yml — Automated releases on version tags
  • 3 reusable workflows for test, validate, and release steps

Linting & Config

  • eslint.config.js — ESLint v9 Flat Config
  • commitlint.config.js — Conventional commits enforcement
  • .markdownlint.json — Markdown quality rules
  • package.json — Dev dependencies and npm scripts

Validation Scripts

  • scripts/ci/validate-agents.js
  • scripts/ci/validate-commands.js
  • scripts/ci/validate-hooks.js
  • scripts/ci/validate-rules.js
  • scripts/ci/validate-skills.js

Schemas

  • schemas/hooks.schema.json
  • schemas/plugin.schema.json
  • schemas/package-manager.schema.json

Tests & Templates

  • tests/integration/hooks.test.js — End-to-end hook validation
  • .github/PULL_REQUEST_TEMPLATE.md — PR template

Test Results

  • ✅ 67 tests passing
  • ✅ All 5 validators passing (12 agents, 23 commands, 15 hooks, 8 rules, 16 skills)

This completes the CI/CD infrastructure originally planned in #72.

Summary by CodeRabbit

  • New Features

    • Full CI workflows: multi-OS/multi-Node/multi-package-manager testing, reusable test/validate/release workflows, and automated releases on version tags.
    • PR template to standardize pull request descriptions.
  • Chores

    • Commit message rules, ESLint and Markdown lint configs, and dev tooling setup.
    • Added JSON schemas and validation scripts for repository configurations.
  • Tests

    • New integration tests for hook behavior and processing.

✏️ Tip: You can customize this high-level summary in your review settings.

- GitHub Actions: ci.yml, maintenance.yml, release.yml + 3 reusable workflows
- Linting: ESLint v9 flat config, commitlint, markdownlint
- Schemas: hooks.schema.json, plugin.schema.json, package-manager.schema.json
- Validation: validate-{agents,commands,hooks,rules,skills}.js
- Tests: integration/hooks.test.js for end-to-end hook validation
- PR template for consistent contributions

Multi-platform matrix: Ubuntu/macOS/Windows × Node 18/20/22 × npm/pnpm/yarn/bun
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Adds CI/CD infrastructure, lint/commit configs, JSON schemas, multiple CI validation scripts, an integration test suite for hooks, and a PR template; plus workflows for test matrices, validation, security, maintenance, and release automation.

Changes

Cohort / File(s) Summary
GitHub Actions workflows
.github/workflows/ci.yml, .github/workflows/maintenance.yml, .github/workflows/release.yml, .github/workflows/reusable-test.yml, .github/workflows/reusable-validate.yml, .github/workflows/reusable-release.yml
New CI, maintenance, release, and reusable workflows: test matrix (OS/node/package managers), per-manager caching/install, validation/security/lint jobs, scheduled maintenance, tag-validated release and reusable release/test/validate workflows.
Validation & CI scripts
scripts/ci/validate-agents.js, scripts/ci/validate-commands.js, scripts/ci/validate-hooks.js, scripts/ci/validate-rules.js, scripts/ci/validate-skills.js
New Node.js validation scripts for agents (frontmatter), commands (non-empty), hooks (structure and types), rules (non-empty), and skills (SKILL.md presence/content); exit codes on failures.
Integration tests
tests/integration/hooks.test.js
New comprehensive integration test suite exercising hook execution, input/output handling, exit codes, resilience, and realistic scenarios.
Schemas
schemas/hooks.schema.json, schemas/package-manager.schema.json, schemas/plugin.schema.json
New JSON Schemas: hooks config (object/array formats, matcher/hooks), package manager enum (npm,pnpm,yarn,bun), and plugin metadata (name required, optional description/author/repository/license).
Linters & commit rules
.markdownlint.json, commitlint.config.js, eslint.config.js
Added markdownlint config, commitlint rules (conventional types, header length), and ESLint config (ES2022, no-unused-vars exceptions, no-undef, eqeqeq).
Tooling config
package.json
New package.json with devDependencies for ESLint and markdownlint tooling.
PR template
.github/PULL_REQUEST_TEMPLATE.md
New standardized pull request template with description, type-of-change checkboxes, and validation checklist.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I dug a tunnel of YAML and script,
Linters and tests in neat little crypt,
Schemas like carrots line every path,
CI hops along — no aftermath! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add missing CI/CD pipeline files' directly and accurately summarizes the main change—adding CI/CD pipeline infrastructure (workflows, config files, validation scripts, schemas, and tests).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @.github/workflows/maintenance.yml:
- Around line 33-39: The "Run security audit" step should install dependencies
before running the audit so the dependency tree is accurate: modify the step
(step name "Run security audit") to run "npm install" (or "npm ci") when
package-lock.json exists and only then run "npm audit --audit-level=high"; keep
the existing conditional that skips the audit when package-lock.json is missing
and ensure the install command runs inside that same conditional branch.

In @.github/workflows/reusable-release.yml:
- Around line 37-48: The Generate changelog step (id: changelog) uses a static
"EOF" delimiter when writing to GITHUB_OUTPUT which can be present in commit
messages and truncate the output; change the delimiter to a unique token (e.g.,
include a timestamp or generated UUID) and use that same token for the three
echo lines that wrap COMMITS so the heredoc cannot be prematurely closed (update
references to PREV_TAG and COMMITS logic remain unchanged).

In `@schemas/hooks.schema.json`:
- Around line 13-48: The JSON schema's additionalProperties currently allows any
event key, which conflicts with the runtime validator in validate-hooks.js that
expects only
['PreToolUse','PostToolUse','PreCompact','SessionStart','SessionEnd','Stop','Notification','SubagentStop'];
update the schema (the additionalProperties definition) to constrain keys to
that exact set (use an enum for property names or switch to
patternProperties/enumerated property names) so schema validation matches the
validator, or alternatively add a clear top-level description noting the
stricter runtime validation in validate-hooks.js.

In `@scripts/ci/validate-hooks.js`:
- Around line 68-71: The code checks hook.command can be a string or an array
(hook.command), but the JSON schema in hooks.schema.json only allows "type":
"string", causing a mismatch; update the schema's "command" property to accept
either a string or an array of strings (e.g., use oneOf with { "type": "string"
} and { "type": "array", "items": { "type": "string" } }) so schema validation
matches the runtime validator, and run/adjust any schema-based tests if present.

In `@tests/integration/hooks.test.js`:
- Around line 117-133: The stdin 'error' handler in _runInlineHook can hit a TDZ
because timer is declared after the handler; declare timer in the outer scope
(e.g., let timer;) before setting up proc.stdin.on('error', ...) and assign
timer = setTimeout(...) later, and guard clearTimeout by checking the variable
(if (timer) clearTimeout(timer); or if (typeof timer !== 'undefined')
clearTimeout(timer);) so clearTimeout cannot run before timer is initialized.
🧹 Nitpick comments (15)
package.json (1)

1-8: Consider adding standard package.json fields and npm scripts.

The package.json is missing standard fields (name, version, description, license) and npm scripts that CI workflows typically reference (e.g., lint, test, validate).

♻️ Suggested additions
 {
+  "name": "everything-claude-code",
+  "version": "1.0.0",
+  "description": "CI/CD infrastructure for Claude Code",
+  "private": true,
+  "scripts": {
+    "lint": "eslint . && markdownlint '**/*.md'",
+    "validate": "node scripts/ci/validate-commands.js"
+  },
   "devDependencies": {
     "@eslint/js": "^9.39.2",
     "eslint": "^9.39.2",
     "globals": "^17.1.0",
     "markdownlint-cli": "^0.47.0"
   }
 }
.github/PULL_REQUEST_TEMPLATE.md (1)

4-11: Consider aligning commit types with commitlint configuration.

The template is missing some commit types defined in commitlint.config.js: style, perf, build, and revert.

♻️ Suggested additions for consistency
 ## Type of Change
 - [ ] `fix:` Bug fix
 - [ ] `feat:` New feature
 - [ ] `refactor:` Code refactoring
 - [ ] `docs:` Documentation
+- [ ] `style:` Code style/formatting
+- [ ] `perf:` Performance improvement
 - [ ] `test:` Tests
 - [ ] `chore:` Maintenance/tooling
 - [ ] `ci:` CI/CD changes
+- [ ] `build:` Build system changes
+- [ ] `revert:` Revert previous commit
scripts/ci/validate-commands.js (2)

20-29: Add error handling for file read operations.

If a file is deleted or becomes unreadable between readdirSync and readFileSync, the script will crash. Wrapping in try-catch provides graceful error handling.

♻️ Proposed fix
   for (const file of files) {
     const filePath = path.join(COMMANDS_DIR, file);
-    const content = fs.readFileSync(filePath, 'utf-8');
-
-    // Validate the file is non-empty readable markdown
-    if (content.trim().length === 0) {
-      console.error(`ERROR: ${file} - Empty command file`);
-      hasErrors = true;
+    try {
+      const content = fs.readFileSync(filePath, 'utf-8');
+
+      // Validate the file is non-empty readable markdown
+      if (content.trim().length === 0) {
+        console.error(`ERROR: ${file} - Empty command file`);
+        hasErrors = true;
+      }
+    } catch (err) {
+      console.error(`ERROR: ${file} - Failed to read: ${err.message}`);
+      hasErrors = true;
     }
   }

11-15: Consider logging when directory is missing for better CI visibility.

Silently exiting with code 0 when the directory doesn't exist may hide configuration issues. A more visible log message would help debugging.

♻️ Suggested improvement
   if (!fs.existsSync(COMMANDS_DIR)) {
-    console.log('No commands directory found, skipping validation');
+    console.log('⚠️  No commands directory found at', COMMANDS_DIR, '- skipping validation');
     process.exit(0);
   }
scripts/ci/validate-skills.js (1)

23-35: Handle non-file SKILL.md and read errors per directory.

A directory or permission issue will throw and abort the entire run. Wrapping in statSync/try-catch keeps validation reporting consistent.

🛠️ Suggested refactor
-    const content = fs.readFileSync(skillMd, 'utf-8');
-    if (content.trim().length === 0) {
-      console.error(`ERROR: ${dir}/SKILL.md - Empty file`);
-      hasErrors = true;
-      continue;
-    }
+    try {
+      const stat = fs.statSync(skillMd);
+      if (!stat.isFile()) {
+        console.error(`ERROR: ${dir}/SKILL.md - Not a file`);
+        hasErrors = true;
+        continue;
+      }
+      const content = fs.readFileSync(skillMd, 'utf-8');
+      if (content.trim().length === 0) {
+        console.error(`ERROR: ${dir}/SKILL.md - Empty file`);
+        hasErrors = true;
+        continue;
+      }
+    } catch (err) {
+      console.error(`ERROR: ${dir}/SKILL.md - ${err.message}`);
+      hasErrors = true;
+      continue;
+    }
schemas/plugin.schema.json (1)

4-12: Consider additionalProperties: false to tighten the schema.

If the goal is to enforce a strict plugin manifest, disallowing unknown keys will prevent config drift.

🛠️ Suggested change
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "Claude Plugin Configuration",
   "type": "object",
   "required": ["name"],
+  "additionalProperties": false,
   "properties": {
     "name": { "type": "string" },
     "description": { "type": "string" },
     "author": { "type": "string" },
     "repository": { "type": "string" },
     "license": { "type": "string" }
   }
 }
.github/workflows/release.yml (2)

28-39: Edge case: First release may fail to generate changelog correctly.

The git describe --tags --abbrev=0 HEAD^ command on line 31 can fail in scenarios beyond "no previous tag":

  1. If HEAD^ doesn't exist (single-commit repo)
  2. If HEAD^ exists but has no reachable tags

When PREV_TAG is empty, line 33 runs git log --pretty=format:"- %s" HEAD, which only logs the HEAD commit, not all commits leading to the first release.

♻️ Suggested fix for robust first-release handling
       - name: Generate changelog
         id: changelog
         run: |
-          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
+          # Get all tags sorted by version, excluding current tag
+          PREV_TAG=$(git tag --sort=-v:refname | grep -v "^${{ github.ref_name }}$" | head -n1 || echo "")
           if [ -z "$PREV_TAG" ]; then
-            COMMITS=$(git log --pretty=format:"- %s" HEAD)
+            # First release: include all commits
+            COMMITS=$(git log --pretty=format:"- %s")
           else
             COMMITS=$(git log --pretty=format:"- %s" ${PREV_TAG}..HEAD)
           fi

3-5: Consider tightening the tag filter pattern.

The current v* pattern triggers the workflow for any tag starting with v (e.g., vfoo, v1). While the validation step catches invalid formats, a more specific pattern would prevent unnecessary workflow runs.

♻️ Optional: Use a stricter tag pattern
 on:
   push:
-    tags: ['v*']
+    tags: ['v[0-9]*']
schemas/hooks.schema.json (1)

67-83: Consider using $defs to avoid duplicating the hook item schema.

The hook item structure (with type and command properties) is defined identically in both the object format (lines 27-41) and array format (lines 69-82). Using $defs would improve maintainability.

♻️ Example using $defs for reusability
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Claude Code Hooks Configuration",
  "$defs": {
    "hookItem": {
      "type": "object",
      "required": ["type", "command"],
      "properties": {
        "type": { "type": "string" },
        "command": { "type": "string" }
      }
    },
    "matcherEntry": {
      "type": "object",
      "required": ["matcher", "hooks"],
      "properties": {
        "matcher": { "type": "string" },
        "hooks": {
          "type": "array",
          "items": { "$ref": "#/$defs/hookItem" }
        },
        "description": { "type": "string" }
      }
    }
  },
  "oneOf": [
    {
      "type": "object",
      "required": ["hooks"],
      "properties": {
        "$schema": { "type": "string" },
        "hooks": {
          "type": "object",
          "additionalProperties": {
            "type": "array",
            "items": { "$ref": "#/$defs/matcherEntry" }
          }
        }
      }
    },
    {
      "type": "array",
      "items": { "$ref": "#/$defs/matcherEntry" }
    }
  ]
}
scripts/ci/validate-hooks.js (1)

77-86: Legacy array format validation is incomplete compared to object format.

The object format (lines 46-76) validates matcher, hooks array presence, and each hook's type/command fields. The legacy array format (lines 79-84) only checks for the matcher field, allowing malformed hook entries to pass validation silently.

♻️ Suggested: Add hooks array validation for legacy format
   } else if (Array.isArray(hooks)) {
     // Array format (legacy)
     for (let i = 0; i < hooks.length; i++) {
       const hook = hooks[i];
       if (!hook.matcher) {
         console.error(`ERROR: Hook ${i} missing 'matcher' field`);
         hasErrors = true;
       }
+      if (!hook.hooks || !Array.isArray(hook.hooks)) {
+        console.error(`ERROR: Hook ${i} missing 'hooks' array`);
+        hasErrors = true;
+      } else {
+        for (let j = 0; j < hook.hooks.length; j++) {
+          const h = hook.hooks[j];
+          if (!h.type || typeof h.type !== 'string') {
+            console.error(`ERROR: Hook ${i}.hooks[${j}] missing or invalid 'type' field`);
+            hasErrors = true;
+          }
+          if (!h.command || (typeof h.command !== 'string' && !Array.isArray(h.command))) {
+            console.error(`ERROR: Hook ${i}.hooks[${j}] missing or invalid 'command' field`);
+            hasErrors = true;
+          }
+        }
+      }
       totalMatchers++;
     }
.github/workflows/ci.yml (3)

186-189: Redundant error suppression in security job.

Line 187 uses || true to prevent failure, and line 188 sets continue-on-error: true. Both achieve the same effect. Additionally, with this configuration, security vulnerabilities will never block CI, which may not be the intended behavior.

Consider either:

  1. Remove || true and keep continue-on-error: true for visibility in job status
  2. Remove both to make high-severity vulnerabilities block the build
       - name: Run npm audit
-        run: npm audit --audit-level=high || true
-        continue-on-error: true
+        run: npm audit --audit-level=high
+        continue-on-error: true  # Allows PR to proceed, but marks job as failed

204-206: Lint job installs dependencies inline rather than from package.json.

Installing eslint, @eslint/js, globals, and markdownlint-cli directly (line 205) bypasses the version constraints in package.json, potentially causing inconsistent linting behavior between CI and local development.

Since these are likely defined in package.json devDependencies, use npm ci instead:

       - name: Install dependencies
-        run: npm install --save-dev eslint `@eslint/js` globals markdownlint-cli
+        run: npm ci

117-117: --ignore-engines may mask Node.js compatibility issues.

The yarn install --ignore-engines flag suppresses engine compatibility warnings. In a matrix that explicitly tests Node 18, 20, and 22, this could hide genuine incompatibilities that users would encounter.

Consider removing the flag to surface engine mismatches, or document why it's necessary:

-            yarn) yarn install --ignore-engines ;;
+            yarn) yarn install ;;
tests/integration/hooks.test.js (1)

295-309: Ensure temp directories are cleaned up on assertion failure.
Both blocks clean up after assertions; if an assert throws, the temp dir is left behind. Wrap in try/finally to guarantee cleanup.

♻️ Suggested change
   if (await asyncTest('hooks handle missing files gracefully', async () => {
     const testDir = createTestDir();
     const transcriptPath = path.join(testDir, 'nonexistent.jsonl');
-
-    const result = await runHookWithInput(
-      path.join(scriptsDir, 'evaluate-session.js'),
-      {},
-      { CLAUDE_TRANSCRIPT_PATH: transcriptPath }
-    );
-
-    // Should not crash, just skip processing
-    assert.strictEqual(result.code, 0, 'Should exit 0 for missing file');
-
-    cleanupTestDir(testDir);
+    try {
+      const result = await runHookWithInput(
+        path.join(scriptsDir, 'evaluate-session.js'),
+        {},
+        { CLAUDE_TRANSCRIPT_PATH: transcriptPath }
+      );
+      // Should not crash, just skip processing
+      assert.strictEqual(result.code, 0, 'Should exit 0 for missing file');
+    } finally {
+      cleanupTestDir(testDir);
+    }
   })) passed++; else failed++;
@@
   if (await asyncTest('evaluate-session processes transcript with sufficient messages', async () => {
     const testDir = createTestDir();
     const transcriptPath = path.join(testDir, 'transcript.jsonl');
@@
-    const result = await runHookWithInput(
-      path.join(scriptsDir, 'evaluate-session.js'),
-      {},
-      { CLAUDE_TRANSCRIPT_PATH: transcriptPath }
-    );
-
-    assert.ok(result.stderr.includes('15 messages'), 'Should process session');
-
-    cleanupTestDir(testDir);
+    try {
+      const result = await runHookWithInput(
+        path.join(scriptsDir, 'evaluate-session.js'),
+        {},
+        { CLAUDE_TRANSCRIPT_PATH: transcriptPath }
+      );
+      assert.ok(result.stderr.includes('15 messages'), 'Should process session');
+    } finally {
+      cleanupTestDir(testDir);
+    }
   })) passed++; else failed++;

Also applies to: 339-363

.github/workflows/reusable-test.yml (1)

77-82: Yarn cache dir command may break on Yarn v2+.
At Line 81, yarn cache dir is v1-specific; Yarn Berry uses yarn config get cacheFolder. Please confirm the Yarn major version in CI and adjust accordingly.

♻️ Compatible option
-      - name: Get yarn cache directory
+      - name: Get yarn cache directory
         if: inputs.package-manager == 'yarn'
         id: yarn-cache-dir
         shell: bash
-        run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
+        run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT

Comment on lines +13 to +48
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"required": [
"matcher",
"hooks"
],
"properties": {
"matcher": {
"type": "string"
},
"hooks": {
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"command"
],
"properties": {
"type": {
"type": "string"
},
"command": {
"type": "string"
}
}
}
},
"description": {
"type": "string"
}
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Schema allows any event type key, but validator restricts to specific events.

The schema uses additionalProperties (lines 13-48) which permits any string as an event type key. However, validate-hooks.js enforces a specific set: ['PreToolUse', 'PostToolUse', 'PreCompact', 'SessionStart', 'SessionEnd', 'Stop', 'Notification', 'SubagentStop'].

This mismatch means documents valid against the schema may fail validation. Consider adding an enum constraint or documenting that runtime validation is stricter than schema validation.

🤖 Prompt for AI Agents
In `@schemas/hooks.schema.json` around lines 13 - 48, The JSON schema's
additionalProperties currently allows any event key, which conflicts with the
runtime validator in validate-hooks.js that expects only
['PreToolUse','PostToolUse','PreCompact','SessionStart','SessionEnd','Stop','Notification','SubagentStop'];
update the schema (the additionalProperties definition) to constrain keys to
that exact set (use an enum for property names or switch to
patternProperties/enumerated property names) so schema validation matches the
validator, or alternatively add a clear top-level description noting the
stricter runtime validation in validate-hooks.js.

Workflow fixes:
- maintenance.yml: Run npm ci before security audit for accurate dependency tree
- reusable-release.yml: Use unique timestamp-based delimiter to prevent heredoc truncation
- ci.yml: Remove redundant || true with continue-on-error, use npm ci for lint job
- ci.yml/reusable-test.yml: Handle both Yarn v1 and v2+ cache directory commands
- ci.yml: Add comment explaining --ignore-engines requirement for Node 18 compat

Schema/Validator fixes:
- hooks.schema.json: Refactor with $defs for reusability, allow command to be string or array
- hooks.schema.json: Add description documenting valid event types
- validate-hooks.js: Add complete validation for legacy array format

Test fixes:
- hooks.test.js: Fix TDZ issue by declaring timer before error handler
- hooks.test.js: Wrap cleanup in try/finally to ensure temp dirs are cleaned on assertion failure
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@schemas/hooks.schema.json`:
- Around line 2-78: The schema declares draft-07 but uses $defs (introduced in
2019-09); update the schema to be consistent by replacing the top-level "$defs"
with "definitions" and update all "$ref" values from "#/$defs/..." to
"#/definitions/..." (references like "$ref": "#/$defs/hookItem" and "$ref":
"#/$defs/matcherEntry") so the document validates under draft-07, or
alternatively change the "$schema" value to the 2019-09 (or later) URI if you
prefer to keep "$defs".

In `@tests/integration/hooks.test.js`:
- Around line 238-244: The tests assume hook.command is a string but the schema
allows arrays; create a normalization helper named toCommandParts(command) that
accepts a string or array and returns [exec, ...args] (for strings, split/regex
to extract executable and args; for arrays, return as-is), then replace direct
uses of command.match() and direct spawn calls—specifically where
blockingCommand is used (in the PreToolUse test), the second spawn site later in
the same test file, and where prHook.hooks[0].command is spawned—by calling
const [exec, ...args] = toCommandParts(...); const proc = spawn(exec, args,
{...}); to ensure array-form commands work safely.
🧹 Nitpick comments (2)
.github/workflows/ci.yml (1)

188-195: Add npm ci for consistency with other jobs.

The security job runs npm audit without installing dependencies, unlike the test and lint jobs. While the lockfile exists and npm audit can work without it, running npm ci ensures a consistent, fresh state across all jobs and aligns with best practices used elsewhere in the workflow.

🛠️ Suggested fix
       - name: Setup Node.js
         uses: actions/setup-node@v4
         with:
           node-version: '20.x'
 
+      - name: Install dependencies for audit
+        run: npm ci
+
       - name: Run npm audit
         run: npm audit --audit-level=high
         continue-on-error: true  # Allows PR to proceed, but marks job as failed if vulnerabilities found
.github/workflows/reusable-test.yml (1)

77-87: Handle Yarn v1 cacheFolder returning undefined.

On Line 83, yarn config get cacheFolder can exit successfully but print undefined on Yarn v1, which sets an invalid cache path and silently skips caching. Guard on the output value before falling back.

🛠️ Suggested fix
-          # Try Yarn Berry first, fall back to Yarn v1
-          if yarn config get cacheFolder >/dev/null 2>&1; then
-            echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
-          else
-            echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
-          fi
+          # Try Yarn Berry first, fall back to Yarn v1
+          cache_dir="$(yarn config get cacheFolder 2>/dev/null || true)"
+          if [ -z "$cache_dir" ] || [ "$cache_dir" = "undefined" ]; then
+            cache_dir="$(yarn cache dir)"
+          fi
+          echo "dir=$cache_dir" >> $GITHUB_OUTPUT
What does `yarn config get cacheFolder` output on Yarn v1 vs Yarn Berry?

Comment on lines +2 to +78
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Claude Code Hooks Configuration",
"description": "Configuration for Claude Code hooks. Event types are validated at runtime and must be one of: PreToolUse, PostToolUse, PreCompact, SessionStart, SessionEnd, Stop, Notification, SubagentStop",
"$defs": {
"hookItem": {
"type": "object",
"required": [
"type",
"command"
],
"properties": {
"type": {
"type": "string"
},
"command": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
},
"matcherEntry": {
"type": "object",
"required": [
"matcher",
"hooks"
],
"properties": {
"matcher": {
"type": "string"
},
"hooks": {
"type": "array",
"items": {
"$ref": "#/$defs/hookItem"
}
},
"description": {
"type": "string"
}
}
}
},
"oneOf": [
{
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"hooks": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/$defs/matcherEntry"
}
}
}
},
"required": [
"hooks"
]
},
{
"type": "array",
"items": {
"$ref": "#/$defs/matcherEntry"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

JSON Schema draft-07 $defs keyword support definitions vs $defs

💡 Result:

  • In JSON Schema draft-07, the standard keyword is definitions, not $defs. $defs did not exist as a draft-07 keyword; it was introduced later as a rename. (opis.io)
  • Starting with draft 2019-09, definitions was renamed to $defs (to align with $ref and shorten the name). (json-schema.org)
  • Compatibility note: later specs/meta-schemas may still allow definitions for backward compatibility in some “default” meta-schemas, but not all validators/meta-schemas treat it as equivalent—some require $defs when using 2019-09 / 2020-12. (datatracker.ietf.org)

So: draft-07 ⇒ use definitions; 2019-09+ ⇒ use $defs (and only rely on definitions in 2019-09+ if you’ve confirmed your chosen meta-schema/validator supports the backward-compat behavior).

Citations:


$defs is not valid in draft‑07; use definitions instead or upgrade the schema version.

Line 2 declares draft‑07, but line 5 uses $defs, which was introduced in draft 2019-09. Draft-07 validators will reject this schema. Either upgrade $schema to http://json-schema.org/draft/2019-09/schema# or use definitions with corresponding $ref updates:

Suggested fix (keep draft‑07)
-  "$defs": {
+  "definitions": {
-            "$ref": "#/$defs/hookItem"
+            "$ref": "#/definitions/hookItem"
-        "$ref": "#/$defs/matcherEntry"
+        "$ref": "#/definitions/matcherEntry"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Claude Code Hooks Configuration",
"description": "Configuration for Claude Code hooks. Event types are validated at runtime and must be one of: PreToolUse, PostToolUse, PreCompact, SessionStart, SessionEnd, Stop, Notification, SubagentStop",
"$defs": {
"hookItem": {
"type": "object",
"required": [
"type",
"command"
],
"properties": {
"type": {
"type": "string"
},
"command": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
},
"matcherEntry": {
"type": "object",
"required": [
"matcher",
"hooks"
],
"properties": {
"matcher": {
"type": "string"
},
"hooks": {
"type": "array",
"items": {
"$ref": "#/$defs/hookItem"
}
},
"description": {
"type": "string"
}
}
}
},
"oneOf": [
{
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"hooks": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/$defs/matcherEntry"
}
}
}
},
"required": [
"hooks"
]
},
{
"type": "array",
"items": {
"$ref": "#/$defs/matcherEntry"
}
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Claude Code Hooks Configuration",
"description": "Configuration for Claude Code hooks. Event types are validated at runtime and must be one of: PreToolUse, PostToolUse, PreCompact, SessionStart, SessionEnd, Stop, Notification, SubagentStop",
"definitions": {
"hookItem": {
"type": "object",
"required": [
"type",
"command"
],
"properties": {
"type": {
"type": "string"
},
"command": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
},
"matcherEntry": {
"type": "object",
"required": [
"matcher",
"hooks"
],
"properties": {
"matcher": {
"type": "string"
},
"hooks": {
"type": "array",
"items": {
"$ref": "#/definitions/hookItem"
}
},
"description": {
"type": "string"
}
}
}
},
"oneOf": [
{
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"hooks": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/matcherEntry"
}
}
}
},
"required": [
"hooks"
]
},
{
"type": "array",
"items": {
"$ref": "#/definitions/matcherEntry"
}
🤖 Prompt for AI Agents
In `@schemas/hooks.schema.json` around lines 2 - 78, The schema declares draft-07
but uses $defs (introduced in 2019-09); update the schema to be consistent by
replacing the top-level "$defs" with "definitions" and update all "$ref" values
from "#/$defs/..." to "#/definitions/..." (references like "$ref":
"#/$defs/hookItem" and "$ref": "#/$defs/matcherEntry") so the document validates
under draft-07, or alternatively change the "$schema" value to the 2019-09 (or
later) URI if you prefer to keep "$defs".

Comment on lines +238 to +244
if (await asyncTest('blocking hooks output BLOCKED message', async () => {
// Test the dev server blocking hook
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
const match = blockingCommand.match(/^node -e "(.+)"$/s);

const proc = spawn('node', ['-e', match[1]], {
stdio: ['pipe', 'pipe', 'pipe']
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's understand the repository structure
git ls-files | head -20
find . -name "*.json" -type f | grep -E "(schema|hook)" | head -20

Repository: affaan-m/everything-claude-code

Length of output: 718


🏁 Script executed:

#!/bin/bash
# Check the test file around the mentioned lines
wc -l tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Read the specific line ranges mentioned in the review
head -n 400 tests/integration/hooks.test.js | tail -n 200 | cat -n

Repository: affaan-m/everything-claude-code

Length of output: 8243


🏁 Script executed:

#!/bin/bash
# Look for schema definitions
fd -t f "schema" -E node_modules
fd -t f -e json | grep -v node_modules | head -20

Repository: affaan-m/everything-claude-code

Length of output: 446


🏁 Script executed:

#!/bin/bash
# Read the complete test file - focus on specific line ranges
sed -n '238,244p' tests/integration/hooks.test.js
sed -n '274,281p' tests/integration/hooks.test.js
sed -n '372,380p' tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 1050


🏁 Script executed:

#!/bin/bash
# Check the hooks schema to see if command can be string or array
cat schemas/hooks.schema.json | head -100

Repository: affaan-m/everything-claude-code

Length of output: 1762


🏁 Script executed:

#!/bin/bash
# Check the actual hooks.json to see what command formats are used
cat hooks/hooks.json | head -200

Repository: affaan-m/everything-claude-code

Length of output: 8432


🏁 Script executed:

#!/bin/bash
# Let's verify the exact line numbers in the test file to confirm they match the review
grep -n "const match = " tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 396


🏁 Script executed:

#!/bin/bash
# Double-check if any hooks.json or test fixtures define command as array
rg '"command":\s*\[' --type json

Repository: affaan-m/everything-claude-code

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Search for all .match( calls in the test file and their context
rg -A 2 -B 2 '\.match\(' tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 1320


🏁 Script executed:

#!/bin/bash
# Check if there's any code that handles array-form commands
rg -A 5 -B 5 'Array.isArray.*command' tests/
rg -A 5 -B 5 'Array.isArray.*command' scripts/
rg -A 5 -B 5 'typeof.*command' tests/

Repository: affaan-m/everything-claude-code

Length of output: 1714


🏁 Script executed:

#!/bin/bash
# Check the _runInlineHook function to see how it handles command
sed -n '1,150p' tests/integration/hooks.test.js | grep -A 20 "_runInlineHook"

Repository: affaan-m/everything-claude-code

Length of output: 693


🏁 Script executed:

#!/bin/bash
# Look for all instances where command is used in test file
rg 'command\[' tests/integration/hooks.test.js
rg '\[0\]\.command' tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 288


🏁 Script executed:

#!/bin/bash
# Check if _runInlineHook is used anywhere else or if it handles array form
grep -n "_runInlineHook" tests/integration/hooks.test.js
sed -n '28,70p' tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 1469


🏁 Script executed:

#!/bin/bash
# Verify the actual structure of PreToolUse[0].hooks[0].command to confirm it's always a string
cat hooks/hooks.json | python3 << 'PYTHON'
import json
import sys

data = json.load(sys.stdin)
hooks = data.get('hooks', {})

# Check PreToolUse commands
if 'PreToolUse' in hooks:
    for i, matcher in enumerate(hooks['PreToolUse']):
        for j, hook in enumerate(matcher.get('hooks', [])):
            cmd = hook.get('command')
            print(f"PreToolUse[{i}].hooks[{j}].command type: {type(cmd).__name__}")
            if isinstance(cmd, str):
                print(f"  String: {cmd[:80]}...")
            elif isinstance(cmd, list):
                print(f"  Array: {cmd}")

# Check PostToolUse commands
if 'PostToolUse' in hooks:
    for i, matcher in enumerate(hooks['PostToolUse']):
        for j, hook in enumerate(matcher.get('hooks', [])):
            cmd = hook.get('command')
            print(f"PostToolUse[{i}].hooks[{j}].command type: {type(cmd).__name__}")
PYTHON

Repository: affaan-m/everything-claude-code

Length of output: 795


🏁 Script executed:

#!/bin/bash
# Read hooks.json properly and check command types
python3 << 'PYTHON'
import json

with open('hooks/hooks.json', 'r') as f:
    data = json.load(f)

hooks = data.get('hooks', {})

# Check all commands in the hooks
for event_type, matchers in hooks.items():
    if isinstance(matchers, list):
        for i, matcher in enumerate(matchers):
            for j, hook in enumerate(matcher.get('hooks', [])):
                cmd = hook.get('command')
                cmd_type = type(cmd).__name__
                print(f"{event_type}[{i}].hooks[{j}]: {cmd_type}")
                if isinstance(cmd, str):
                    if len(cmd) > 60:
                        print(f"  Value: {cmd[:60]}...")
                    else:
                        print(f"  Value: {cmd}")
                elif isinstance(cmd, list):
                    print(f"  Array with {len(cmd)} elements: {cmd}")
PYTHON

Repository: affaan-m/everything-claude-code

Length of output: 1574


🏁 Script executed:

#!/bin/bash
# Check if there's any real test data or fixtures that use array-form commands
find . -name "*.json" -o -name "*.js" | xargs grep -l "command.*\[" | grep -v node_modules | head -10

Repository: affaan-m/everything-claude-code

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Look at the actual line numbers in the test to confirm the exact context
sed -n '235,250p' tests/integration/hooks.test.js
sed -n '270,285p' tests/integration/hooks.test.js
sed -n '370,385p' tests/integration/hooks.test.js

Repository: affaan-m/everything-claude-code

Length of output: 1747


Tests assume hook.command is a string; array form will crash.

The schema allows command to be string or array, and the validator confirms array is valid. However, lines 241, 277, and 378 call .match() directly, so array-form commands will throw command.match is not a function. Add a normalization helper and apply it at all spawn sites to keep tests aligned with the schema.

🛠️ Suggested fix
+function toCommandParts(command) {
+  if (Array.isArray(command)) return command;
+  const match = command.match(/^node -e "(.+)"$/s);
+  if (!match) throw new Error('Invalid inline hook command format');
+  return ['node', '-e', match[1]];
+}

Then replace the three occurrences:

  • Line 241: const [exec, ...args] = toCommandParts(blockingCommand); const proc = spawn(exec, args, {...});
  • Line 277: const [exec, ...args] = toCommandParts(blockingCommand); const proc = spawn(exec, args, {...});
  • Line 378: const [exec, ...args] = toCommandParts(prHook.hooks[0].command); const proc = spawn(exec, args, {...});
🤖 Prompt for AI Agents
In `@tests/integration/hooks.test.js` around lines 238 - 244, The tests assume
hook.command is a string but the schema allows arrays; create a normalization
helper named toCommandParts(command) that accepts a string or array and returns
[exec, ...args] (for strings, split/regex to extract executable and args; for
arrays, return as-is), then replace direct uses of command.match() and direct
spawn calls—specifically where blockingCommand is used (in the PreToolUse test),
the second spawn site later in the same test file, and where
prHook.hooks[0].command is spawned—by calling const [exec, ...args] =
toCommandParts(...); const proc = spawn(exec, args, {...}); to ensure array-form
commands work safely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants