fix(hooks,lib): fix hook detection and parsing edge cases (#2405)

* fix(hooks,lib): fix hook detection and parsing edge cases

- auto-tmux-dev: dev\b -> dev(?![\w-]) so one-shot dev-build/dev-docs scripts
  are not detached into tmux; align command shapes (yarn run dev, bun dev) with
  pre-bash-dev-server-block.js DEV_PATTERN.
- pre-bash-commit-quality: skip obvious non-secret placeholders (env refs,
  ${...}, <...>, whitelisted tokens) in the api-key rule without suppressing
  real high-entropy secrets; make -m message extraction quote- and
  escaped-quote-aware so `-m "fix: \"x\""` / apostrophes are not truncated.
- pre-compact: annotate the CURRENT worktree's session (match **Worktree:** /
  legacy **Project:**) instead of the newest *-session.tmp across all projects,
  layered onto the LLM-summary flow from #2388; a present-but-blank Worktree
  header is treated as non-legacy (no foreign project fallback).
- shell-substitution: stop double-appending a trailing backslash in an
  unterminated backtick span.
- utils readStdinJson: on overflow, settle and resolve {} immediately (clear
  timer + listeners) instead of waiting for end/timeout and parsing a partial
  prefix; surface the overflow on stderr.

Regression tests added/extended (new tests/hooks/pre-compact.test.js).

Addresses review feedback on #2405. The earlier block-no-verify change was
dropped: its message-value skip on merge/cherry-pick/am/rebase would let
`git rebase -m --no-verify` bypass the hook (rebase's -m is the boolean
--merge), a false-negative worse than the contrived false-positive it fixed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): align hook fixtures and drain oversized stdin

---------

Co-authored-by: djpjronline-netizen <276112803+djpjronline-netizen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
This commit is contained in:
djpjronline-netizen
2026-07-28 21:32:42 -04:00
committed by GitHub
co-authored by Claude Opus 4.8 djpjronline-netizen haelyra
parent 6be87a56ae
commit 837acaf20b
11 changed files with 517 additions and 53 deletions
+44 -8
View File
@@ -57,9 +57,32 @@ function shouldCheckFile(filePath) {
return checkableExtensions.some(ext => filePath.endsWith(ext));
}
/**
* Decide whether a captured api-key value is an OBVIOUS non-secret placeholder so
* the heuristic generic api-key rule does not emit a false positive. Deliberately
* narrow: only suppresses whole-value env references / interpolations / angle-bracket
* tokens and a short explicit whitelist of placeholder + env-var NAME tokens. It must
* NOT suppress arbitrary high-entropy data (uppercase-hex, base32, digit-only, mixed
* tokens), since the generic rule is the only net catching non-prefixed secrets and a
* false-negative there is the safety-critical failure this hook exists to prevent.
* @param {string} value
* @returns {boolean}
*/
function isPlaceholderSecret(value) {
const v = (value || '').trim();
if (v.length === 0) return true; // empty value
if (/^process\.env\.[A-Za-z0-9_]+$/.test(v)) return true; // entire value is a process.env.NAME reference
if (/^\$\{[^}]*\}$/.test(v)) return true; // entire value is a ${...} interpolation
if (/^<[^<>]*>$/.test(v)) return true; // entire value is a <PLACEHOLDER> token
// Short explicit whitelist of placeholder + env-var NAME tokens (whole-value match only).
// No general all-caps clause: real all-caps/hex/base32/digit secrets must still flag.
if (/^(REPLACE_ME|CHANGE_?ME|YOUR[_-]?API[_-]?KEY|YOUR[_-]?KEY[_-]?HERE|API[_-]?KEY|SECRET|TOKEN|KEY|TODO|TBD|FIXME|XXX+)$/i.test(v)) return true;
return false;
}
/**
* Find issues in file content
* @param {string} filePath
* @param {string} filePath
* @returns {object[]} Array of issues found
*/
function findFileIssues(filePath) {
@@ -112,11 +135,21 @@ function findFileIssues(filePath) {
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API key' },
{ pattern: /ghp_[a-zA-Z0-9]{36}/, name: 'GitHub PAT' },
{ pattern: /AKIA[A-Z0-9]{16}/, name: 'AWS Access Key' },
{ pattern: /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i, name: 'API key' }
// Capture the quoted value so obvious non-secret placeholders can be excluded
{ pattern: /api[_-]?key\s*[=:]\s*['"]([^'"]+)['"]/i, name: 'API key', valueGroup: 1 },
// Unquoted form (API_KEY=..., api_key: ... without quotes). Scoped to a
// single alnum/underscore/hyphen token of 12+ chars containing at least
// one digit — real secrets are near-always alphanumeric, whereas bare
// identifiers/expressions common in this hook's checkable languages
// (config.apiKey, getApiKey(), process.env.API_KEY) are pure-alpha or
// contain '.'/'(' that fall outside the character class, so they don't
// match. Kept deliberately narrow to avoid flagging ordinary code.
{ pattern: /api[_-]?key\s*[=:]\s*(?!['"])((?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{12,})/i, name: 'API key', valueGroup: 1 }
];
for (const { pattern, name } of secretPatterns) {
if (pattern.test(line)) {
for (const { pattern, name, valueGroup } of secretPatterns) {
const secretMatch = line.match(pattern);
if (secretMatch && !(valueGroup && isPlaceholderSecret(secretMatch[valueGroup]))) {
issues.push({
type: 'secret',
message: `Potential ${name} exposed at line ${lineNum}`,
@@ -139,11 +172,14 @@ function findFileIssues(filePath) {
* @returns {object|null} Validation result or null if no message to validate
*/
function validateCommitMessage(command) {
// Extract commit message from command
const messageMatch = command.match(/(?:-m|--message)[=\s]+["']?([^"']+)["']?/);
// Extract commit message from command (quote-aware: when quoted, capture to the
// matching closing quote, consuming escaped chars (\") so an embedded escaped
// quote does not truncate the subject, and allowing the OTHER quote char inside
// the body; when unquoted, capture the full remaining tail, not just the first token)
const messageMatch = command.match(/(?:-m|--message)[=\s]+(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^"']+?)\s*$)/);
if (!messageMatch) return null;
const message = messageMatch[1];
const message = messageMatch[1] ?? messageMatch[2] ?? messageMatch[3];
const issues = [];
// Check conventional commit format
@@ -445,4 +481,4 @@ if (require.main === module) {
});
}
module.exports = { run, evaluate };
module.exports = { run, evaluate, validateCommitMessage, findFileIssues, isPlaceholderSecret };