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/.git-hooks/pre-push b/.git-hooks/pre-push index 92e7ba7f4..96f159284 100755 --- a/.git-hooks/pre-push +++ b/.git-hooks/pre-push @@ -154,7 +154,7 @@ while read local_ref local_sha remote_ref remote_sha; do file_text=$(cat "$file" 2>/dev/null) fi - # Hardcoded personal paths (/Users/foo/, /home/foo/, C:\Users\foo\). + # Hardcoded personal paths (/Users/foo/, /home/foo/, C:\\Users\\foo\\). if echo "$file_text" | grep -qE '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)'; then printf "${RED}✗ BLOCKED: Hardcoded personal path found in: %s${NC}\n" "$file" echo "$file_text" | grep -nE '(/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' | head -3 @@ -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 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 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/CLAUDE.md b/CLAUDE.md index 6aa55880f..59affa0e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,9 +68,7 @@ If user repeats instruction 2+ times, ask: "Should I add this to CLAUDE.md?" ## SHARED STANDARDS -**Canonical reference**: `../socket-registry/CLAUDE.md` -All shared standards (git, testing, code style, cross-platform, CI) defined in socket-registry/CLAUDE.md. - Commits: [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) `(): ` -- NO AI attribution - Scripts: Prefer `pnpm run foo --flag` over `foo:bar` scripts 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:", 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/pnpm-lock.yaml b/pnpm-lock.yaml index 56b06f254..a64bfed0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -627,6 +627,9 @@ importers: '@types/yargs-parser': specifier: 'catalog:' version: 21.0.3 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260415.1 + version: 7.0.0-dev.20260415.1 '@vitest/coverage-v8': specifier: 'catalog:' version: 4.0.3(vitest@4.0.3(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1)) @@ -2524,6 +2527,45 @@ packages: '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-yGyyDb9bP3XfaIm8VUiaq7xkKwFSxLQ44XGYV78lrne12GhXgZ7Smbf2BVnT5MrTgT5uooMzww85P3I3XNVZng==} + cpu: [arm64] + os: [darwin] + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-vzac8BSbSkGPV/FMKyY/3cNZN+FgvjT1E+NNR8xWO8DfvSz4hYqbxvAL+zWPUno6R8afNFLZeJTfuIge0tJJ1g==} + cpu: [x64] + os: [darwin] + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-jiXu+SrpCL/6J3LwuUSxU8scYs5H0wBkqu3CopdSTcJxQuzUDe6QAEoEW3O81cdrsq5qrIlfFxWH3bdohsvhJA==} + cpu: [arm64] + os: [linux] + + '@typescript/native-preview-linux-arm@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-1CI2nLfsoEibEAt6ApMVEr5M/v9EeXHmn9iD2nyyomO34ky3zqBtEPHakvgXD4QmpZg9O4WxRKc6u6EIy0Imxg==} + cpu: [arm] + os: [linux] + + '@typescript/native-preview-linux-x64@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-+4r4UqGE5ccy9vJNOhjXTqbZPkVGlqdiEgTJbdz8+EpUNQaLGMBhDj1cBMdrdK1YRuj/3C+pLfE3PD9kEsxf6Q==} + cpu: [x64] + os: [linux] + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-QsCAG7la4GE4Dp7i9MUz7Qv+HbKVDdmwlmn+8sOo0M3aSC6WssCU5gKVBUjp43WPtml/JticwzSz1qXLfdxHFA==} + cpu: [arm64] + os: [win32] + + '@typescript/native-preview-win32-x64@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-Z+rclKC7FqkUsqQ+ErgWJmf5J55LEl/rooFq71prC6V0vCBa5yLMmLBmFxZLLj2BsCBuwbN2O7aWUWrpCUEkmw==} + cpu: [x64] + os: [win32] + + '@typescript/native-preview@7.0.0-dev.20260415.1': + resolution: {integrity: sha512-kRQ0x4DgXZBI0bNTck65EUaj48+hbMlCHiJKfc0Se5ZVUG0SKRC6JBPLwIBCX5TfljKsm8SstuJ3qn6uw1IWpA==} + hasBin: true + '@vitest/coverage-v8@4.0.3': resolution: {integrity: sha512-I+MlLwyJRBjmJr1kFYSxoseINbIdpxIAeK10jmXgB0FUtIfdYsvM3lGAvBu5yk8WPyhefzdmbCHCc1idFbNRcg==} peerDependencies: @@ -6027,6 +6069,37 @@ snapshots: '@types/yargs-parser@21.0.3': {} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview-linux-arm@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview-linux-x64@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview-win32-x64@7.0.0-dev.20260415.1': + optional: true + + '@typescript/native-preview@7.0.0-dev.20260415.1': + optionalDependencies: + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260415.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260415.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260415.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260415.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260415.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260415.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260415.1 + '@vitest/coverage-v8@4.0.3(vitest@4.0.3(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 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 }