@@ -280,6 +280,51 @@ export type LineHit = {
280280 suggested ?: string
281281}
282282
283+ // Generic line-walk scanner factory. Splits text into lines once,
284+ // applies the regex per line, optionally skips lines via `filter` (for
285+ // allowlists) and/or via `skipDocs` (for documentation-style
286+ // detection), and optionally attaches a suggested rewrite. Centralizes
287+ // the loop shape that every concrete scanner used to inline.
288+ //
289+ // Options:
290+ // filter — return true to drop a line (e.g. allowlist match).
291+ // skipDocs.rule — when set, calls looksLikeDocumentation() with the
292+ // same regex + this rule name and skips lines that match.
293+ // suggest — produces the per-line `suggested` rewrite shown to users.
294+ function scanLines (
295+ text : string ,
296+ pattern : RegExp ,
297+ options : {
298+ filter ?: ( line : string ) => boolean
299+ skipDocs ?: { rule : string }
300+ suggest ?: ( line : string ) => string
301+ } = { } ,
302+ ) : LineHit [ ] {
303+ const hits : LineHit [ ] = [ ]
304+ const lines = text . split ( '\n' )
305+ for ( let i = 0 ; i < lines . length ; i ++ ) {
306+ const line = lines [ i ] !
307+ if ( ! pattern . test ( line ) ) {
308+ continue
309+ }
310+ if ( options . filter && options . filter ( line ) ) {
311+ continue
312+ }
313+ if (
314+ options . skipDocs &&
315+ looksLikeDocumentation ( line , pattern , options . skipDocs . rule )
316+ ) {
317+ continue
318+ }
319+ const hit : LineHit = { lineNumber : i + 1 , line }
320+ if ( options . suggest ) {
321+ hit . suggested = options . suggest ( line )
322+ }
323+ hits . push ( hit )
324+ }
325+ return hits
326+ }
327+
283328// Build a suggested rewrite for a documentation-style personal path.
284329// Replaces the matched real-path username segment with the canonical
285330// placeholder form: `<user>` / `<USERNAME>` (matching the platform
@@ -295,34 +340,23 @@ export function suggestPlaceholder(line: string): string {
295340// are pure placeholders or look like documentation examples). Each hit
296341// carries a `suggested` rewrite when the scanner can offer one — the
297342// caller surfaces it to the user as the fix recipe.
298- export const scanPersonalPaths = ( text : string ) : LineHit [ ] => {
299- const hits : LineHit [ ] = [ ]
300- const lines = text . split ( '\n' )
301- for ( let i = 0 ; i < lines . length ; i ++ ) {
302- const line = lines [ i ] !
303- if ( ! PERSONAL_PATH_RE . test ( line ) ) {
304- continue
305- }
306- if ( PERSONAL_PATH_PLACEHOLDER_RE . test ( line ) ) {
343+ export const scanPersonalPaths = ( text : string ) : LineHit [ ] =>
344+ scanLines ( text , PERSONAL_PATH_RE , {
345+ filter : line => {
346+ // Pure-placeholder lines (no real path remains after stripping
347+ // every `<...>` placeholder) are documentation, not leaks.
348+ if ( ! PERSONAL_PATH_PLACEHOLDER_RE . test ( line ) ) {
349+ return false
350+ }
307351 const stripped = line . replace (
308352 new RegExp ( PERSONAL_PATH_PLACEHOLDER_RE , 'g' ) ,
309353 '' ,
310354 )
311- if ( ! PERSONAL_PATH_RE . test ( stripped ) ) {
312- continue
313- }
314- }
315- if ( looksLikeDocumentation ( line , PERSONAL_PATH_RE , 'personal-path' ) ) {
316- continue
317- }
318- hits . push ( {
319- lineNumber : i + 1 ,
320- line,
321- suggested : suggestPlaceholder ( line ) ,
322- } )
323- }
324- return hits
325- }
355+ return ! PERSONAL_PATH_RE . test ( stripped )
356+ } ,
357+ skipDocs : { rule : 'personal-path' } ,
358+ suggest : suggestPlaceholder ,
359+ } )
326360
327361// ── Secret scanners ────────────────────────────────────────────────
328362
@@ -331,53 +365,17 @@ const AWS_KEY_RE = /(aws_access_key|aws_secret|\bAKIA[0-9A-Z]{16}\b)/i
331365const GITHUB_TOKEN_RE = / g h [ p s ] _ [ a - z A - Z 0 - 9 ] { 36 } /
332366const PRIVATE_KEY_RE = / - - - - - B E G I N ( R S A | E C | D S A ) ? P R I V A T E K E Y - - - - - /
333367
334- export const scanSocketApiKeys = ( text : string ) : LineHit [ ] => {
335- const hits : LineHit [ ] = [ ]
336- const lines = text . split ( '\n' )
337- for ( let i = 0 ; i < lines . length ; i ++ ) {
338- const line = lines [ i ] !
339- if ( SOCKET_API_KEY_RE . test ( line ) && ! isAllowedApiKey ( line ) ) {
340- hits . push ( { lineNumber : i + 1 , line } )
341- }
342- }
343- return hits
344- }
368+ export const scanSocketApiKeys = ( text : string ) : LineHit [ ] =>
369+ scanLines ( text , SOCKET_API_KEY_RE , { filter : isAllowedApiKey } )
345370
346- export const scanAwsKeys = ( text : string ) : LineHit [ ] => {
347- const hits : LineHit [ ] = [ ]
348- const lines = text . split ( '\n' )
349- for ( let i = 0 ; i < lines . length ; i ++ ) {
350- const line = lines [ i ] !
351- if ( AWS_KEY_RE . test ( line ) ) {
352- hits . push ( { lineNumber : i + 1 , line } )
353- }
354- }
355- return hits
356- }
371+ export const scanAwsKeys = ( text : string ) : LineHit [ ] =>
372+ scanLines ( text , AWS_KEY_RE )
357373
358- export const scanGitHubTokens = ( text : string ) : LineHit [ ] => {
359- const hits : LineHit [ ] = [ ]
360- const lines = text . split ( '\n' )
361- for ( let i = 0 ; i < lines . length ; i ++ ) {
362- const line = lines [ i ] !
363- if ( GITHUB_TOKEN_RE . test ( line ) ) {
364- hits . push ( { lineNumber : i + 1 , line } )
365- }
366- }
367- return hits
368- }
374+ export const scanGitHubTokens = ( text : string ) : LineHit [ ] =>
375+ scanLines ( text , GITHUB_TOKEN_RE )
369376
370- export const scanPrivateKeys = ( text : string ) : LineHit [ ] => {
371- const hits : LineHit [ ] = [ ]
372- const lines = text . split ( '\n' )
373- for ( let i = 0 ; i < lines . length ; i ++ ) {
374- const line = lines [ i ] !
375- if ( PRIVATE_KEY_RE . test ( line ) ) {
376- hits . push ( { lineNumber : i + 1 , line } )
377- }
378- }
379- return hits
380- }
377+ export const scanPrivateKeys = ( text : string ) : LineHit [ ] =>
378+ scanLines ( text , PRIVATE_KEY_RE )
381379
382380// ── npx/dlx scanner ────────────────────────────────────────────────
383381//
@@ -407,34 +405,24 @@ const NPX_DLX_RE = /(?<![\w\-:=.])\b(npx|yarn dlx)\b(?![\w\-:=.])/
407405// looksLikeDocumentation(); we only ever land here for code lines, where
408406// the right swap is `pnpm exec` (since `pnpm` is the fleet's package
409407// manager) or `pnpm run` for script entries. For documentation lines
410- // that legitimately need a fetch-and-run command (user-facing
411- // instructions where the consumer doesn't have the package pinned),
412- // use `pnpm dlx` or its pnpm v11 shorthand `pnx` instead of `npx`.
408+ // All dlx-style invocations rewrite to `pnpm exec`. This matches the
409+ // `socket/no-npx-dlx` oxlint rule's autofix and the CLAUDE.md tooling
410+ // rule (NEVER use npx / pnpm dlx / yarn dlx — use pnpm exec). Keep
411+ // the alternation ordered longest-prefix-first so `pnpm dlx` matches
412+ // before any future `pnpm`-anchored rule could shadow it.
413413export function suggestNpxReplacement ( line : string ) : string {
414414 return line
415+ . replace ( / \b p n p m d l x \b / g, 'pnpm exec' )
415416 . replace ( / \b y a r n d l x \b / g, 'pnpm exec' )
416- . replace ( / \b n p x \b / g, 'pnpm dlx' )
417+ . replace ( / \b p n x \b / g, 'pnpm exec' )
418+ . replace ( / \b n p x \b / g, 'pnpm exec' )
417419}
418420
419- export const scanNpxDlx = ( text : string ) : LineHit [ ] => {
420- const hits : LineHit [ ] = [ ]
421- const lines = text . split ( '\n' )
422- for ( let i = 0 ; i < lines . length ; i ++ ) {
423- const line = lines [ i ] !
424- if ( ! NPX_DLX_RE . test ( line ) ) {
425- continue
426- }
427- if ( looksLikeDocumentation ( line , NPX_DLX_RE , 'npx' ) ) {
428- continue
429- }
430- hits . push ( {
431- lineNumber : i + 1 ,
432- line,
433- suggested : suggestNpxReplacement ( line ) ,
434- } )
435- }
436- return hits
437- }
421+ export const scanNpxDlx = ( text : string ) : LineHit [ ] =>
422+ scanLines ( text , NPX_DLX_RE , {
423+ skipDocs : { rule : 'npx' } ,
424+ suggest : suggestNpxReplacement ,
425+ } )
438426
439427// ── Logger leak scanner ────────────────────────────────────────────
440428//
@@ -464,25 +452,11 @@ export function suggestLoggerReplacement(line: string): string {
464452 . replace ( / \b c o n s o l e \. l o g \s * \( / g, 'logger.info(' )
465453}
466454
467- export const scanLoggerLeaks = ( text : string ) : LineHit [ ] => {
468- const hits : LineHit [ ] = [ ]
469- const lines = text . split ( '\n' )
470- for ( let i = 0 ; i < lines . length ; i ++ ) {
471- const line = lines [ i ] !
472- if ( ! LOGGER_LEAK_RE . test ( line ) ) {
473- continue
474- }
475- if ( looksLikeDocumentation ( line , LOGGER_LEAK_RE , 'console' ) ) {
476- continue
477- }
478- hits . push ( {
479- lineNumber : i + 1 ,
480- line,
481- suggested : suggestLoggerReplacement ( line ) ,
482- } )
483- }
484- return hits
485- }
455+ export const scanLoggerLeaks = ( text : string ) : LineHit [ ] =>
456+ scanLines ( text , LOGGER_LEAK_RE , {
457+ skipDocs : { rule : 'console' } ,
458+ suggest : suggestLoggerReplacement ,
459+ } )
486460
487461// ── Cross-repo path scanner ────────────────────────────────────────
488462//
@@ -566,9 +540,15 @@ export const scanCrossRepoPaths = (
566540}
567541
568542// ── AI attribution scanner ─────────────────────────────────────────
543+ //
544+ // Matches BOILERPLATE attribution patterns ("Generated with Claude",
545+ // "Co-Authored-By: Claude", emoji prefixes, vendor email addresses) —
546+ // not legitimate product / directory references. Bare "Claude" /
547+ // "Claude Code" / ".claude/" are valid prose; only the
548+ // attribution-verb-anchored forms trigger the hook.
569549
570550const AI_ATTRIBUTION_RE =
571- / ( G e n e r a t e d w i t h . * ( C l a u d e | A I ) | C o - A u t h o r e d - B y : C l a u d e | C o - A u t h o r e d - B y : A I | 🤖 G e n e r a t e d | A I g e n e r a t e d | @ a n t h r o p i c \. c o m | A s s i s t a n t : | G e n e r a t e d b y C l a u d e | M a c h i n e g e n e r a t e d | C l a u d e C o d e ) / i
551+ / (?: (?: G e n e r a t e d | B u i l t | C r e a t e d | M a d e | W r i t t e n | A u t h o r e d | P o w e r e d | C r a f t e d ) \s + (?: w i t h | b y ) \s + (?: C l a u d e | A I | G P T | C h a t G P T | C o p i l o t | C u r s o r | B a r d | G e m i n i ) | C o - A u t h o r e d - B y : \s + (?: C l a u d e | A I | G P T | C h a t G P T | C o p i l o t | C u r s o r | B a r d | G e m i n i ) | 🤖 \s + G e n e r a t e d | A I [ \s - ] g e n e r a t e d | M a c h i n e [ \s - ] g e n e r a t e d | @ (?: a n t h r o p i c | o p e n a i ) \. c o m | ^ A s s i s t a n t : ) / im
572552
573553export const containsAiAttribution = ( text : string ) : boolean =>
574554 AI_ATTRIBUTION_RE . test ( text )
0 commit comments