<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Evyn John</title>
    <description>The latest articles on DEV Community by Evyn John (@heynatively).</description>
    <link>https://dev.to/heynatively</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4241841%2Fe5428526-f4ff-4089-aa22-c14ab1d874f2.jpg</url>
      <title>DEV Community: Evyn John</title>
      <link>https://dev.to/heynatively</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heynatively"/>
    <language>en</language>
    <item>
      <title>Hiding an Electron window from screen share: 4 things setContentProtection doesn't tell you</title>
      <dc:creator>Evyn John</dc:creator>
      <pubDate>Thu, 12 Nov 2026 22:11:34 +0000</pubDate>
      <link>https://dev.to/heynatively/hiding-an-electron-window-from-screen-share-4-things-setcontentprotection-doesnt-tell-you-525e</link>
      <guid>https://dev.to/heynatively/hiding-an-electron-window-from-screen-share-4-things-setcontentprotection-doesnt-tell-you-525e</guid>
      <description>&lt;p&gt;&lt;a href="https://heynatively.com" rel="noopener noreferrer"&gt;Natively&lt;/a&gt; is a desktop AI assistant for meetings and interviews. It listens to the call, transcribes it, and shows answers and notes in a small overlay. People use it while they share their screen, so the overlay has an obvious requirement: when the user turns on Undetectable mode, the overlay must not appear in the share or the recording.&lt;/p&gt;

&lt;p&gt;Electron gives you one line for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS it sets the window's &lt;code&gt;sharingType&lt;/code&gt; to &lt;code&gt;NSWindowSharingNone&lt;/code&gt;. On Windows it calls &lt;code&gt;SetWindowDisplayAffinity&lt;/code&gt; with &lt;code&gt;WDA_EXCLUDEFROMCAPTURE&lt;/code&gt; (Windows 10 2004 and later). That line gets you most of the way. Here is what it took to get the rest, from code that ships in Natively today.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Apply it to every window, not just the one you think of
&lt;/h2&gt;

&lt;p&gt;An "overlay" in a real app is several windows: the answer panel, a pill, a toggle, a popover catcher, sometimes a launcher. If one of them is missed, that one shows up in the share.&lt;/p&gt;

&lt;p&gt;We keep a single method that walks every live window and pushes the same value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;applyContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launcherWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlayWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pillWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toggleWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;popoverCatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDestroyed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any window created later (settings, model picker, screenshot cropper) reads the current state when it is created, so a window opened mid-meeting doesn't slip through.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dedupe the calls on Windows
&lt;/h2&gt;

&lt;p&gt;Our setter was called from several places: the settings toggle, every overlay show, and a workaround around Win+Tab. On Windows, setting the same display affinity again and again churns DWM, and the window can go black or blank for a few hundred milliseconds. So the public setter does nothing when the value hasn't changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;setContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentProtection&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentProtection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. On macOS, re-assert after you touch the Dock
&lt;/h2&gt;

&lt;p&gt;This one cost us the most time. Undetectable mode also hides the app from the Dock with &lt;code&gt;app.dock.hide()&lt;/code&gt;. That flips the app's activation policy, and WindowServer re-evaluates every window. It can quietly reset &lt;code&gt;sharingType&lt;/code&gt; to the default, which means the overlay is visible in the share again. Your in-memory flag still says &lt;code&gt;true&lt;/code&gt;, so the deduped setter above would skip the call.&lt;/p&gt;

&lt;p&gt;The fix is a second method that skips the dedupe and pushes the current value to the OS again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;reassertContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyContentProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentProtection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it after every &lt;code&gt;app.dock.hide()&lt;/code&gt; / &lt;code&gt;app.dock.show()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Windows has a taskbar, and the taskbar is not the window
&lt;/h2&gt;

&lt;p&gt;On macOS, hiding from the Dock removes the app's visible presence. On Windows, a window that is excluded from capture can still have a taskbar button with the app's name on it, and the taskbar is part of a full-screen share. Most of our windows are created with &lt;code&gt;skipTaskbar: true&lt;/code&gt;. The launcher is the exception because in normal mode it is the main window, so we sync it with the mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;syncLauncherTaskbarForStealth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'win32'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launcherWindow&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSkipTaskbar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUndetectable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The macOS path doesn't use this at all. Forcing &lt;code&gt;skipTaskbar&lt;/code&gt; there would change behaviour on a platform that handles it through the Dock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the capture paths you actually care about
&lt;/h2&gt;

&lt;p&gt;Content protection is a request to the OS compositor, and different capture APIs respect it differently. Modern APIs (ScreenCaptureKit on macOS, Windows Graphics Capture) honour it. Some older capture paths may not. So we test with the apps people really share from (Zoom, Google Meet in Chrome, Teams, OBS, the system screenshot tool) on both platforms, with the mode on and off, and we treat "it was hidden on my machine" as one data point, not proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this lives
&lt;/h2&gt;

&lt;p&gt;All of this is in Natively's source on GitHub: &lt;a href="https://github.com/Natively-AI-assistant/natively-cluely-ai-assistant" rel="noopener noreferrer"&gt;Natively-AI-assistant/natively-cluely-ai-assistant&lt;/a&gt;, in &lt;code&gt;electron/WindowHelper.ts&lt;/code&gt;. Natively runs on macOS and Windows and is free for personal use. You can bring your own AI keys or run models locally with Ollama. If you're building something with an always-on-top overlay, I hope this saves you an afternoon.&lt;/p&gt;

&lt;p&gt;Download and docs: &lt;a href="https://heynatively.com" rel="noopener noreferrer"&gt;heynatively.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://heynatively.com/docs/zoom-screen-share" rel="noopener noreferrer"&gt;How to keep Natively hidden in a Zoom screen share&lt;/a&gt;: the user-side settings that go with the code above.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://heynatively.com/blog/is-ai-interview-assistant-detectable" rel="noopener noreferrer"&gt;Can an AI interview assistant be detected?&lt;/a&gt;: a longer look at what other people on the call can and can't see.&lt;/li&gt;
&lt;li&gt;Installing Natively on &lt;a href="https://heynatively.com/docs/install-macos" rel="noopener noreferrer"&gt;macOS&lt;/a&gt; and &lt;a href="https://heynatively.com/docs/install-windows" rel="noopener noreferrer"&gt;Windows&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://heynatively.com/local-ai-assistant" rel="noopener noreferrer"&gt;Running a local AI assistant&lt;/a&gt; with your own keys or Ollama.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>electron</category>
      <category>macos</category>
      <category>windows</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
