Git Commit Safety Mechanism
Date: 2026-04-06 Status: Design Context: Prevent test credentials from leaking to production commits
Problem Analysis
Root Causes
- Application Layer:
git_committool didn't validate credentials - Test Layer: Test repos shared credential namespace with production repo
- Repository Layer: No pre-commit hooks to block suspicious commits
- CI/CD Layer: No pipeline validation of commit metadata
- Monitoring Layer: No detection of suspicious commits post-merge
Attack Surface
┌─────────────────────────────────────────────────────────────┐
│ How Test Credentials Can Leak │
├─────────────────────────────────────────────────────────────┤
│ 1. AI Agent uses git_commit tool in wrong directory │
│ 2. Test runs in production repo directory │
│ 3. Test crashes without cleanup, leaves git config │
│ 4. Developer manually runs test code in production repo │
│ 5. Worktree created from test template │
│ 6. Git config --global accidentally set during tests │
└─────────────────────────────────────────────────────────────┘
Defense in Depth Strategy
Layer 1: Prevention (Before Commit)
Layer 2: Detection (At Commit Time)
Layer 3: Rejection (Pre-Push)
Layer 4: Validation (CI/CD)
Layer 5: Monitoring (Post-Merge)
Layer 1: Prevention (Before Commit)
1.1 Application-Level Validation ✅ (Already Implemented)
File: src/core/tools/workspace-tools.ts
// Validate before every commit
- Block test@example.com
- Block "Routa Test", "Test", "placeholder"
- Require valid git identity
1.2 Test Framework Isolation
Principle: Tests should NEVER touch production git config
// Enforce test isolation pattern
pub struct IsolatedGitRepo {
_temp_dir: TempDir,
pub path: PathBuf,
}
impl IsolatedGitRepo {
pub fn new() -> Self {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().to_path_buf();
// Initialize with LOCAL config only
run_git(&path, &["init"]);
run_git(&path, &["config", "--local", "user.name", "Routa Test"]);
run_git(&path, &["config", "--local", "user.email", "test@example.com"]);
Self { _temp_dir: temp_dir, path }
}
// Prevent access to production repo
pub fn assert_isolated(&self) {
assert!(self.path.starts_with(std::env::temp_dir()));
}
}
1.3 Environment Detection
// Detect if running in test context
function isTestEnvironment(): boolean {
return (
process.env.NODE_ENV === 'test' ||
process.env.VITEST === 'true' ||
process.env.JEST_WORKER_ID !== undefined ||
// Rust test detection
process.env.CARGO_TEST === 'true'
);
}
// Block test credentials in production context
async function validateGitCommitContext(cwd: string) {
if (!isTestEnvironment() && isTestCredential(cwd)) {
throw new Error(
'Test credentials detected in production environment. ' +
'This indicates a test isolation failure.'
);
}
}
Layer 2: Detection (At Commit Time)
2.1 Git Pre-Commit Hook
File: .husky/pre-commit
#!/usr/bin/env sh
# Validate commit author before allowing commit
AUTHOR_NAME=$(git config user.name)
AUTHOR_EMAIL=$(git config user.email)
# Block test credentials
if echo "$AUTHOR_EMAIL" | grep -qi "test@example\.com"; then
echo "❌ COMMIT BLOCKED: Test email detected"
echo " Found: $AUTHOR_EMAIL"
echo ""
echo " Configure your real git identity:"
echo " git config user.name \"Your Name\""
echo " git config user.email \"your.email@example.com\""
exit 1
fi
if echo "$AUTHOR_NAME" | grep -qi "routa test"; then
echo "❌ COMMIT BLOCKED: Test name detected"
echo " Found: $AUTHOR_NAME"
exit 1
fi
# Require valid email format
if ! echo "$AUTHOR_EMAIL" | grep -qE "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; then
echo "❌ COMMIT BLOCKED: Invalid email format"
echo " Found: $AUTHOR_EMAIL"
exit 1
fi
2.2 Commit Message Validation
Layer 3: Rejection (Pre-Push)
3.1 Git Pre-Push Hook
File: .husky/pre-push
#!/usr/bin/env sh
# Scan all commits being pushed for suspicious authors
REMOTE="$1"
URL="$2"
# Get list of commits to be pushed
while read local_ref local_sha remote_ref remote_sha; do
if [ "$local_sha" != "0000000000000000000000000000000000000000" ]; then
# Check commits from remote_sha to local_sha
RANGE="$remote_sha..$local_sha"
# Find commits with test credentials
SUSPICIOUS=$(git log "$RANGE" --format="%H %ae %an" | \
grep -iE "(test@example\.com|routa test)" || true)
if [ -n "$SUSPICIOUS" ]; then
echo "❌ PUSH BLOCKED: Commits with test credentials detected"
echo ""
echo "$SUSPICIOUS" | while read hash email name; do
echo " Commit: $hash"
echo " Author: $name <$email>"
echo ""
done
echo "Fix these commits before pushing:"
echo " git rebase -i origin/main"
echo " # Mark commits for 'edit', then:"
echo " git commit --amend --author=\"Your Name <your@email.com>\""
exit 1
fi
fi
done
echo "✅ Push validation passed"
3.2 Server-Side Pre-Receive Hook
For self-hosted repositories, add server-side validation:
#!/bin/bash
# .git/hooks/pre-receive (on server)
while read oldrev newrev refname; do
# Scan all commits in push
for commit in $(git rev-list $oldrev..$newrev); do
AUTHOR_EMAIL=$(git log -1 --format=%ae $commit)
AUTHOR_NAME=$(git log -1 --format=%an $commit)
if echo "$AUTHOR_EMAIL" | grep -qi "test@example\.com"; then
echo "ERROR: Push rejected - commit $commit has test email"
exit 1
fi
if echo "$AUTHOR_NAME" | grep -qi "routa test"; then
echo "ERROR: Push rejected - commit $commit has test name"
exit 1
fi
done
done
Layer 4: CI/CD Validation
4.1 GitHub Actions Validation
File: .github/workflows/defense.yml
jobs:
validate-commit-metadata:
name: "Validate Commit Metadata"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history
- name: Check for test credentials in commits
run: |
# Check all commits in PR
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
if [ -n "$BASE_SHA" ]; then
RANGE="$BASE_SHA..$HEAD_SHA"
else
RANGE="HEAD~10..HEAD" # Last 10 commits for push
fi
echo "Checking commits in range: $RANGE"
SUSPICIOUS=$(git log "$RANGE" --format="%H %ae %an" | \
grep -iE "(test@example\.com|routa test|placeholder)" || true)
if [ -n "$SUSPICIOUS" ]; then
echo "❌ Test credentials found in commits:"
echo "$SUSPICIOUS"
exit 1
fi
echo "✅ All commits have valid author metadata"
- name: Validate email domains
run: |
# Optional: enforce company email domain
INVALID=$(git log "$RANGE" --format="%ae" | \
grep -vE "@(gmail|github|users.noreply.github|phodal)\.com$" || true)
if [ -n "$INVALID" ]; then
echo "⚠️ Warning: Unexpected email domains: $INVALID"
# Don't fail, just warn
fi
4.2 Pre-Merge Validation
block-merge-if-test-credentials:
name: "Block Merge - Test Credentials"
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Scan PR commits
id: scan
run: |
# More strict for PR merge
if git log origin/${{ github.base_ref }}..${{ github.sha }} \
--format="%ae %an" | grep -qiE "(test@example|routa test)"; then
echo "has_test_creds=true" >> $GITHUB_OUTPUT
else
echo "has_test_creds=false" >> $GITHUB_OUTPUT
fi
- name: Block merge
if: steps.scan.outputs.has_test_creds == 'true'
run: |
echo "::error::PR contains commits with test credentials"
echo "::error::These commits must be amended before merge"
exit 1
Layer 5: Monitoring & Alerts
5.1 Post-Merge Detection
// scripts/check-commit-history.ts
import { execSync } from 'child_process';
async function scanRecentCommits(days = 7) {
const since = new Date();
since.setDate(since.getDate() - days);
const commits = execSync(
`git log --since="${since.toISOString()}" --format="%H|%ae|%an"`,
{ encoding: 'utf-8' }
).trim().split('\n');
const suspicious = commits.filter(line => {
const [hash, email, name] = line.split('|');
return (
email.includes('test@example.com') ||
name.toLowerCase().includes('routa test') ||
name.toLowerCase() === 'test'
);
});
if (suspicious.length > 0) {
await sendAlert({
severity: 'HIGH',
title: 'Test credentials detected in git history',
commits: suspicious,
action: 'Review and amend commits immediately'
});
}
}
5.2 Daily Scheduled Check
File: .github/workflows/daily-health-check.yml
name: Daily Repository Health Check
on:
schedule:
- cron: '0 2 * * *' # 2 AM daily
workflow_dispatch:
jobs:
check-git-history:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan last 30 days for test credentials
run: |
SUSPICIOUS=$(git log --since="30 days ago" --format="%H %ae %an" | \
grep -iE "(test@example\.com|routa test)" || true)
if [ -n "$SUSPICIOUS" ]; then
echo "⚠️ Test credentials found in recent history:"
echo "$SUSPICIOUS"
# Create GitHub issue
gh issue create \
--title "🚨 Test credentials detected in git history" \
--body "Found commits with test credentials in last 30 days" \
--label "security,git-hygiene"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Implementation Roadmap
Phase 1: Immediate (Already Done ✅)
- ✅ Application-level validation in
git_committool - ✅ Test isolation with
--localconfig - ✅ RAII cleanup with
TempDir
Phase 2: Git Hooks (High Priority)
- Add pre-commit hook to block test credentials
- Add pre-push hook to scan commit history
- Update
.husky/hooks with validation
Phase 3: CI/CD Gates (High Priority)
- Add commit metadata validation to Defense workflow
- Add pre-merge validation for PRs
- Block merge if test credentials detected
Phase 4: Monitoring (Medium Priority)
- Daily scheduled health check
- Alert on suspicious commits
- Auto-create issues for violations
Phase 5: Test Framework (Medium Priority)
- Create
IsolatedGitRepohelper in Rust - Create
createTestRepo()helper in TypeScript - Enforce usage in test guidelines
Configuration
Blocked Patterns
File: scripts/git-safety-config.json
{
"blockedEmails": [
"test@example.com",
"noreply@test.com",
"placeholder@example.com"
],
"blockedNames": [
"routa test",
"test",
"placeholder",
"example user"
],
"allowedDomains": [
"gmail.com",
"github.com",
"users.noreply.github.com"
],
"strictMode": false
}
Testing the Safety Mechanism
Manual Test
# 1. Try to commit with test credentials
cd /tmp
mkdir test-safety
cd test-safety
git init
git config --local user.name "Routa Test"
git config --local user.email "test@example.com"
echo "test" > file.txt
git add file.txt
git commit -m "test" # Should be BLOCKED by pre-commit hook
# 2. Try to use git_commit tool with test creds
# Should fail with validation error
Automated Test
// tests/git-safety.test.ts
describe('Git Safety Mechanism', () => {
it('blocks commits with test email', async () => {
const tempRepo = createTempRepo();
await execAsync('git config --local user.email test@example.com', { cwd: tempRepo });
await expect(
commitTool.execute({ message: 'test', cwd: tempRepo })
).rejects.toThrow('suspicious test value');
});
it('blocks commits with test name', async () => {
const tempRepo = createTempRepo();
await execAsync('git config --local user.name "Routa Test"', { cwd: tempRepo });
await expect(
commitTool.execute({ message: 'test', cwd: tempRepo })
).rejects.toThrow('suspicious test value');
});
});
Metrics & KPIs
Track effectiveness of the safety mechanism:
- Commits blocked at pre-commit: Count of local blocks
- Commits blocked at pre-push: Count of push blocks
- CI failures due to credentials: Count of CI blocks
- Leakage incidents: Count of test credentials in main branch
- Mean time to detection: Time from commit to detection
- Mean time to remediation: Time from detection to fix
Target: Zero test credentials in main branch history after implementation.
Emergency Response
If test credentials are detected in main:
- Immediate: Create incident issue
- Assess: Determine scope (how many commits, which branches)
- Decide: Rewrite history or accept and move forward
- Fix: If rewriting, coordinate with all contributors
- Prevent: Ensure all layers are active
- Review: Post-mortem to strengthen mechanism
Summary
This defense-in-depth approach provides 5 layers of protection:
- ✅ Prevention: Application validates before commit
- 🔄 Detection: Git hooks block at commit time
- 🔄 Rejection: Pre-push hooks scan history
- 🔄 Validation: CI/CD enforces in pipeline
- 🔄 Monitoring: Daily scans detect escapees
Key Principle: Make it impossible for test credentials to reach production, not just unlikely.
References
- Issue:
docs/issues/2026-04-06-test-git-credentials-leak.md - Implementation: Commits a75a2901, 160c9600
- Git Hooks:
.husky/pre-commit,.husky/pre-push - CI/CD:
.github/workflows/defense.yml