-
-
Save Richard-Weiss/95f8bf90b55a3a41b4ae0ddd7a614942 to your computer and use it in GitHub Desktop.
// Trusted servers, all tools are allowed by default | |
const trustedServers = [ | |
'file-system-windows-python', | |
]; | |
// Tools to explicitly allow | |
const trustedTools = [ | |
'google_search' | |
]; | |
// Tools to explictly block, useful with server combination | |
const blockedTools = [ | |
'write-file' | |
]; | |
// Cooldown tracking | |
let lastClickTime = 0; | |
const COOLDOWN_MS = 1000; // 1 second cooldown | |
// Log throttling | |
const logHistory = {}; | |
const LOG_THROTTLE_MS = 5000; // Only log the same message every 5 seconds | |
// Smart logging with throttling | |
function throttledLog(level, message, ...args) { | |
const key = message + JSON.stringify(args); | |
const now = Date.now(); | |
if (logHistory[key] && now - logHistory[key] < LOG_THROTTLE_MS) { | |
return; | |
} | |
logHistory[key] = now; | |
console[level](message, ...args); | |
} | |
const log = { | |
debug: (message, ...args) => throttledLog('debug', `[AutoApprove] ${message}`, ...args), | |
log: (message, ...args) => throttledLog('log', `[AutoApprove] ${message}`, ...args), | |
warn: (message, ...args) => throttledLog('warn', `[AutoApprove] ${message}`, ...args), | |
error: (message, ...args) => throttledLog('error', `[AutoApprove] ${message}`, ...args) | |
}; | |
const observer = new MutationObserver((mutations) => { | |
// Check if we're still in cooldown | |
const now = Date.now(); | |
if (now - lastClickTime < COOLDOWN_MS) { | |
log.debug('π Still in cooldown period, skipping...'); | |
return; | |
} | |
// Find the dialog using its role attribute | |
const dialog = document.querySelector('[role="dialog"][data-state="open"]'); | |
if (!dialog) { | |
log.debug('Dialog not found'); | |
return; | |
} | |
log.debug('π Dialog found, checking content...'); | |
// --- FIXED SELECTOR LOGIC --- | |
let toolName = null; | |
let serverName = null; | |
// Look for the specific element structure in the dialog | |
// First, find the flex column container that holds both tool and server info | |
const flexColumn = dialog.querySelector('.flex.flex-col.py-1'); | |
if (flexColumn) { | |
// Extract tool name (looking for the font-mono element within the flex column) | |
const toolElement = flexColumn.querySelector('.font-mono'); | |
if (toolElement && toolElement.textContent) { | |
toolName = toolElement.textContent.trim(); | |
log.log('π οΈ Tool name extracted:', toolName); | |
} else { | |
log.warn('β οΈ Tool name element not found or empty.'); | |
} | |
// Extract server name (looking for the font-medium element within the flex column) | |
const serverElement = flexColumn.querySelector('.font-medium'); | |
if (serverElement && serverElement.textContent) { | |
serverName = serverElement.textContent.trim(); | |
log.log('π Server name extracted:', serverName); | |
} else { | |
log.warn('β οΈ Server name element not found or empty.'); | |
} | |
} else { | |
log.warn('β οΈ Could not find the flex column container for tool and server info.'); | |
} | |
// If neither name could be extracted, we can't make a decision. | |
if (!toolName && !serverName) { | |
log.error('β Both tool and server name extraction failed. Cannot proceed.'); | |
return; | |
} else if (!toolName) { | |
log.warn('β οΈ Tool name extraction failed, decision based on server name only.'); | |
} else if (!serverName) { | |
log.warn('β οΈ Server name extraction failed, decision based on tool name only.'); | |
} | |
// Decision logic (prioritizing server, then tool) | |
let shouldApprove = false; | |
if (serverName && trustedServers.includes(serverName)) { | |
// Server is trusted | |
if (toolName && blockedTools.includes(toolName)) { | |
log.log(`π« Tool '${toolName}' is explicitly blocked for trusted server '${serverName}'. Declining.`); | |
shouldApprove = false; | |
} else { | |
log.log(`β Server '${serverName}' is trusted. Approving (Tool: ${toolName || 'N/A'})`); | |
shouldApprove = true; | |
} | |
} else if (toolName && trustedTools.includes(toolName)) { | |
// Server isn't trusted (or not found), but tool is allowed | |
log.log(`β Tool '${toolName}' is explicitly allowed. Approving (Server: ${serverName || 'N/A'})`); | |
shouldApprove = true; | |
} else { | |
log.log(`β Approval criteria not met (Server: ${serverName || 'N/A'}, Tool: ${toolName || 'N/A'})`); | |
shouldApprove = false; | |
} | |
// Find and click "Allow always" button if approval is decided | |
if (shouldApprove) { | |
// Looking for the button with exact text "Allow always" | |
const buttons = Array.from(dialog.querySelectorAll('button')); | |
const allowAlwaysButton = buttons.find(button => | |
button.textContent && button.textContent.trim() === 'Allow always' | |
); | |
if (allowAlwaysButton) { | |
log.log('π Auto-approving request...'); | |
lastClickTime = now; // Set cooldown *before* clicking | |
allowAlwaysButton.click(); | |
log.log('π±οΈ Clicked "Allow always" button.'); | |
} else { | |
log.error("β οΈ Could not find the 'Allow always' button!"); | |
} | |
} else { | |
log.log("β Request not approved based on rules."); | |
} | |
}); | |
// Start observing the body for changes | |
console.log('[AutoApprove] π Starting observer...'); | |
console.log('[AutoApprove] Trusted Servers:', trustedServers); | |
console.log('[AutoApprove] Trusted Tools:', trustedTools); | |
console.log('[AutoApprove] Blocked Tools:', blockedTools); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); |
I've created an MCP which injects a JS like this into Claude: https://github.com/PyneSys/claude_autoapprove_mcp
It is very easy to use, just insert the MCP config into Claude app config and it just works. After starting Claude, it loads the MCP, then restarts in injectable mode. It is much-much easier to work with Claude with this MCP, I think. I don't want to approve all save commands, just the unsafe ones.
Thanks for the tool! It makes the inject even more convenient.
Great work, I've tried this for the past week but today it seems like it doesn't work anymore with today's new UI update?
@dinhphieu They've changed that whole approval dialog, I'll need some time to to update and test it, would ping you when it's working again.
@dinhphieu Should work again now, just gave Gemini 2.5 Pro the last script and new HTML and did some small manual adjustments and testing, Claude 3.7 Sonnet changed too much stuff for me one shot.
I've created an MCP which injects a JS like this into Claude: https://github.com/PyneSys/claude_autoapprove_mcp
It is very easy to use, just insert the MCP config into Claude app config and it just works. After starting Claude, it loads the MCP, then restarts in injectable mode. It is much-much easier to work with Claude with this MCP, I think. I don't want to approve all safe commands, just the unsafe ones.