Skip to content

Commit 584abec

Browse files
committed
chore(claude): sync setup-security-tools to canonical
Replaces the older zod-based setup-security-tools with canonical TypeBox version (sourced from socket-repo-template/template). Key changes: - TypeBox schemas (matches the rest of the fleet's xport pattern) - PURL-based AgentShield package spec (pkg:npm/ecc-agentshield@1.4.0) - downloadPackage from @socketsecurity/lib/dlx/package — installs AgentShield via dlx instead of requiring it as a workspace devDep, so consumers don't need ecc-agentshield in devDependencies - mkdtemp (collision-safe) instead of Date.now()-only naming - normalizePath on binary paths - parseSchema from @socketsecurity/lib/schema/parse - pip3 added to ecosystems lists The hook's package.json now declares @sinclair/typebox + @socketregistry/packageurl-js (catalog refs); the new socket-registry setup action provisions all three zero-dep packages (@socketsecurity/lib + @socketregistry/packageurl-js + @sinclair/typebox) via the multi-package bootstrap loop, so a fresh checkout has them resolvable at hook-load time.
1 parent a22fcfd commit 584abec

4 files changed

Lines changed: 91 additions & 46 deletions

File tree

.claude/hooks/setup-security-tools/README.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,24 @@ Sets up all three Socket security tools for local development in one command.
55
## Tools
66

77
### 1. AgentShield
8+
89
Scans your Claude Code configuration (`.claude/` directory) for security issues like prompt injection, leaked secrets, and overly permissive tool permissions.
910

10-
**How it's installed**: Already a devDependency (`ecc-agentshield`). The setup script just verifies it's available — if not, run `pnpm install`.
11+
**How it's installed**: npm package downloaded via the dlx system (pinned version + integrity hash from `external-tools.json`), cached at `~/.socket/_dlx/`. Subsequent runs reuse the cache. No `devDependencies` entry required in the consumer repo.
1112

1213
### 2. Zizmor
14+
1315
Static analysis tool for GitHub Actions workflows. Catches unpinned actions, secret exposure, template injection, and permission issues.
1416

1517
**How it's installed**: Binary downloaded from [GitHub releases](https://github.com/zizmorcore/zizmor/releases), SHA-256 verified, cached via the dlx system at `~/.socket/_dlx/`. If you already have it via `brew install zizmor`, the download is skipped.
1618

1719
### 3. SFW (Socket Firewall)
20+
1821
Intercepts package manager commands (`npm install`, `pnpm add`, etc.) and scans packages against Socket.dev's malware database before installation.
1922

2023
**How it's installed**: Binary downloaded from GitHub, SHA-256 verified, cached via the dlx system at `~/.socket/_dlx/`. Small wrapper scripts ("shims") are created at `~/.socket/sfw/shims/` that transparently route commands through the firewall.
2124

22-
**Free vs Enterprise**: If you have a `SOCKET_API_KEY` (in env, `.env`, or `.env.local`), enterprise mode is used with additional ecosystem support (gem, bundler, nuget, go). Otherwise, free mode covers npm, yarn, pnpm, pip, uv, and cargo.
25+
**Free vs Enterprise**: If you have a `SOCKET_API_KEY` (in env, `.env`, or `.env.local`), enterprise mode is used with additional ecosystem support (gem, bundler, nuget, go). Otherwise, free mode covers npm, yarn, pnpm, pip, pip3, uv, and cargo.
2326

2427
## How to use
2528

@@ -31,16 +34,17 @@ Claude will ask if you have an API key, then run the setup script.
3134

3235
## What gets installed where
3336

34-
| Tool | Location | Persists across repos? |
35-
|------|----------|----------------------|
36-
| AgentShield | `node_modules/.bin/agentshield` | No (per-repo devDep) |
37-
| Zizmor | `~/.socket/_dlx/<hash>/zizmor` | Yes |
38-
| SFW binary | `~/.socket/_dlx/<hash>/sfw` | Yes |
39-
| SFW shims | `~/.socket/sfw/shims/npm`, etc. | Yes |
37+
| Tool | Location | Persists across repos? |
38+
| ----------- | ----------------------------- | ---------------------- |
39+
| AgentShield | `~/.socket/_dlx/<hash>/agentshield` | Yes |
40+
| Zizmor | `~/.socket/_dlx/<hash>/zizmor` | Yes |
41+
| SFW binary | `~/.socket/_dlx/<hash>/sfw` | Yes |
42+
| SFW shims | `~/.socket/sfw/shims/npm`, etc. | Yes |
4043

4144
## Pre-push integration
4245

4346
The `.git-hooks/pre-push` hook automatically runs:
47+
4448
- **AgentShield scan** (blocks push on failure)
4549
- **Zizmor scan** (blocks push on failure)
4650

@@ -49,7 +53,8 @@ This means every push is checked — you don't have to remember to run `/securit
4953
## Re-running
5054

5155
Safe to run multiple times:
52-
- AgentShield: just re-checks availability
56+
57+
- AgentShield: skips download if cached binary matches the pinned version
5358
- Zizmor: skips download if cached binary matches expected version
5459
- SFW: skips download if cached, only rewrites shims if content changed
5560

@@ -58,16 +63,16 @@ Safe to run multiple times:
5863
Self-contained. To add to another Socket repo:
5964

6065
1. Copy `.claude/hooks/setup-security-tools/` and `.claude/commands/setup-security-tools.md`
61-
2. Run `cd .claude/hooks/setup-security-tools && npm install`
66+
2. Ensure the consumer repo has `@socketsecurity/lib`, `@socketregistry/packageurl-js`, and `@sinclair/typebox` available (via workspace catalog or direct deps)
6267
3. Ensure `.claude/hooks/` is not gitignored (add `!/.claude/hooks/` to `.gitignore`)
63-
4. Ensure `ecc-agentshield` is a devDep in the target repo
68+
4. Run `pnpm install` in the consumer repo so the hook's workspace deps resolve
6469

6570
## Troubleshooting
6671

67-
**"AgentShield not found"**Run `pnpm install`. It's the `ecc-agentshield` devDependency.
72+
**"AgentShield install failed"**Check network access to npm registry. The dlx system caches at `~/.socket/_dlx/`; clear the cache (`rm -rf ~/.socket/_dlx/`) to force a fresh download.
6873

6974
**"zizmor found but wrong version"** — The script downloads the expected version via the dlx cache. Your system version (e.g. from brew) will be ignored in favor of the correct version.
7075

7176
**"No supported package managers found"** — SFW only creates shims for package managers found on your PATH. Install npm/pnpm/etc. first.
7277

73-
**SFW shims not intercepting** — Make sure `~/.socket/sfw/shims` is at the *front* of PATH. Run `which npm` — it should point to the shim, not the real binary.
78+
**SFW shims not intercepting** — Make sure `~/.socket/sfw/shims` is at the _front_ of PATH. Run `which npm` — it should point to the shim, not the real binary.

.claude/hooks/setup-security-tools/external-tools.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"description": "Security tools for Claude Code hooks (self-contained, no external deps)",
33
"tools": {
4+
"agentshield": {
5+
"description": "Claude AI config security scanner (prompt injection, secrets)",
6+
"purl": "pkg:npm/ecc-agentshield@1.4.0",
7+
"integrity": "sha512-R98OO1Ujyk2lezDLb+iQmMhF6FwTJCHajy3G4FCB6x7wkSTqR9f8+eAelC5KDzYDsGSbc0sOZvjXOOPRBtMpDg=="
8+
},
49
"zizmor": {
510
"description": "GitHub Actions security scanner",
611
"version": "1.23.1",
@@ -56,7 +61,7 @@
5661
"sha256": "c953e62ad7928d4d8f2302f5737884ea1a757babc26bed6a42b9b6b68a5d54af"
5762
}
5863
},
59-
"ecosystems": ["npm", "yarn", "pnpm", "pip", "uv", "cargo"]
64+
"ecosystems": ["npm", "yarn", "pnpm", "pip", "pip3", "uv", "cargo"]
6065
},
6166
"sfw-enterprise": {
6267
"description": "Socket Firewall (enterprise tier)",
@@ -85,7 +90,7 @@
8590
"sha256": "9a50e1ddaf038138c3f85418dc5df0113bbe6fc884f5abe158beaa9aea18d70a"
8691
}
8792
},
88-
"ecosystems": ["npm", "yarn", "pnpm", "pip", "uv", "cargo", "gem", "bundler", "nuget"]
93+
"ecosystems": ["npm", "yarn", "pnpm", "pip", "pip3", "uv", "cargo", "gem", "bundler", "nuget"]
8994
}
9095
}
9196
}

.claude/hooks/setup-security-tools/index.mts

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,63 @@
33
//
44
// Configures three tools:
55
// 1. AgentShield — scans Claude AI config for prompt injection / secrets.
6-
// Already a devDep (ecc-agentshield); this script verifies it's installed.
6+
// Downloaded as npm package via dlx (pinned version, cached).
77
// 2. Zizmor — static analysis for GitHub Actions workflows. Downloads the
88
// correct binary, verifies SHA-256, cached via the dlx system.
99
// 3. SFW (Socket Firewall) — intercepts package manager commands to scan
1010
// for malware. Downloads binary, verifies SHA-256, creates PATH shims.
1111
// Enterprise vs free determined by SOCKET_API_KEY in env / .env / .env.local.
1212

13-
import { existsSync, readFileSync, promises as fs } from 'node:fs'
13+
import { existsSync, promises as fs, readFileSync } from 'node:fs'
1414
import { tmpdir } from 'node:os'
1515
import path from 'node:path'
1616
import process from 'node:process'
1717
import { fileURLToPath } from 'node:url'
1818

19+
import { PackageURL } from '@socketregistry/packageurl-js'
20+
import { Type } from '@sinclair/typebox'
21+
1922
import { whichSync } from '@socketsecurity/lib/bin'
2023
import { downloadBinary } from '@socketsecurity/lib/dlx/binary'
24+
import { downloadPackage } from '@socketsecurity/lib/dlx/package'
2125
import { safeDelete } from '@socketsecurity/lib/fs'
2226
import { getDefaultLogger } from '@socketsecurity/lib/logger'
27+
import { normalizePath } from '@socketsecurity/lib/paths/normalize'
2328
import { getSocketHomePath } from '@socketsecurity/lib/paths/socket'
24-
import { spawn, spawnSync } from '@socketsecurity/lib/spawn'
25-
import { z } from 'zod'
29+
import { spawn } from '@socketsecurity/lib/spawn'
30+
import { parseSchema } from '@socketsecurity/lib/schema/parse'
2631

2732
const logger = getDefaultLogger()
2833

2934
// ── Tool config loaded from external-tools.json (self-contained) ──
3035

31-
const toolSchema = z.object({
32-
description: z.string().optional(),
33-
version: z.string(),
34-
repository: z.string().optional(),
35-
assets: z.record(z.string(), z.string()).optional(),
36-
platforms: z.record(z.string(), z.string()).optional(),
37-
checksums: z.record(z.string(), z.string()).optional(),
38-
ecosystems: z.array(z.string()).optional(),
36+
const checksumEntrySchema = Type.Object({
37+
asset: Type.String(),
38+
sha256: Type.String(),
39+
})
40+
41+
const toolSchema = Type.Object({
42+
description: Type.Optional(Type.String()),
43+
version: Type.Optional(Type.String()),
44+
purl: Type.Optional(Type.String()),
45+
integrity: Type.Optional(Type.String()),
46+
repository: Type.Optional(Type.String()),
47+
release: Type.Optional(Type.String()),
48+
checksums: Type.Optional(Type.Record(Type.String(), checksumEntrySchema)),
49+
ecosystems: Type.Optional(Type.Array(Type.String())),
3950
})
4051

41-
const configSchema = z.object({
42-
description: z.string().optional(),
43-
tools: z.record(z.string(), toolSchema),
52+
const configSchema = Type.Object({
53+
description: Type.Optional(Type.String()),
54+
tools: Type.Record(Type.String(), toolSchema),
4455
})
4556

4657
const __dirname = path.dirname(fileURLToPath(import.meta.url))
4758
const configPath = path.join(__dirname, 'external-tools.json')
4859
const rawConfig = JSON.parse(readFileSync(configPath, 'utf8'))
49-
const config = configSchema.parse(rawConfig)
60+
const config = parseSchema(configSchema, rawConfig)
5061

62+
const AGENTSHIELD = config.tools['agentshield']!
5163
const ZIZMOR = config.tools['zizmor']!
5264
const SFW_FREE = config.tools['sfw-free']!
5365
const SFW_ENTERPRISE = config.tools['sfw-enterprise']!
@@ -79,19 +91,37 @@ function findApiKey(): string | undefined {
7991

8092
// ── AgentShield ──
8193

82-
function setupAgentShield(): boolean {
94+
async function setupAgentShield(): Promise<boolean> {
8395
logger.log('=== AgentShield ===')
84-
const bin = whichSync('agentshield', { nothrow: true })
85-
if (bin && typeof bin === 'string') {
86-
const result = spawnSync(bin, ['--version'], { stdio: 'pipe' })
96+
const purl = PackageURL.fromString(AGENTSHIELD.purl!)
97+
if (purl.type !== 'npm') {
98+
throw new Error(`Unsupported PURL type "${purl.type}" — only npm is supported`)
99+
}
100+
const npmPackage = purl.namespace ? `${purl.namespace}/${purl.name}` : purl.name!
101+
const version = AGENTSHIELD.version ?? purl.version
102+
const packageSpec = version ? `${npmPackage}@${version}` : npmPackage
103+
104+
logger.log(`Installing ${packageSpec} via dlx...`)
105+
const { binaryPath, installed } = await downloadPackage({
106+
package: packageSpec,
107+
binaryName: 'agentshield',
108+
})
109+
110+
// Verify version matches pinned config.
111+
if (version) {
112+
const result = await spawn(binaryPath, ['--version'], { stdio: 'pipe' })
87113
const ver = typeof result.stdout === 'string'
88114
? result.stdout.trim()
89115
: result.stdout.toString().trim()
90-
logger.log(`Found: ${bin} (${ver})`)
91-
return true
116+
if (!ver.includes(version)) {
117+
logger.warn(`Version mismatch: expected ${version}, got ${ver}`)
118+
return false
119+
}
120+
logger.log(installed ? `Installed: ${binaryPath} (${ver})` : `Cached: ${binaryPath} (${ver})`)
121+
} else {
122+
logger.log(installed ? `Installed: ${binaryPath}` : `Cached: ${binaryPath}`)
92123
}
93-
logger.warn('Not found. Run "pnpm install" to install ecc-agentshield.')
94-
return false
124+
return true
95125
}
96126

97127
// ── Zizmor ──
@@ -148,8 +178,8 @@ async function setupZizmor(): Promise<boolean> {
148178
}
149179

150180
const isZip = asset.endsWith('.zip')
151-
const extractDir = path.join(tmpdir(), `zizmor-extract-${Date.now()}`)
152-
await fs.mkdir(extractDir, { recursive: true })
181+
// mkdtemp is collision-safe, unlike Date.now()-only naming.
182+
const extractDir = await fs.mkdtemp(path.join(tmpdir(), 'zizmor-extract-'))
153183
try {
154184
if (isZip) {
155185
await spawn('powershell', ['-NoProfile', '-Command',
@@ -195,6 +225,7 @@ async function setupSfw(apiKey: string | undefined): Promise<boolean> {
195225

196226
// Create shims.
197227
const isWindows = process.platform === 'win32'
228+
198229
const shimDir = path.join(getSocketHomePath(), 'sfw', 'shims')
199230
await fs.mkdir(shimDir, { recursive: true })
200231
const ecosystems = [...(sfwConfig.ecosystems ?? [])]
@@ -203,12 +234,14 @@ async function setupSfw(apiKey: string | undefined): Promise<boolean> {
203234
}
204235
const cleanPath = (process.env['PATH'] ?? '').split(path.delimiter)
205236
.filter(p => p !== shimDir).join(path.delimiter)
237+
const sfwBin = normalizePath(binaryPath)
206238
const created: string[] = []
207239
for (const cmd of ecosystems) {
208-
const realBin = whichSync(cmd, { nothrow: true, path: cleanPath })
240+
let realBin = whichSync(cmd, { nothrow: true, path: cleanPath })
209241
if (!realBin || typeof realBin !== 'string') continue
242+
realBin = normalizePath(realBin)
210243

211-
// Bash shim (macOS/Linux).
244+
// Bash shim (macOS/Linux/Windows Git Bash).
212245
const bashLines = [
213246
'#!/bin/bash',
214247
`export PATH="$(echo "$PATH" | tr ':' '\\n' | grep -vxF '${shimDir}' | paste -sd: -)"`,
@@ -227,7 +260,7 @@ async function setupSfw(apiKey: string | undefined): Promise<boolean> {
227260
'fi',
228261
)
229262
}
230-
bashLines.push(`exec "${binaryPath}" "${realBin}" "$@"`)
263+
bashLines.push(`exec "${sfwBin}" "${realBin}" "$@"`)
231264
const bashContent = bashLines.join('\n') + '\n'
232265
const bashPath = path.join(shimDir, cmd)
233266
if (!existsSync(bashPath) || await fs.readFile(bashPath, 'utf8').catch(() => '') !== bashContent) {
@@ -257,7 +290,7 @@ async function setupSfw(apiKey: string | undefined): Promise<boolean> {
257290
+ `set "PATH=%PATH:;${shimDir};=%"\r\n`
258291
+ `set "PATH=%PATH:~1,-1%"\r\n`
259292
+ cmdApiKeyBlock
260-
+ `"${binaryPath}" "${realBin}" %*\r\n`
293+
+ `"${sfwBin}" "${realBin}" %*\r\n`
261294
const cmdPath = path.join(shimDir, `${cmd}.cmd`)
262295
if (!existsSync(cmdPath) || await fs.readFile(cmdPath, 'utf8').catch(() => '') !== cmdContent) {
263296
await fs.writeFile(cmdPath, cmdContent)
@@ -282,7 +315,7 @@ async function main(): Promise<void> {
282315

283316
const apiKey = findApiKey()
284317

285-
const agentshieldOk = setupAgentShield()
318+
const agentshieldOk = await setupAgentShield()
286319
logger.log('')
287320
const zizmorOk = await setupZizmor()
288321
logger.log('')

.claude/hooks/setup-security-tools/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"type": "module",
55
"main": "./index.mts",
66
"dependencies": {
7+
"@sinclair/typebox": "catalog:",
8+
"@socketregistry/packageurl-js": "catalog:",
79
"@socketsecurity/lib": "catalog:"
810
}
911
}

0 commit comments

Comments
 (0)