From e150380bb0bffc4eae9f54cb659610fe5c5bbafc Mon Sep 17 00:00:00 2001 From: jdalton Date: Tue, 14 Apr 2026 22:53:56 -0400 Subject: [PATCH 1/6] chore: sync hooks and skills from socket-repo-template --- .claude/skills/_shared/security-tools.md | 2 +- .claude/skills/security-scan/SKILL.md | 8 +------- .git-hooks/commit-msg | 15 ++++++++++----- .husky/commit-msg | 7 ++++++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.claude/skills/_shared/security-tools.md b/.claude/skills/_shared/security-tools.md index 345ab80b1..9a1f02716 100644 --- a/.claude/skills/_shared/security-tools.md +++ b/.claude/skills/_shared/security-tools.md @@ -11,7 +11,7 @@ No install step needed — available after `pnpm install`. ## Zizmor Not an npm package. Installed via `pnpm run setup` which downloads the pinned version -from GitHub releases with SHA256 checksum verification (see `bundle-tools.json`). +from GitHub releases with SHA256 checksum verification (see `external-tools.json`). The binary is cached at `.cache/external-tools/zizmor/{version}-{platform}/zizmor`. diff --git a/.claude/skills/security-scan/SKILL.md b/.claude/skills/security-scan/SKILL.md index 640bf210d..f8eaf37ad 100644 --- a/.claude/skills/security-scan/SKILL.md +++ b/.claude/skills/security-scan/SKILL.md @@ -1,19 +1,13 @@ --- name: security-scan description: Runs a multi-tool security scan — AgentShield for Claude config, zizmor for GitHub Actions, and optionally Socket CLI for dependency scanning. Produces an A-F graded security report. +user-invocable: true --- # Security Scan Multi-tool security scanning pipeline for the repository. -## Related: check-new-deps Hook - -This repo includes a pre-tool hook (`.claude/hooks/check-new-deps/`) that automatically -checks new dependencies against Socket.dev's malware API before Claude adds them. -The hook runs on every Edit/Write to manifest files — see its README for details. -This skill covers broader security scanning; the hook provides real-time dependency protection. - ## When to Use - After modifying `.claude/` config, settings, hooks, or agent definitions diff --git a/.git-hooks/commit-msg b/.git-hooks/commit-msg index 92dcc04fa..f637d0310 100755 --- a/.git-hooks/commit-msg +++ b/.git-hooks/commit-msg @@ -23,14 +23,14 @@ if [ -n "$COMMITTED_FILES" ]; then if [ -f "$file" ]; then # Check for Socket API keys (except allowed). if grep -E 'sktsec_[a-zA-Z0-9_-]+' "$file" 2>/dev/null | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | grep -v '\.example' | grep -q .; then - echo "${RED}✗ SECURITY: Potential API key detected in commit!${NC}" + printf "${RED}✗ SECURITY: Potential API key detected in commit!${NC}\n" printf "File: %s\n" "$file" ERRORS=$((ERRORS + 1)) fi # Check for .env files. if echo "$file" | grep -qE '^\.env(\.local)?$'; then - echo "${RED}✗ SECURITY: .env file in commit!${NC}" + printf "${RED}✗ SECURITY: .env file in commit!${NC}\n" ERRORS=$((ERRORS + 1)) fi fi @@ -41,7 +41,12 @@ fi COMMIT_MSG_FILE="$1" if [ -f "$COMMIT_MSG_FILE" ]; then # Create a temporary file to store the cleaned message. - TEMP_FILE=$(mktemp) + TEMP_FILE=$(mktemp) || { + printf "${RED}✗ Failed to create temporary file${NC}\n" >&2 + exit 1 + } + # Ensure cleanup on exit + trap 'rm -f "$TEMP_FILE"' EXIT REMOVED_LINES=0 # Read the commit message line by line and filter out AI attribution. @@ -58,7 +63,7 @@ if [ -f "$COMMIT_MSG_FILE" ]; then # Replace the original commit message with the cleaned version. if [ $REMOVED_LINES -gt 0 ]; then mv "$TEMP_FILE" "$COMMIT_MSG_FILE" - echo "${GREEN}✓ Auto-stripped${NC} $REMOVED_LINES AI attribution line(s) from commit message" + printf "${GREEN}✓ Auto-stripped${NC} $REMOVED_LINES AI attribution line(s) from commit message\n" else # No lines were removed, just clean up the temp file. rm -f "$TEMP_FILE" @@ -66,7 +71,7 @@ if [ -f "$COMMIT_MSG_FILE" ]; then fi if [ $ERRORS -gt 0 ]; then - echo "${RED}✗ Commit blocked by security validation${NC}" + printf "${RED}✗ Commit blocked by security validation${NC}\n" exit 1 fi diff --git a/.husky/commit-msg b/.husky/commit-msg index 09dec27aa..650c1f84b 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,2 +1,7 @@ # Run commit message validation and auto-strip AI attribution. -.git-hooks/commit-msg "$1" +if [ -x ".git-hooks/commit-msg" ]; then + .git-hooks/commit-msg "$1" +else + printf "\033[0;31m✗ Error: .git-hooks/commit-msg not found or not executable\033[0m\n" >&2 + exit 1 +fi From 9c4228b41ed63e240e7ad34820216490078cdd54 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 15 Apr 2026 14:53:21 -0400 Subject: [PATCH 2/6] chore: enable eslint/curly rule to require braces on all control flow --- .oxlintrc.json | 2 +- packages/cli/scripts/sync-checksums.mjs | 64 ++++++++++++++++++------- scripts/validate-checksums.mjs | 17 ++++--- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index ea0d712bf..6758f2864 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -6,7 +6,7 @@ "suspicious": "error" }, "rules": { - "eslint/curly": "off", + "eslint/curly": ["error", "all"], "eslint/no-await-in-loop": "off", "eslint/no-console": "off", "eslint/no-control-regex": "off", diff --git a/packages/cli/scripts/sync-checksums.mjs b/packages/cli/scripts/sync-checksums.mjs index 06ecd9e2e..db070e00a 100644 --- a/packages/cli/scripts/sync-checksums.mjs +++ b/packages/cli/scripts/sync-checksums.mjs @@ -17,7 +17,12 @@ */ import { createHash } from 'node:crypto' -import { createReadStream, existsSync, readFileSync, promises as fs } from 'node:fs' +import { + createReadStream, + existsSync, + readFileSync, + promises as fs, +} from 'node:fs' import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -48,7 +53,9 @@ function parseChecksums(content) { const checksums = {} for (const line of content.split('\n')) { const trimmed = line.trim() - if (!trimmed) continue + if (!trimmed) { + continue + } // Format: hash filename (two spaces or whitespace between) const match = trimmed.match(/^([a-f0-9]{64})\s+(.+)$/) if (match) { @@ -64,7 +71,7 @@ function parseChecksums(content) { async function downloadFile(url, destPath) { const response = await fetch(url, { headers: { - 'Accept': 'application/octet-stream', + Accept: 'application/octet-stream', 'User-Agent': 'socket-cli-sync-checksums', }, redirect: 'follow', @@ -87,7 +94,11 @@ async function downloadFile(url, destPath) { * Fetch checksums for a GitHub release. * First tries checksums.txt, then falls back to downloading assets. */ -async function fetchGitHubReleaseChecksums(repo, releaseTag, existingChecksums = {}) { +async function fetchGitHubReleaseChecksums( + repo, + releaseTag, + existingChecksums = {}, +) { const [owner, repoName] = repo.split('/') const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/releases/tags/${releaseTag}` @@ -95,13 +106,15 @@ async function fetchGitHubReleaseChecksums(repo, releaseTag, existingChecksums = const response = await fetch(apiUrl, { headers: { - 'Accept': 'application/vnd.github.v3+json', + Accept: 'application/vnd.github.v3+json', 'User-Agent': 'socket-cli-sync-checksums', }, }) if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) + throw new Error( + `GitHub API error: ${response.status} ${response.statusText}`, + ) } const release = await response.json() @@ -111,7 +124,9 @@ async function fetchGitHubReleaseChecksums(repo, releaseTag, existingChecksums = const checksumsAsset = assets.find(a => a.name === 'checksums.txt') if (checksumsAsset) { console.log(` Found checksums.txt, downloading...`) - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'socket-checksums-')) + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'socket-checksums-'), + ) const checksumPath = path.join(tempDir, 'checksums.txt') try { @@ -122,7 +137,9 @@ async function fetchGitHubReleaseChecksums(repo, releaseTag, existingChecksums = // Clean up. await fs.rm(tempDir, { recursive: true }) - console.log(` Parsed ${Object.keys(checksums).length} checksums from checksums.txt`) + console.log( + ` Parsed ${Object.keys(checksums).length} checksums from checksums.txt`, + ) return checksums } catch (error) { console.log(` Failed to download checksums.txt: ${error.message}`) @@ -139,7 +156,9 @@ async function fetchGitHubReleaseChecksums(repo, releaseTag, existingChecksums = return {} } - console.log(` No checksums.txt found, downloading ${assetNames.length} assets to compute checksums...`) + console.log( + ` No checksums.txt found, downloading ${assetNames.length} assets to compute checksums...`, + ) const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'socket-checksums-')) const checksums = {} @@ -192,7 +211,9 @@ async function main() { // Find all GitHub-released tools. const githubTools = Object.entries(externalTools) .filter(([key, value]) => { - if (key.startsWith('$')) return false // Skip schema keys + if (key.startsWith('$')) { + return false + } // Skip schema keys return value.release === 'asset' }) .map(([key, value]) => ({ key, ...value })) @@ -200,8 +221,12 @@ async function main() { if (toolFilter) { const filtered = githubTools.filter(t => t.key === toolFilter) if (filtered.length === 0) { - console.error(`Error: Tool '${toolFilter}' not found or is not a GitHub release tool`) - console.log(`Available GitHub release tools: ${githubTools.map(t => t.key).join(', ')}`) + console.error( + `Error: Tool '${toolFilter}' not found or is not a GitHub release tool`, + ) + console.log( + `Available GitHub release tools: ${githubTools.map(t => t.key).join(', ')}`, + ) process.exitCode = 1 return } @@ -209,7 +234,9 @@ async function main() { githubTools.push(...filtered) } - console.log(`Syncing checksums for ${githubTools.length} GitHub release tool(s)...\n`) + console.log( + `Syncing checksums for ${githubTools.length} GitHub release tool(s)...\n`, + ) let updated = 0 let unchanged = 0 @@ -235,10 +262,13 @@ async function main() { // Check if update is needed. const oldChecksums = tool.checksums || {} - const checksumChanged = JSON.stringify(newChecksums) !== JSON.stringify(oldChecksums) + const checksumChanged = + JSON.stringify(newChecksums) !== JSON.stringify(oldChecksums) if (!force && !checksumChanged) { - console.log(` Unchanged: ${Object.keys(newChecksums).length} checksums\n`) + console.log( + ` Unchanged: ${Object.keys(newChecksums).length} checksums\n`, + ) unchanged++ continue } @@ -269,7 +299,9 @@ async function main() { } // Summary. - console.log(`\nSummary: ${updated} updated, ${unchanged} unchanged, ${failed} failed`) + console.log( + `\nSummary: ${updated} updated, ${unchanged} unchanged, ${failed} failed`, + ) if (failed > 0) { process.exitCode = 1 diff --git a/scripts/validate-checksums.mjs b/scripts/validate-checksums.mjs index 8066ee92b..acf79a73b 100644 --- a/scripts/validate-checksums.mjs +++ b/scripts/validate-checksums.mjs @@ -26,10 +26,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '..') // Load external tools configuration. -const externalToolsPath = path.join( - rootPath, - 'packages/cli/bundle-tools.json', -) +const externalToolsPath = path.join(rootPath, 'packages/cli/bundle-tools.json') const externalTools = JSON.parse(readFileSync(externalToolsPath, 'utf8')) /** @@ -47,10 +44,14 @@ function validateChecksums() { // Collect all assets needed across all platforms. for (const [platform, tools] of Object.entries(PLATFORM_MAP_TOOLS)) { - if (!tools) continue + if (!tools) { + continue + } for (const [toolName, assetName] of Object.entries(tools)) { - if (!assetName) continue + if (!assetName) { + continue + } if (!requiredAssets.has(toolName)) { requiredAssets.set(toolName, new Set()) @@ -130,7 +131,9 @@ function validateChecksums() { logger.error( 'All external tool assets MUST have SHA-256 checksums defined in bundle-tools.json.', ) - logger.error('This is a security requirement to prevent supply chain attacks.') + logger.error( + 'This is a security requirement to prevent supply chain attacks.', + ) return false } From bd1acd82924e1fb0082000c59091fddc14abf5fb Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 15 Apr 2026 16:12:50 -0400 Subject: [PATCH 3/6] chore: update @typescript/native-preview to 7.0.0-dev.20260415.1 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 36d46d3c4..f871442b9 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "pretest": "pnpm run build:cli" }, "devDependencies": { + "@typescript/native-preview": "7.0.0-dev.20260415.1", "@anthropic-ai/claude-code": "catalog:", "@babel/core": "catalog:", "@babel/parser": "catalog:", From 4a87f9ec6e2c4ff226d9a1d8d2c9a3029c63d362 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 15 Apr 2026 16:13:17 -0400 Subject: [PATCH 4/6] feat: add "typecheck" script using tsgo (typescript-go) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index f871442b9..37a7e3843 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "type:all": "node scripts/type.mjs", "// Testing": "", "test": "node scripts/test-monorepo.mjs", + "typecheck": "tsgo --noEmit", "test:all": "node scripts/test-monorepo.mjs --all", "test:unit": "pnpm --filter @socketsecurity/cli run test:unit", "pretest:all": "pnpm run build", From c07fa7689a8ff691a3890aa575d7e67206e53dac Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 15 Apr 2026 16:16:48 -0400 Subject: [PATCH 5/6] revert: remove standalone typecheck script --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 37a7e3843..f871442b9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "type:all": "node scripts/type.mjs", "// Testing": "", "test": "node scripts/test-monorepo.mjs", - "typecheck": "tsgo --noEmit", "test:all": "node scripts/test-monorepo.mjs --all", "test:unit": "pnpm --filter @socketsecurity/cli run test:unit", "pretest:all": "pnpm run build", From 236273c8a01d8813dcef29a33aff67fa8d808660 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 15 Apr 2026 16:20:37 -0400 Subject: [PATCH 6/6] fix: use word boundaries in AWS key detection to avoid base64 false positives --- .git-hooks/pre-push | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.git-hooks/pre-push b/.git-hooks/pre-push index 92e7ba7f4..8e25cdf49 100755 --- a/.git-hooks/pre-push +++ b/.git-hooks/pre-push @@ -168,10 +168,10 @@ while read local_ref local_sha remote_ref remote_sha; do ERRORS=$((ERRORS + 1)) fi - # AWS keys. - if echo "$file_text" | grep -iqE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})'; then + # AWS keys (word-boundary match to avoid false positives in base64 data). + if echo "$file_text" | grep -iqE '(aws_access_key|aws_secret|\bAKIA[0-9A-Z]{16}\b)'; then printf "${RED}✗ BLOCKED: Potential AWS credentials found in: %s${NC}\n" "$file" - echo "$file_text" | grep -niE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' | head -3 + echo "$file_text" | grep -niE '(aws_access_key|aws_secret|\bAKIA[0-9A-Z]{16}\b)' | head -3 ERRORS=$((ERRORS + 1)) fi