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
2 changes: 1 addition & 1 deletion .claude/skills/_shared/security-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
8 changes: 1 addition & 7 deletions .claude/skills/security-scan/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 10 additions & 5 deletions .git-hooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -58,15 +63,15 @@ 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"
fi
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

Expand Down
8 changes: 4 additions & 4 deletions .git-hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
7 changes: 6 additions & 1 deletion .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/) `<type>(<scope>): <description>` -- NO AI attribution
- Scripts: Prefer `pnpm run foo --flag` over `foo:bar` scripts
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
64 changes: 48 additions & 16 deletions packages/cli/scripts/sync-checksums.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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',
Expand All @@ -87,21 +94,27 @@ 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}`

console.log(` Fetching release info from ${apiUrl}...`)

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()
Expand All @@ -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 {
Expand All @@ -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}`)
Expand All @@ -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 = {}
Expand Down Expand Up @@ -192,24 +211,32 @@ 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 }))

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
}
githubTools.length = 0
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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading