Security·Secrets Scanning·CI/CD

Secrets scanning for MCP server CI pipelines: gitleaks, trufflehog, and pre-commit hooks

A committed secret is a revoked secret — or should be. The problem is that revocation is slow, public repositories are indexed by bots within minutes of a push, and "I'll just remove it in the next commit" leaves the credential visible in git history forever. For MCP server repositories, the stakes are higher than average: MCP API keys, GitHub PATs, and upstream service tokens committed to source give any reader the ability to call your tools as an authenticated user, exfiltrate data via your tool's upstream API connections, or impersonate your server entirely. Secrets scanning blocks this before it happens.

Layer 1: pre-commit hook with gitleaks

The highest-value layer is blocking secrets before they touch git history at all. A pre-commit hook runs gitleaks detect on the staged changes; if it finds a pattern match, the commit is rejected with a clear error before the secret enters the repository.

Install gitleaks and wire the hook:

# Install gitleaks (macOS)
brew install gitleaks

# Install gitleaks (Linux — download binary)
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.18.2/gitleaks_8.18.2_linux_x64.tar.gz \
  | tar -xz && sudo mv gitleaks /usr/local/bin/

# Wire as a pre-commit hook (project-local)
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
gitleaks protect --staged --redact --exit-code 1
if [ $? -ne 0 ]; then
  echo "gitleaks: secret detected in staged changes. Commit blocked."
  echo "Remove the secret and store it in your environment instead."
  exit 1
fi
EOF
chmod +x .git/hooks/pre-commit

For teams, use pre-commit to manage hooks declaratively so every contributor gets the same check on clone:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.2
    hooks:
      - id: gitleaks

Custom patterns for MCP-specific secrets

Off-the-shelf gitleaks rulesets cover common provider patterns (GitHub PATs, AWS keys, Stripe keys) but won't know your MCP server's own API key format. Add custom rules in .gitleaks.toml:

# .gitleaks.toml
[extend]
  useDefault = true  # inherit the built-in 150+ patterns

[[rules]]
  id = "mcp-api-key"
  description = "SkillAudit / MCP server API key"
  regex = '''mcp_[a-zA-Z0-9]{32,64}'''
  tags = ["api-key", "mcp"]

[[rules]]
  id = "mcp-server-private-key"
  description = "MCP server signing key (base64 encoded)"
  regex = '''mcp_sk_[a-zA-Z0-9+/]{40,}={0,2}'''
  tags = ["signing-key", "mcp"]

# Allowlist for test fixtures that use obviously-fake keys
[allowlist]
  regexes = [
    '''mcp_test_[a-z]+_placeholder''',
    '''example_token_do_not_use''',
  ]

Layer 2: trufflehog in GitHub Actions CI

Pre-commit hooks can be skipped with --no-verify. The CI layer is the safety net that runs unconditionally on every push. Trufflehog is well-suited for CI: it scans not just the latest diff but the entire commit range being pushed, catching secrets committed in an intermediate commit that was later "cleaned" in a follow-up commit (the secret is still in history):

# .github/workflows/secrets-scan.yml
name: Secrets Scan

on:
  push:
    branches: ["**"]
  pull_request:

jobs:
  trufflehog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # full history for branch diff scan

      - name: Run TruffleHog on commit range
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --only-verified --fail

The --only-verified flag reduces false positives by only flagging secrets that trufflehog can verify are valid (live) credentials by making an API call to the issuing service. This is the correct default for CI — you want zero false positives blocking CI, and any unverified potential secret should be reviewed in a separate workflow that doesn't block merges.

Layer 3: git history scan on first setup

Before setting up secrets scanning, scan the entire git history of your MCP server repository to check whether any secrets have already been committed and should be revoked. This is a one-time operation:

# Scan full git history (can take minutes on large repos)
trufflehog git file://. --only-verified

# Or with gitleaks
gitleaks detect --source . --log-opts="--all"

If either tool finds a live credential in history: revoke it immediately, then rotate it in whatever service issued it. Removing it from the current branch is not enough — the commit is in history, and any clone of the repository has the secret. Revocation is the only effective remediation.

Emergency response: secret committed to main

If a secret reaches the main branch before the CI scan blocks it:

  1. Revoke immediately. Before anything else, invalidate the credential at the issuing service. Speed matters — bots index GitHub within seconds.
  2. Rotate. Issue a new credential. Do not re-use the revoked one.
  3. Remove from history using BFG or git filter-repo. This rewrites history — coordinate with all active contributors before force-pushing.
  4. Force-push after cleanup. All contributors must re-clone or reset to the new history.
  5. Post-mortem. Add a custom gitleaks rule for the credential format if one was missing.

What SkillAudit checks for

SkillAudit's credential exposure scan runs trufflehog-equivalent pattern matching against your source code at scan time. Findings include:

Check your MCP server repo for committed secrets

Run a free audit →