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)
121 changes: 71 additions & 50 deletions .git-hooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,62 +1,87 @@
#!/bin/bash
# Socket Security Pre-push Hook
# MANDATORY ENFORCEMENT LAYER - Cannot be bypassed with --no-verify.
# Validates all commits being pushed for security issues and AI attribution.
# Security enforcement layer for all pushes.
# 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

# Colors for output.
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
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
# Get the range of commits being pushed.
# 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 (local_sha is all zeros when deleting a remote branch).
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
continue
fi

# ── 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 - find the latest published release tag to limit scope.
latest_release=$(git tag --list 'v*' --sort=-version:refname --merged "$local_sha" | head -1)
if [ -n "$latest_release" ]; then
# Check commits since the latest published release.
range="$latest_release..$local_sha"
else
# No release tags found - check all commits.
range="$local_sha"
# 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
else
# Existing branch - check new commits since remote.
# Limit scope to commits after the latest published release on this branch.
latest_release=$(git tag --list 'v*' --sort=-version:refname --merged "$remote_sha" | head -1)
if [ -n "$latest_release" ]; then
# Only check commits after the latest release that are being pushed.
range="$latest_release..$local_sha"
if git rev-parse "$remote/$default_branch" >/dev/null 2>&1; then
range="$remote/$default_branch..$local_sha"
else
# No release tags found - check new commits only.
range="$remote_sha..$local_sha"
# 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.
range="$remote_sha..$local_sha"
fi

# Validate the computed range before using it.
if ! git rev-list "$range" >/dev/null 2>&1; then
printf "${RED}✗ Invalid commit range: %s${NC}\n" "$range" >&2
exit 1
fi

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 @@ -67,59 +92,55 @@ 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"
printf "These commits were likely created with --no-verify, bypassing the\n"
printf "commit-msg hook that strips AI attribution.\n"
printf "\n"
range_base="${range%%\.\.*}"
printf "To fix:\n"
printf " git rebase -i %s\n" "$remote_sha"
printf " git rebase -i %s\n" "$range_base"
printf " Mark commits as 'reword', remove AI attribution, save\n"
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.
for file in $CHANGED_FILES; do
# 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 @@ -128,46 +149,46 @@ while read local_ref local_sha remote_ref remote_sha; do
fi

if [ "$is_binary" = true ]; then
file_text=$(strings "$file" 2>/dev/null || echo "")
file_text=$(strings "$file" 2>/dev/null)
else
file_text=$(cat "$file" 2>/dev/null || echo "")
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))
fi
fi
done
done <<< "$CHANGED_FILES"
fi

TOTAL_ERRORS=$((TOTAL_ERRORS + ERRORS))
Expand Down
10 changes: 1 addition & 9 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
# Run pre-push security validation.
SCRIPT_DIR="$(dirname "$0")"
PRE_PUSH_HOOK="$SCRIPT_DIR/../.git-hooks/pre-push"

if [ ! -f "$PRE_PUSH_HOOK" ]; then
printf "\033[0;31m✗ ERROR: .git-hooks/pre-push not found\033[0m\n"
printf "Please ensure the repository is properly set up.\n"
exit 1
fi
"$PRE_PUSH_HOOK" "$@"
.git-hooks/pre-push "$@"
125 changes: 0 additions & 125 deletions .husky/security-checks.sh

This file was deleted.

Loading