From 3c4c29ae00f24b3f3e5d53766e5c33fea746a2f6 Mon Sep 17 00:00:00 2001 From: David Larsen Date: Sun, 5 Apr 2026 09:19:11 -0400 Subject: [PATCH 1/2] Add optional Claude Code hook for blocking malicious packages Adds a PreToolUse hook (socket-gate.ts) that intercepts npm/yarn/bun/pnpm install commands and checks packages against the Socket API. Blocks packages with critical or high severity alerts (typosquats, malware, supply chain attacks). Fails open on all errors. Includes tests and README documentation. --- README.md | 61 +++++++++ hooks/socket-gate.test.ts | 112 ++++++++++++++++ hooks/socket-gate.ts | 264 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 437 insertions(+) create mode 100644 hooks/socket-gate.test.ts create mode 100644 hooks/socket-gate.ts diff --git a/README.md b/README.md index 6f8e319..83ee125 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,67 @@ This approach automatically uses the latest version without requiring global ins } ``` +## Claude Code Hook (Optional) + +The repo includes an optional [Claude Code hook](https://code.claude.com/docs/en/hooks) that automatically blocks malicious packages before installation. When Claude Code runs `npm install`, `yarn add`, `bun add`, or `pnpm add`, the hook checks the package against Socket and blocks it if critical or high severity alerts are found (typosquats, malware, supply chain attacks). + +The hook fails open on all errors, so it never blocks legitimate work. + +### Hook Setup + +**Prerequisites:** Node.js 22+ and a [Socket API key](https://docs.socket.dev/reference/creating-and-managing-api-tokens) (`packages:list` scope). + +1. Copy the hook script: + +```bash +mkdir -p ~/.claude/hooks +cp hooks/socket-gate.ts ~/.claude/hooks/ +``` + +2. Add to `~/.claude/settings.json`: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "SOCKET_API_KEY=your-api-key-here node --experimental-strip-types ~/.claude/hooks/socket-gate.ts" + } + ] + } + ] + } +} +``` + +If `SOCKET_API_KEY` is already in your shell environment, you can omit it from the command. + +### How it works + +| Alert Severity | Decision | Example | +|----------------|----------|---------| +| **Critical** | Block installation | `browserlist` (typosquat of `browserslist`) | +| **High** | Block installation | Packages with known supply chain risks | +| **Low/None** | Allow | `express`, `lodash`, `react` | + +### Testing the hook + +```bash +# Should block (typosquat) +echo '{"session_id":"test","tool_name":"Bash","tool_input":{"command":"npm install browserlist"}}' \ + | SOCKET_API_KEY=your-key node --experimental-strip-types hooks/socket-gate.ts + +# Should allow (safe package) +echo '{"session_id":"test","tool_name":"Bash","tool_input":{"command":"npm install express"}}' \ + | SOCKET_API_KEY=your-key node --experimental-strip-types hooks/socket-gate.ts +``` + +Inspired by [Jimmy Vo's dependency hook](https://blog.jimmyvo.com/posts/claudes-dependency-hook/). + ## Tools exposed by the Socket MCP Server ### depscore diff --git a/hooks/socket-gate.test.ts b/hooks/socket-gate.test.ts new file mode 100644 index 0000000..c235ff7 --- /dev/null +++ b/hooks/socket-gate.test.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node +import { test } from 'node:test' +import assert from 'node:assert' +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' + +const hookPath = join(import.meta.dirname, 'socket-gate.ts') + +function runHook (input: string, env?: Record): string { + return execFileSync('node', ['--experimental-strip-types', hookPath], { + input, + encoding: 'utf-8', + timeout: 30_000, + env: { + ...process.env, + ...env + } + }).trim() +} + +function parseOutput (output: string): { decision: string, reason?: string } { + const parsed = JSON.parse(output) + return { + decision: parsed.hookSpecificOutput.permissionDecision, + reason: parsed.hookSpecificOutput.permissionDecisionReason + } +} + +function makeInput (command: string): string { + return JSON.stringify({ + session_id: 'test', + tool_name: 'Bash', + tool_input: { command } + }) +} + +test('socket-gate hook', async (t) => { + const apiKey = process.env['SOCKET_API_KEY'] + + // ======================================== + // Unit tests (no API key required) + // ======================================== + + await t.test('allows non-Bash tools', () => { + const input = JSON.stringify({ session_id: 'test', tool_name: 'Read', tool_input: { path: '/tmp/foo' } }) + const result = parseOutput(runHook(input)) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('allows non-install commands', () => { + const result = parseOutput(runHook(makeInput('ls -la'))) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('allows lockfile-only installs', () => { + for (const cmd of ['npm install', 'npm i', 'npm ci', 'yarn', 'yarn install', 'bun install', 'pnpm install']) { + const result = parseOutput(runHook(makeInput(cmd))) + assert.strictEqual(result.decision, 'allow', `should allow: ${cmd}`) + } + }) + + await t.test('allows empty input', () => { + const result = parseOutput(runHook('')) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('allows invalid JSON', () => { + const result = parseOutput(runHook('not json')) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('allows when no API key is set', () => { + const result = parseOutput(runHook(makeInput('npm install malicious-pkg'), { SOCKET_API_KEY: '' })) + assert.strictEqual(result.decision, 'allow') + }) + + // ======================================== + // Integration tests (require SOCKET_API_KEY) + // ======================================== + + await t.test('allows safe package (lodash)', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + const result = parseOutput(runHook(makeInput('npm install lodash'))) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('allows safe scoped package (@types/node)', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + const result = parseOutput(runHook(makeInput('yarn add @types/node'))) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('blocks typosquat (browserlist)', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + const result = parseOutput(runHook(makeInput('npm install browserlist'))) + assert.strictEqual(result.decision, 'deny') + assert.ok(result.reason?.includes('browserlist'), 'reason should mention package name') + assert.ok(result.reason?.includes('socket.dev'), 'reason should include review link') + }) + + await t.test('handles versioned install', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + const result = parseOutput(runHook(makeInput('npm install express@4.18.2'))) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('handles pnpm add', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + const result = parseOutput(runHook(makeInput('pnpm add express'))) + assert.strictEqual(result.decision, 'allow') + }) + + await t.test('handles bun add', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + const result = parseOutput(runHook(makeInput('bun add express'))) + assert.strictEqual(result.decision, 'allow') + }) +}) diff --git a/hooks/socket-gate.ts b/hooks/socket-gate.ts new file mode 100644 index 0000000..dc8a284 --- /dev/null +++ b/hooks/socket-gate.ts @@ -0,0 +1,264 @@ +#!/usr/bin/env -S node --experimental-strip-types +/** + * socket-gate.ts — Claude Code PreToolUse hook + * + * Intercepts npm/yarn/bun/pnpm install commands and checks packages against + * the Socket API. Blocks packages with critical alerts (malware, typosquats) + * and warns on high severity supply chain risks. + * + * Setup: + * 1. Copy this file to ~/.claude/hooks/socket-gate.ts + * 2. Add to ~/.claude/settings.json (see README) + * 3. Set SOCKET_API_KEY env var + * + * Fails open on all errors (network, auth, parse) so it never blocks + * legitimate work. + */ + +import { readFileSync } from 'node:fs' + +// ======================================== +// Types +// ======================================== + +interface HookInput { + session_id: string + tool_name: string + tool_input: Record | string +} + +interface SocketAlert { + type: string + severity: string + category?: string + props?: Record +} + +interface PurlResponseLine { + _type?: string + score?: Record + alerts?: SocketAlert[] + name?: string + namespace?: string + type?: string + version?: string + [key: string]: unknown +} + +// ======================================== +// Hook output helpers (Claude Code PreToolUse format) +// ======================================== + +function outputAllow (): void { + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'allow' + } + })) +} + +function outputDeny (reason: string): void { + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: reason + } + })) +} + +// ======================================== +// Package extraction +// ======================================== + +const INSTALL_PATTERNS = [ + /npm\s+(?:install|i|add)\s+([^\s-][^\s]*)/i, + /yarn\s+add\s+([^\s-][^\s]*)/i, + /bun\s+add\s+([^\s-][^\s]*)/i, + /pnpm\s+add\s+([^\s-][^\s]*)/i +] + +const LOCKFILE_PATTERNS = [ + /^npm\s+(install|i|ci)\s*$/i, + /^yarn\s*(install)?\s*$/i, + /^bun\s+install\s*$/i, + /^pnpm\s+install\s*$/i +] + +export function extractPackageName (command: string): string | null { + if (LOCKFILE_PATTERNS.some(p => p.test(command.trim()))) { + return null + } + + for (const pattern of INSTALL_PATTERNS) { + const match = command.match(pattern) + if (match) { + const pkg = match[1] + // Strip version specifiers: @scope/pkg@1.2.3 -> @scope/pkg + return pkg.replace(/@[\d^~].*/u, '').replace(/@latest$/u, '') + } + } + + return null +} + +// ======================================== +// PURL construction (npm only, inline) +// ======================================== + +export function buildNpmPurl (packageName: string): string { + if (packageName.startsWith('@') && packageName.includes('/')) { + const slash = packageName.indexOf('/') + const scope = encodeURIComponent(packageName.slice(0, slash)) + const name = packageName.slice(slash + 1) + return `pkg:npm/${scope}/${name}` + } + return `pkg:npm/${packageName}` +} + +// ======================================== +// Socket API +// ======================================== + +const DEFAULT_SOCKET_API_URL = 'https://api.socket.dev/v0/purl' + +function getSocketApiUrl (): string { + if (process.env['SOCKET_API_URL']) { + return process.env['SOCKET_API_URL'] + } + return `${DEFAULT_SOCKET_API_URL}?alerts=true&compact=false&fixable=false&licenseattrib=false&licensedetails=false` +} + +export async function checkPackage (packageName: string, apiKey: string): Promise<{ decision: 'allow' | 'deny', reason: string }> { + const purl = buildNpmPurl(packageName) + + const response = await fetch(getSocketApiUrl(), { + method: 'POST', + headers: { + 'user-agent': 'socket-mcp-hook/1.0', + accept: 'application/x-ndjson', + 'content-type': 'application/json', + authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify({ components: [{ purl }] }), + signal: AbortSignal.timeout(15_000) + }) + + if (!response.ok) { + return { decision: 'allow', reason: '' } + } + + const text = await response.text() + if (!text.trim()) { + return { decision: 'allow', reason: '' } + } + + const lines: PurlResponseLine[] = text + .split('\n') + .filter(line => line.trim()) + .map(line => JSON.parse(line) as PurlResponseLine) + .filter(obj => !obj._type) + + if (lines.length === 0) { + return { decision: 'allow', reason: '' } + } + + const pkg = lines[0] + const alerts = pkg.alerts || [] + + const critical = alerts.filter(a => a.severity === 'critical') + const high = alerts.filter(a => a.severity === 'high') + + if (critical.length > 0) { + const details = critical + .map(a => ` - ${a.type}: ${a.category || 'detected'}`) + .join('\n') + + return { + decision: 'deny', + reason: `Socket blocked "${packageName}" (${critical.length} critical alert${critical.length > 1 ? 's' : ''}):\n\n${details}\n\nReview: https://socket.dev/npm/package/${packageName}` + } + } + + if (high.length > 0) { + const details = high + .map(a => ` - ${a.type}: ${a.category || 'detected'}`) + .join('\n') + + return { + decision: 'deny', + reason: `Socket blocked "${packageName}" (${high.length} high severity alert${high.length > 1 ? 's' : ''}):\n\n${details}\n\nReview: https://socket.dev/npm/package/${packageName}` + } + } + + return { decision: 'allow', reason: '' } +} + +// ======================================== +// Main +// ======================================== + +async function main (): Promise { + let raw: string + try { + raw = readFileSync(0, 'utf-8') + } catch { + outputAllow() + return + } + + if (!raw.trim()) { + outputAllow() + return + } + + let input: HookInput + try { + input = JSON.parse(raw) + } catch { + outputAllow() + return + } + + if (input.tool_name !== 'Bash') { + outputAllow() + return + } + + const command = typeof input.tool_input === 'string' + ? input.tool_input + : (input.tool_input?.command as string) || '' + + if (!command) { + outputAllow() + return + } + + const packageName = extractPackageName(command) + if (!packageName) { + outputAllow() + return + } + + const apiKey = process.env['SOCKET_API_KEY'] + if (!apiKey) { + outputAllow() + return + } + + try { + const result = await checkPackage(packageName, apiKey) + if (result.decision === 'deny') { + outputDeny(result.reason) + } else { + outputAllow() + } + } catch { + outputAllow() + } +} + +main().catch(() => { + outputAllow() +}) From df90d327b973dd53fbf35a9853f66aa14cc1f290 Mon Sep 17 00:00:00 2001 From: David Larsen Date: Sun, 5 Apr 2026 09:27:09 -0400 Subject: [PATCH 2/2] Switch hook from direct API to Socket CLI (no API key needed) Uses 'socket package score' instead of calling the /v0/purl endpoint directly. Auth is handled by the CLI's own config (socket login), so no SOCKET_API_KEY env var is required. --- README.md | 13 +++-- hooks/socket-gate.test.ts | 43 +++++++------- hooks/socket-gate.ts | 119 +++++++++++++------------------------- 3 files changed, 69 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 83ee125..581611b 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,10 @@ The hook fails open on all errors, so it never blocks legitimate work. ### Hook Setup -**Prerequisites:** Node.js 22+ and a [Socket API key](https://docs.socket.dev/reference/creating-and-managing-api-tokens) (`packages:list` scope). +**Prerequisites:** +- Node.js 22+ +- [Socket CLI](https://www.npmjs.com/package/@socketsecurity/cli): `npm install -g @socketsecurity/cli` +- Run `socket login` to authenticate (one-time setup, no env vars needed) 1. Copy the hook script: @@ -251,7 +254,7 @@ cp hooks/socket-gate.ts ~/.claude/hooks/ "hooks": [ { "type": "command", - "command": "SOCKET_API_KEY=your-api-key-here node --experimental-strip-types ~/.claude/hooks/socket-gate.ts" + "command": "node --experimental-strip-types ~/.claude/hooks/socket-gate.ts" } ] } @@ -260,8 +263,6 @@ cp hooks/socket-gate.ts ~/.claude/hooks/ } ``` -If `SOCKET_API_KEY` is already in your shell environment, you can omit it from the command. - ### How it works | Alert Severity | Decision | Example | @@ -275,11 +276,11 @@ If `SOCKET_API_KEY` is already in your shell environment, you can omit it from t ```bash # Should block (typosquat) echo '{"session_id":"test","tool_name":"Bash","tool_input":{"command":"npm install browserlist"}}' \ - | SOCKET_API_KEY=your-key node --experimental-strip-types hooks/socket-gate.ts + | node --experimental-strip-types hooks/socket-gate.ts # Should allow (safe package) echo '{"session_id":"test","tool_name":"Bash","tool_input":{"command":"npm install express"}}' \ - | SOCKET_API_KEY=your-key node --experimental-strip-types hooks/socket-gate.ts + | node --experimental-strip-types hooks/socket-gate.ts ``` Inspired by [Jimmy Vo's dependency hook](https://blog.jimmyvo.com/posts/claudes-dependency-hook/). diff --git a/hooks/socket-gate.test.ts b/hooks/socket-gate.test.ts index c235ff7..0e26fdf 100644 --- a/hooks/socket-gate.test.ts +++ b/hooks/socket-gate.test.ts @@ -6,15 +6,12 @@ import { join } from 'node:path' const hookPath = join(import.meta.dirname, 'socket-gate.ts') -function runHook (input: string, env?: Record): string { +function runHook (input: string): string { return execFileSync('node', ['--experimental-strip-types', hookPath], { input, encoding: 'utf-8', - timeout: 30_000, - env: { - ...process.env, - ...env - } + timeout: 60_000, + env: { ...process.env } }).trim() } @@ -34,11 +31,20 @@ function makeInput (command: string): string { }) } -test('socket-gate hook', async (t) => { - const apiKey = process.env['SOCKET_API_KEY'] +function socketCliAvailable (): boolean { + try { + execFileSync('which', ['socket'], { encoding: 'utf-8', timeout: 5_000 }) + return true + } catch { + return false + } +} + +const hasCli = socketCliAvailable() +test('socket-gate hook', async (t) => { // ======================================== - // Unit tests (no API key required) + // Unit tests (no Socket CLI required) // ======================================== await t.test('allows non-Bash tools', () => { @@ -69,43 +75,38 @@ test('socket-gate hook', async (t) => { assert.strictEqual(result.decision, 'allow') }) - await t.test('allows when no API key is set', () => { - const result = parseOutput(runHook(makeInput('npm install malicious-pkg'), { SOCKET_API_KEY: '' })) - assert.strictEqual(result.decision, 'allow') - }) - // ======================================== - // Integration tests (require SOCKET_API_KEY) + // Integration tests (require Socket CLI with `socket login`) // ======================================== - await t.test('allows safe package (lodash)', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + await t.test('allows safe package (lodash)', { skip: !hasCli && 'Socket CLI not installed' }, () => { const result = parseOutput(runHook(makeInput('npm install lodash'))) assert.strictEqual(result.decision, 'allow') }) - await t.test('allows safe scoped package (@types/node)', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + await t.test('allows safe scoped package (@types/node)', { skip: !hasCli && 'Socket CLI not installed' }, () => { const result = parseOutput(runHook(makeInput('yarn add @types/node'))) assert.strictEqual(result.decision, 'allow') }) - await t.test('blocks typosquat (browserlist)', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + await t.test('blocks typosquat (browserlist)', { skip: !hasCli && 'Socket CLI not installed' }, () => { const result = parseOutput(runHook(makeInput('npm install browserlist'))) assert.strictEqual(result.decision, 'deny') assert.ok(result.reason?.includes('browserlist'), 'reason should mention package name') assert.ok(result.reason?.includes('socket.dev'), 'reason should include review link') }) - await t.test('handles versioned install', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + await t.test('handles versioned install', { skip: !hasCli && 'Socket CLI not installed' }, () => { const result = parseOutput(runHook(makeInput('npm install express@4.18.2'))) assert.strictEqual(result.decision, 'allow') }) - await t.test('handles pnpm add', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + await t.test('handles pnpm add', { skip: !hasCli && 'Socket CLI not installed' }, () => { const result = parseOutput(runHook(makeInput('pnpm add express'))) assert.strictEqual(result.decision, 'allow') }) - await t.test('handles bun add', { skip: !apiKey && 'SOCKET_API_KEY not set' }, () => { + await t.test('handles bun add', { skip: !hasCli && 'Socket CLI not installed' }, () => { const result = parseOutput(runHook(makeInput('bun add express'))) assert.strictEqual(result.decision, 'allow') }) diff --git a/hooks/socket-gate.ts b/hooks/socket-gate.ts index dc8a284..bb183a9 100644 --- a/hooks/socket-gate.ts +++ b/hooks/socket-gate.ts @@ -3,19 +3,23 @@ * socket-gate.ts — Claude Code PreToolUse hook * * Intercepts npm/yarn/bun/pnpm install commands and checks packages against - * the Socket API. Blocks packages with critical alerts (malware, typosquats) - * and warns on high severity supply chain risks. + * Socket. Blocks packages with critical alerts (malware, typosquats) + * and high severity supply chain risks. + * + * Uses the Socket CLI (`socket package score`) which handles its own auth + * via `socket login`. No API key env var needed. * * Setup: - * 1. Copy this file to ~/.claude/hooks/socket-gate.ts - * 2. Add to ~/.claude/settings.json (see README) - * 3. Set SOCKET_API_KEY env var + * 1. Install Socket CLI: npm install -g @socketsecurity/cli && socket login + * 2. Copy this file to ~/.claude/hooks/socket-gate.ts + * 3. Add to ~/.claude/settings.json (see README) * - * Fails open on all errors (network, auth, parse) so it never blocks - * legitimate work. + * Fails open on all errors (CLI missing, network timeout, parse failures) + * so it never blocks legitimate work. */ import { readFileSync } from 'node:fs' +import { execFileSync } from 'node:child_process' // ======================================== // Types @@ -28,21 +32,18 @@ interface HookInput { } interface SocketAlert { - type: string + name: string severity: string category?: string - props?: Record } -interface PurlResponseLine { - _type?: string - score?: Record - alerts?: SocketAlert[] - name?: string - namespace?: string - type?: string - version?: string - [key: string]: unknown +interface SocketScoreResult { + ok?: boolean + data?: { + self?: { + alerts?: SocketAlert[] + } + } } // ======================================== @@ -104,75 +105,34 @@ export function extractPackageName (command: string): string | null { } // ======================================== -// PURL construction (npm only, inline) -// ======================================== - -export function buildNpmPurl (packageName: string): string { - if (packageName.startsWith('@') && packageName.includes('/')) { - const slash = packageName.indexOf('/') - const scope = encodeURIComponent(packageName.slice(0, slash)) - const name = packageName.slice(slash + 1) - return `pkg:npm/${scope}/${name}` - } - return `pkg:npm/${packageName}` -} - -// ======================================== -// Socket API +// Socket CLI // ======================================== -const DEFAULT_SOCKET_API_URL = 'https://api.socket.dev/v0/purl' - -function getSocketApiUrl (): string { - if (process.env['SOCKET_API_URL']) { - return process.env['SOCKET_API_URL'] +function isSocketInstalled (): boolean { + try { + execFileSync('which', ['socket'], { encoding: 'utf-8', timeout: 5_000 }) + return true + } catch { + return false } - return `${DEFAULT_SOCKET_API_URL}?alerts=true&compact=false&fixable=false&licenseattrib=false&licensedetails=false` } -export async function checkPackage (packageName: string, apiKey: string): Promise<{ decision: 'allow' | 'deny', reason: string }> { - const purl = buildNpmPurl(packageName) - - const response = await fetch(getSocketApiUrl(), { - method: 'POST', - headers: { - 'user-agent': 'socket-mcp-hook/1.0', - accept: 'application/x-ndjson', - 'content-type': 'application/json', - authorization: `Bearer ${apiKey}` - }, - body: JSON.stringify({ components: [{ purl }] }), - signal: AbortSignal.timeout(15_000) - }) - - if (!response.ok) { - return { decision: 'allow', reason: '' } - } - - const text = await response.text() - if (!text.trim()) { - return { decision: 'allow', reason: '' } - } - - const lines: PurlResponseLine[] = text - .split('\n') - .filter(line => line.trim()) - .map(line => JSON.parse(line) as PurlResponseLine) - .filter(obj => !obj._type) - - if (lines.length === 0) { - return { decision: 'allow', reason: '' } - } +export function checkPackage (packageName: string): { decision: 'allow' | 'deny', reason: string } { + const result = execFileSync( + 'socket', + ['package', 'score', 'npm', packageName, '--json', '--no-banner'], + { encoding: 'utf-8', timeout: 30_000, maxBuffer: 10 * 1024 * 1024 } + ) - const pkg = lines[0] - const alerts = pkg.alerts || [] + const parsed: SocketScoreResult = JSON.parse(result) + const alerts = parsed.data?.self?.alerts || [] const critical = alerts.filter(a => a.severity === 'critical') const high = alerts.filter(a => a.severity === 'high') if (critical.length > 0) { const details = critical - .map(a => ` - ${a.type}: ${a.category || 'detected'}`) + .map(a => ` - ${a.name}: ${a.category || 'detected'}`) .join('\n') return { @@ -183,7 +143,7 @@ export async function checkPackage (packageName: string, apiKey: string): Promis if (high.length > 0) { const details = high - .map(a => ` - ${a.type}: ${a.category || 'detected'}`) + .map(a => ` - ${a.name}: ${a.category || 'detected'}`) .join('\n') return { @@ -241,20 +201,21 @@ async function main (): Promise { return } - const apiKey = process.env['SOCKET_API_KEY'] - if (!apiKey) { + if (!isSocketInstalled()) { + // CLI not installed, fail open outputAllow() return } try { - const result = await checkPackage(packageName, apiKey) + const result = checkPackage(packageName) if (result.decision === 'deny') { outputDeny(result.reason) } else { outputAllow() } } catch { + // Fail open on any error outputAllow() } }