11#! /bin/bash
22# Socket Security Pre-push Hook
33# Security enforcement layer for all pushes.
4- # Validates all commits being pushed for security issues and AI attribution.
5- # NOTE: Security checks parallel .husky/security-checks.sh — keep in sync.
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.
615
716set -e
817
@@ -13,76 +22,46 @@ NC='\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- # ============================================================================
26- # PRE-CHECK 1: AgentShield scan on Claude config (blocks push on failure)
27- # ============================================================================
28- if command -v agentshield > /dev/null 2>&1 || [ -x " $( pnpm bin 2> /dev/null) /agentshield" ]; then
29- AGENTSHIELD=" $( command -v agentshield 2> /dev/null || echo " $( pnpm bin) /agentshield" ) "
30- if ! " $AGENTSHIELD " scan --quiet < /dev/null 2> /dev/null; then
31- printf " ${RED} ✗ AgentShield: security issues found in Claude config${NC} \n"
32- printf " Run 'pnpm exec agentshield scan' for details\n"
33- TOTAL_ERRORS=$(( TOTAL_ERRORS + 1 ))
34- fi
35- fi
36-
37- # ============================================================================
38- # PRE-CHECK 2: zizmor scan on GitHub Actions workflows
39- # ============================================================================
40- ZIZMOR=" "
41- if command -v zizmor > /dev/null 2>&1 ; then
42- ZIZMOR=" $( command -v zizmor) "
43- elif [ -x " $HOME /.socket/zizmor/bin/zizmor" ]; then
44- ZIZMOR=" $HOME /.socket/zizmor/bin/zizmor"
45- fi
46- if [ -n " $ZIZMOR " ] && [ -d " .github/" ]; then
47- if ! " $ZIZMOR " .github/ < /dev/null 2> /dev/null; then
48- printf " ${RED} ✗ Zizmor: workflow security issues found${NC} \n"
49- printf " Run 'zizmor .github/' for details\n"
50- TOTAL_ERRORS=$(( TOTAL_ERRORS + 1 ))
51- fi
52- fi
53-
54- # Read stdin for refs being pushed.
34+ # Read stdin for refs being pushed (git provides: local_ref local_sha remote_ref remote_sha).
5535while read local_ref local_sha remote_ref remote_sha; do
5636 # Skip tag pushes: tags point to existing commits already validated.
5737 if echo " $local_ref " | grep -q ' ^refs/tags/' ; then
5838 printf " ${GREEN} Skipping tag push: %s${NC} \n" " $local_ref "
5939 continue
6040 fi
6141
62- # Skip delete pushes.
42+ # Skip delete pushes (local_sha is all zeros when deleting a remote branch) .
6343 if [ " $local_sha " = " 0000000000000000000000000000000000000000" ]; then
6444 continue
6545 fi
6646
67- # Get the range of commits being pushed.
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.
6851 if [ " $remote_sha " = " 0000000000000000000000000000000000000000" ]; then
69- # New branch - only check commits not on the default remote branch.
52+ # New branch — compare against the remote's default branch (usually main).
53+ # This ensures we only check commits unique to this branch.
7054 default_branch=$( git symbolic-ref " refs/remotes/$remote /HEAD" 2> /dev/null | sed " s@^refs/remotes/$remote /@@" )
7155 if [ -z " $default_branch " ]; then
7256 default_branch=" main"
7357 fi
7458 if git rev-parse " $remote /$default_branch " > /dev/null 2>&1 ; then
7559 range=" $remote /$default_branch ..$local_sha "
7660 else
77- # No remote default branch, fall back to release tag.
78- latest_release=$( git tag --list ' v*' --sort=-version:refname --merged " $local_sha " | head -1)
79- if [ -n " $latest_release " ]; then
80- range=" $latest_release ..$local_sha "
81- else
82- # No remote branch or tags — skip scan to avoid walking entire history.
83- printf " ${GREEN} ✓ Skipping validation (no baseline to compare against)${NC} \n"
84- continue
85- fi
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
8665 fi
8766 else
8867 # Existing branch — only check commits not yet on the remote.
@@ -97,13 +76,12 @@ while read local_ref local_sha remote_ref remote_sha; do
9776
9877 ERRORS=0
9978
100- # ============================================================================
101- # CHECK 1: Scan commit messages for AI attribution
102- # ============================================================================
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.
10382 printf " Checking commit messages for AI attribution...\n"
10483
105- # Check each commit in the range for AI patterns.
106- while IFS= read -r commit_sha; do
84+ for commit_sha in $( git rev-list " $range " ) ; do
10785 full_msg=$( git log -1 --format=' %B' " $commit_sha " )
10886
10987 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
@@ -114,7 +92,7 @@ while read local_ref local_sha remote_ref remote_sha; do
11492 printf " - %s\n" " $( git log -1 --oneline " $commit_sha " ) "
11593 ERRORS=$(( ERRORS + 1 ))
11694 fi
117- done < <( git rev-list " $range " )
95+ done
11896
11997 if [ $ERRORS -gt 0 ]; then
12098 printf " \n"
@@ -128,46 +106,41 @@ while read local_ref local_sha remote_ref remote_sha; do
128106 printf " git push\n"
129107 fi
130108
131- # ============================================================================
132- # CHECK 2: File content security checks
133- # ============================================================================
109+ # ── CHECK 2: File content security checks ─────────────────────────────
110+ # Scans files changed in the push range for secrets, keys, and mistakes.
134111 printf " Checking files for security issues...\n"
135112
136- # Get all files changed in these commits.
137113 CHANGED_FILES=$( git diff --name-only " $range " 2> /dev/null || echo " " )
138114
139115 if [ -n " $CHANGED_FILES " ]; then
140- # Check for sensitive files.
116+ # Check for sensitive files (.env, .DS_Store, log files) .
141117 if echo " $CHANGED_FILES " | grep -qE ' ^\.env(\.local)?$' ; then
142118 printf " ${RED} ✗ BLOCKED: Attempting to push .env file!${NC} \n"
143119 printf " Files: %s\n" " $( echo " $CHANGED_FILES " | grep -E ' ^\.env(\.local)?$' ) "
144120 ERRORS=$(( ERRORS + 1 ))
145121 fi
146122
147- # Check for .DS_Store.
148123 if echo " $CHANGED_FILES " | grep -q ' \.DS_Store' ; then
149124 printf " ${RED} ✗ BLOCKED: .DS_Store file in push!${NC} \n"
150125 printf " Files: %s\n" " $( echo " $CHANGED_FILES " | grep ' \.DS_Store' ) "
151126 ERRORS=$(( ERRORS + 1 ))
152127 fi
153128
154- # Check for log files.
155129 if echo " $CHANGED_FILES " | grep -E ' \.log$' | grep -v ' test.*\.log' | grep -q . ; then
156130 printf " ${RED} ✗ BLOCKED: Log file in push!${NC} \n"
157131 printf " Files: %s\n" " $( echo " $CHANGED_FILES " | grep -E ' \.log$' | grep -v ' test.*\.log' ) "
158132 ERRORS=$(( ERRORS + 1 ))
159133 fi
160134
161- # Check file contents for secrets.
135+ # Check file contents for secrets and hardcoded paths .
162136 while IFS= read -r file; do
163137 if [ -f " $file " ] && [ ! -d " $file " ]; then
164- # Skip test files, example files, and hook scripts.
138+ # Skip test files, example files, and hook scripts themselves .
165139 if echo " $file " | grep -qE ' \.(test|spec)\.(m?[jt]s|tsx?)$|\.example$|/test/|/tests/|fixtures/|\.git-hooks/|\.husky/' ; then
166140 continue
167141 fi
168142
169143 # Use strings for binary files, grep directly for text files.
170- # This correctly extracts printable strings from WASM, .lockb, etc.
171144 is_binary=false
172145 if grep -qI ' ' " $file " 2> /dev/null; then
173146 is_binary=false
@@ -176,40 +149,40 @@ while read local_ref local_sha remote_ref remote_sha; do
176149 fi
177150
178151 if [ " $is_binary " = true ]; then
179- file_text=$( strings " $file " 2> /dev/null || echo " " )
152+ file_text=$( strings " $file " 2> /dev/null)
180153 else
181- file_text=$( cat " $file " 2> /dev/null || echo " " )
154+ file_text=$( cat " $file " 2> /dev/null)
182155 fi
183156
184- # Check for hardcoded user paths .
157+ # Hardcoded personal paths (/Users/foo/, /home/foo/, C:\Users\foo\) .
185158 if echo " $file_text " | grep -qE ' (/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' ; then
186159 printf " ${RED} ✗ BLOCKED: Hardcoded personal path found in: %s${NC} \n" " $file "
187160 echo " $file_text " | grep -nE ' (/Users/[^/\s]+/|/home/[^/\s]+/|C:\\Users\\[^\\]+\\)' | head -3
188161 ERRORS=$(( ERRORS + 1 ))
189162 fi
190163
191- # Check for Socket API keys.
164+ # Socket API keys (except allowed public key and test placeholders) .
192165 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
193166 printf " ${RED} ✗ BLOCKED: Real API key detected in: %s${NC} \n" " $file "
194167 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
195168 ERRORS=$(( ERRORS + 1 ))
196169 fi
197170
198- # Check for AWS keys.
171+ # AWS keys.
199172 if echo " $file_text " | grep -iqE ' (aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' ; then
200173 printf " ${RED} ✗ BLOCKED: Potential AWS credentials found in: %s${NC} \n" " $file "
201174 echo " $file_text " | grep -niE ' (aws_access_key|aws_secret|AKIA[0-9A-Z]{16})' | head -3
202175 ERRORS=$(( ERRORS + 1 ))
203176 fi
204177
205- # Check for GitHub tokens.
178+ # GitHub tokens.
206179 if echo " $file_text " | grep -qE ' gh[ps]_[a-zA-Z0-9]{36}' ; then
207180 printf " ${RED} ✗ BLOCKED: Potential GitHub token found in: %s${NC} \n" " $file "
208181 echo " $file_text " | grep -nE ' gh[ps]_[a-zA-Z0-9]{36}' | head -3
209182 ERRORS=$(( ERRORS + 1 ))
210183 fi
211184
212- # Check for private keys.
185+ # Private keys.
213186 if echo " $file_text " | grep -qE -- ' -----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----' ; then
214187 printf " ${RED} ✗ BLOCKED: Private key found in: %s${NC} \n" " $file "
215188 ERRORS=$(( ERRORS + 1 ))
0 commit comments