Skip to content

Instantly share code, notes, and snippets.

@Richard-Weiss
Last active May 2, 2025 10:09
Show Gist options
  • Save Richard-Weiss/95f8bf90b55a3a41b4ae0ddd7a614942 to your computer and use it in GitHub Desktop.
Save Richard-Weiss/95f8bf90b55a3a41b4ae0ddd7a614942 to your computer and use it in GitHub Desktop.
Claude MCP auto approve
// 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
});
@maoyeedy
Copy link

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.

@dinhphieu
Copy link

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?

@Richard-Weiss
Copy link
Author

@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.

@Richard-Weiss
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment