The 'GH009: Secrets detected' error occurs when GitHub's push protection feature detects credentials, API keys, or other sensitive data in your commits. This security feature blocks the push to prevent accidental exposure of secrets in your repository.
This error indicates that GitHub's secret scanning push protection has detected what appears to be sensitive information in one or more of your commits. GitHub automatically scans pushes for known secret patterns such as API keys, tokens, passwords, and private keys to prevent accidental credential exposure. When push protection is enabled (which is the default for public repositories and can be enabled for private repositories), GitHub analyzes the content of your commits before accepting the push. If it detects patterns matching known secret formats from supported providers, it blocks the push entirely. The error message typically includes: - The type of secret detected (e.g., "GitHub Personal Access Token", "AWS Access Key ID") - The commit hash(es) containing the secret - The file path and line number where the secret was found This is a critical security feature because secrets accidentally pushed to a repository (especially public ones) can be harvested by malicious actors within minutes. Even if you quickly delete the file, the secret remains in Git history and can be extracted.
When GitHub blocks your push, it provides detailed information about what was detected. Examine the error message carefully:
remote: error: GH009: Secrets detected. Push blocked.
remote:
remote: —— GitHub Personal Access Token ——————————————
remote: locations:
remote: - commit: 8728dbe67
remote: path: config/settings.js:15
remote:
remote: —— AWS Access Key ID ——————————————————————
remote: locations:
remote: - commit: a4f2c9e12
remote: path: .env:3Note down:
1. Secret type - What kind of credential was detected
2. Commit hash - Which commit(s) contain the secret
3. File path and line - Exact location of the secret
If the secret is in the most recent commit, remediation is straightforward. If it's in an earlier commit, you'll need to rewrite history.
Before fixing your Git history, revoke or rotate the exposed credential. Even though the push was blocked, treat any secret that reached this point as potentially compromised.
For GitHub tokens:
1. Go to [github.com/settings/tokens](https://github.com/settings/tokens)
2. Find and delete the exposed token
3. Generate a new token with the same permissions
For AWS credentials:
1. Go to AWS IAM Console
2. Find the exposed access key
3. Click "Make Inactive" or "Delete"
4. Create new credentials
For other services:
- Rotate the credential in the respective service's dashboard
- Update any applications using the old credential
- Check service logs for unauthorized access
Important: Always revoke first, then fix Git history. The secret may have been logged or cached by GitHub's systems.
If the secret was introduced in your most recent commit, you can amend it:
# Remove the secret from your code
# Edit the file and delete or replace the credential
# Stage the changes
git add <file-with-secret>
# Amend the last commit (rewrites history)
git commit --amend --all
# Force push (since history was rewritten)
git push --force-with-leaseIf the secret is in a file that shouldn't be tracked:
# Remove from Git but keep locally
git rm --cached .env
git rm --cached config/secrets.json
# Add to .gitignore
echo ".env" >> .gitignore
echo "config/secrets.json" >> .gitignore
# Amend the commit
git add .gitignore
git commit --amend --all
# Push
git push --force-with-leaseReplace hardcoded secrets with environment variables:
// Before (bad)
const API_KEY = "sk-abc123...";
// After (good)
const API_KEY = process.env.API_KEY;If the secret is in an older commit, use interactive rebase to edit that specific commit:
# Find which commit introduced the secret
git log --oneline --all -- <path-to-file>
# Start interactive rebase from the commit before the one with the secret
git rebase -i <commit-hash>~1
# In the editor, change 'pick' to 'edit' for the target commit
# Save and close the editorWhen Git stops at the commit:
# Remove or fix the secret
# Edit the file to remove the credential
# Stage the fix
git add <file>
# Amend this commit
git commit --amend
# Continue the rebase
git rebase --continueAfter completing the rebase:
# Force push the rewritten history
git push --force-with-leaseNote: This rewrites commit hashes for all commits after the edited one. Coordinate with your team if working on a shared branch.
For secrets spread across many commits or deep in history, BFG Repo-Cleaner is faster and easier than interactive rebase:
# Install BFG (requires Java 11+)
# macOS: brew install bfg
# Or download from https://rtyley.github.io/bfg-repo-cleaner/
# Clone a fresh mirror of your repository
git clone --mirror [email protected]:user/repo.git
# Create a file with secrets to remove
echo "sk-abc123secretkey" >> secrets.txt
echo "AKIAIOSFODNN7EXAMPLE" >> secrets.txt
# Run BFG to replace secrets with ***REMOVED***
java -jar bfg.jar --replace-text secrets.txt repo.git
# Or delete entire files containing secrets
java -jar bfg.jar --delete-files .env repo.git
# Clean up and push
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --forceAlternative: git-filter-repo (recommended by Git project):
# Install git-filter-repo
pip install git-filter-repo
# Remove a file from all history
git filter-repo --path .env --invert-paths
# Or replace text patterns
git filter-repo --replace-text expressions.txtAfter force pushing, all collaborators must reclone or rebase their local copies.
If the detected "secret" is actually a test value, example placeholder, or false positive, you can bypass push protection:
Option 1: Bypass via the GitHub UI
When the push is blocked, GitHub provides a URL in the error message:
remote: To push, visit:
remote: https://github.com/user/repo/security/secret-scanning/...1. Visit the provided URL
2. Select a bypass reason:
- It's used in tests - For test credentials not used in production
- It's a false positive - The pattern matches but isn't a real secret
- I'll fix it later - Real secret you'll remediate soon
3. Complete the bypass within 3 hours
Option 2: Request bypass if you don't have permission
If your organization restricts bypasses, you can request approval:
1. Click the link in the error message
2. Select "Request bypass"
3. Provide justification
4. Wait for an organization admin to approve
Best practice for test secrets:
Instead of bypassing, use obviously fake values that won't trigger detection:
// Instead of realistic-looking test values
const TEST_KEY = "test-key-12345";
// Use obviously fake placeholders
const TEST_KEY = "REPLACE_WITH_REAL_KEY";Set up local checks to catch secrets before they reach GitHub:
Using git-secrets (AWS tool):
# Install git-secrets
# macOS: brew install git-secrets
# Linux: see https://github.com/awslabs/git-secrets
# Initialize in your repository
cd your-repo
git secrets --install
git secrets --register-aws
# Add custom patterns
git secrets --add 'sk-[a-zA-Z0-9]{48}' # OpenAI keys
git secrets --add 'ghp_[a-zA-Z0-9]{36}' # GitHub tokens
# Scan existing history
git secrets --scan-historyUsing pre-commit with detect-secrets:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']# Install and run
pip install pre-commit detect-secrets
pre-commit install
# Create baseline (for existing false positives)
detect-secrets scan > .secrets.baselineUsing gitleaks:
# Install gitleaks
# macOS: brew install gitleaks
# Scan before committing
gitleaks detect --source . --verbose
# Add as pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaksPrevent secrets from being committed by ignoring sensitive files:
# .gitignore
# Environment files
.env
.env.local
.env.*.local
.env.development
.env.production
# Secret configuration
secrets.json
secrets.yaml
secrets.yml
config/secrets.*
# Private keys and certificates
*.pem
*.key
*.p12
*.pfx
id_rsa
id_ed25519
# Cloud provider credentials
.aws/credentials
.azure/credentials
gcloud-service-account.json
*-credentials.json
# Editor and IDE secrets
.idea/httpRequests/
.vscode/settings.json
# Docker secrets
docker-compose.override.ymlUse template files for configuration:
# Create a template without real secrets
cp config.json config.json.example
# Edit config.json.example to use placeholders
# Add config.json to .gitignore
echo "config.json" >> .gitignore
# Commit only the example
git add config.json.example .gitignore
git commit -m "Add config template"For environment variables, use .env.example:
# .env.example (committed)
DATABASE_URL=postgres://user:password@localhost:5432/db
API_KEY=your-api-key-here
# .env (gitignored, real values)
DATABASE_URL=postgres://realuser:[email protected]:5432/proddb
API_KEY=sk-abc123...### Understanding GitHub's Secret Scanning Patterns
GitHub secret scanning detects secrets from over 200 service providers including:
- Cloud providers: AWS, Azure, GCP, DigitalOcean
- Payment processors: Stripe, PayPal, Square
- Communication: Twilio, SendGrid, Mailchimp
- Development tools: npm, PyPI, Docker Hub
- Databases: MongoDB, Redis, PostgreSQL connection strings
The detection uses pattern matching for known token formats. For example:
- GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_ prefixes
- AWS access keys: AKIA prefix with 16 uppercase alphanumeric characters
- OpenAI keys: sk- prefix with 48 alphanumeric characters
### Push Protection vs. Secret Scanning Alerts
GitHub offers two related features:
1. Push Protection (blocks pushes) - Prevents secrets from entering the repository
2. Secret Scanning Alerts (notifies after push) - Scans existing history and alerts on findings
Push protection is more secure but can block legitimate work if false positives occur.
### Organization-Level Configuration
Repository admins and organization owners can configure push protection:
Repository Settings > Code security and analysis > Secret scanning
✓ Enable secret scanning
✓ Enable push protectionOrganizations can also:
- Enable push protection for all repositories
- Restrict who can bypass push protection
- Require review for bypass requests
- Define custom secret patterns
### Working with Custom Secret Patterns
Organizations can define custom patterns for internal credentials:
# Example custom pattern for internal API keys
internal-api-key-[a-f0-9]{32}Custom patterns may have higher false positive rates than GitHub's built-in patterns.
### Dealing with Secrets in Pull Requests
If a PR contains secrets:
1. The PR cannot be merged until secrets are removed
2. Close the PR without merging
3. Have the contributor rewrite their branch history
4. Open a new PR from the cleaned branch
### History Rewriting Impact
When you force push to remove secrets:
- All commit hashes after the rewritten commit change
- Open PRs may need to be rebased
- CI/CD pipelines referencing specific commits may break
- Collaborators need to rebase or reclone
Communicate with your team before force pushing to shared branches.
### Checking if Your Secret Was Exposed
Even though push protection blocked the push, check for exposure:
# Check GitHub's API for secret scanning alerts
gh api repos/{owner}/{repo}/secret-scanning/alerts
# Check if the secret appears in any public repository
# Use GitHub search: "YOUR_SECRET_HERE" (be careful with this)### Enterprise Considerations
For GitHub Enterprise:
- Push protection requires GitHub Advanced Security license
- Admins can configure organization-wide policies
- Self-hosted runners may need specific network access for scanning
- Custom patterns can be managed at the enterprise level
kex_exchange_identification: Connection closed by remote host
Connection closed by remote host when connecting to Git server
fatal: unable to access: Proxy auto-configuration failed
How to fix 'Proxy auto-configuration failed' in Git
fatal: unable to access: Authentication failed (proxy requires basic auth)
How to fix 'Authentication failed (proxy requires basic auth)' in Git
fatal: unable to access: no_proxy configuration not working
How to fix 'no_proxy configuration not working' in Git
fatal: unable to read tree object in treeless clone
How to fix 'unable to read tree object in treeless clone' in Git