Skip to content

refactor(hooks): convert git hooks from .sh to .mts (Node 25+)#1281

Closed
John-David Dalton (jdalton) wants to merge 3 commits intomainfrom
hooks-mts
Closed

refactor(hooks): convert git hooks from .sh to .mts (Node 25+)#1281
John-David Dalton (jdalton) wants to merge 3 commits intomainfrom
hooks-mts

Conversation

@jdalton
Copy link
Copy Markdown
Contributor

@jdalton John-David Dalton (jdalton) commented Apr 26, 2026

Converts the four shell-based git-hook files into TypeScript-first .mts modules running on Node 25+ (stable type stripping, no flag needed).

Files:

  • .git-hooks/_helpers.mts (was _helpers.sh) — exports filterAllowedApiKeys + scanners for personal paths, AWS keys, GitHub tokens, private keys, AI attribution.
  • .git-hooks/commit-msg.mts
  • .git-hooks/pre-commit.mts
  • .git-hooks/pre-push.mts

_helpers.mts hard-fails at module load if Node < 25.

Husky shims invoke node directly.

Companion: socket-btm #120/#121 (merged), socket-repo-template@f415207.


Note

Medium Risk
Touches mandatory pre-push/commit-msg enforcement and introduces a hard Node >=25 requirement, which can block commits/pushes if environments or regex/skip rules behave unexpectedly.

Overview
Migrates the repo’s git hook security enforcement from shell scripts to Node .mts modules, centralizing shared logic in .git-hooks/_helpers.mts (API-key allowlist, secret/path scanners, git wrappers) and hard-failing on Node < 25.

Updates commit-msg, pre-commit, and pre-push to use the new scanners (including Linear reference blocking and AI-attribution stripping) and tightens/standardizes file skipping and .env allowlists, while Husky wrappers now invoke the hooks via node.

Reviewed by Cursor Bugbot for commit f504816. Configure here.

Sync from socket-repo-template@f415207. All four hook files become
.mts modules running on Node 25+ (stable type stripping, no flag).

- .git-hooks/_helpers.mts (was _helpers.sh) — exports
  filterAllowedApiKeys + scanners for personal paths, AWS keys,
  GitHub tokens, private keys, AI attribution.
- .git-hooks/commit-msg.mts
- .git-hooks/pre-commit.mts
- .git-hooks/pre-push.mts

_helpers.mts hard-fails at module load if Node < 25.

Husky shims invoke node directly.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Linear issue reference check dropped during refactor
    • Added scanLinearReferences to _helpers.mts (matching the old shell hook's LINEAR_TEAM_KEYS and linear.app URL patterns) and wired it into commit-msg.mts to block commits containing Linear issue references.

Create PR

Or push these changes by commenting:

@cursor push dfdc208492
Preview (dfdc208492)
diff --git a/.git-hooks/_helpers.mts b/.git-hooks/_helpers.mts
--- a/.git-hooks/_helpers.mts
+++ b/.git-hooks/_helpers.mts
@@ -198,6 +198,34 @@
   return hits
 }
 
+// ── Linear issue reference scanner ─────────────────────────────────
+// CLAUDE.md "ABSOLUTE RULES": NEVER reference Linear issues in commits.
+// Team keys enumerated from the Socket workspace. PATCH listed before
+// PAT so the alternation matches the longer prefix first.
+
+const LINEAR_TEAM_KEYS =
+  'ASK|AUTO|BOT|CE|CORE|DAT|DES|DEV|ENG|INFRA|LAB|MAR|MET|OPS|PAR|PATCH|PAT|PLAT|REA|SALES|SBOM|SEC|SMO|SUP|TES|TI|WEB'
+
+const LINEAR_ISSUE_RE = new RegExp(
+  `(?:^|[^A-Za-z0-9_])((?:${LINEAR_TEAM_KEYS})-[0-9]+)(?:$|[^A-Za-z0-9_])`,
+  'gm',
+)
+
+const LINEAR_URL_RE = /linear\.app\/[A-Za-z0-9/_-]+/g
+
+export const scanLinearReferences = (commitMsg: string): string[] => {
+  const hits: string[] = []
+  const lines = commitMsg.split('\n').filter(l => !l.startsWith('#'))
+  const body = lines.join('\n')
+  for (const m of body.matchAll(LINEAR_ISSUE_RE)) {
+    hits.push(m[1]!)
+  }
+  for (const m of body.matchAll(LINEAR_URL_RE)) {
+    hits.push(m[0]!)
+  }
+  return hits.slice(0, 5)
+}
+
 // ── AI attribution scanner ─────────────────────────────────────────
 
 const AI_ATTRIBUTION_RE =

diff --git a/.git-hooks/commit-msg.mts b/.git-hooks/commit-msg.mts
--- a/.git-hooks/commit-msg.mts
+++ b/.git-hooks/commit-msg.mts
@@ -23,6 +23,7 @@
   out,
   red,
   readFileForScan,
+  scanLinearReferences,
   scanSocketApiKeys,
   shouldSkipFile,
   stripAiAttribution,
@@ -67,10 +68,26 @@
     }
   }
 
-  // Auto-strip AI attribution lines from the commit message.
   const commitMsgFile = process.argv[2]
   if (commitMsgFile && existsSync(commitMsgFile)) {
     const original = readFileSync(commitMsgFile, 'utf8')
+
+    // Block Linear issue references in the commit message.
+    const linearHits = scanLinearReferences(original)
+    if (linearHits.length) {
+      out(red('✗ Commit message references Linear issue(s):'))
+      for (const hit of linearHits) {
+        out(`  ${hit}`)
+      }
+      out(
+        red(
+          'Linear tracking lives in Linear. Remove the reference from the commit message.',
+        ),
+      )
+      errors++
+    }
+
+    // Auto-strip AI attribution lines from the commit message.
     const { cleaned, removed } = stripAiAttribution(original)
     if (removed > 0) {
       writeFileSync(commitMsgFile, cleaned)

You can send follow-ups to the cloud agent here.

Comment thread .git-hooks/commit-msg.mts
Comment thread .git-hooks/_helpers.mts Outdated
Comment thread .husky/pre-commit Outdated
Comment thread .git-hooks/pre-commit.mts Outdated
Bugbot review on commit 1e30641 surfaced 4 regressions from the
.sh→.mts conversion. Fixed all four:

1. **Linear issue reference check restored** (high) — the original
   commit-msg.sh blocked Socket Linear team-key references
   (ASK-123, ENG-456, linear.app URLs). Re-added scanLinearReferences
   to _helpers.mts and wired into commit-msg.mts.
2. **scanSocketApiKeys duplicate-line bug** (low) — the post-filter
   reconstructed LineHits via `hits.find(h => h.line === line)`,
   which collapses duplicates onto the first match's line number.
   Replaced with Set-membership filter that preserves all hits.
3. **SOCKET_CLI_NO_API_TOKEN restored in .husky/pre-commit** (medium)
   — original husky shim ran `SOCKET_CLI_NO_API_TOKEN=1 pnpm test
   --staged` so contributors without a real API token don't see
   test failures pre-commit.
4. **.env.precommit allowlist** (low) — pre-commit.mts allowlist
   only had example/test variants; commit-msg.mts had precommit
   too. Aligned both to allow .env.{example,test,precommit}.

Smoke-tested commit-msg.mts:
  - "ENG-123 in body" → exit 1 (blocked) ✓
  - "fix: legit" → exit 0 (clean) ✓
@jdalton
Copy link
Copy Markdown
Contributor Author

bugbot run

1 similar comment
@jdalton
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f504816. Configure here.

Comment thread .git-hooks/pre-commit.mts
f =>
/^\.env(\.[^/]+)?$/.test(f) &&
!/^\.env\.(example|test|precommit)$/.test(f),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pre-commit .env check misses subdirectory files

Medium Severity

The .env file check in pre-commit.mts applies the regex /^\.env(\.[^/]+)?$/ directly against the full relative path f, so only root-level .env files match. A staged file like packages/app/.env.local won't match because ^\.env fails against a path starting with packages/. In contrast, commit-msg.mts correctly uses basename(file) before the same regex test, catching .env files at any depth. The comment on line 66 explicitly says "Match commit-msg.mts allowlist", confirming the intent for parity.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f504816. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing set -e lets security check failures pass silently
    • Added set -e after the shebang so any non-zero exit (including the security check) immediately aborts the hook.

Create PR

Or push these changes by commenting:

@cursor push aeee0e4362
Preview (aeee0e4362)
diff --git a/.husky/pre-commit b/.husky/pre-commit
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,5 @@
 #!/bin/sh
+set -e
 # Optional checks - can be bypassed with --no-verify for fast local commits.
 # Mandatory security checks ALSO run in pre-push hook.
 #

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f504816. Configure here.

Comment thread .husky/pre-commit

# Check prerequisites.
# Run Socket security pre-commit checks (API keys, .DS_Store, etc.).
node .git-hooks/pre-commit.mts
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing set -e lets security check failures pass silently

High Severity

The new node .git-hooks/pre-commit.mts security check is added to a shell script that lacks set -e. If the security check exits with code 1 (e.g. detecting an API key or .DS_Store), the shell script continues executing pnpm lint and pnpm test. If those subsequent commands succeed, the overall hook exit code is 0 and the commit goes through despite the security failure. This silently defeats the purpose of the pre-commit security gate.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f504816. Configure here.

John-David Dalton (jdalton) added a commit that referenced this pull request Apr 27, 2026
…y/ scope)

Consolidates PR #1280 (path-guard infra) and #1281 (.sh→.mts hook
conversion) into this branch. Resolves the modify/delete conflict
on .git-hooks/{commit-msg,pre-push} by accepting the .mts versions
— the env allowlist tweak from #1279 (.env.precommit + skip-hook-
scripts) is already covered in commit-msg.mts via shouldSkipFile
and the precommit allowlist.

Also renames internal hook packages to drop the @socketsecurity/
scope (hook-path-guard, hook-token-guard, hook-check-new-deps) —
they're private:true and never published.
@jdalton
Copy link
Copy Markdown
Contributor Author

Superseded by #1279 (consolidated). All commits, tests, and Bugbot fixes from this branch are now in #1279.

@jdalton John-David Dalton (jdalton) deleted the hooks-mts branch April 27, 2026 00:58
John-David Dalton (jdalton) added a commit that referenced this pull request Apr 27, 2026
….mts conversion + bootstrap-from-registry

Consolidates the work previously split across PRs #1279
(NODE_COMPILE_CACHE drop), #1280 (path-guard infra), and #1281
(.sh→.mts hook conversion) into a single commit.

What's included:

  Env allowlist + .cache/ + CLAUDE.md
  - Drop NODE_COMPILE_CACHE convention from .env.precommit, .env.test
  - Allow .env.precommit at any depth in commit-msg hook
  - Skip hook scripts in scanners (they contain the literal regex)
  - Restore .cache/** exclude in tsconfigs
  - Propagate CLAUDE.md sorting + open-PR + paths + inclusive-language
    rules; Set constructor sort rule; don't-revert-untouched rule;
    replace whitelist/blacklist with allowlist/denylist

  Path-guard infra (.claude/hooks/path-guard/, scripts/check-paths.mts,
  .github/paths-allowlist.yml, .claude/skills/path-guard/)
  - Mantra: 1 path, 1 reference. PreToolUse hook on Edit|Write blocks
    multi-stage build paths constructed inline; companion gate runs
    in pnpm check
  - Template-literal path detection
  - Drift-resistant allowlist via exact-line OR snippet_hash match
  - --show-hashes CLI flag for authoring allowlist entries
  - Centralized vocabulary in segments.mts (hook + gate share one
    source for stage / build-root / mode / sibling-package sets)
  - Paren-balanced parser handles nested function-call args
  - Multi-line YAML reasons (| and > block scalars)
  - scripts/check.mts resolves the gate via path.join(scriptsDir,...)
    so it runs from any cwd (root or workspace package)

  Token-guard renamed from token-hygiene
  - Word-boundary match for sensitive env names
  - Step 1 (ALWAYS_DANGEROUS) now gates on hasRedaction so
    'env | sed s/=.*/=<redacted>/' (the suggested fix) actually passes

  .sh → .mts hook conversion (Node 25+)
  - .git-hooks/_helpers.mts (was _helpers.sh) — exports
    filterAllowedApiKeys + scanners (personal paths, AWS keys,
    GitHub tokens, private keys, AI attribution, Linear issue refs)
  - .git-hooks/{commit-msg,pre-commit,pre-push}.mts (were .sh)
  - _helpers.mts hard-fails at module load if Node < 25 (relies on
    stable type stripping, no flag)
  - Husky shims invoke node directly
  - .husky/pre-commit runs tests with SOCKET_CLI_NO_API_TOKEN=1 so
    contributors without a real token don't see test failures

  Hook package rename
  - Drop @socketsecurity/ scope from internal hook packages
    (hook-path-guard, hook-token-guard, hook-check-new-deps); they
    are private:true and never published

  Bootstrap-from-registry (NEW)
  - scripts/bootstrap-from-registry.mts downloads zero-dep Socket
    packages (currently @socketsecurity/lib) from the npm registry
    directly into node_modules/ before pnpm install runs
  - Wired via package.json preinstall hook
  - Reads pinned version from pnpm-workspace.yaml catalog: OR root
    package.json devDependencies (whichever is set)
  - Solves the chicken-and-egg where setup.mts needs
    @socketsecurity/lib at module-load time but pnpm install hasn't
    run yet on a fresh clone
John-David Dalton (jdalton) added a commit that referenced this pull request Apr 27, 2026
…rap + cascade

Consolidated PR — combines the original work from #1279, #1280,
#1281 plus follow-up commits (private-name rule, socket-registry
pin cascades) into a single squashed commit.

Includes:

- env allowlist + .cache/ + CLAUDE.md hygiene (drop NODE_COMPILE_CACHE
  convention; restore .cache/** exclude in tsconfigs; propagate
  CLAUDE.md sorting/open-PR/paths/inclusive-language/Set-sort/
  don't-revert-untouched/private-name rules; replace
  whitelist/blacklist with allowlist/denylist)
- path-guard infra (PreToolUse hook + scripts/check-paths.mts gate +
  .github/paths-allowlist.yml + /path-guard skill — enforces
  "1 path, 1 reference" so multi-stage build paths are constructed
  exactly once)
- token-guard hook (renamed from token-hygiene; word-boundary match
  for sensitive env names; ALWAYS_DANGEROUS gates on hasRedaction so
  redacted env dumps pass)
- .sh -> .mts hook conversion on Node 25+ (stable type stripping;
  _helpers.mts hard-fails at module load if Node < 25; husky shims
  invoke node directly; SOCKET_CLI_NO_API_TOKEN=1 for pre-commit
  tests)
- internal hook package rename (drop @socketsecurity/ scope from
  hook-path-guard, hook-token-guard, hook-check-new-deps; private,
  never published)
- xport lock-step manifest (scripts/xport.mts +
  scripts/xport-schema.mts + scripts/xport-emit-schema.mts +
  xport.schema.json)
- bootstrap-from-registry (scripts/bootstrap-from-registry.mts
  downloads zero-dep Socket packages from npm registry into
  node_modules/ via preinstall hook, solving fresh-clone
  chicken-and-egg)
- socket-registry pins cascaded to ceab1e26 (picks up the
  @socketsecurity/lib bootstrap move from the install action into
  setup, so consumers calling only setup also benefit)
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