Created
March 7, 2026 18:28
-
-
Save Xitee1/39d58234e1fc267d95dafffac24b2e1f to your computer and use it in GitHub Desktop.
Claude.ai Opus default
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Claude.ai – Default to Opus | |
| // @namespace https://github.com/xitee1/claude-opus-default | |
| // @version 1.1.0 | |
| // @description Automatically selects Claude Opus as the default model on claude.ai instead of Sonnet. Shows a warning banner when Sonnet is active. | |
| // @author Mato (via Claude) | |
| // @match https://claude.ai/* | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ── Configuration ────────────────────────────────────────────────── | |
| const CONFIG = { | |
| // Which model to auto-select (case-insensitive substring match) | |
| preferredModel: 'opus', | |
| // Auto-switch: true = click Opus automatically, false = only show warning | |
| autoSwitch: true, | |
| // Show a warning banner when the preferred model is NOT active | |
| showWarning: true, | |
| // How often to check (ms) — lightweight polling as fallback | |
| pollInterval: 2000, | |
| // Max attempts to auto-switch per page navigation | |
| maxAutoSwitchAttempts: 3, | |
| // Delay before first check after navigation (let React render) | |
| initialDelay: 800, | |
| // Debug logging | |
| debug: false, | |
| }; | |
| // ── State ────────────────────────────────────────────────────────── | |
| let warningBanner = null; | |
| let autoSwitchAttempts = 0; | |
| let lastUrl = location.href; | |
| let isProcessing = false; | |
| const log = (...args) => CONFIG.debug && console.log('[Opus Default]', ...args); | |
| // ── DOM Helpers ──────────────────────────────────────────────────── | |
| /** | |
| * Find the model selector button. | |
| * Claude.ai uses a button/trigger near the chat input or top bar that | |
| * displays the current model name. We search by text content patterns | |
| * rather than class names since React hashes those. | |
| */ | |
| function findModelSelectorButton() { | |
| // Strategy 1: Look for buttons containing known model names | |
| const modelNames = ['sonnet', 'opus', 'haiku']; | |
| const buttons = document.querySelectorAll('button, [role="button"]'); | |
| for (const btn of buttons) { | |
| const text = btn.textContent?.toLowerCase().trim() || ''; | |
| // The model selector typically shows just the model name or "Claude X Sonnet/Opus" | |
| // Exclude very long text (those are likely chat messages, not selectors) | |
| if (text.length > 80) continue; | |
| for (const name of modelNames) { | |
| if (text.includes(name)) { | |
| // Verify it looks like a selector (small-ish element, not a chat bubble) | |
| const rect = btn.getBoundingClientRect(); | |
| if (rect.width > 0 && rect.width < 400 && rect.height < 80) { | |
| log('Found model selector button:', text, btn); | |
| return { button: btn, currentModel: text }; | |
| } | |
| } | |
| } | |
| } | |
| // Strategy 2: Look for data-testid or aria-label patterns | |
| const testIdBtn = document.querySelector( | |
| '[data-testid*="model"], [aria-label*="model" i], [aria-label*="Model" i]' | |
| ); | |
| if (testIdBtn) { | |
| log('Found model selector via data-testid/aria-label:', testIdBtn); | |
| return { button: testIdBtn, currentModel: testIdBtn.textContent?.toLowerCase() || '' }; | |
| } | |
| return null; | |
| } | |
| /** | |
| * After clicking the model selector, find and click the preferred model | |
| * in the dropdown/popover that appears. | |
| */ | |
| function findAndClickPreferredModel() { | |
| // Look in popovers, dropdowns, listboxes, menus | |
| const containers = document.querySelectorAll( | |
| '[role="listbox"], [role="menu"], [role="dialog"], [data-radix-popper-content-wrapper], [data-state="open"]' | |
| ); | |
| // Also check generic divs that might be dropdowns (React portals) | |
| const allClickables = document.querySelectorAll( | |
| '[role="option"], [role="menuitem"], [role="menuitemradio"], li, div[class]' | |
| ); | |
| for (const el of allClickables) { | |
| const text = el.textContent?.toLowerCase().trim() || ''; | |
| if (text.length > 100) continue; | |
| if (text.includes(CONFIG.preferredModel.toLowerCase())) { | |
| // Make sure it's visible and in a dropdown-like context | |
| const rect = el.getBoundingClientRect(); | |
| if (rect.width > 0 && rect.height > 0) { | |
| log('Clicking preferred model option:', text, el); | |
| el.click(); | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // ── Warning Banner ───────────────────────────────────────────────── | |
| function createWarningBanner() { | |
| if (warningBanner) return warningBanner; | |
| warningBanner = document.createElement('div'); | |
| warningBanner.id = 'opus-default-warning'; | |
| warningBanner.innerHTML = ` | |
| <span style="margin-right:8px;">⚠️</span> | |
| <strong>Achtung:</strong> Aktuelles Modell ist nicht Opus! | |
| <button id="opus-switch-btn" style=" | |
| margin-left: 12px; | |
| padding: 2px 10px; | |
| border-radius: 4px; | |
| border: 1px solid rgba(255,255,255,0.5); | |
| background: rgba(255,255,255,0.15); | |
| color: white; | |
| cursor: pointer; | |
| font-size: 13px; | |
| ">Zu Opus wechseln</button> | |
| <button id="opus-dismiss-btn" style=" | |
| margin-left: 6px; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| border: none; | |
| background: transparent; | |
| color: rgba(255,255,255,0.7); | |
| cursor: pointer; | |
| font-size: 18px; | |
| line-height: 1; | |
| ">×</button> | |
| `; | |
| Object.assign(warningBanner.style, { | |
| position: 'fixed', | |
| top: '0', | |
| left: '0', | |
| right: '0', | |
| zIndex: '99999', | |
| background: 'linear-gradient(135deg, #d97706, #b45309)', | |
| color: 'white', | |
| padding: '8px 16px', | |
| fontSize: '14px', | |
| fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| boxShadow: '0 2px 8px rgba(0,0,0,0.3)', | |
| transition: 'transform 0.3s ease', | |
| transform: 'translateY(-100%)', | |
| }); | |
| document.body.appendChild(warningBanner); | |
| // "Zu Opus wechseln" button | |
| document.getElementById('opus-switch-btn')?.addEventListener('click', () => { | |
| autoSwitchAttempts = 0; | |
| attemptAutoSwitch(); | |
| }); | |
| // Dismiss button | |
| document.getElementById('opus-dismiss-btn')?.addEventListener('click', () => { | |
| hideWarning(); | |
| }); | |
| return warningBanner; | |
| } | |
| function showWarning() { | |
| if (!CONFIG.showWarning) return; | |
| const banner = createWarningBanner(); | |
| banner.style.transform = 'translateY(0)'; | |
| } | |
| function hideWarning() { | |
| if (warningBanner) { | |
| warningBanner.style.transform = 'translateY(-100%)'; | |
| } | |
| } | |
| // ── Auto-Switch Logic ────────────────────────────────────────────── | |
| async function attemptAutoSwitch() { | |
| if (isProcessing) return; | |
| isProcessing = true; | |
| try { | |
| const selector = findModelSelectorButton(); | |
| if (!selector) { | |
| log('Model selector not found'); | |
| isProcessing = false; | |
| return; | |
| } | |
| // Already on preferred model? | |
| if (selector.currentModel.includes(CONFIG.preferredModel.toLowerCase())) { | |
| log('Already on preferred model'); | |
| hideWarning(); | |
| isProcessing = false; | |
| return; | |
| } | |
| log('Current model is not preferred:', selector.currentModel); | |
| if (CONFIG.autoSwitch && autoSwitchAttempts < CONFIG.maxAutoSwitchAttempts) { | |
| autoSwitchAttempts++; | |
| log(`Auto-switch attempt ${autoSwitchAttempts}/${CONFIG.maxAutoSwitchAttempts}`); | |
| // Click the model selector to open dropdown | |
| selector.button.click(); | |
| // Wait for dropdown to render | |
| await sleep(400); | |
| // Find and click the preferred model | |
| const switched = findAndClickPreferredModel(); | |
| if (switched) { | |
| log('Successfully switched to preferred model'); | |
| hideWarning(); | |
| await sleep(300); | |
| // Verify the switch worked | |
| const verify = findModelSelectorButton(); | |
| if (verify && verify.currentModel.includes(CONFIG.preferredModel.toLowerCase())) { | |
| log('Switch verified'); | |
| hideWarning(); | |
| } else { | |
| log('Switch may not have worked, showing warning'); | |
| showWarning(); | |
| } | |
| } else { | |
| log('Could not find preferred model in dropdown, closing'); | |
| // Press Escape to close the dropdown | |
| document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); | |
| showWarning(); | |
| } | |
| } else { | |
| showWarning(); | |
| } | |
| } catch (e) { | |
| log('Error during auto-switch:', e); | |
| showWarning(); | |
| } | |
| isProcessing = false; | |
| } | |
| // ── Main Check ───────────────────────────────────────────────────── | |
| function checkModel() { | |
| const selector = findModelSelectorButton(); | |
| if (!selector) return; | |
| if (selector.currentModel.includes(CONFIG.preferredModel.toLowerCase())) { | |
| hideWarning(); | |
| } else { | |
| if (CONFIG.autoSwitch && autoSwitchAttempts < CONFIG.maxAutoSwitchAttempts) { | |
| attemptAutoSwitch(); | |
| } else { | |
| showWarning(); | |
| } | |
| } | |
| } | |
| // ── URL Change Detection (SPA navigation) ────────────────────────── | |
| function onUrlChange() { | |
| log('URL changed to:', location.href); | |
| autoSwitchAttempts = 0; // Reset attempts on navigation | |
| setTimeout(checkModel, CONFIG.initialDelay); | |
| } | |
| // Detect SPA navigation via History API | |
| const origPushState = history.pushState; | |
| history.pushState = function (...args) { | |
| origPushState.apply(this, args); | |
| if (location.href !== lastUrl) { | |
| lastUrl = location.href; | |
| onUrlChange(); | |
| } | |
| }; | |
| const origReplaceState = history.replaceState; | |
| history.replaceState = function (...args) { | |
| origReplaceState.apply(this, args); | |
| if (location.href !== lastUrl) { | |
| lastUrl = location.href; | |
| onUrlChange(); | |
| } | |
| }; | |
| window.addEventListener('popstate', () => { | |
| if (location.href !== lastUrl) { | |
| lastUrl = location.href; | |
| onUrlChange(); | |
| } | |
| }); | |
| // ── Utilities ────────────────────────────────────────────────────── | |
| function sleep(ms) { | |
| return new Promise((r) => setTimeout(r, ms)); | |
| } | |
| // ── Init ─────────────────────────────────────────────────────────── | |
| function init() { | |
| log('Script loaded, initial delay before first check...'); | |
| // Initial check after React has rendered | |
| setTimeout(() => { | |
| checkModel(); | |
| // Periodic polling as fallback (lightweight — just reads DOM text) | |
| setInterval(() => { | |
| // Only poll on chat pages, not settings etc. | |
| if (location.pathname.startsWith('/chat') || location.pathname === '/') { | |
| const selector = findModelSelectorButton(); | |
| if (selector && !selector.currentModel.includes(CONFIG.preferredModel.toLowerCase())) { | |
| if (autoSwitchAttempts >= CONFIG.maxAutoSwitchAttempts) { | |
| showWarning(); | |
| } else if (CONFIG.autoSwitch) { | |
| attemptAutoSwitch(); | |
| } | |
| } else if (selector) { | |
| hideWarning(); | |
| } | |
| } | |
| }, CONFIG.pollInterval); | |
| }, CONFIG.initialDelay); | |
| } | |
| init(); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick and dirty single prompt Tampermonkey userscript created by Claude Opus to always select Opus as default model because Anthropic wants to squeeze every penny from their customers and always selects Sonnet as default