Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/hooks/setup-security-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Scans your Claude Code configuration (`.claude/` directory) for security issues
### 2. Zizmor
Static analysis tool for GitHub Actions workflows. Catches unpinned actions, secret exposure, template injection, and permission issues.

**How it's installed**: Binary downloaded from [GitHub releases](https://github.com/woodruffw/zizmor/releases), SHA-256 verified, cached at `~/.socket/zizmor/bin/zizmor`. If you already have it via `brew install zizmor`, the download is skipped.
**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.

### 3. SFW (Socket Firewall)
Intercepts package manager commands (`npm install`, `pnpm add`, etc.) and scans packages against Socket.dev's malware database before installation.
Expand All @@ -34,7 +34,7 @@ Claude will ask if you have an API key, then run the setup script.
| Tool | Location | Persists across repos? |
|------|----------|----------------------|
| AgentShield | `node_modules/.bin/agentshield` | No (per-repo devDep) |
| Zizmor | `~/.socket/zizmor/bin/zizmor` | Yes |
| Zizmor | `~/.socket/_dlx/<hash>/zizmor` | Yes |
| SFW binary | `~/.socket/_dlx/<hash>/sfw` | Yes |
| SFW shims | `~/.socket/sfw/shims/npm`, etc. | Yes |

Expand Down Expand Up @@ -66,7 +66,7 @@ Self-contained. To add to another Socket repo:

**"AgentShield not found"** — Run `pnpm install`. It's the `ecc-agentshield` devDependency.

**"zizmor found but wrong version"** — The script downloads the expected version to `~/.socket/zizmor/bin/`. Your system version (e.g. from brew) will be ignored in favor of the correct version.
**"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.

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

Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/setup-security-tools/external-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"zizmor": {
"description": "GitHub Actions security scanner",
"version": "1.23.1",
"repository": "woodruffw/zizmor",
"repository": "zizmorcore/zizmor",
"assets": {
"darwin-arm64": "zizmor-aarch64-apple-darwin.tar.gz",
"darwin-x64": "zizmor-x86_64-apple-darwin.tar.gz",
Expand Down
56 changes: 26 additions & 30 deletions .claude/hooks/setup-security-tools/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// 1. AgentShield — scans Claude AI config for prompt injection / secrets.
// Already a devDep (ecc-agentshield); this script verifies it's installed.
// 2. Zizmor — static analysis for GitHub Actions workflows. Downloads the
// correct binary, verifies SHA-256, caches at ~/.socket/zizmor/bin/zizmor.
// correct binary, verifies SHA-256, cached via the dlx system.
// 3. SFW (Socket Firewall) — intercepts package manager commands to scan
// for malware. Downloads binary, verifies SHA-256, creates PATH shims.
// Enterprise vs free determined by SOCKET_API_KEY in env / .env / .env.local.
Expand All @@ -18,7 +18,6 @@ import { fileURLToPath } from 'node:url'

import { whichSync } from '@socketsecurity/lib/bin'
import { downloadBinary } from '@socketsecurity/lib/dlx/binary'
import { httpDownload } from '@socketsecurity/lib/http-request'
import { getDefaultLogger } from '@socketsecurity/lib/logger'
import { getSocketHomePath } from '@socketsecurity/lib/paths/socket'
import { spawn, spawnSync } from '@socketsecurity/lib/spawn'
Expand Down Expand Up @@ -121,53 +120,50 @@ async function setupZizmor(): Promise<boolean> {
logger.log(`Found on PATH but wrong version (need v${ZIZMOR.version})`)
}

// Check cached binary.
const ext = process.platform === 'win32' ? '.exe' : ''
const binDir = path.join(getSocketHomePath(), 'zizmor', 'bin')
const binPath = path.join(binDir, `zizmor${ext}`)
if (existsSync(binPath) && await checkZizmorVersion(binPath)) {
logger.log(`Cached: ${binPath} (v${ZIZMOR.version})`)
return true
}

// Download.
// Download archive via dlx (handles caching + checksum).
const platformKey = `${process.platform}-${process.arch}`
const asset = ZIZMOR.assets?.[platformKey]
if (!asset) throw new Error(`Unsupported platform: ${platformKey}`)
const expectedSha = ZIZMOR.checksums?.[asset]
if (!expectedSha) throw new Error(`No checksum for: ${asset}`)
const url = `https://github.com/${ZIZMOR.repository}/releases/download/v${ZIZMOR.version}/${asset}`
const isZip = asset.endsWith('.zip')

logger.log(`Downloading zizmor v${ZIZMOR.version} (${asset})...`)
const tmpFile = path.join(tmpdir(), `zizmor-${Date.now()}-${asset}`)
try {
await httpDownload(url, tmpFile, { sha256: expectedSha })
logger.log('Download complete, checksum verified.')
const { binaryPath: archivePath, downloaded } = await downloadBinary({
url,
name: `zizmor-${ZIZMOR.version}-${asset}`,
sha256: expectedSha,
})
logger.log(downloaded ? 'Download complete, checksum verified.' : `Using cached archive: ${archivePath}`)

// Extract binary from the cached archive.
const ext = process.platform === 'win32' ? '.exe' : ''
const binPath = path.join(path.dirname(archivePath), `zizmor${ext}`)
if (existsSync(binPath) && await checkZizmorVersion(binPath)) {
logger.log(`Cached: ${binPath} (v${ZIZMOR.version})`)
return true
}

// Extract.
const extractDir = path.join(tmpdir(), `zizmor-extract-${Date.now()}`)
await fs.mkdir(extractDir, { recursive: true })
const isZip = asset.endsWith('.zip')
const extractDir = path.join(tmpdir(), `zizmor-extract-${Date.now()}`)
await fs.mkdir(extractDir, { recursive: true })
try {
if (isZip) {
await spawn('powershell', ['-NoProfile', '-Command',
`Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force`], { stdio: 'pipe' })
`Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`], { stdio: 'pipe' })
} else {
await spawn('tar', ['xzf', tmpFile, '-C', extractDir], { stdio: 'pipe' })
await spawn('tar', ['xzf', archivePath, '-C', extractDir], { stdio: 'pipe' })
}

// Install.
const extractedBin = path.join(extractDir, `zizmor${ext}`)
if (!existsSync(extractedBin)) throw new Error(`Binary not found after extraction: ${extractedBin}`)
await fs.mkdir(binDir, { recursive: true })
await fs.copyFile(extractedBin, binPath)
await fs.chmod(binPath, 0o755)
await fs.rm(extractDir, { recursive: true, force: true })

logger.log(`Installed to ${binPath}`)
return true
} finally {
if (existsSync(tmpFile)) await fs.unlink(tmpFile).catch(() => {})
await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {})
}

logger.log(`Installed to ${binPath}`)
return true
}

// ── SFW ──
Expand Down
Loading