# Detecting Which AI Chat Platform You're On: URL and DOM Patterns for ChatGPT, Claude, Gemini, and Copilot

> Source: <https://dev.to/ktg0215/detecting-which-ai-chat-platform-youre-on-url-and-dom-patterns-for-chatgpt-claude-gemini-and-3fkp>
> Published: 2026-06-30 16:25:30+00:00

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 `<rich-textarea>`

custom element was the most surprising — it wraps a Quill editor inside a shadow DOM subtree, which breaks standard querySelector behavior.
