Detecting Which AI Chat Platform You're On: URL and DOM Patterns for ChatGPT, Claude, Gemini, and Copilot A developer created a detection system for identifying which AI chat platform a user is on, using URL hostname matching as a primary check and DOM signatures as a fallback. The system covers ChatGPT, Claude, Gemini, and Copilot, with a majority-vote approach to reduce false positives. It also addresses SPA loading timing by recommending document_idle injection and MutationObserver for dynamic state changes. If you're building a Chrome extension that works with AI chat platforms — prompt injection, session export, UI customization — you need to know which platform the user is on before you do anything. A content script that injects a prompt into ChatGPT's textarea will silently fail on Claude, which has a completely different DOM. Here's how to reliably detect the platform from a content script, with patterns that hold up through UI redesigns. The fastest check: location.hostname . AI platforms use distinct domains with predictable structures: js const AI PLATFORM HOSTS = { 'chatgpt.com': 'chatgpt', 'chat.openai.com': 'chatgpt', // legacy domain 'claude.ai': 'claude', 'gemini.google.com': 'gemini', 'copilot.microsoft.com': 'copilot', 'bard.google.com': 'gemini', // legacy redirect 'poe.com': 'poe', 'character.ai': 'characterai', }; function detectPlatformByHost { return AI PLATFORM HOSTS location.hostname ?? null; } This covers the common case: user is on claude.ai , hostname matches, platform identified. No DOM access needed. Edge cases this misses: For those, fall through to DOM-based detection. Each platform has distinctive DOM signatures that are more stable than class names or element IDs which change frequently . Look for structural patterns, not implementation details: js const PLATFORM SIGNATURES = { platform: 'chatgpt', checks: = document.querySelector ' data-testid="send-button" ' , = document.querySelector ' prompt-textarea' , = document.title.includes 'ChatGPT' , , }, { platform: 'claude', checks: = document.querySelector ' data-placeholder ="Reply" ' , = document.querySelector '.claude-message' , = document.title.toLowerCase .includes 'claude' , , }, { platform: 'gemini', checks: = document.querySelector 'rich-textarea' , // custom element = document.title.includes 'Gemini' , , }, { platform: 'copilot', checks: = document.querySelector ' data-testid="composer-input" ' , = location.hostname.includes 'copilot.microsoft.com' , , }, ; function detectPlatformByDOM { for const sig of PLATFORM SIGNATURES { const passedChecks = sig.checks.filter fn = { try { return fn ; } catch { return false; } } ; if passedChecks.length = 2 return sig.platform; // majority vote } return null; } The majority vote requiring at least 2 checks to pass reduces false positives when only one signature element happens to be present on an unrelated page. Wrapping each check in try/catch handles DOM state issues — some checks might throw if the element doesn't exist in the way you expect. js function detectPlatform { // Tier 1: URL-based instant const byHost = detectPlatformByHost ; if byHost return byHost; // Tier 2: DOM-based for edge cases const byDOM = detectPlatformByDOM ; return byDOM; } In practice, tier 1 handles 95%+ of real usage. Tier 2 is the safety net. AI chat platforms are SPAs. The initial HTML is usually a shell; the actual UI loads asynchronously. Running detection immediately on document start will fail — the DOM isn't built yet. Options: document idle injection simplest : // manifest.json "content scripts": { "matches": "https://chatgpt.com/ ", "https://claude.ai/ ", "..." , "js": "content.js" , "run at": "document idle" } document idle fires after DOMContentLoaded and after any deferred scripts run, but before images load. For most AI platforms this is sufficient — the UI is rendered by then. MutationObserver for SPAs: If the platform changes state after initial load navigating between conversations, switching models , you need to react to DOM changes: js const observer = new MutationObserver = { const platform = detectPlatform ; if platform && platform == currentPlatform { currentPlatform = platform; onPlatformChanged platform ; } } ; observer.observe document.body, { childList: true, subtree: false } ; subtree: false is intentional here — you only need to detect major layout changes, not every DOM mutation inside the conversation thread. Once you know the platform, you need the textarea to inject a prompt: js const INPUT SELECTORS = { chatgpt: ' prompt-textarea', 'div contenteditable="true" data-id="root" ', , claude: 'div contenteditable="true" .ProseMirror', ' data-placeholder ="Reply to Claude" ', , gemini: 'rich-textarea .ql-editor', 'div contenteditable="true" .textarea', , copilot: 'textarea data-testid="composer-input" ', 'cib-text-input textarea', , }; function getInputElement platform { const selectors = INPUT SELECTORS platform ?? ; for const selector of selectors { const el = document.querySelector selector ; if el return el; } return null; } ContentEditable divs used by Claude and Gemini need different injection code than standard textareas: function injectPrompt element, text { if element.tagName === 'TEXTAREA' { // Standard textarea element.value = text; element.dispatchEvent new Event 'input', { bubbles: true } ; } else if element.contentEditable === 'true' { // ContentEditable ProseMirror, Quill, etc. element.focus ; document.execCommand 'selectAll', false, null ; document.execCommand 'insertText', false, text ; } } document.execCommand is technically deprecated but remains the most reliable way to trigger React/Vue reactivity on contentEditable elements. The synthetic event approach new InputEvent 'input', { data: text, bubbles: true } doesn't consistently trigger framework state updates. ChatGPT and Claude both navigate without page reloads new conversation = SPA navigation, not a full reload . Your detection logic needs to handle this: js let currentPlatform = detectPlatform ; let lastUrl = location.href; const navObserver = new MutationObserver = { if location.href == lastUrl { lastUrl = location.href; // Re-run detection after navigation setTimeout = { currentPlatform = detectPlatform ; }, 500 ; // brief delay for new route to render } } ; navObserver.observe document, { subtree: true, childList: true } ; This is the detection layer under PromptStash https://chromewebstore.google.com/detail/promptstash/ocgkponbnolpgobllplcamfobolbjbcj — a Chrome extension for saving and reusing prompts across AI platforms. One shortcut inserts your saved prompt wherever you are. What platform-specific quirks have you run into? The Gemini