on June 30, 2026 at 9:25 am Detecting Which AI Chat Platform You’re On: URL and DOM Patterns for ChatGPT, Claude, Gemini, and Copilot​SHOTA

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.


Matching by URL (Tier 1: Fast and Reliable)

The fastest check: location.hostname. AI platforms use distinct domains with predictable structures:

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;

Enter fullscreen modeExit fullscreen mode

This covers the common case: user is on claude.ai, hostname matches, platform identified. No DOM access needed.

Edge cases this misses:

  • Embedded interfaces (ChatGPT embedded in other products)
  • API playgrounds that aren’t the primary chat UI
  • White-label deployments running on custom domains

For those, fall through to DOM-based detection.

DOM-Based Detection (Tier 2: Layout Signatures)

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:

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;

Enter fullscreen modeExit fullscreen mode

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.

Combining Both Approaches

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;

Enter fullscreen modeExit fullscreen mode

In practice, tier 1 handles 95%+ of real usage. Tier 2 is the safety net.

Timing: When to Run Detection

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"]
Enter fullscreen modeExit fullscreen mode

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:

const observer = new MutationObserver(() => 
  const platform = detectPlatform();
  if (platform && platform !== currentPlatform) 
    currentPlatform = platform;
    onPlatformChanged(platform);
  
);
observer.observe(document.body,  childList: true, subtree: false );
Enter fullscreen modeExit fullscreen mode

subtree: false is intentional here — you only need to detect major layout changes, not every DOM mutation inside the conversation thread.

Finding the Input Textarea

Once you know the platform, you need the textarea to inject a prompt:

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;

Enter fullscreen modeExit fullscreen mode

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);
  

Enter fullscreen modeExit fullscreen mode

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.

Handling Page Navigation in SPAs

ChatGPT and Claude both navigate without page reloads (new conversation = SPA navigation, not a full reload). Your detection logic needs to handle this:

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 );
Enter fullscreen modeExit fullscreen mode

This is the detection layer under PromptStash — 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 <rich-textarea> custom element was the most surprising — it wraps a Quill editor inside a shadow DOM subtree, which breaks standard querySelector behavior.

 

Leave a Reply

Your email address will not be published. Required fields are marked *