33 * socket-gate.ts — Claude Code PreToolUse hook
44 *
55 * Intercepts npm/yarn/bun/pnpm install commands and checks packages against
6- * the Socket API. Blocks packages with critical alerts (malware, typosquats)
7- * and warns on high severity supply chain risks.
6+ * Socket. Blocks packages with critical alerts (malware, typosquats)
7+ * and high severity supply chain risks.
8+ *
9+ * Uses the Socket CLI (`socket package score`) which handles its own auth
10+ * via `socket login`. No API key env var needed.
811 *
912 * Setup:
10- * 1. Copy this file to ~/.claude/hooks/ socket-gate.ts
11- * 2. Add to ~/.claude/settings.json (see README)
12- * 3. Set SOCKET_API_KEY env var
13+ * 1. Install Socket CLI: npm install -g @socketsecurity/cli && socket login
14+ * 2. Copy this file to ~/.claude/hooks/socket-gate.ts
15+ * 3. Add to ~/.claude/settings.json (see README)
1316 *
14- * Fails open on all errors (network, auth , parse) so it never blocks
15- * legitimate work.
17+ * Fails open on all errors (CLI missing, network timeout , parse failures)
18+ * so it never blocks legitimate work.
1619 */
1720
1821import { readFileSync } from 'node:fs'
22+ import { execFileSync } from 'node:child_process'
1923
2024// ========================================
2125// Types
@@ -28,21 +32,18 @@ interface HookInput {
2832}
2933
3034interface SocketAlert {
31- type : string
35+ name : string
3236 severity: string
3337 category ? : string
34- props ? : Record < string , unknown >
3538}
3639
37- interface PurlResponseLine {
38- _type ?: string
39- score ?: Record < string , unknown>
40- alerts ?: SocketAlert [ ]
41- name ?: string
42- namespace ? : string
43- type ? : string
44- version ?: string
45- [ key : string ] : unknown
40+ interface SocketScoreResult {
41+ ok ? : boolean
42+ data ?: {
43+ self ?: {
44+ alerts ?: SocketAlert [ ]
45+ }
46+ }
4647}
4748
4849// ========================================
@@ -104,75 +105,34 @@ export function extractPackageName (command: string): string | null {
104105}
105106
106107// ========================================
107- // PURL construction (npm only, inline)
108- // ========================================
109-
110- export function buildNpmPurl ( packageName : string ) : string {
111- if ( packageName . startsWith ( '@' ) && packageName . includes ( '/' ) ) {
112- const slash = packageName . indexOf ( '/' )
113- const scope = encodeURIComponent ( packageName . slice ( 0 , slash ) )
114- const name = packageName . slice ( slash + 1 )
115- return `pkg:npm/${ scope } /${ name } `
116- }
117- return `pkg:npm/${ packageName } `
118- }
119-
120- // ========================================
121- // Socket API
108+ // Socket CLI
122109// ========================================
123110
124- const DEFAULT_SOCKET_API_URL = 'https://api.socket.dev/v0/purl'
125-
126- function getSocketApiUrl ( ) : string {
127- if ( process . env [ 'SOCKET_API_URL' ] ) {
128- return process . env [ 'SOCKET_API_URL' ]
111+ function isSocketInstalled ( ) : boolean {
112+ try {
113+ execFileSync ( 'which' , [ 'socket' ] , { encoding : 'utf-8' , timeout : 5_000 } )
114+ return true
115+ } catch {
116+ return false
129117 }
130- return `${ DEFAULT_SOCKET_API_URL } ?alerts=true&compact=false&fixable=false&licenseattrib=false&licensedetails=false`
131118}
132119
133- export async function checkPackage ( packageName : string , apiKey : string ) : Promise < { decision: 'allow' | 'deny' , reason : string } > {
134- const purl = buildNpmPurl ( packageName )
135-
136- const response = await fetch ( getSocketApiUrl ( ) , {
137- method : 'POST' ,
138- headers : {
139- 'user-agent' : 'socket-mcp-hook/1.0' ,
140- accept : 'application/x-ndjson' ,
141- 'content-type' : 'application/json' ,
142- authorization : `Bearer ${ apiKey } `
143- } ,
144- body : JSON . stringify ( { components : [ { purl } ] } ) ,
145- signal : AbortSignal . timeout ( 15_000 )
146- } )
147-
148- if ( ! response . ok ) {
149- return { decision : 'allow' , reason : '' }
150- }
151-
152- const text = await response . text ( )
153- if ( ! text . trim ( ) ) {
154- return { decision : 'allow' , reason : '' }
155- }
156-
157- const lines : PurlResponseLine [ ] = text
158- . split ( '\n' )
159- . filter ( line => line . trim ( ) )
160- . map ( line => JSON . parse ( line ) as PurlResponseLine )
161- . filter ( obj => ! obj . _type )
162-
163- if ( lines . length === 0 ) {
164- return { decision : 'allow' , reason : '' }
165- }
120+ export function checkPackage ( packageName : string ) : { decision: 'allow' | 'deny' , reason : string } {
121+ const result = execFileSync (
122+ 'socket' ,
123+ [ 'package' , 'score' , 'npm' , packageName , '--json' , '--no-banner' ] ,
124+ { encoding : 'utf-8' , timeout : 30_000 , maxBuffer : 10 * 1024 * 1024 }
125+ )
166126
167- const pkg = lines [ 0 ]
168- const alerts = pkg . alerts || [ ]
127+ const parsed : SocketScoreResult = JSON . parse ( result )
128+ const alerts = parsed . data ?. self ? .alerts || [ ]
169129
170130 const critical = alerts . filter ( a => a . severity === 'critical' )
171131 const high = alerts . filter ( a => a . severity === 'high' )
172132
173133 if ( critical . length > 0 ) {
174134 const details = critical
175- . map ( a => ` - ${ a . type } : ${ a . category || 'detected' } ` )
135+ . map ( a => ` - ${ a . name } : ${ a . category || 'detected' } ` )
176136 . join ( '\n' )
177137
178138 return {
@@ -183,7 +143,7 @@ export async function checkPackage (packageName: string, apiKey: string): Promis
183143
184144 if ( high . length > 0 ) {
185145 const details = high
186- . map ( a => ` - ${ a . type } : ${ a . category || 'detected' } ` )
146+ . map ( a => ` - ${ a . name } : ${ a . category || 'detected' } ` )
187147 . join ( '\n' )
188148
189149 return {
@@ -241,20 +201,21 @@ async function main (): Promise<void> {
241201 return
242202 }
243203
244- const apiKey = process . env [ 'SOCKET_API_KEY' ]
245- if ( ! apiKey ) {
204+ if ( ! isSocketInstalled ( ) ) {
205+ // CLI not installed, fail open
246206 outputAllow ( )
247207 return
248208 }
249209
250210 try {
251- const result = await checkPackage ( packageName , apiKey )
211+ const result = checkPackage ( packageName )
252212 if ( result . decision === 'deny' ) {
253213 outputDeny ( result . reason )
254214 } else {
255215 outputAllow ( )
256216 }
257217 } catch {
218+ // Fail open on any error
258219 outputAllow ( )
259220 }
260221}
0 commit comments