I built a local prompt scanner to catch secrets before they reach AI chats
Zero-Trust Clipboard Guard: Local Secret Detection for LLM Workflows
Current Situation Analysis
The adoption of Large Language Models (LLMs) for debugging has fundamentally altered developer workflows. Engineers routinely copy error logs, .env fragments, configuration files, and database connection strings directly into AI chat interfaces to accelerate troubleshooting. This behavior creates a high-risk "shadow exfiltration" vector where sensitive data bypasses traditional network security controls.
Traditional Data Loss Prevention (DLP) solutions typically monitor network egress or endpoint file access. They rarely inspect the clipboard-to-input-field transaction within a browser tab. Consequently, developers frequently paste AWS access keys, Stripe API tokens, signed S3 URLs, private key blocks, and credential-bearing database URIs into third-party AI models without awareness.
The scale of potential leakage is significant. Effective detection requires scanning for dozens of secret formats, including:
- Cloud provider credentials (AWS, Azure, GCP).
- Service tokens (GitHub, GitLab, npm, PyPI, Docker Hub, CI/CD runners).
- Payment and communication APIs (Stripe, Twilio, Resend, Postmark).
- Observability keys (Sentry, Datadog, New Relic).
- Webhook URLs (Slack, Discord, Telegram).
- Authorization headers, session cookies, and URL secret parameters.
- Private data patterns (emails, phone numbers, SSNs).
The core challenge is not merely detection; it is maintaining a workflow that is frictionless enough to be used but rigorous enough to prevent leaks. A scanner that generates excessive false positives will be disabled by developers, rendering the protection useless. Furthermore, any solution that transmits clipboard content to a remote server for analysis introduces a paradox: the act of scanning for secrets becomes the mechanism that leaks them.
WOW Moment: Key Findings
The critical insight in building a clipboard guard is that context-awareness is more valuable than raw pattern matching. A naive regex engine will flag UUIDs, documentation placeholders, and masked values as high-risk secrets, causing alert fatigue. A production-grade solution must suppress these false positives while maintaining zero data egress.
The following comparison highlights the operational difference between a naive approach and a context-aware local guard:
| Approach | False Positive Rate | Latency | Privacy Risk | Workflow Viability |
|---|---|---|---|---|
| Naive Regex Scanner | High (>40%) | Low (Local) | None | Low: Developers disable due to noise. |
| Remote ML Scanner | Medium (~15%) | High (Network) | Critical: Text sent to backend. | None: Violates zero-trust principle. |
| Context-Aware Local Guard | Low (<2%) | Instant (Local) | None | High: Accurate warnings, instant feedback. |
Why this matters:
The context-aware local guard achieves a false positive rate low enough to be trusted. It distinguishes between a real AWS key and a UUID-shaped trace ID. It recognizes that your_api_key_here is a placeholder, not a credential. It understands that postgres://localhost:5432/db is a bare URL, whereas postgres://user:pass@host/db contains embedded secrets. This precision allows the tool to operate silently until a genuine risk is detected, preserving developer flow while mitigating risk.
Core Solution
The architecture for a zero-trust clipboard guard relies on a client-side pattern matching engine with context validation layers. The implementation must run entirely within the browser environment (e.g., as a Web Extension or Web Worker) to ensure sensitive text never leaves the device.
Architecture Decisions
- Local-Only Execution: The scanner must not make network requests. All rule evaluation and redaction occur in memory.
- Rule-Based Engine: Deterministic pattern matching is preferred over probabilistic models for clipboard scanning. Rules provide predictable behavior, instant execution, and no dependency on external model availability.
- Context Filters: Rules are augmented with context validators that inspect surrounding text to suppress false positives (e.g., checking for placeholder keywords or masked characters).
- Redaction Capability: The engine should generate a sanitized version of the input text, replacing detected secrets with safe placeholders, allowing the developer to proceed with the AI query safely.
Implementation Example
The following TypeScript implementation demonstrates a SecretGuardEngine with a rule definition system, context validation, and redaction logic. This structure separates pattern detection from context analysis to maintain modularity.
// types.ts
export type SecretCategory = 'credential' | 'pii' | 'token' | 'url';
export type Severity = 'critical' | 'high' | 'medium';
export interface DetectionRule {
id: string;
pattern: RegExp;
category: SecretCategory;
severity: Severity;
contextValidator?: (match: string, context: string) => boolean;
}
export interface ScanResult {
ruleId: string;
category: SecretCategory;
severity: Severity;
match: string;
startIndex: number;
endIndex: number;
isFalsePositive: boolean;
}
// engine.ts
export class SecretGuardEngine {
private rules: DetectionRule[];
private piiEnabled: boolean;
constructor(rules: DetectionRule[], piiEnabled: boolean = false) {
this.rules = rules.filter(r => r.category !== 'pii' || piiEnabled);
}
public scan(text: string): ScanResult[] {
const results: ScanResult[] = [];
for (const rule of this.rules) {
const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
const context = text.substring(
Math.max(0, match.index - 50),
Math.min(text.length, match.index + match[0].length + 50)
);
const isFalsePositive = rule.contextValidator
? !rule.contextValidator(match[0], context)
: false;
results.push({
ruleId: rule.id,
category: rule.category,
severity: rule.severity,
match: match[0],
startIndex: match.index,
endIndex: match.index + match[0].length,
isFalsePositive,
});
}
}
return results;
}
public redact(text: string, results: ScanResult[]): string {
// Sort by start index descending to replace from end to start
const sortedResults = results
.filter(r => !r.isFalsePositive)
.sort((a, b) => b.startIndex - a.startIndex);
let redactedText = text;
for (const result of sortedResults) {
const replacement = `[REDACTED:${result.category.toUpperCase()}]`;
redactedText =
redactedText.substring(0, result.startIndex) +
replacement +
redactedText.substring(result.endIndex);
}
return redactedText;
}
}
Rule Configuration with Context Filters
The power of the engine lies in the rules. Below are examples of rules that handle common false positive scenarios identified in production usage.
// rules.ts
import { DetectionRule } from './types';
export const standardRules: DetectionRule[] = [
{
id: 'AWS_ACCESS_KEY',
pattern: /AKIA[0-9A-Z]{16}/,
category: 'credential',
severity: 'critical',
// Context check: Ensure not part of a masked string or placeholder
contextValidator: (match, context) => {
if (context.includes('********') || context.includes('xxxx')) return false;
if (context.match(/your_|example_|placeholder_/i)) return false;
return true;
},
},
{
id: 'DB_URL_WITH_CREDS',
pattern: /(postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/,
category: 'credential',
severity: 'high',
// Context check: Bare URLs without user:pass@ are safe
// The regex already enforces user:pass@, but we add a check for localhost-only
contextValidator: (match, context) => {
// If the URL points to localhost and uses default ports, it might be safe
// depending on policy. Here we flag it but allow context override.
if (match.includes('localhost') && match.includes('5432')) {
// Downgrade severity or suppress based on policy
return true; // Still flag, but could be handled by severity logic
}
return true;
},
},
{
id: 'UUID_TRACE_ID',
pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
category: 'token',
severity: 'medium',
// Context check: UUIDs are often trace IDs, not secrets
contextValidator: (match, context) => {
// If surrounded by "trace_id", "request_id", or "correlation_id", suppress
if (context.match(/(trace|request|correlation|session)_id/i)) return false;
// If the UUID is the only content, likely a test value
if (context.trim() === match) return false;
return true;
},
},
{
id: 'EMAIL_PII',
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/,
category: 'pii',
severity: 'medium',
contextValidator: () => true, // Always flag emails if PII enabled
},
];
Rationale
- Modular Rules: Separating
patternfromcontextValidatorallows the engine to remain generic while rules encapsulate domain-specific logic. - Context Window: The
contextValidatorreceives a snippet of text surrounding the match. This enables checks for placeholders (your_api_key_here), masked values (********), and semantic cues (e.g.,trace_id). - Redaction Safety: The redaction function processes matches in reverse order to preserve index integrity. It excludes false positives from redaction, ensuring the developer sees the original text for non-issues.
- PII Toggle: PII rules (emails, phones, SSNs) are filtered during initialization. This allows organizations to enforce credential scanning without flagging benign PII, reducing noise.
Pitfall Guide
Building a reliable secret scanner requires navigating numerous edge cases. The following pitfalls are derived from production experience with clipboard scanning tools.
The UUID Trap
- Explanation: UUIDs resemble hex-encoded secrets. A naive regex will flag trace IDs, request IDs, and session tokens as high-risk credentials.
- Fix: Implement context validators that check for semantic keywords like
trace_id,request_id, orcorrelation_id. Suppress UUIDs that appear in isolation or within known ID contexts.
Placeholder Noise
- Explanation: Developers often paste configuration templates containing
your_api_key_here,example_token, orINSERT_KEY. Flagging these causes immediate alert fatigue. - Fix: Add context checks for placeholder patterns. If the match is adjacent to keywords like
your_,example_,placeholder_, orchange_me, suppress the alert.
- Explanation: Developers often paste configuration templates containing
Bare URL False Alarms
- Explanation: Database connection strings like
postgres://localhost:5432/mydbare common in logs but do not contain secrets. Flagging them generates false positives. - Fix: Ensure the regex requires a credential component (e.g.,
user:pass@). A bare URL without embedded credentials should not trigger a credential alert.
- Explanation: Database connection strings like
The Remote Scanning Paradox
- Explanation: Sending clipboard text to a remote API for analysis defeats the purpose of the tool. If the text contains a secret, the scanner itself becomes the leak vector.
- Fix: Enforce local-only execution. The scanner must run in the browser or client application with no network calls. Store only configuration and license data remotely; never transmit scan content.
PII vs. Credential Conflation
- Explanation: Treating an email address with the same severity as an AWS root key leads to desensitization. Developers will ignore warnings if everything is "critical."
- Fix: Separate PII detection from credential detection. Use distinct severity levels and provide a toggle to enable/disable PII scanning based on policy.
Masked Value Detection
- Explanation: Logs often redact secrets with
********or***. A scanner might attempt to match the masked string or flag the presence of masking as suspicious. - Fix: Ignore matches that consist entirely of masking characters. Additionally, check context for masking patterns to suppress alerts on already-redacted values.
- Explanation: Logs often redact secrets with
The "Not DLP" Fallacy
- Explanation: Assuming the scanner catches 100% of secrets. Some providers use ambiguous token formats without stable prefixes. Context-dependent secrets may be missed.
- Fix: Document limitations clearly. The tool is a guardrail, not a guarantee. If a leak is suspected, rotation is still required. Focus on high-signal patterns and common formats.
Production Bundle
Action Checklist
- Define Rule Set: Curate a list of detection rules covering critical categories (Cloud, CI/CD, Payment, DB URLs). Start with high-signal patterns.
- Implement Context Filters: Add validators for placeholders, masked values, UUIDs, and bare URLs to suppress false positives.
- Enable Redaction: Build a redaction utility that generates a sanitized version of the input text for safe AI usage.
- Enforce Local-Only: Verify the scanner runs entirely client-side with no network requests. Audit the code for data exfiltration risks.
- Separate PII/Creds: Implement a toggle to distinguish between credential scanning and PII detection. Allow users to configure based on risk tolerance.
- Test Against Logs: Validate the scanner against real error logs,
.envfiles, and config snippets. Measure false positive rate and adjust rules. - Establish Rotation Protocol: Create a response plan for detected leaks. If a secret is found, mandate immediate rotation regardless of scanner confidence.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Clipboard Guard for AI | Local Regex + Context | Zero privacy risk, instant feedback, low FP rate. | Low (Client-side compute). |
| Log Analysis Pipeline | Remote ML/Regex | Can handle large volumes, complex context, and historical data. | Medium (Compute/Storage costs). |
| Enterprise DLP | Network/Endpoint DLP | Covers broad data paths, compliance requirements. | High (Infrastructure/Licensing). |
| Ambiguous Token Detection | Manual Review + Rotation | Regex/ML may miss context-dependent secrets. | Operational overhead. |
Configuration Template
Use this JSON structure to configure the SecretGuardEngine. This template includes rule definitions and global settings.
{
"version": "1.0",
"settings": {
"piiEnabled": false,
"maxScanLength": 50000,
"redactionMask": "[REDACTED]"
},
"rules": [
{
"id": "AWS_KEY",
"pattern": "AKIA[0-9A-Z]{16}",
"category": "credential",
"severity": "critical",
"contextFilters": ["placeholder", "masked"]
},
{
"id": "DB_URL",
"pattern": "(postgres|mysql|mongodb|redis):\\/\\/[^:]+:[^@]+@",
"category": "credential",
"severity": "high",
"contextFilters": ["bare_url"]
},
{
"id": "UUID",
"pattern": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
"category": "token",
"severity": "medium",
"contextFilters": ["trace_id", "isolated"]
}
]
}
Quick Start Guide
- Initialize Engine: Import
SecretGuardEngineand load rules from configuration.const engine = new SecretGuardEngine(standardRules, { piiEnabled: false }); - Bind to Input: Attach a listener to the clipboard paste event or input field
beforeinputevent in your application.inputElement.addEventListener('paste', async (event) => { const text = event.clipboardData.getData('text'); const results = engine.scan(text); // Handle results... }); - Handle Results: Check for non-false-positive results. If critical secrets are detected, block the paste or display a warning.
const leaks = results.filter(r => !r.isFalsePositive && r.severity === 'critical'); if (leaks.length > 0) { showWarning('Potential secret detected in clipboard.'); event.preventDefault(); } - Offer Redaction: Provide a button to redact the text and allow the user to proceed with the sanitized content.
const redactedText = engine.redact(text, results); navigator.clipboard.writeText(redactedText); - Verify Local Execution: Ensure the scanner code is bundled client-side and contains no network calls. Audit dependencies for telemetry.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
