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/agents/code-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Apply the rules from CLAUDE.md sections listed below. Reference the full section

**Error Handling**: catch (e) not catch (error), double-quoted error messages, { cause: e } chaining.

**Backward Compatibility**: FORBIDDEN — actively remove compat shims, don't maintain them.
**Compat shims**: FORBIDDEN — actively remove compat shims, don't maintain them.

**Test Style**: Functional tests over source scanning. Never read source files and assert on contents. Verify behavior with real function calls.

Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/refactor-cleaner.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ Apply these rules from CLAUDE.md exactly:
- Unreachable code paths
- Duplicate logic that should be consolidated
- Files >400 LOC that should be split (flag to user, don't split without approval)
- Backward compatibility shims (FORBIDDEN per CLAUDE.md — actively remove)
- Compat shims (FORBIDDEN per CLAUDE.md — actively remove)
78 changes: 40 additions & 38 deletions .git-hooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
#!/bin/bash
# Socket Security Pre-push Hook
# Security enforcement layer for all pushes.
# Validates all commits being pushed for security issues and AI attribution.
# NOTE: Security checks parallel .husky/security-checks.sh — keep in sync.
# Validates commits being pushed for AI attribution and secrets.
#
# Architecture:
# .husky/pre-push (thin wrapper) → .git-hooks/pre-push (this file)
# Husky sets core.hooksPath=.husky/_ which delegates to .husky/pre-push.
# This file contains all the actual logic.
#
# Range logic:
# New branch: remote/<default_branch>..<local_sha> (only new commits)
# Existing: <remote_sha>..<local_sha> (only new commits)
# We never use release tags — that would re-scan already-merged history.

set -e

Expand All @@ -13,47 +22,46 @@ NC='\033[0m'

printf "${GREEN}Running mandatory pre-push validation...${NC}\n"

# Allowed public API key (used in socket-lib).
# Allowed public API key (used in socket-lib test fixtures).
ALLOWED_PUBLIC_KEY="sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api"

# Get the remote name and URL.
# Get the remote name and URL from git (passed as arguments to pre-push hooks).
remote="$1"
url="$2"

TOTAL_ERRORS=0

# Read stdin for refs being pushed.
# Read stdin for refs being pushed (git provides: local_ref local_sha remote_ref remote_sha).
while read local_ref local_sha remote_ref remote_sha; do
# Skip tag pushes: tags point to existing commits already validated.
if echo "$local_ref" | grep -q '^refs/tags/'; then
printf "${GREEN}Skipping tag push: %s${NC}\n" "$local_ref"
continue
fi

# Skip delete pushes.
# Skip delete pushes (local_sha is all zeros when deleting a remote branch).
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
continue
fi

# Get the range of commits being pushed.
# ── Compute commit range ──────────────────────────────────────────────
# Goal: only scan commits that are NEW in this push, never re-scan
# commits already on the remote. This prevents false positives from
# old AI-attributed commits that were merged before the hook existed.
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
# New branch - only check commits not on the default remote branch.
# New branch — compare against the remote's default branch (usually main).
# This ensures we only check commits unique to this branch.
default_branch=$(git symbolic-ref "refs/remotes/$remote/HEAD" 2>/dev/null | sed "s@^refs/remotes/$remote/@@")
if [ -z "$default_branch" ]; then
default_branch="main"
fi
if git rev-parse "$remote/$default_branch" >/dev/null 2>&1; then
range="$remote/$default_branch..$local_sha"
else
# No remote default branch, fall back to release tag.
latest_release=$(git tag --list 'v*' --sort=-version:refname --merged "$local_sha" | head -1)
if [ -n "$latest_release" ]; then
range="$latest_release..$local_sha"
else
# No remote branch or tags — skip scan to avoid walking entire history.
printf "${GREEN}✓ Skipping validation (no baseline to compare against)${NC}\n"
continue
fi
# No remote default branch (shallow clone, etc.) — skip to avoid
# walking entire history which would cause false positives.
printf "${GREEN}✓ Skipping validation (no baseline to compare against)${NC}\n"
continue
fi
else
# Existing branch — only check commits not yet on the remote.
Expand All @@ -68,13 +76,12 @@ while read local_ref local_sha remote_ref remote_sha; do

ERRORS=0

# ============================================================================
# CHECK 1: Scan commit messages for AI attribution
# ============================================================================
# ── CHECK 1: AI attribution in commit messages ────────────────────────
# Strips these at commit time via commit-msg hook, but this catches
# commits made with --no-verify or on other machines.
printf "Checking commit messages for AI attribution...\n"

# Check each commit in the range for AI patterns.
while IFS= read -r commit_sha; do
for commit_sha in $(git rev-list "$range"); do
full_msg=$(git log -1 --format='%B' "$commit_sha")

if echo "$full_msg" | grep -qiE "(Generated with.*(Claude|AI)|Co-Authored-By: Claude|Co-Authored-By: AI|🤖 Generated|AI generated|@anthropic\.com|Assistant:|Generated by Claude|Machine generated)"; then
Expand All @@ -85,7 +92,7 @@ while read local_ref local_sha remote_ref remote_sha; do
printf " - %s\n" "$(git log -1 --oneline "$commit_sha")"
ERRORS=$((ERRORS + 1))
fi
done < <(git rev-list "$range")
done

if [ $ERRORS -gt 0 ]; then
printf "\n"
Expand All @@ -99,46 +106,41 @@ while read local_ref local_sha remote_ref remote_sha; do
printf " git push\n"
fi

# ============================================================================
# CHECK 2: File content security checks
# ============================================================================
# ── CHECK 2: File content security checks ─────────────────────────────
# Scans files changed in the push range for secrets, keys, and mistakes.
printf "Checking files for security issues...\n"

# Get all files changed in these commits.
CHANGED_FILES=$(git diff --name-only "$range" 2>/dev/null || echo "")

if [ -n "$CHANGED_FILES" ]; then
# Check for sensitive files.
# Check for sensitive files (.env, .DS_Store, log files).
if echo "$CHANGED_FILES" | grep -qE '^\.env(\.local)?$'; then
printf "${RED}✗ BLOCKED: Attempting to push .env file!${NC}\n"
printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep -E '^\.env(\.local)?$')"
ERRORS=$((ERRORS + 1))
fi

# Check for .DS_Store.
if echo "$CHANGED_FILES" | grep -q '\.DS_Store'; then
printf "${RED}✗ BLOCKED: .DS_Store file in push!${NC}\n"
printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep '\.DS_Store')"
ERRORS=$((ERRORS + 1))
fi

# Check for log files.
if echo "$CHANGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log' | grep -q .; then
printf "${RED}✗ BLOCKED: Log file in push!${NC}\n"
printf "Files: %s\n" "$(echo "$CHANGED_FILES" | grep -E '\.log$' | grep -v 'test.*\.log')"
ERRORS=$((ERRORS + 1))
fi

# Check file contents for secrets.
# Check file contents for secrets and hardcoded paths.
while IFS= read -r file; do
if [ -f "$file" ] && [ ! -d "$file" ]; then
# Skip test files, example files, and hook scripts.
# Skip test files, example files, and hook scripts themselves.
if echo "$file" | grep -qE '\.(test|spec)\.(m?[jt]s|tsx?)$|\.example$|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/'; then
continue
fi

# Use strings for binary files, grep directly for text files.
# This correctly extracts printable strings from WASM, .lockb, etc.
is_binary=false
if grep -qI '' "$file" 2>/dev/null; then
is_binary=false
Expand All @@ -152,35 +154,35 @@ while read local_ref local_sha remote_ref remote_sha; do
file_text=$(cat "$file" 2>/dev/null)
fi

# Check for hardcoded user paths.
# 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
ERRORS=$((ERRORS + 1))
fi

# Check for Socket API keys.
# Socket API keys (except allowed public key and test placeholders).
if echo "$file_text" | grep -E 'sktsec_[a-zA-Z0-9_-]+' | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'SOCKET_SECURITY_API_KEY=' | grep -v 'fake-token' | grep -v 'test-token' | grep -q .; then
printf "${RED}✗ BLOCKED: Real API key detected in: %s${NC}\n" "$file"
echo "$file_text" | grep -n 'sktsec_' | grep -v "$ALLOWED_PUBLIC_KEY" | grep -v 'your_api_key_here' | grep -v 'fake-token' | grep -v 'test-token' | head -3
ERRORS=$((ERRORS + 1))
fi

# Check for AWS keys.
# AWS keys.
if echo "$file_text" | grep -iqE '(aws_access_key|aws_secret|AKIA[0-9A-Z]{16})'; 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
ERRORS=$((ERRORS + 1))
fi

# Check for GitHub tokens.
# GitHub tokens.
if echo "$file_text" | grep -qE 'gh[ps]_[a-zA-Z0-9]{36}'; then
printf "${RED}✗ BLOCKED: Potential GitHub token found in: %s${NC}\n" "$file"
echo "$file_text" | grep -nE 'gh[ps]_[a-zA-Z0-9]{36}' | head -3
ERRORS=$((ERRORS + 1))
fi

# Check for private keys.
# Private keys.
if echo "$file_text" | grep -qE -- '-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----'; then
printf "${RED}✗ BLOCKED: Private key found in: %s${NC}\n" "$file"
ERRORS=$((ERRORS + 1))
Expand Down
125 changes: 0 additions & 125 deletions .husky/security-checks.sh

This file was deleted.

Loading