Shipped: Agent Identity & Human Trust Layer
WebDecoy now verifies AI agents with Web Bot Auth (RFC 9421), catches agent impersonation, and grades human trust on clearance tokens. Everything that shipped.
Every day, millions of users browse the web with AI assistants riding shotgun. Claude’s extension reads your screen. ChatGPT’s plugin intercepts your content. Copilot parses your code. These tools inject themselves into the DOM, wrap native APIs, and register custom elements—all to provide “helpful” AI features.
For security teams, this is a visibility problem. You don’t know who’s reading your content, what’s being exfiltrated to AI providers, or whether that “user” is a human browsing with an AI assistant or an AI agent puppeting a browser.
This post is a technical deep dive into detecting AI browser extensions. We’ll cover the fingerprints they leave behind, the techniques that work, and the detection code you can deploy today.
The AI browser extension market exploded in 2024. Every major AI company now ships browser plugins:
| Extension | Users (Est.) | Capabilities |
|---|---|---|
| ChatGPT for Chrome | 8M+ | Page summarization, content generation |
| Claude | 1M+ | Screen reading, context injection |
| GitHub Copilot | 15M+ | Code analysis, suggestions |
| Gemini (Google) | 5M+ | Search enhancement, summarization |
| Perplexity | 2M+ | Research, citation generation |
| Monica AI | 3M+ | Multi-model access, content tools |
These extensions don’t just passively observe. They actively modify the page, intercept network requests, and inject their UI components. The browser becomes a shared environment between your application and AI systems you never invited.
The security implications are significant:
Detection isn’t about blocking legitimate users. It’s about visibility into who—and what—is accessing your application.
AI extensions inject themselves into pages through predictable patterns. Understanding these patterns is the foundation of detection.
Extensions inject visible UI components (sidebars, floating buttons, context menus) and invisible scaffolding (observers, listeners, state containers).
Claude’s injection pattern:
<!-- Sidebar container -->
<div class="claude-sidebar" data-claude-root="true">
<claude-assistant>
<shadow-root>
<!-- Isolated UI components -->
</shadow-root>
</claude-assistant>
</div>
<!-- Content markers -->
<span data-claude-selected="true" class="claude-highlight">
Selected text for context
</span>ChatGPT’s injection pattern:
<!-- Floating action button -->
<div id="chatgpt-extension-root" data-chatgpt="true">
<div class="chatgpt-fab">
<!-- Trigger button -->
</div>
</div>
<!-- Content wrapper -->
<div data-chatgpt-wrapper="true">
<!-- Page content with observers -->
</div>Extensions need to maintain state across the page. They expose global variables, often namespaced to avoid conflicts:
// Claude
window.__claude__
window.__claude_extension__
window.__anthropic__
// ChatGPT
window.__chatgpt__
window.__openai__
window.__chatgpt_extension_state__
// Copilot
window.__github_copilot__
window.__copilot__
// Gemini
window.__google_ai__
window.__gemini_extension__
// Generic patterns
window.__ai_assistant__
window.__llm_extension__Modern extensions use Web Components with custom element names:
// Extensions register custom elements
customElements.define('claude-assistant', ClaudeAssistant);
customElements.define('chatgpt-sidebar', ChatGPTSidebar);
customElements.define('copilot-suggestions', CopilotSuggestions);
customElements.define('gemini-panel', GeminiPanel);
customElements.define('perplexity-widget', PerplexityWidget);Extensions intercept native browser APIs to observe network requests and DOM mutations:
// Fetch API wrapping
const originalFetch = window.fetch;
window.fetch = async function(...args) {
// Extension intercepts request
extensionObserver.logRequest(args);
return originalFetch.apply(this, args);
};
// XHR wrapping
const originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new originalXHR();
// Extension wraps methods
const originalOpen = xhr.open;
xhr.open = function(...args) {
extensionObserver.logRequest(args);
return originalOpen.apply(this, args);
};
return xhr;
};
// MutationObserver wrapping
const OriginalMutationObserver = window.MutationObserver;
window.MutationObserver = function(callback) {
// Extension intercepts DOM changes
const wrappedCallback = (mutations) => {
extensionObserver.logMutations(mutations);
callback(mutations);
};
return new OriginalMutationObserver(wrappedCallback);
};Scan for known injection patterns using attribute and class selectors:
class AIExtensionDetector {
constructor() {
this.detections = [];
this.patterns = {
claude: {
selectors: [
'[class*="claude"]',
'[data-claude]',
'[id*="claude"]',
'claude-assistant',
'claude-sidebar',
'[data-anthropic]'
],
weight: 15
},
chatgpt: {
selectors: [
'[class*="chatgpt"]',
'[data-chatgpt]',
'[id*="chatgpt"]',
'[data-openai]',
'chatgpt-sidebar',
'chatgpt-extension-root'
],
weight: 15
},
copilot: {
selectors: [
'[class*="copilot"]',
'[data-copilot]',
'[data-github-copilot]',
'copilot-suggestions',
'[id*="copilot"]'
],
weight: 15
},
gemini: {
selectors: [
'[class*="gemini"]',
'[data-gemini]',
'[data-google-ai]',
'gemini-panel',
'[id*="gemini-ext"]'
],
weight: 15
},
perplexity: {
selectors: [
'[class*="perplexity"]',
'[data-perplexity]',
'perplexity-widget',
'[id*="perplexity"]'
],
weight: 15
},
monica: {
selectors: [
'[class*="monica"]',
'[data-monica]',
'monica-ai',
'[id*="monica"]'
],
weight: 10
}
};
}
scanDOM() {
for (const [extension, config] of Object.entries(this.patterns)) {
for (const selector of config.selectors) {
try {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
this.detections.push({
type: 'dom_pattern',
extension,
selector,
count: elements.length,
weight: config.weight
});
}
} catch (e) {
// Invalid selector, skip
}
}
}
return this.detections;
}
}Check for extension-specific global variables:
detectGlobalVariables() {
const signatures = {
// Claude / Anthropic
'__claude__': { extension: 'claude', weight: 15 },
'__claude_extension__': { extension: 'claude', weight: 15 },
'__anthropic__': { extension: 'claude', weight: 15 },
// ChatGPT / OpenAI
'__chatgpt__': { extension: 'chatgpt', weight: 15 },
'__openai__': { extension: 'chatgpt', weight: 15 },
'__chatgpt_extension_state__': { extension: 'chatgpt', weight: 15 },
// GitHub Copilot
'__github_copilot__': { extension: 'copilot', weight: 15 },
'__copilot__': { extension: 'copilot', weight: 15 },
// Google Gemini
'__google_ai__': { extension: 'gemini', weight: 15 },
'__gemini_extension__': { extension: 'gemini', weight: 15 },
// Perplexity
'__perplexity__': { extension: 'perplexity', weight: 15 },
// Monica AI
'__monica__': { extension: 'monica', weight: 10 },
'__monica_ai__': { extension: 'monica', weight: 10 },
// Generic AI patterns
'__ai_assistant__': { extension: 'unknown_ai', weight: 10 },
'__llm_extension__': { extension: 'unknown_ai', weight: 10 }
};
for (const [variable, config] of Object.entries(signatures)) {
if (typeof window[variable] !== 'undefined') {
this.detections.push({
type: 'global_variable',
extension: config.extension,
variable,
weight: config.weight
});
}
}
}Modern extensions register custom HTML elements. Inspect the registry:
detectCustomElements() {
const suspiciousPatterns = [
// Primary AI assistants
{ pattern: /^claude-/, extension: 'claude', weight: 15 },
{ pattern: /^chatgpt-/, extension: 'chatgpt', weight: 15 },
{ pattern: /^copilot-/, extension: 'copilot', weight: 15 },
{ pattern: /^gemini-/, extension: 'gemini', weight: 15 },
{ pattern: /^perplexity-/, extension: 'perplexity', weight: 15 },
// Secondary AI tools
{ pattern: /^monica-/, extension: 'monica', weight: 10 },
{ pattern: /^jasper-/, extension: 'jasper', weight: 10 },
{ pattern: /^writesonic-/, extension: 'writesonic', weight: 10 },
// Generic AI patterns
{ pattern: /^ai-assistant/, extension: 'unknown_ai', weight: 10 },
{ pattern: /^llm-/, extension: 'unknown_ai', weight: 10 }
];
// Get all custom elements in the document
const allElements = document.querySelectorAll('*');
const customTags = new Set();
allElements.forEach(el => {
const tagName = el.tagName.toLowerCase();
// Custom elements must contain a hyphen
if (tagName.includes('-')) {
customTags.add(tagName);
}
});
// Check against suspicious patterns
for (const tag of customTags) {
for (const { pattern, extension, weight } of suspiciousPatterns) {
if (pattern.test(tag)) {
this.detections.push({
type: 'custom_element',
extension,
element: tag,
weight
});
}
}
}
}Extensions use Shadow DOM to isolate their UI. Inspect shadow roots:
detectShadowDOM() {
const elementsWithShadow = [];
// Walk the DOM tree
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
null,
false
);
while (walker.nextNode()) {
const node = walker.currentNode;
if (node.shadowRoot) {
elementsWithShadow.push({
element: node,
tagName: node.tagName.toLowerCase(),
shadowRoot: node.shadowRoot
});
}
}
// Analyze shadow roots for AI extension patterns
for (const { element, tagName, shadowRoot } of elementsWithShadow) {
const shadowHTML = shadowRoot.innerHTML || '';
// Check for AI-related content in shadow DOM
const aiPatterns = [
{ regex: /claude/i, extension: 'claude' },
{ regex: /chatgpt|openai/i, extension: 'chatgpt' },
{ regex: /copilot/i, extension: 'copilot' },
{ regex: /gemini/i, extension: 'gemini' },
{ regex: /perplexity/i, extension: 'perplexity' },
{ regex: /ai-assist|llm-/i, extension: 'unknown_ai' }
];
for (const { regex, extension } of aiPatterns) {
if (regex.test(shadowHTML) || regex.test(tagName)) {
this.detections.push({
type: 'shadow_dom',
extension,
element: tagName,
weight: 15
});
}
}
}
}Detect when native APIs have been wrapped by extensions:
detectAPIWrappers() {
const wrapperSignals = [];
// Check if fetch has been wrapped
const fetchString = window.fetch.toString();
if (!fetchString.includes('[native code]')) {
wrapperSignals.push({
api: 'fetch',
reason: 'Modified fetch implementation',
weight: 5
});
}
// Check if XMLHttpRequest has been wrapped
const xhrString = window.XMLHttpRequest.toString();
if (!xhrString.includes('[native code]')) {
wrapperSignals.push({
api: 'XMLHttpRequest',
reason: 'Modified XHR implementation',
weight: 5
});
}
// Check if MutationObserver has been wrapped
const moString = window.MutationObserver.toString();
if (!moString.includes('[native code]')) {
wrapperSignals.push({
api: 'MutationObserver',
reason: 'Modified MutationObserver',
weight: 5
});
}
// Check for modified prototype methods
const prototypeChecks = [
{ obj: Element.prototype, method: 'appendChild' },
{ obj: Element.prototype, method: 'insertBefore' },
{ obj: Document.prototype, method: 'createElement' },
{ obj: EventTarget.prototype, method: 'addEventListener' }
];
for (const { obj, method } of prototypeChecks) {
const methodString = obj[method].toString();
if (!methodString.includes('[native code]')) {
wrapperSignals.push({
api: `${obj.constructor.name}.${method}`,
reason: 'Modified prototype method',
weight: 3
});
}
}
// Add detections
for (const signal of wrapperSignals) {
this.detections.push({
type: 'api_wrapper',
...signal
});
}
}Here’s a production-ready detector combining all techniques:
class AIExtensionDetector {
constructor(options = {}) {
this.detections = [];
this.score = 0;
this.threshold = options.threshold || 30;
this.onDetection = options.onDetection || console.log;
}
async analyze() {
// Run all detection methods
this.scanDOM();
this.detectGlobalVariables();
this.detectCustomElements();
this.detectShadowDOM();
this.detectAPIWrappers();
// Calculate total score
this.score = this.detections.reduce((sum, d) => sum + d.weight, 0);
// Deduplicate and summarize by extension
const byExtension = {};
for (const detection of this.detections) {
const ext = detection.extension;
if (!byExtension[ext]) {
byExtension[ext] = {
extension: ext,
signals: [],
totalWeight: 0
};
}
byExtension[ext].signals.push(detection);
byExtension[ext].totalWeight += detection.weight;
}
const result = {
detected: this.score >= this.threshold,
score: Math.min(this.score, 100), // Cap at 100
extensions: Object.values(byExtension),
signals: this.detections,
timestamp: new Date().toISOString()
};
if (result.detected) {
this.onDetection(result);
}
return result;
}
// Include all detection methods from above...
scanDOM() { /* ... */ }
detectGlobalVariables() { /* ... */ }
detectCustomElements() { /* ... */ }
detectShadowDOM() { /* ... */ }
detectAPIWrappers() { /* ... */ }
}
// Usage
const detector = new AIExtensionDetector({
threshold: 30,
onDetection: (result) => {
// Send to your analytics/security backend
fetch('/api/security/ai-detection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
});
}
});
// Run detection
detector.analyze().then(result => {
if (result.detected) {
console.log('AI extensions detected:', result.extensions);
}
});WebDecoy uses a weighted scoring system for AI extension detection:
| Signal Type | Weight | Description |
|---|---|---|
| Primary AI Extension DOM | +15 | Claude, ChatGPT, Copilot, Gemini, Perplexity |
| Secondary AI Tool DOM | +10 | Monica, Jasper, other AI assistants |
| Global Variable (named) | +15 | window.__claude__, window.__chatgpt__, etc. |
| Custom Element (AI pattern) | +15 | <claude-assistant>, <chatgpt-sidebar> |
| Shadow DOM AI content | +15 | AI-related content in shadow roots |
| Wrapped Fetch API | +5 | Non-native fetch implementation |
| Wrapped XHR | +5 | Non-native XMLHttpRequest |
| Modified MutationObserver | +5 | Non-native MutationObserver |
| Modified prototype method | +3 | Wrapped DOM methods |
Score interpretation:
The score is capped at 100 and integrated with other threat signals (webdriver detection, headless browser detection, behavioral analysis) into a unified threat score.
A user with the Claude browser extension visits your application:
// Detection results
{
detected: true,
score: 45,
extensions: [{
extension: 'claude',
signals: [
{ type: 'dom_pattern', selector: '[data-claude]' },
{ type: 'global_variable', variable: '__claude__' },
{ type: 'custom_element', element: 'claude-assistant' }
],
totalWeight: 45
}]
}What this tells you: The user has Claude’s extension active. Their page content may be sent to Anthropic’s servers for context. Not necessarily malicious, but important for data governance.
A power user with ChatGPT, Copilot, and Monica installed:
// Detection results
{
detected: true,
score: 85,
extensions: [
{ extension: 'chatgpt', totalWeight: 30 },
{ extension: 'copilot', totalWeight: 30 },
{ extension: 'monica', totalWeight: 25 }
]
}What this tells you: This user has significant AI tooling active. Every page they visit is potentially being analyzed by three different AI systems. Consider whether this user should access sensitive content.
Detection finds wrapped APIs but no specific extension:
// Detection results
{
detected: false,
score: 15,
extensions: [{
extension: 'unknown',
signals: [
{ type: 'api_wrapper', api: 'fetch', weight: 5 },
{ type: 'api_wrapper', api: 'MutationObserver', weight: 5 },
{ type: 'api_wrapper', api: 'Element.appendChild', weight: 3 }
],
totalWeight: 15
}]
}What this tells you: Something is wrapping browser APIs, but it’s below the detection threshold. Could be an AI extension you don’t have signatures for, a browser dev tool, or a legitimate extension (password manager, ad blocker). Worth monitoring but not blocking.
Client-side detection should be validated server-side:
# Python/Flask example
@app.route('/api/security/ai-detection', methods=['POST'])
def log_ai_detection():
data = request.json
client_ip = request.remote_addr
user_agent = request.headers.get('User-Agent', '')
# Log detection event
detection_record = {
'timestamp': datetime.utcnow(),
'ip': client_ip,
'user_agent': user_agent,
'score': data.get('score', 0),
'extensions': data.get('extensions', []),
'session_id': session.get('id')
}
db.ai_detections.insert(detection_record)
# Correlate with other signals
session_signals = get_session_signals(session.get('id'))
# AI extension + headless browser = likely bot
if data['score'] > 30 and session_signals.get('headless_score', 0) > 50:
flag_suspicious_session(session.get('id'))
# AI extension + unusual request patterns = data scraping
if data['score'] > 30 and session_signals.get('request_rate', 0) > 100:
flag_potential_scraper(session.get('id'))
return jsonify({'status': 'logged'})AI extension detection raises privacy questions. Be thoughtful about:
Detection should inform your security posture, not create a surveillance system.
Sophisticated extensions will evolve to avoid detection:
claude-sidebar becomes xK7mN2pQ[native code] for wrapped APIsHow to stay ahead:
AI browser extensions are everywhere. Your users have them. Your competitors use them. And increasingly, AI agents use browser automation to interact with your application through these same patterns.
Detection isn’t about blocking users who want AI assistance. It’s about visibility:
The techniques in this post give you that visibility. DOM scanning catches injected elements. Global variable detection finds extension state. Custom element inspection reveals Web Components. Shadow DOM analysis uncovers isolated UI. API wrapper detection shows when native functions are intercepted.
Combined with headless browser detection and behavioral analysis, you can build a complete picture of who—and what—is accessing your application.
The AI extension landscape is evolving rapidly. New extensions ship monthly. Existing ones update weekly. Detection must be continuous, not one-time.
WebDecoy provides real-time AI extension detection as part of our bot detection platform. We maintain signatures for all major AI extensions, detect unknown extensions through behavioral patterns, and integrate with your existing security infrastructure.
Want to detect AI extensions in your application? Explore WebDecoy’s detection capabilities or contact our team for a demo.
WebDecoy now verifies AI agents with Web Bot Auth (RFC 9421), catches agent impersonation, and grades human trust on clearance tokens. Everything that shipped.
WebDecoy is now live on the WordPress.org plugin directory. One-click install, automatic updates, and 100% local bot protection with no API key required.
Google signs crawler traffic with Web Bot Auth (RFC 9421). What signed bots mean for detection, verified-bot allowlists, and how WebDecoy verifies them.