@@ -35,8 +35,9 @@ import {
3535import { SocketSdk } from '@socketsecurity/sdk'
3636import type { MalwareCheckPackage } from '@socketsecurity/sdk'
3737
38- // Hook runs standalone with only @socketsecurity /* deps, so this
39- // one-liner lives here instead of importing a shared helper.
38+ // Local mirror of build-infra/lib/error-utils#errorMessage. Hook runs
39+ // standalone (no workspace deps beyond @socketsecurity/*) so we can't import
40+ // the shared helper, but the contract is identical.
4041function errorMessage ( error : unknown ) : string {
4142 return error instanceof Error ? error . message : String ( error )
4243}
@@ -159,23 +160,46 @@ const extractors: Record<string, Extractor> = {
159160 ( m ) : Dep => ( { type : 'cargo' , name : m [ 1 ] } )
160161 ) ,
161162 'Cargo.toml' : ( content : string ) : Dep [ ] => {
162- // Rust: only extract from [dependencies], [dev-dependencies], [build-dependencies] sections.
163- // Skip [package], [lib], [bin], [workspace], [profile] metadata sections.
163+ // Rust: extract crate names from dep lines.
164+ //
165+ // Two-mode strategy because the hook receives either a full
166+ // Cargo.toml (Write) or a fragment (Edit's new_string, often just
167+ // the added line with no section header):
168+ //
169+ // Full file — scan only [dependencies] / [dev-dependencies] /
170+ // [build-dependencies] (incl. target-specific
171+ // [target.*.dependencies] via the `.<name>` suffix)
172+ // and skip [package], [features], [profile], etc.
173+ // Fragment — no section headers at all → treat the whole
174+ // content as an implicit [dependencies] body and
175+ // match any `name = "..."` or `name = { version = "..." }`.
176+ //
177+ // The lineRe requires the value to look like a version spec
178+ // (string or table with a `version` key), so `[features]`-style
179+ // `key = ["derive"]` array values don't match even in fragment mode.
164180 const deps : Dep [ ] = [ ]
165- const depSectionRe = / ^ \[ (?: (?: d e v - | b u i l d - ) ? d e p e n d e n c i e s (?: \. [ ^ \] ] + ) ? ) \] \s * $ / gm
181+ const depSectionRe = / ^ \[ (?: (?: d e v - | b u i l d - ) ? d e p e n d e n c i e s (?: \. [ ^ \] ] + ) ? | t a r g e t \. [ ^ \] ] + \. (?: d e v - | b u i l d - ) ? d e p e n d e n c i e s (?: \. [ ^ \] ] + ) ? ) \] \s * $ / gm
166182 const anySectionRe = / ^ \[ / gm
183+ const lineRe = / ^ ( \w [ \w - ] * ) \s * = \s * (?: \{ [ ^ } ] * v e r s i o n \s * = \s * " [ ^ " ] * " | \s * " [ ^ " ] * " ) / gm
184+ const push = ( section : string ) => {
185+ let m
186+ while ( ( m = lineRe . exec ( section ) ) !== null ) {
187+ deps . push ( { type : 'cargo' , name : m [ 1 ] } )
188+ }
189+ lineRe . lastIndex = 0
190+ }
191+ const hasAnySection = / ^ \[ / m. test ( content )
192+ if ( ! hasAnySection ) {
193+ push ( content )
194+ return deps
195+ }
167196 let sectionMatch
168197 while ( ( sectionMatch = depSectionRe . exec ( content ) ) !== null ) {
169198 const sectionStart = sectionMatch . index + sectionMatch [ 0 ] . length
170199 anySectionRe . lastIndex = sectionStart
171200 const nextSection = anySectionRe . exec ( content )
172201 const sectionEnd = nextSection ? nextSection . index : content . length
173- const sectionText = content . slice ( sectionStart , sectionEnd )
174- const lineRe = / ^ ( \w [ \w - ] * ) \s * = \s * (?: \{ [ ^ } ] * v e r s i o n \s * = \s * " [ ^ " ] * " | \s * " [ ^ " ] * " ) / gm
175- let m
176- while ( ( m = lineRe . exec ( sectionText ) ) !== null ) {
177- deps . push ( { type : 'cargo' , name : m [ 1 ] } )
178- }
202+ push ( content . slice ( sectionStart , sectionEnd ) )
179203 }
180204 return deps
181205 } ,
@@ -280,21 +304,6 @@ const extractors: Record<string, Extractor> = {
280304 'yarn.lock' : extractNpmLockfile ,
281305}
282306
283- // --- main (only when executed directly, not imported) ---
284-
285- if ( fileURLToPath ( import . meta. url ) === path . resolve ( process . argv [ 1 ] ) ) {
286- // Read the full JSON blob from stdin (piped by Claude Code).
287- let input = ''
288- for await ( const chunk of process . stdin ) input += chunk
289- const hook : HookInput = JSON . parse ( input )
290-
291- if ( hook . tool_name !== 'Edit' && hook . tool_name !== 'Write' ) {
292- process . exitCode = 0
293- } else {
294- process . exitCode = await check ( hook )
295- }
296- }
297-
298307// --- core ---
299308
300309// Orchestrates the full check: extract deps, diff against old, query API.
@@ -728,3 +737,26 @@ export {
728737 extractTerraform ,
729738 findExtractor ,
730739}
740+
741+ // --- main (only when executed directly, not imported) ---
742+ //
743+ // Kept at the bottom because the module uses top-level await
744+ // (`for await (const chunk of process.stdin)`) to read the hook payload.
745+ // Top-level await suspends module evaluation at the suspension point, so
746+ // any `const` declared AFTER the suspending block is still in the TDZ
747+ // when the awaited work calls back into the module (e.g. extractNpm →
748+ // PACKAGE_JSON_METADATA_KEYS). Placing main last guarantees every
749+ // module-level declaration is initialized before main runs.
750+
751+ if ( fileURLToPath ( import . meta. url ) === path . resolve ( process . argv [ 1 ] ) ) {
752+ // Read the full JSON blob from stdin (piped by Claude Code).
753+ let input = ''
754+ for await ( const chunk of process . stdin ) input += chunk
755+ const hook : HookInput = JSON . parse ( input )
756+
757+ if ( hook . tool_name !== 'Edit' && hook . tool_name !== 'Write' ) {
758+ process . exitCode = 0
759+ } else {
760+ process . exitCode = await check ( hook )
761+ }
762+ }
0 commit comments