11#! /bin/bash
22# Socket Security Pre-push Hook
3- # MANDATORY ENFORCEMENT LAYER - Cannot be bypassed with --no-verify.
4- # Validates all commits being pushed for security issues and AI attribution.
3+ # Security enforcement layer for all pushes.
4+ # Validates commits being pushed for AI attribution and secrets.
5+ #
6+ # Architecture:
7+ # .husky/pre-push (thin wrapper) → .git-hooks/pre-push (this file)
8+ # Husky sets core.hooksPath=.husky/_ which delegates to .husky/pre-push.
9+ # This file contains all the actual logic.
10+ #
11+ # Range logic:
12+ # New branch: remote/<default_branch>..<local_sha> (only new commits)
13+ # Existing: <remote_sha>..<local_sha> (only new commits)
14+ # We never use release tags — that would re-scan already-merged history.
515
616set -e
717
818# Colors for output.
919RED=' \033[0;31m'
10- YELLOW=' \033[1;33m'
1120GREEN=' \033[0;32m'
1221NC=' \033[0m'
1322
1423printf " ${GREEN} Running mandatory pre-push validation...${NC} \n"
1524
16- # Allowed public API key (used in socket-lib).
25+ # Allowed public API key (used in socket-lib test fixtures ).
1726ALLOWED_PUBLIC_KEY=" sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api"
1827
19- # Get the remote name and URL.
28+ # Get the remote name and URL from git (passed as arguments to pre-push hooks) .
2029remote=" $1 "
2130url=" $2 "
2231
2332TOTAL_ERRORS=0
2433
25- # Read stdin for refs being pushed.
34+ # Read stdin for refs being pushed (git provides: local_ref local_sha remote_ref remote_sha) .
2635while read local_ref local_sha remote_ref remote_sha; do
27- # Get the range of commits being pushed.
36+ # Skip tag pushes: tags point to existing commits already validated.
37+ if echo " $local_ref " | grep -q ' ^refs/tags/' ; then
38+ printf " ${GREEN} Skipping tag push: %s${NC} \n" " $local_ref "
39+ continue
40+ fi
41+
42+ # Skip delete pushes (local_sha is all zeros when deleting a remote branch).
43+ if [ " $local_sha " = " 0000000000000000000000000000000000000000" ]; then
44+ continue
45+ fi
46+
47+ # ── Compute commit range ──────────────────────────────────────────────
48+ # Goal: only scan commits that are NEW in this push, never re-scan
49+ # commits already on the remote. This prevents false positives from
50+ # old AI-attributed commits that were merged before the hook existed.
2851 if [ " $remote_sha " = " 0000000000000000000000000000000000000000" ]; then
29- # New branch - find the latest published release tag to limit scope.
30- latest_release=$( git tag --list ' v*' --sort=-version:refname --merged " $local_sha " | head -1)
31- if [ -n " $latest_release " ]; then
32- # Check commits since the latest published release.
33- range=" $latest_release ..$local_sha "
34- else
35- # No release tags found - check all commits.
36- range=" $local_sha "
52+ # New branch — compare against the remote's default branch (usually main).
53+ # This ensures we only check commits unique to this branch.
54+ default_branch=$( git symbolic-ref " refs/remotes/$remote /HEAD" 2> /dev/null | sed " s@^refs/remotes/$remote /@@" )
55+ if [ -z " $default_branch " ]; then
56+ default_branch=" main"
3757 fi
38- else
39- # Existing branch - check new commits since remote.
40- # Limit scope to commits after the latest published release on this branch.
41- latest_release=$( git tag --list ' v*' --sort=-version:refname --merged " $remote_sha " | head -1)
42- if [ -n " $latest_release " ]; then
43- # Only check commits after the latest release that are being pushed.
44- range=" $latest_release ..$local_sha "
58+ if git rev-parse " $remote /$default_branch " > /dev/null 2>&1 ; then
59+ range=" $remote /$default_branch ..$local_sha "
4560 else
46- # No release tags found - check new commits only.
47- range=" $remote_sha ..$local_sha "
61+ # No remote default branch (shallow clone, etc.) — skip to avoid
62+ # walking entire history which would cause false positives.
63+ printf " ${GREEN} ✓ Skipping validation (no baseline to compare against)${NC} \n"
64+ continue
4865 fi
66+ else
67+ # Existing branch — only check commits not yet on the remote.
68+ range=" $remote_sha ..$local_sha "
4969 fi
5070
51- # Validate the computed range before using it
71+ # Validate the computed range before using it.
5272 if ! git rev-list " $range " > /dev/null 2>&1 ; then
53- printf " ${RED} ✗ Invalid commit range: $range $ {NC} \n" >&2
73+ printf " ${RED} ✗ Invalid commit range: %s $ {NC} \n" " $range " >&2
5474 exit 1
5575 fi
5676
5777 ERRORS=0
5878
59- # ============================================================================
60- # CHECK 1: Scan commit messages for AI attribution
61- # ============================================================================
79+ # ── CHECK 1: AI attribution in commit messages ────────────────────────
80+ # Strips these at commit time via commit-msg hook, but this catches
81+ # commits made with --no-verify or on other machines.
6282 printf " Checking commit messages for AI attribution...\n"
6383
64- # Check each commit in the range for AI patterns.
65- while IFS= read -r commit_sha; do
84+ for commit_sha in $( git rev-list " $range " ) ; do
6685 full_msg=$( git log -1 --format=' %B' " $commit_sha " )
6786
6887 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
6988 if [ $ERRORS -eq 0 ]; then
7089 printf " ${RED} ✗ BLOCKED: AI attribution found in commit messages!${NC} \n"
7190 printf " Commits with AI attribution:\n"
7291 fi
73- echo " - $( git log -1 --oneline " $commit_sha " ) "
92+ printf " - %s\n " " $( git log -1 --oneline " $commit_sha " ) "
7493 ERRORS=$(( ERRORS + 1 ))
7594 fi
76- done < <( git rev-list " $range " )
95+ done
7796
7897 if [ $ERRORS -gt 0 ]; then
7998 printf " \n"
8099 printf " These commits were likely created with --no-verify, bypassing the\n"
81100 printf " commit-msg hook that strips AI attribution.\n"
82101 printf " \n"
102+ range_base=" ${range%% \.\. * } "
83103 printf " To fix:\n"
84- printf " git rebase -i %s\n" " $remote_sha "
85- printf " Mark commits as . reword. , remove AI attribution, save\n"
104+ printf " git rebase -i %s\n" " $range_base "
105+ printf " Mark commits as ' reword' , remove AI attribution, save\n"
86106 printf " git push\n"
87107 fi
88108
89- # ============================================================================
90- # CHECK 2: File content security checks
91- # ============================================================================
109+ # ── CHECK 2: File content security checks ─────────────────────────────
110+ # Scans files changed in the push range for secrets, keys, and mistakes.
92111 printf " Checking files for security issues...\n"
93112
94- # Get all files changed in these commits.
95- CHANGED_FILES=$( git diff --name-only " $range " 2> /dev/null || printf " \n" )
113+ CHANGED_FILES=$( git diff --name-only " $range " 2> /dev/null || echo " " )
96114
97115 if [ -n " $CHANGED_FILES " ]; then
98- # Check for sensitive files.
116+ # Check for sensitive files (.env, .DS_Store, log files) .
99117 if echo " $CHANGED_FILES " | grep -qE ' ^\.env(\.local)?$' ; then
100118 printf " ${RED} ✗ BLOCKED: Attempting to push .env file!${NC} \n"
101119 printf " Files: %s\n" " $( echo " $CHANGED_FILES " | grep -E ' ^\.env(\.local)?$' ) "
102120 ERRORS=$(( ERRORS + 1 ))
103121 fi
104122
105- # Check for .DS_Store.
106123 if echo " $CHANGED_FILES " | grep -q ' \.DS_Store' ; then
107124 printf " ${RED} ✗ BLOCKED: .DS_Store file in push!${NC} \n"
108125 printf " Files: %s\n" " $( echo " $CHANGED_FILES " | grep ' \.DS_Store' ) "
109126 ERRORS=$(( ERRORS + 1 ))
110127 fi
111128
112- # Check for log files.
113129 if echo " $CHANGED_FILES " | grep -E ' \.log$' | grep -v ' test.*\.log' | grep -q . ; then
114130 printf " ${RED} ✗ BLOCKED: Log file in push!${NC} \n"
115131 printf " Files: %s\n" " $( echo " $CHANGED_FILES " | grep -E ' \.log$' | grep -v ' test.*\.log' ) "
116132 ERRORS=$(( ERRORS + 1 ))
117133 fi
118134
119- # Check file contents for secrets.
135+ # Check file contents for secrets and hardcoded paths .
120136 while IFS= read -r file; do
121137 if [ -f " $file " ] && [ ! -d " $file " ]; then
122- # Skip test files, example files, and hook scripts.
138+ # Skip test files, example files, and hook scripts themselves .
123139 if echo " $file " | grep -qE ' \.(test|spec)\.(m?[jt]s|tsx?)$|\.example$|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/' ; then
124140 continue
125141 fi
126142
127143 # Use strings for binary files, grep directly for text files.
128- # This correctly extracts printable strings from WASM, .lockb, etc.
129144 is_binary=false
130145 if grep -qI ' ' " $file " 2> /dev/null; then
131146 is_binary=false
@@ -134,42 +149,42 @@ while read local_ref local_sha remote_ref remote_sha; do
134149 fi
135150
136151 if [ " $is_binary " = true ]; then
137- file_text=$( strings " $file " 2> /dev/null || echo " " )
152+ file_text=$( strings " $file " 2> /dev/null)
138153 else
139- file_text=$( cat " $file " 2> /dev/null || echo " " )
154+ file_text=$( cat " $file " 2> /dev/null)
140155 fi
141156
142- # Check for hardcoded user paths .
157+ # Hardcoded personal paths (/Users/foo/, /home/foo/, C:\Users\foo\) .
143158 if echo " $file_text " | grep -qE ' (/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' ; then
144- printf " ${RED} ✗ BLOCKED: Hardcoded personal path found in: $file $ {NC} \n"
159+ printf " ${RED} ✗ BLOCKED: Hardcoded personal path found in: %s $ {NC} \n" " $file "
145160 echo " $file_text " | grep -nE ' (/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' | head -3
146161 ERRORS=$(( ERRORS + 1 ))
147162 fi
148163
149- # Check for Socket API keys.
164+ # Socket API keys (except allowed public key and test placeholders) .
150165 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
151- printf " ${RED} ✗ BLOCKED: Real API key detected in: $file $ {NC} \n"
166+ printf " ${RED} ✗ BLOCKED: Real API key detected in: %s $ {NC} \n" " $file "
152167 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
153168 ERRORS=$(( ERRORS + 1 ))
154169 fi
155170
156- # Check for AWS keys.
171+ # AWS keys.
157172 if echo " $file_text " | grep -iqE ' (aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' ; then
158- printf " ${RED} ✗ BLOCKED: Potential AWS credentials found in: $file $ {NC} \n"
173+ printf " ${RED} ✗ BLOCKED: Potential AWS credentials found in: %s $ {NC} \n" " $file "
159174 echo " $file_text " | grep -niE ' (aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' | head -3
160175 ERRORS=$(( ERRORS + 1 ))
161176 fi
162177
163- # Check for GitHub tokens.
178+ # GitHub tokens.
164179 if echo " $file_text " | grep -qE ' gh[ps]_[a-zA-Z0-9]{36}' ; then
165- printf " ${RED} ✗ BLOCKED: Potential GitHub token found in: $file $ {NC} \n"
180+ printf " ${RED} ✗ BLOCKED: Potential GitHub token found in: %s $ {NC} \n" " $file "
166181 echo " $file_text " | grep -nE ' gh[ps]_[a-zA-Z0-9]{36}' | head -3
167182 ERRORS=$(( ERRORS + 1 ))
168183 fi
169184
170- # Check for private keys.
185+ # Private keys.
171186 if echo " $file_text " | grep -qE -- ' -----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----' ; then
172- printf " ${RED} ✗ BLOCKED: Private key found in: $file $ {NC} \n"
187+ printf " ${RED} ✗ BLOCKED: Private key found in: %s $ {NC} \n" " $file "
173188 ERRORS=$(( ERRORS + 1 ))
174189 fi
175190 fi
0 commit comments