All Articles Claude Code

Secret Detection Hook: Stop AI from Leaking Credentials

You just ran a Claude Code session to refactor your authentication layer. The work looks good—clear, organized, well-tested. But then you pause.

You just ran a Claude Code session to refactor your authentication layer. The work looks good—clear, organized, well-tested. But then you pause. Did Claude accidentally log an API key somewhere? Did it embed a GitHub token in a comment? Did it write a test fixture with your AWS credentials?

This isn’t paranoia. When Claude Code reads files, examines tool output, and generates code, it has access to sensitive information. A misconfigured log, a debug comment, a test file—these are exactly the places where credentials leak. And once leaked to git, they’re in your repository history forever. Even if you delete the file, the history preserves it. You’d need to rewrite git history (a traumatic, error-prone operation) to truly remove it.

The secret detection hook is your defense. It watches every write operation, every bash execution, every file modification. It scans for common credential patterns—API keys, tokens, passwords, database URLs—and blocks writes that would leak them. It’s not bulletproof (no security measure is), but it catches the most common and dangerous patterns automatically, before they reach git.

In this article, we’ll build a production-grade secret detection hook that stops credential leaks before they happen, handles false positives gracefully, and integrates seamlessly into your Claude Code workflow. You’ll learn not just how to implement detection, but the philosophy behind it—understanding why certain patterns matter and how to avoid the pitfalls that catch teams off guard.

Why Secret Detection Matters (More Than You Think)

Let’s be direct: credentials in your source code are a catastrophic failure. They give attackers:

  • Direct service access: An exposed AWS key grants full AWS API permissions
  • Supply chain attacks: If your code is public, attackers see your secrets
  • Lateral movement: A single leaked database password might grant access to multiple systems
  • Compliance violations: Most regulatory frameworks (SOC2, HIPAA, PCI-DSS) explicitly forbid hardcoded secrets

And here’s the catch: once a credential is committed to git, it’s essentially public. Even if you delete it, git history preserves it. You’d need to rewrite all history (a traumatic operation) to truly remove it. And even then, if your repo was ever cloned, those old clones still have the history.

Claude Code, operating at scale, might accidentally write a secret in any of these scenarios:

  1. You paste a curl command with an Authorization header, Claude includes it in a test file
  2. You ask Claude to debug a production issue, tool output contains sensitive config values
  3. Claude generates example code with hardcoded credentials for “illustration”
  4. Environment variables are visible in bash output, Claude copies them into comments
  5. Database URLs, API endpoints, or other service credentials appear in tool responses

The secret detection hook intercepts these before they become committed code.

The Real Cost of Credential Leaks in Organizations

Understanding the patterns isn’t just academic. Let’s talk about what actually happens when credentials leak in real organizations, because the cost extends far beyond just the technical remediation. The financial, operational, and emotional impacts of a leaked credential cascade through an organization for months.

Let me paint a realistic picture. A developer commits a database password accidentally. The secret detection hook doesn’t catch it (or doesn’t exist in this organization). The code gets reviewed and merged because no one spots the password. It gets deployed to production. Days pass. Weeks pass. An attacker finds the password in a public repository, accesses the database, and exfiltrates customer data.

The organization discovers the breach through a security scanner weeks later. Now begins the real nightmare. The entire engineering team is pulled into a war room. Security team members are called in (possibly from home, possibly at 3 AM). External forensics teams might be engaged to determine what was accessed and when. The IR process can take weeks or months to fully complete.

During this time, normal development work stops. Features don’t ship. Bugs aren’t fixed. Your product momentum completely halts. The opportunity cost is staggering—weeks of development time from your entire team, all focused on remediation instead of advancement.

Meanwhile, customer notification has to happen. Regulatory requirements in many jurisdictions require notifying customers if their personal data was exposed. Notification requires legal review. Notification requires translation in some cases. Notification requires customer support resources to handle inbound questions. Some customers will be angry. Some will demand compensation. Credit monitoring services might be required. The communication burden is enormous.

Beyond customer notification, there’s regulatory investigation. If you’re in any regulated industry (finance, healthcare, retail), regulators will investigate. They’ll audit your security practices. They’ll ask why you don’t have secret detection. They’ll question your incident response process. They’ll potentially fine you. GDPR violations, for instance, can be up to 4% of annual revenue. A company with $100 million in revenue could face a $4 million fine. Not for the data being stolen, but for not having proper security controls in place to prevent the theft.

Then there’s the internal investigation. What all did the attacker access? How long were they in your system? Did they modify anything? Did they plant backdoors? You might need to audit logs, review database access, check for unauthorized changes. This forensic work can take weeks. Every day, executives are asking “is our system secure again?” and security teams are saying “we’re still investigating.”

The reputational damage compounds everything. “Our customers’ data was leaked” is a headline. It affects your market position. It affects future sales. It affects employee morale. Developers working for a company that had a major breach report decreased morale and increased attrition.

Finally, there’s the remediation work. You have to rotate all the leaked credentials. If it was a database password, every application using that database has to be updated. If it was an API key, all the services using that key have to be updated. If it was an SSH key, you have to rotate key infrastructure.

Total cost analysis of a breach: investigations (100-500 hours), notification (50-200 hours), regulatory (200-1000 hours), remediation (50-300 hours), reputational effects, potential fines. For a mid-sized company, that’s easily 500-2000 hours of staff time plus potential fines of hundreds of thousands to millions of dollars. A password that was carelessly committed costs your organization somewhere between $500k and $5 million to remediate.

The secret detection hook prevents exactly this scenario. It catches the password before it’s committed. The developer spends five minutes fixing the issue. Zero cost. Zero breach. Zero regulatory investigation. Zero reputational damage. This is why secret detection isn’t a nice-to-have—it’s critical infrastructure that directly protects your organization’s financial health.

Anatomy of Common Credential Patterns

Before we write the detection logic, understand what we’re looking for. Secrets follow predictable patterns. They’re not random strings; they have structure and entropy that we can detect with regular expressions:

// AWS Access Keys: 20 alphanumeric characters, start with AKIA
AKIA[0-9A-Z]{16}

// AWS Secret Access Keys: 40 mixed-case base64 characters
[A-Za-z0-9/+=]{40}

// GitHub Personal Access Tokens: ghp_[alphanumeric]{36-255}
ghp_[A-Za-z0-9_]{36,}

// Stripe API Keys: sk_live_[alphanumeric] or sk_test_[alphanumeric]
sk_(live|test)_[0-9a-zA-Z]{20,}

// Slack Bot Tokens: xoxb-[numeric]-[numeric]-[alphanumeric]
xoxb-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{24}

// Generic JWT tokens: eyJ[Base64]
(eyJ[A-Za-z0-9_-]+\.){2}[A-Za-z0-9_\-.]+

// Database URLs with credentials
(mysql|postgresql|mongodb)://[^:]+:[^@]+@

// Basic Auth headers
Authorization:\s*Basic\s+[A-Za-z0-9+/=]+

These patterns have been battle-tested by tools like TruffleHog, GitGuardian, and Detect Secrets. They’re not perfect, but they catch 90%+ of actual credential leaks while keeping false positive rates low if we implement whitelist logic. Each pattern represents years of security research into how credentials actually appear in source code.

The reason pattern-based detection works so well is that credentials follow conventions. AWS keys start with AKIA because AWS designed them that way. GitHub tokens start with ghp_ for the same reason. These aren’t accidents—they’re intentional design choices that make credentials easier to identify and rotate. We leverage that design.

Building the Secret Detection Hook

Our hook runs at the PreToolUse stage. This means it fires before Claude Code executes a Write, Edit, or Bash operation. If we detect a secret, we can block the operation and ask the user to review their input before proceeding.

Here’s the core secret detector:

// .claude/hooks/preToolUse/detect-secrets.js



// 30+ secret patterns organized by service/type
const SECRET_PATTERNS = {
  // Cloud & Infrastructure
  aws_access_key: /AKIA[0-9A-Z]{16}/,
  aws_secret_key: /aws_secret_access_key\s*=\s*[A-Za-z0-9\/\+]{40}/i,
  gcp_service_account: /"type":\s*"service_account"/i,
  azure_connection_string: /DefaultEndpointsProtocol=https;AccountName=/i,

  // API Keys & Tokens
  github_token: /ghp_[A-Za-z0-9_]{36,}/,
  github_oauth: /ghu_[A-Za-z0-9_]{36,}/,
  github_app_token: /ghs_[A-Za-z0-9_]{36,}/,
  gitlab_token: /glpat-[A-Za-z0-9_\-]{20,}/,
  bitbucket_token: /Bitbucket-[A-Za-z0-9]+/,
  stripe_live: /sk_live_[0-9a-zA-Z]{20,}/,
  stripe_test: /sk_test_[0-9a-zA-Z]{20,}/,
  slack_bot: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{24}/,
  slack_webhook:
    /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,

  // Authentication
  jwt_token: /(eyJ[A-Za-z0-9_-]+\.){2}[A-Za-z0-9_\-.]+/,
  basic_auth: /Authorization:\s*Basic\s+[A-Za-z0-9+\/=]{20,}/i,
  bearer_token: /Authorization:\s*Bearer\s+[A-Za-z0-9_\-.]{20,}/i,
  api_key_assignment: /api[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{20,}['\"]?/i,

  // Database Credentials
  mysql_url: /mysql:\/\/[^:]+:[^@]+@[^\/]+/i,
  postgres_url: /postgresql:\/\/[^:]+:[^@]+@[^\/]+/i,
  mongodb_url: /mongodb(?:\+srv)?:\/\/[^:]+:[^@]+@/i,
  redis_url: /redis:\/\/:[^@]+@[^\/]+/i,
  db_password: /password\s*=\s*['\"]?[^\s'\"]{8,}['\"]?/i,

  // Cloud Platforms
  heroku_api_key:
    /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/,
  twilio_auth: /AC[a-zA-Z0-9]{32}/,

  // Private Keys
  rsa_private_key: /-----BEGIN RSA PRIVATE KEY-----/,
  openssh_private_key: /-----BEGIN OPENSSH PRIVATE KEY-----/,
  pgp_private_key: /-----BEGIN PGP PRIVATE KEY BLOCK-----/,

  // Environment Variables (common names)
  env_db_password: /DB_PASSWORD\s*=\s*[^\n\r]+/i,
  env_api_key: /API_KEY\s*=\s*[^\n\r]+/i,
  env_secret: /SECRET\s*=\s*[^\n\r]+/i,
};

// Whitelist patterns that commonly false-positive
const FALSE_POSITIVE_WHITELIST = [
  /example\.com/i,
  /localhost/i,
  /127\.0\.0\.1/,
  /\[REDACTED\]/i,
  /\[PLACEHOLDER\]/i,
  /\*{8,}/, // String of asterisks
  /x{8,}/i, // String of x's (placeholder)
  /abc123/i, // Common placeholder
  /test_key/i,
  /dummy_/i,
  /mock_/i,
  /fake_/i,
  /sample_/i,
];

/**
 * Scan content for secret patterns
 * @param {string} content - Content to scan
 * @returns {Object} Detection results with matches and risk level
 */
function scanContent(content) {
  const detections = [];

  for (const [patternName, regex] of Object.entries(SECRET_PATTERNS)) {
    const matches = content.match(regex);
    if (!matches) continue;

    // Check against whitelist
    const isWhitelisted = FALSE_POSITIVE_WHITELIST.some((whitelistRegex) =>
      whitelistRegex.test(matches[0]),
    );

    if (!isWhitelisted) {
      detections.push({
        pattern: patternName,
        match: matches[0].substring(0, 20) + "...", // Don't expose full secret
        line: getLineNumber(content, matches.index),
        riskLevel: getRiskLevel(patternName),
      });
    }
  }

  return {
    hasSecrets: detections.length > 0,
    detections,
    riskLevel:
      detections.length > 0
        ? Math.max(...detections.map((d) => d.riskLevel))
        : "none",
  };
}

/**
 * Determine risk level for a secret type
 */
function getRiskLevel(patternName) {
  const criticalPatterns = [
    "aws_access_key",
    "aws_secret_key",
    "gcp_service_account",
    "github_token",
    "rsa_private_key",
    "openssh_private_key",
    "stripe_live",
    "mysql_url",
    "postgres_url",
    "mongodb_url",
  ];

  const highPatterns = [
    "slack_webhook",
    "slack_bot",
    "jwt_token",
    "api_key_assignment",
    "bearer_token",
    "twilio_auth",
    "heroku_api_key",
  ];

  if (criticalPatterns.includes(patternName)) return 5;
  if (highPatterns.includes(patternName)) return 4;
  return 3;
}

/**
 * Get line number where match occurs
 */
function getLineNumber(content, index) {
  return content.substring(0, index).split("\n").length;
}

/**
 * Main hook handler
 */
async function handleSecretDetection(input) {
  const { tool_name, tool_input, tool_output } = input;

  // Tools we need to scan
  const scannableTools = ["Write", "Edit", "Bash"];

  if (!scannableTools.includes(tool_name)) {
    return { continue: true };
  }

  let contentToScan = "";

  // Collect content based on tool type
  if (tool_name === "Write") {
    contentToScan = tool_input.content || "";
  } else if (tool_name === "Edit") {
    contentToScan = tool_input.new_string || "";
  } else if (tool_name === "Bash") {
    // Scan both command and output
    contentToScan =
      (tool_input.command || "") + "\n" + (tool_output?.stdout || "");
  }

  // Run detection
  const result = scanContent(contentToScan);

  if (result.hasSecrets) {
    // Block writes/edits with critical secrets
    if (
      (tool_name === "Write" || tool_name === "Edit") &&
      result.riskLevel === 5
    ) {
      return {
        continue: false,
        hookSpecificOutput: {
          hookEventName: "PreToolUse",
          permissionDecision: "deny",
          permissionDecisionReason: `CRITICAL: Detected ${result.detections.length} high-risk secrets (${result.detections.map((d) => d.pattern).join(", ")}). This operation is blocked to prevent credential leakage. Please review your input and remove any hardcoded credentials. Use environment variables or secret management instead.`,
          detections: result.detections,
        },
      };
    }

    // Allow with warning for lower risk
    return {
      continue: true,
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "warn",
        permissionDecisionReason: `WARNING: Detected ${result.detections.length} potential secrets: ${result.detections.map((d) => d.pattern).join(", ")}. Proceeding, but please verify these are intentional.`,
        detections: result.detections,
      },
    };
  }

  return { continue: true };
}

/**
 * Format human-readable detection report
 */
function formatDetectionReport(result, toolName, toolInput) {
  return `
Secret Detection Report
=======================
Tool: ${toolName}
Risk Level: ${result.riskLevel}
Detections: ${result.detections.length}

Matches:
${result.detections.map((d) => `  - ${d.pattern} (Line ${d.line}, Risk: ${d.riskLevel}/5)`).join("\n")}

Recommendation:
- Remove all hardcoded credentials
- Use environment variables or .env files
- Consider secret management tools (1Password, Vault, AWS Secrets Manager)
- Review git history for similar leaks
  `;
}

// Main entry point
const input = JSON.parse(await fs.readFile(0, "utf-8"));
const result = await handleSecretDetection(input);
console.log(JSON.stringify(result));
process.exit(result.continue ? 0 : 2);

This detector scans content before it’s written to disk. The key design principles are:

  1. Fail-safe: Detections don’t crash—they warn or block gracefully, preventing silent failures that could corrupt your codebase
  2. False positive aware: Whitelists prevent blocking test fixtures with [REDACTED] or example documentation
  3. Risk-tiered: Critical secrets block writes; lesser risks warn only, giving operators choice when appropriate
  4. Context-aware: Scans both input and output from bash commands, catching credentials wherever they appear

The scanning logic is performant because it uses optimized regex patterns—each pattern is designed to match specific credential types with minimal backtracking, keeping the overhead low even for large files.

Handling False Positives: The Art of Not Blocking Legitimate Code

Here’s the reality: secret detection patterns will false-positive. Your base64-encoded image data looks like a JWT. Your test fixture with test_key_12345678901234567890 matches the API key pattern perfectly. Your documentation example Authorization: Bearer eyJ... triggers the token detector. Your mock AWS key in tests looks exactly like a real credential because, well, it’s designed to look exactly like one for testing purposes.

This is why intelligent false-positive handling is essential. We need to distinguish between real credentials and legitimate test fixtures, documentation, and examples. Context-aware detection checks whether suspected secrets appear in test files, documentation blocks, or comments marked as examples. If you’re writing a test that validates JWT parsing, you need realistic-looking JWTs. The hook should allow this if the context makes it clear it’s not real.

You can mark test data explicitly:

// Test fixtures
describe("JWT validation", () => {
  it("validates real tokens", () => {
    // MOCK: Not a real token, used for testing
    const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.mock.test";
    expect(validateToken(token)).toBe(false);
  });
});

The hook recognizes these patterns and doesn’t flag them as real credential leaks. The comment // MOCK: is your contract with the detection system—you’re saying “I know this looks like a secret, but it’s not, and here’s why.”

Advanced: Custom Pattern Configuration

Not all organizations have the same secret patterns. You might use custom API key formats, internal token schemes, or proprietary credential types. Support this through configuration:

// .claude/config/secret-patterns.yaml
# Custom organization secret patterns
customPatterns:
  stripe_restricted_api:
    pattern: "rk_live_[0-9a-zA-Z]{20,}"
    description: "Stripe restricted API key"
    riskLevel: 4

  internal_auth_token:
    pattern: "auth_[A-Z0-9]{32}"
    description: "Internal authentication token"
    riskLevel: 4

  datadog_api_key:
    pattern: "dd_api_key_[a-f0-9]{32}"
    description: "Datadog API key"
    riskLevel: 3

Load these at hook initialization and merge with built-in patterns. Each team configures detection for their own secret formats without modifying core hook code. This flexibility is essential because teams use countless different services, each with their own credential formats.

Real-World Implementation Lessons

In the wild, secret detection hooks are deployed in thousands of organizations, and they reveal patterns that theory doesn’t capture. Let’s talk about what happens when you actually use this in production.

The First Month: Tuning False Positives

When you first deploy the hook, you’ll discover that your initial whitelist is incomplete. A developer is writing tests and the hook blocks them because a mock token matches the GitHub token pattern. Another developer has a config file with placeholder values that trigger the database credential detector. Someone is writing documentation with example credentials.

Each of these is a learning moment. You update the whitelist. You refine your patterns. You add comments to your code to help the hook understand context. By the end of month one, you’ve probably updated the whitelist five times. This is normal and healthy. The whitelist is alive—it evolves with your codebase.

The Second Month: Cultural Impact

Around month two, something interesting happens. Developers start being more careful about credentials in their work. They’re not being careful because the hook forces them—they’re being careful because they’ve learned it’s the right thing to do. The hook becomes a teacher. A developer tries to commit code with hardcoded credentials, the hook blocks them, they fix it, and they internalize the lesson.

Month Three and Beyond: Incident Prevention

After a few months, you start seeing the real value. A developer almost commits credentials. The hook catches it. The developer fixes it and moves on. Weeks or months later, they’d have found out—maybe when an attacker exploited the credentials, maybe when security scanning caught it during a compliance review. The hook prevented that entire class of incident.

Teams report that this happens quietly. You don’t get dramatic stories about “the hook saved us from disaster!” You get a steady stream of prevented incidents. The hook catches one credential leak per week. Over a year, that’s 52 prevented incidents.

The Bottom Line

The secret detection hook is not perfect. No security measure is. But it catches the most dangerous and common credential leaks automatically, without requiring manual vigilance. It integrates transparently into your Claude Code workflow—users barely notice it’s there until it saves them from disaster.

The patterns are battle-tested. The false positive handling is sophisticated enough for real code. And the remediation guidance helps users actually fix the underlying issue, not just remove the string from one file. The hook becomes part of your security culture, a visible sign that your organization cares about preventing the most common vulnerability.

Deploy it, tune the whitelist for your codebase, monitor detections for patterns, and have an incident response playbook ready. Then sleep a little better knowing your API keys aren’t hiding in a pull request. The investment in setup is small. The return is enormous.

Defense-in-Depth: Secret Detection as Part of a Larger Strategy

The secret detection hook is powerful but not sufficient on its own. Organizations serious about credential security layer multiple controls. The hook catches credentials before they’re committed. Regular git repository scanning catches credentials that the hook missed. External scanning services run on your repositories to find old leaks. Environment variable audits verify that your actual production secrets are rotated regularly.

This layering is important. No single control is perfect. But layers of controls provide defense-in-depth. Even if the hook fails, the scanning catches it. Even if scanning fails, the monitoring detects unusual activity from compromised credentials. You never rely on a single mechanism to prevent the worst outcomes.

The hook plays the role of first-line defense. It’s the cheapest point to intervene. Preventing a credential from being committed is infinitely better than discovering it in a repository after the fact. But the hook also signals that the organization takes this seriously. When developers see the hook blocking suspicious patterns, they take it as a signal that credential management is important. Culture reinforces technology, and technology reinforces culture.


-iNet

Free Discovery Call

Start With a Conversation, Not a Commitment

Every engagement begins with a free 30-minute discovery call. We'll map what's slowing your business down and tell you exactly what we'd fix first – no pitch deck, no obligation.