Skip to content

fix: check-dead-exports hook silently no-ops on ESM codebases#394

Open
carlos-alm wants to merge 8 commits intomainfrom
fix/dead-exports-hook-esm
Open

fix: check-dead-exports hook silently no-ops on ESM codebases#394
carlos-alm wants to merge 8 commits intomainfrom
fix/dead-exports-hook-esm

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • The check-dead-exports.sh hook used require() (CJS) to load src/queries.js, but this codebase is pure ESM. The require() call silently failed (caught by try/catch), making the entire hook a no-op — it never actually detected dead exports.
  • Switched the inline Node.js script to use --input-type=module with import declarations and await import() for the dynamic queries.js load, using a file:// URL for Windows compatibility.
  • Escaped double-quote chars in regex patterns as \u0022 to avoid conflicts with the shell's double-quote delimiters.

Test plan

  • Verify the hook runs without errors on a git commit that includes staged src/ files
  • Verify the hook correctly detects dead exports (exports with zero consumers) and blocks the commit
  • Verify the hook still passes cleanly when all exports have consumers

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @carlos-alm's task —— View job


PR Review Complete ✅

Reviewing PR #394: fix: check-dead-exports hook silently no-ops on ESM codebases

Todo List

  • Read and analyze the changed file
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity Assessment
  • Phase 4: Code Quality Review
  • Final Assessment and Rating

Phase 1: Root Cause Analysis

Problem Being Solved: The git hook check-dead-exports.sh was silently failing because it used CJS require() to load src/queries.js in an ESM-only codebase. The try/catch around the dynamic import was masking the failure, making the hook a complete no-op.

Why This Approach: Converting to ESM with --input-type=module and await import() with a file:// URL is the correct solution. The escape of quotes as \u0022 avoids shell quoting conflicts.

Risk Assessment: Low risk. This is a targeted fix that addresses the exact root cause without changing functionality.

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • No linting rules disabled or relaxed
  • No test timeouts/thresholds modified
  • No build configuration changes
  • No TypeScript strict mode changes

This is purely a bug fix with no configuration relaxation.

Phase 3: Test Integrity ✅

No RED FLAGS detected:

  • No test files modified
  • No assertions removed or weakened
  • No test skipping or conditional logic added
  • Small, focused change (+7/-6 lines in single file)
  • Single concern: fixing ESM import issue

Phase 4: Code Quality Review

Technical Implementation Analysis:

Correct ESM Migration (lines 67-74):

  • Proper use of --input-type=module flag
  • Correct await import() with file URL
  • Windows-compatible path handling with replace(/\\\\/g, '/')

Shell Quoting Fix (lines 98, 105):

  • Smart use of \u0022 Unicode escapes instead of double quotes
  • Prevents shell interpretation conflicts
  • Maintains regex functionality

Error Handling Preserved:

  • 2>/dev/null and || true patterns maintained
  • Graceful fallback behavior unchanged

No Security Issues:

  • No command injection risks introduced
  • File URL construction is safe
  • No privilege escalation concerns

Minor Considerations:

  • The regex patterns remain complex but are unchanged from original
  • File URL construction works but could benefit from Node's pathToFileURL() in future iterations

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Git hook was silently failing due to CJS/ESM mismatch - `require()` calls failing in ESM-only codebase
**Why This Approach**: Direct ESM conversion with proper Node.js flags is the correct architectural fix
**Risk Assessment**: Low risk - targeted fix with no side effects

## Critical Concerns
None. This is a well-executed bug fix that addresses the exact root cause.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: **APPROVE**
- Reasoning: Clean fix that solves a real issue without introducing risks. The hook will now actually function as intended. Only minor point for improvement would be using Node's `pathToFileURL()` utility, but the current implementation is correct and cross-platform compatible.

Test Plan Validation

The provided test plan is appropriate:

  • ✅ Verify hook runs without errors
  • ✅ Verify hook correctly detects dead exports
  • ✅ Verify hook passes when exports have consumers

This covers the critical functionality that was previously broken.


@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR fixes a long-standing silent no-op in the check-dead-exports.sh hook: the original require() call failed quietly in this ESM-only codebase, so dead exports were never actually detected. The fix switches the inline Node.js script to --input-type=module with static import declarations, replaces the manual file:/// URL construction with pathToFileURL for cross-platform correctness, shifts process.argv indices from [1]/[2] to [2]/[3] to account for -e scripts, and escapes literal " chars in regex patterns as \u0022 to avoid breaking the shell double-quote delimiter. The same fixes are applied identically to a new docs/examples/ copy of the hook, and the example settings.json / README.md are updated to document the hook.

Key changes:

  • Switched from CJS require() to ESM --input-type=module + await import() to load src/queries.js in this pure-ESM codebase
  • Used pathToFileURL() (from node:url) instead of manually prepending file:/// — prevents a double-slash bug on Unix paths
  • Fixed process.argv off-by-one in both the main detection block ([2]/[3]) and the denial JSON emitter ([2]) — without this, the hook is still a no-op even after the ESM fix
  • Replaced " with \u0022 in two regex character classes to avoid prematurely closing the shell double-quoted heredoc-style string
  • Added docs/examples/ counterpart and updated README.md / settings.json for discoverability

Confidence Score: 4/5

  • Safe to merge — all previously identified blocking bugs have been addressed in follow-up commits.
  • All critical issues raised in prior review threads (argv off-by-one, double-slash file URL, static pathToFileURL import, denial-block argv) are confirmed fixed in the current HEAD. The ESM migration itself is correct: --input-type=module enables top-level await, pathToFileURL handles both Unix and Windows, and \u0022 safely avoids shell quoting conflicts. One pre-existing limitation (scanDynamic only scans .js files while FILES_TO_CHECK admits .ts/.tsx) may cause false negatives for TypeScript consumers, but this is out of scope for this PR.
  • No files require special attention — the two hook files are in sync and the config/doc changes are straightforward.

Important Files Changed

Filename Overview
.claude/hooks/check-dead-exports.sh Core hook updated from CJS to ESM: switched to --input-type=module, added pathToFileURL for portable file URLs, fixed argv indices (now [2]/[3]), fixed denial-block argv index ([2]), and escaped double-quotes in regex as \u0022 — all previously-flagged issues addressed.
docs/examples/claude-code-hooks/check-dead-exports.sh New example file, identical in content to .claude/hooks/check-dead-exports.sh — all fixes applied consistently across both copies.
docs/examples/claude-code-hooks/README.md Documentation update adding check-dead-exports.sh to the "Code quality hooks" section and the recommended setup matrix — accurate and complete.
docs/examples/claude-code-hooks/settings.json Adds check-dead-exports.sh to the Bash PreToolUse hook list with a 15-second timeout; path convention matches other hooks in the file.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([PreToolUse: Bash tool invoked]) --> B[Parse tool_input.command via node -e CJS]
    B --> C{COMMAND empty?}
    C -- yes --> Z([exit 0 — pass])
    C -- no --> D{matches git commit?}
    D -- no --> Z
    D -- yes --> E{graph.db exists?}
    E -- no --> Z
    E -- yes --> F{staged files?}
    F -- no --> Z
    F -- yes --> G{session-edits.log exists?}
    G -- no --> Z
    G -- yes --> H[Filter staged files to src/ JS/TS edited this session]
    H --> I{FILES_TO_CHECK empty?}
    I -- yes --> Z
    I -- no --> J["node --input-type=module -e (ESM)
    import fs, path, pathToFileURL
    argv[2]=WORK_ROOT, argv[3]=FILES_TO_CHECK
    pathToFileURL → await import(queries.js)
    Build publicAPI from index.js exports
    scanDynamic: collect dynamic-import names
    exportsData per file → collect dead names"]
    J --> K{DEAD_EXPORTS non-empty?}
    K -- no --> Z
    K -- yes --> L[Build REASON string]
    L --> M["node -e (CJS) emit JSON deny response
    argv[2]=REASON → permissionDecisionReason"]
    M --> N([exit 0 — commit blocked])
Loading

Last reviewed commit: 5b2ef44

Comment on lines 70 to 71
const root = process.argv[1];
const files = process.argv[2].split('\n').filter(Boolean);
Copy link
Contributor

Choose a reason for hiding this comment

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

process.argv off-by-one — hook is still a no-op

When Node.js is invoked with node -e "..." "$WORK_ROOT" "$FILES_TO_CHECK", process.argv[1] is not $WORK_ROOT — it is set to '' (empty string) or [stdin] for inline -e/--input-type=module scripts. The actual user-supplied arguments begin at index 2 and 3 respectively.

This means:

  • root = process.argv[1]'' (empty string)
  • path.join('', 'src/queries.js')'src/queries.js' (relative)
  • fileUrl becomes 'file:///src/queries.js', which resolves to /src/queries.js on the filesystem — a path that does not exist.

The await import(fileUrl) call will throw, the error is swallowed by 2>/dev/null and || true, and DEAD_EXPORTS remains empty — the hook is still a silent no-op even after the ESM fix.

The fix requires bumping both indices by one:

Suggested change
const root = process.argv[1];
const files = process.argv[2].split('\n').filter(Boolean);
const root = process.argv[2];
const files = process.argv[3].split('\n').filter(Boolean);

(This is a pre-existing indexing bug that was masked while require() was in place, but it now becomes the primary blocker since the ESM import path is correctly reached.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 062b404. Changed process.argv[1] to process.argv[2] and process.argv[2] to process.argv[3] to account for the -e flag consuming argv[1] when using node -e "...".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in commit 062b404 — argv indices were shifted to [2]/[3] to account for -e scripts.

const files = process.argv[2].split('\n').filter(Boolean);

const { exportsData } = require(path.join(root, 'src/queries.js'));
const fileUrl = 'file:///' + path.join(root, 'src/queries.js').replace(/\\\\/g, '/');
Copy link
Contributor

Choose a reason for hiding this comment

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

Double-slash in Unix file URL — use pathToFileURL instead

On Unix, path.join(root, 'src/queries.js') returns an absolute path like /home/user/project/src/queries.js. Prepending 'file:///' produces:

file:////home/user/project/src/queries.js   // 4 slashes

The WHATWG URL parser sees an empty host and a pathname of //home/user/project/src/queries.js. Node's fileURLToPath then returns //home/user/project/src/queries.js, which POSIX treats like /path on most Linux distros — but the double-leading-slash is implementation-defined behaviour and not guaranteed.

On Windows the result (file:///C:/...) is correct, so this only surfaces on Unix.

The idiomatic Node.js solution is to let url.pathToFileURL handle both platforms:

Suggested change
const fileUrl = 'file:///' + path.join(root, 'src/queries.js').replace(/\\\\/g, '/');
const { pathToFileURL } = await import('node:url');
const fileUrl = pathToFileURL(path.join(root, 'src/queries.js')).href;

Alternatively, use 'file://' + path.join(root, 'src/queries.js') (two slashes, not three) on Unix, but pathToFileURL is the recommended portable API.

Copy link
Contributor Author

@carlos-alm carlos-alm Mar 10, 2026

Choose a reason for hiding this comment

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

Fixed in 062b404. Replaced the manual file:/// + path construction with pathToFileURL() from node:url, which correctly handles both Unix and Windows paths (no double-slash on Unix, proper drive-letter handling on Windows).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in commit 062b404 — switched to pathToFileURL for portable file URLs.

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @carlos-alm's task —— View job


Reviewing PR #394: fix: check-dead-exports hook silently no-ops on ESM codebases

Todo List

  • Read and analyze the changed file
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity Assessment
  • Phase 4: Code Quality Review
  • Final Assessment and Rating

Phase 1: Root Cause Analysis

Problem Being Solved: The git hook check-dead-exports.sh was silently failing because it used CJS require() to load src/queries.js in an ESM-only codebase (confirmed by "type": "module" in package.json and export statements in queries.js). The try/catch around the dynamic import was masking the failure, making the hook a complete no-op.

Why This Approach: Converting to ESM with --input-type=module and await import() with a file:// URL is the correct solution. The escape of quotes as \u0022 avoids shell quoting conflicts.

Risk Assessment: Low risk. This is a targeted fix that addresses the exact root cause without changing functionality.

CRITICAL ISSUE CONFIRMED: Both Greptile and the previous reviewer are correct about the process.argv off-by-one error. When Node.js runs an inline -e script, process.argv[1] is empty string, not the first user argument. This means:

  • Line 70: process.argv[1]'' (empty string)
  • Line 71: process.argv[2] → gets $WORK_ROOT
  • The fileUrl construction will fail because root is empty

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • No linting rules disabled or relaxed
  • No test timeouts/thresholds modified
  • No build configuration changes
  • No TypeScript strict mode changes

This is purely a bug fix with no configuration relaxation.

Phase 3: Test Integrity ✅

No RED FLAGS detected:

  • No test files modified
  • No assertions removed or weakened
  • No test skipping or conditional logic added
  • Small, focused change (+7/-6 lines in single file)
  • Single concern: fixing ESM import issue

Phase 4: Code Quality Review

Technical Implementation Analysis:

Correct ESM Migration (lines 67-74):

  • Proper use of --input-type=module flag
  • Correct await import() with file URL
  • Windows-compatible path handling with replace(/\\\\/g, '/')

Shell Quoting Fix (lines 98, 105):

  • Smart use of \u0022 Unicode escapes instead of double quotes
  • Prevents shell interpretation conflicts
  • Maintains regex functionality

CRITICAL BUG (lines 70-71):

  • process.argv[1] is empty string for inline -e scripts
  • Should be process.argv[2] and process.argv[3]
  • Results in malformed file URL and continued no-op behavior

File URL Construction Issue (line 73):

  • Manual 'file:///' + path creates file:////home/... (4 slashes on Unix)
  • Should use Node's pathToFileURL() from node:url for portability

Error Handling Preserved:

  • 2>/dev/null and || true patterns maintained
  • Graceful fallback behavior unchanged

No Security Issues:

  • No command injection risks introduced
  • File URL construction is safe
  • No privilege escalation concerns

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Git hook was silently failing due to CJS/ESM mismatch - `require()` calls failing in ESM-only codebase
**Why This Approach**: Direct ESM conversion with proper Node.js flags is the correct architectural fix
**Risk Assessment**: Low risk - targeted fix with no side effects

## Critical Concerns
1. **BLOCKER: process.argv off-by-one error** - Lines 70-71 use wrong indices (1,2 instead of 2,3), causing root to be empty string and fileUrl to target non-existent path
2. **File URL construction flaw** - Line 73 manual concatenation creates invalid URLs with 4 slashes on Unix systems

## Final Recommendation
- Rating: ⭐⭐☆☆☆ (2/5)
- Action: **REQUEST CHANGES**
- Reasoning: While the ESM migration approach is correct, a pre-existing process.argv indexing bug means the hook remains a silent no-op after this PR. The fix addresses the symptom (CJS→ESM) but not the underlying argument parsing issue that prevents the script from actually running.

Required Changes

Fix process.argv indexing (.claude/hooks/check-dead-exports.sh:70-71):

const root = process.argv[2];  // was process.argv[1]
const files = process.argv[3].split('\n').filter(Boolean);  // was process.argv[2]

Improve file URL construction (.claude/hooks/check-dead-exports.sh:73):

const { pathToFileURL } = await import('node:url');
const fileUrl = pathToFileURL(path.join(root, 'src/queries.js')).href;

Test Plan Validation

The provided test plan is appropriate:

  • ✅ Verify hook runs without errors
  • ✅ Verify hook correctly detects dead exports
  • ✅ Verify hook passes when exports have consumers

This covers the critical functionality that was previously broken.


@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @carlos-alm's task —— View job


Reviewing PR #394: fix: check-dead-exports hook silently no-ops on ESM codebases

Todo List

  • Read and analyze the changed file
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity Assessment
  • Phase 4: Code Quality Review
  • Final Assessment and Rating

Phase 1: Root Cause Analysis ✅

Problem Being Solved: The git hook was actually already using ESM (--input-type=module) but had a process.argv off-by-one error that made root an empty string, causing the dynamic import to fail silently.

Root Cause: The original code used process.argv[1] for root and process.argv[2] for files, but when Node.js runs an inline -e script, argv[1] is empty. The actual arguments start at indices 2 and 3.

Why This Approach: The fix correctly shifts the indices to argv[2] and argv[3], and improves the file URL construction by using pathToFileURL() instead of manual concatenation.

Risk Assessment: Very low risk. This is a precise bug fix that addresses the exact indexing issue.

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • No linting rules disabled or relaxed
  • No test timeouts/thresholds modified
  • No build configuration changes
  • No TypeScript strict mode changes

This is purely a bug fix with no configuration relaxation.

Phase 3: Test Integrity ✅

No RED FLAGS detected:

  • No test files modified
  • No assertions removed or weakened
  • No test skipping or conditional logic added
  • Small, focused change (+2/-1 lines for core logic)
  • Single concern: fixing process.argv indexing and file URL construction

Phase 4: Code Quality Review ✅

Technical Implementation Analysis:

Correct process.argv Fix (lines 70-71):

  • Fixed off-by-one error: argv[1]argv[2] for root, argv[2]argv[3] for files
  • When Node.js runs node -e "script" arg1 arg2, the args are at indices 2 and 3, not 1 and 2
  • This was the actual root cause making the hook a no-op

Proper File URL Construction (lines 73-74):

  • Replaced manual 'file:///' + path concatenation with pathToFileURL()
  • Eliminates cross-platform path separator issues (\ vs /)
  • Eliminates the replace(/\\\\/g, '/') Windows workaround
  • Produces correctly formatted file URLs on all platforms

No Security Issues:

  • No command injection risks
  • Safe use of Node.js built-in APIs
  • No privilege escalation concerns

Error Handling Preserved:

  • 2>/dev/null || true patterns maintained for graceful fallback

Technical Correctness:

  • The unicode escape sequences (\u0022) in regex patterns (lines 99, 106) remain unchanged and are correct for avoiding shell quote conflicts
  • The ESM module loading pattern is proper Node.js ESM syntax

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Git hook had process.argv off-by-one error causing root path to be empty string, making dynamic import fail silently
**Why This Approach**: Direct fix of argument indexing + proper file URL construction is the precise solution
**Risk Assessment**: Very low risk - targeted fix with no side effects

## Critical Concerns
None. This addresses the exact root cause identified by previous reviewers.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: **APPROVE**
- Reasoning: Clean, precise fix that solves the real issue. The hook will now function correctly. Uses proper Node.js APIs (`pathToFileURL`) instead of manual string concatenation. The previous reviewers correctly identified the argv indexing bug, and this PR fixes it properly.

Test Plan Validation ✅

The provided test plan is appropriate and will verify the fix:

  • ✅ Hook runs without errors (argv indexing fixed)
  • ✅ Hook detects dead exports (fileUrl now resolves correctly)
  • ✅ Hook passes when exports have consumers (full functionality working)

This PR correctly addresses the core issue that made the hook non-functional.


@carlos-alm
Copy link
Contributor Author

@greptileai

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @carlos-alm's task —— View job


Reviewing PR #394: fix: check-dead-exports hook silently no-ops on ESM codebases

Todo List

  • Read and analyze the changed files
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity Assessment
  • Phase 4: Code Quality Review
  • Final Assessment and Rating

Phase 1: Root Cause Analysis ✅

Problem Being Solved: The git hook was actually already using ESM (--input-type=module) but had a process.argv off-by-one error that made root an empty string, causing the dynamic import to fail silently.

Root Cause: The original code used process.argv[1] for root and process.argv[2] for files, but when Node.js runs an inline -e script, argv[1] is empty. The actual arguments start at indices 2 and 3.

Why This Approach: The fix correctly shifts the indices to argv[2] and argv[3], and improves the file URL construction by using pathToFileURL() instead of manual concatenation.

Risk Assessment: Very low risk. This is a precise bug fix that addresses the exact indexing issue.

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • No linting rules disabled or relaxed
  • No test timeouts/thresholds modified
  • No build configuration changes
  • No TypeScript strict mode changes

This is purely a bug fix with no configuration relaxation.

Phase 3: Test Integrity ✅

No RED FLAGS detected:

  • No test files modified
  • No assertions removed or weakened
  • No test skipping or conditional logic added
  • Small, focused change (+2/-1 lines for core logic)
  • Single concern: fixing process.argv indexing and file URL construction

Phase 4: Code Quality Review ✅

Technical Implementation Analysis:

Correct process.argv Fix (lines 70-71):

  • Fixed off-by-one error: argv[1]argv[2] for root, argv[2]argv[3] for files
  • When Node.js runs node -e "script" arg1 arg2, the args are at indices 2 and 3, not 1 and 2
  • This was the actual root cause making the hook a no-op

Proper File URL Construction (lines 73-74):

  • Replaced manual 'file:///' + path concatenation with pathToFileURL()
  • Eliminates cross-platform path separator issues (\ vs /)
  • Eliminates the replace(/\\\\/g, '/') Windows workaround
  • Produces correctly formatted file URLs on all platforms

No Security Issues:

  • No command injection risks
  • Safe use of Node.js built-in APIs
  • No privilege escalation concerns

Error Handling Preserved:

  • 2>/dev/null || true patterns maintained for graceful fallback

Technical Correctness:

  • The unicode escape sequences (\u0022) in regex patterns (lines 99, 106) remain unchanged and are correct for avoiding shell quote conflicts
  • The ESM module loading pattern is proper Node.js ESM syntax

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Git hook had process.argv off-by-one error causing root path to be empty string, making dynamic import fail silently
**Why This Approach**: Direct fix of argument indexing + proper file URL construction is the precise solution
**Risk Assessment**: Very low risk - targeted fix with no side effects

## Critical Concerns
None. This addresses the exact root cause identified by previous reviewers.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: **APPROVE**
- Reasoning: Clean, precise fix that solves the real issue. The hook will now function correctly. Uses proper Node.js APIs (`pathToFileURL`) instead of manual string concatenation. The previous reviewers correctly identified the argv indexing bug, and this PR fixes it properly.

Test Plan Validation ✅

The provided test plan is appropriate and will verify the fix:

  • ✅ Hook runs without errors (argv indexing fixed)
  • ✅ Hook detects dead exports (fileUrl now resolves correctly)
  • ✅ Hook passes when exports have consumers (full functionality working)

This PR correctly addresses the core issue that made the hook non-functional.


Comment on lines +73 to +74
const { pathToFileURL } = await import('node:url');
const fileUrl = pathToFileURL(path.join(root, 'src/queries.js')).href;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unnecessary dynamic import for a static built-in

pathToFileURL is a static dependency just like fs and path, yet it is loaded via await import('node:url') rather than as a static top-level import. This is inconsistent with the surrounding code and introduces an unnecessary runtime await for a module that is always needed. Move it to the static import block at the top:

Suggested change
const { pathToFileURL } = await import('node:url');
const fileUrl = pathToFileURL(path.join(root, 'src/queries.js')).href;
import { pathToFileURL } from 'node:url';

Then remove the await import('node:url') line entirely and adjust line 74 to just use pathToFileURL directly (it will already be in scope).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in df6b204 — moved \ to a static \ alongside \ and . Applied to both the live hook and the example hook.

Address Greptile review — pathToFileURL should be a static import
like fs and path, not a dynamic await import() for a built-in module.
@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @carlos-alm's task —— View job


Reviewing PR #394: fix: check-dead-exports hook silently no-ops on ESM codebases

Todo List

  • Read and analyze the changed files
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity Assessment
  • Phase 4: Code Quality Review
  • Final Assessment and Rating

Phase 1: Root Cause Analysis ✅

Problem Being Solved: The git hook was actually already using ESM (--input-type=module) but had a process.argv off-by-one error that made root an empty string, causing the dynamic import to fail silently.

Root Cause: The original code used process.argv[1] for root and process.argv[2] for files, but when Node.js runs an inline -e script, argv[1] is empty. The actual arguments start at indices 2 and 3.

Why This Approach: The fix correctly shifts the indices to argv[2] and argv[3], and improves the file URL construction by using pathToFileURL() instead of manual concatenation.

Risk Assessment: Very low risk. This is a precise bug fix that addresses the exact indexing issue.

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • No linting rules disabled or relaxed
  • No test timeouts/thresholds modified
  • No build configuration changes
  • No TypeScript strict mode changes

This is purely a bug fix with no configuration relaxation.

Phase 3: Test Integrity ✅

No RED FLAGS detected:

  • No test files modified
  • No assertions removed or weakened
  • No test skipping or conditional logic added
  • Small, focused change (+2/-1 lines for core logic)
  • Single concern: fixing process.argv indexing and file URL construction

Phase 4: Code Quality Review ✅

Technical Implementation Analysis:

Correct process.argv Fix (lines 70-71):

  • Fixed off-by-one error: argv[1]argv[2] for root, argv[2]argv[3] for files
  • When Node.js runs node -e "script" arg1 arg2, the args are at indices 2 and 3, not 1 and 2
  • This was the actual root cause making the hook a no-op

Proper File URL Construction (lines 73-74):

  • Replaced manual 'file:///' + path concatenation with pathToFileURL()
  • Eliminates cross-platform path separator issues (\ vs /)
  • Eliminates the replace(/\\\\/g, '/') Windows workaround
  • Produces correctly formatted file URLs on all platforms

No Security Issues:

  • No command injection risks
  • Safe use of Node.js built-in APIs
  • No privilege escalation concerns

Error Handling Preserved:

  • 2>/dev/null || true patterns maintained for graceful fallback

Technical Correctness:

  • The unicode escape sequences (\u0022) in regex patterns (lines 99, 106) remain unchanged and are correct for avoiding shell quote conflicts
  • The ESM module loading pattern is proper Node.js ESM syntax

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Git hook had process.argv off-by-one error causing root path to be empty string, making dynamic import fail silently
**Why This Approach**: Direct fix of argument indexing + proper file URL construction is the precise solution
**Risk Assessment**: Very low risk - targeted fix with no side effects

## Critical Concerns
None. This addresses the exact root cause identified by previous reviewers.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: **APPROVE**
- Reasoning: Clean, precise fix that solves the real issue. The hook will now function correctly. Uses proper Node.js APIs (`pathToFileURL`) instead of manual string concatenation. The previous reviewers correctly identified the argv indexing bug, and this PR fixes it properly.

Test Plan Validation ✅

The provided test plan is appropriate and will verify the fix:

  • ✅ Hook runs without errors (argv indexing fixed)
  • ✅ Hook detects dead exports (fileUrl now resolves correctly)
  • ✅ Hook passes when exports have consumers (full functionality working)

This PR correctly addresses the core issue that made the hook non-functional.


View job

@carlos-alm
Copy link
Contributor Author

@greptileai

hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: process.argv[1]
Copy link
Contributor

Choose a reason for hiding this comment

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

process.argv[1] off-by-one in denial block — reason is always empty

This is the same argv-indexing issue that was fixed in the main Node.js block (where [1]/[2] were shifted to [2]/[3]), but this second node -e invocation was not updated.

When Node.js is started with node -e "script" "$REASON":

  • process.argv[0]/path/to/node
  • process.argv[1]'' (empty string — Node uses [stdin]/'' for -e scripts, not the first user argument)
  • process.argv[2] → the value of $REASON ← the actual argument

As a result permissionDecisionReason always emits undefined/an empty string, and the AI/user sees a blank denial message with no explanation of which exports are dead.

The same bug is present identically at line 140 of .claude/hooks/check-dead-exports.sh.

Suggested change
permissionDecisionReason: process.argv[1]
permissionDecisionReason: process.argv[2]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0373a79. Changed process.argv[1] to process.argv[2] in the denial block to account for -e scripts. Applied to both .claude/hooks/check-dead-exports.sh and docs/examples/.

For `node -e "..." arg`, process.argv[1] is empty — the actual
argument lands at process.argv[2]. This caused the denial reason
to be undefined when blocking commits with dead exports.
@carlos-alm
Copy link
Contributor Author

Fixed the remaining argv off-by-one in the denial block (0373a79). All Greptile comments on this PR are now addressed.

@greptileai

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


Reviewing PR #394: fix: check-dead-exports hook silently no-ops on ESM codebases

Todo List

  • Read and analyze the changed files
  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity Assessment
  • Phase 4: Code Quality Review
  • Final Assessment and Rating

Phase 1: Root Cause Analysis ✅

Problem Being Solved: The git hook check-dead-exports.sh had two critical issues preventing it from functioning:

  1. Original Issue: ESM/CJS mismatch - The hook was already using --input-type=module but had a process.argv off-by-one error
  2. Root Cause: When Node.js runs node -e "script" arg1 arg2, process.argv[1] is empty string, not arg1. The actual arguments start at indices 2 and 3
  3. Impact: This made root an empty string, causing pathToFileURL(path.join('', 'src/queries.js')) to resolve to a non-existent path, making the dynamic import fail silently

Why This Approach: The fix correctly shifts indices from argv[1]/argv[2] to argv[2]/argv[3] and uses pathToFileURL() for proper cross-platform file URL construction.

Previous Review Feedback: Both Greptile and Claude identified this argv indexing bug in previous reviews. The author claims to have addressed this in commit 0373a79.

Risk Assessment: Very low risk - this is a precise bug fix addressing the exact root cause.

Phase 2: Configuration Change Detection ✅

NO HIGH-RISK changes detected:

  • ✅ No linting rules disabled or relaxed
  • ✅ No test timeouts increased or coverage thresholds lowered
  • ✅ No build configuration or module resolution modified
  • ✅ No TypeScript strict mode flags disabled
  • ✅ Only documentation additions (README.md, settings.json)

This is purely a bug fix with no configuration relaxation.

Phase 3: Test Integrity ✅

NO RED FLAGS detected:

  • ✅ No test files modified
  • ✅ No assertions removed or weakened
  • ✅ No test skipping or conditional logic added
  • ✅ Small, focused change (+11/-9 lines in hook file)
  • ✅ Single concern: fixing process.argv indexing and file URL construction
  • ✅ No eslint-disable or workaround comments

This is a targeted bug fix maintaining the original functionality.

Phase 4: Code Quality Review ✅

Technical Implementation Analysis:

Correct process.argv Fix (lines 71-72):

  • Fixed off-by-one error: argv[1]argv[2] for root, argv[2]argv[3] for files
  • When Node.js runs node -e "script" arg1 arg2, the args are at indices 2 and 3, not 1 and 2
  • This was the actual root cause making the hook a no-op

Proper File URL Construction (lines 70, 74):

  • Added static import import { pathToFileURL } from 'node:url';
  • Replaced manual concatenation with pathToFileURL(path.join(root, 'src/queries.js')).href
  • Eliminates cross-platform path separator issues and produces correctly formatted file URLs

Denial Block Fixed (line 140):

  • Fixed the same argv off-by-one error in the denial block: process.argv[1]process.argv[2]
  • Ensures permissionDecisionReason gets the actual reason string, not empty string
  • Applied to both .claude/hooks/check-dead-exports.sh and docs/examples/ version

No Security Issues:

  • No command injection risks introduced
  • Safe use of Node.js built-in APIs
  • No privilege escalation concerns

Error Handling Preserved:

  • 2>/dev/null || true patterns maintained for graceful fallback
  • Hook fails safely if codegraph DB missing or no staged files

Unicode Escapes Maintained (lines 99, 106):

  • Properly preserved \u0022 escapes in regex patterns to avoid shell quote conflicts
  • Maintains regex functionality while preventing shell interpretation issues

Documentation Changes:

  • ✅ Added check-dead-exports.sh to README.md hook table
  • ✅ Added hook to settings.json with appropriate 15s timeout
  • ✅ Example hook is identical to live version (consistency maintained)

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Git hook had process.argv off-by-one error causing root path to be empty string, making dynamic import fail silently
**Why This Approach**: Direct fix of argument indexing + proper file URL construction addresses the precise issue
**Risk Assessment**: Very low risk - targeted fix with no side effects

## Critical Concerns
None. This addresses all issues identified by previous reviewers and correctly implements the fix.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: **APPROVE**
- Reasoning: Clean, precise fix that solves the real issue. The hook will now function correctly. Uses proper Node.js APIs (`pathToFileURL`) instead of manual string concatenation. All previous review feedback has been properly addressed, including the denial block argv fix. The implementation is technically sound and maintains error handling while fixing the core functionality.

Test Plan Validation ✅

The provided test plan will properly verify the fix:

  • ✅ Hook runs without errors (argv indexing fixed)
  • ✅ Hook detects dead exports (fileUrl now resolves correctly)
  • ✅ Hook passes when exports have consumers (full functionality working)

This PR successfully addresses the core issue that made the hook non-functional.


View job

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.

1 participant