macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

Description

@radroid

Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

Diagnosis

Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

$ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
Identifier=Electron # <- not com.t3tools.t3code
CodeDirectory flags=0x20002(adhoc,linker-signed)
Signature=adhoc
Info.plist=not bound # <- signature does not cover our Info.plist
TeamIdentifier=not set
Sealed Resources=none

Compare upstream's official build, installed alongside:

$ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
Identifier=com.t3tools.t3code
CodeDirectory flags=0x10000(runtime)
Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
$ spctl -a -vvv ... -> accepted, source=Notarized Developer ID

TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

Two things that are not the cause, so we do not chase them:

  • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
  • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

  1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
  2. Mark it Always Trust for code signing.
  3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
  4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

Trade-offs, stated plainly:

  • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
  • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
  • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

Acceptance

Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

Notes

Related but distinct: #41 (autobuild relaunch race).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

      Description

      @radroid

      Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

      Diagnosis

      Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

      $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
      Identifier=Electron # <- not com.t3tools.t3code
      CodeDirectory flags=0x20002(adhoc,linker-signed)
      Signature=adhoc
      Info.plist=not bound # <- signature does not cover our Info.plist
      TeamIdentifier=not set
      Sealed Resources=none
      

      Compare upstream's official build, installed alongside:

      $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
      Identifier=com.t3tools.t3code
      CodeDirectory flags=0x10000(runtime)
      Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
      $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
      

      TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

      Two things that are not the cause, so we do not chase them:

      • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
      • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

      Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

      We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

      1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
      2. Mark it Always Trust for code signing.
      3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
      4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

      The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

      Trade-offs, stated plainly:

      • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
      • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
      • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

      Acceptance

      Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

      Notes

      Related but distinct: #41 (autobuild relaunch race).

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

          Description

          @radroid

          Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

          Diagnosis

          Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

          $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
          Identifier=Electron # <- not com.t3tools.t3code
          CodeDirectory flags=0x20002(adhoc,linker-signed)
          Signature=adhoc
          Info.plist=not bound # <- signature does not cover our Info.plist
          TeamIdentifier=not set
          Sealed Resources=none
          

          Compare upstream's official build, installed alongside:

          $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
          Identifier=com.t3tools.t3code
          CodeDirectory flags=0x10000(runtime)
          Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
          $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
          

          TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

          Two things that are not the cause, so we do not chase them:

          • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
          • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

          Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

          We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

          1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
          2. Mark it Always Trust for code signing.
          3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
          4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

          The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

          Trade-offs, stated plainly:

          • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
          • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
          • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

          Acceptance

          Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

          Notes

          Related but distinct: #41 (autobuild relaunch race).

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

              Description

              @radroid

              Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

              Diagnosis

              Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

              $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
              Identifier=Electron # <- not com.t3tools.t3code
              CodeDirectory flags=0x20002(adhoc,linker-signed)
              Signature=adhoc
              Info.plist=not bound # <- signature does not cover our Info.plist
              TeamIdentifier=not set
              Sealed Resources=none
              

              Compare upstream's official build, installed alongside:

              $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
              Identifier=com.t3tools.t3code
              CodeDirectory flags=0x10000(runtime)
              Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
              $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
              

              TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

              Two things that are not the cause, so we do not chase them:

              • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
              • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

              Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

              We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

              1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
              2. Mark it Always Trust for code signing.
              3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
              4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

              The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

              Trade-offs, stated plainly:

              • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
              • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
              • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

              Acceptance

              Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

              Notes

              Related but distinct: #41 (autobuild relaunch race).

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

                  Description

                  @radroid

                  Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

                  Diagnosis

                  Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

                  $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
                  Identifier=Electron # <- not com.t3tools.t3code
                  CodeDirectory flags=0x20002(adhoc,linker-signed)
                  Signature=adhoc
                  Info.plist=not bound # <- signature does not cover our Info.plist
                  TeamIdentifier=not set
                  Sealed Resources=none
                  

                  Compare upstream's official build, installed alongside:

                  $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
                  Identifier=com.t3tools.t3code
                  CodeDirectory flags=0x10000(runtime)
                  Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
                  $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
                  

                  TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

                  Two things that are not the cause, so we do not chase them:

                  • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
                  • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

                  Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

                  We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

                  1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
                  2. Mark it Always Trust for code signing.
                  3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
                  4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

                  The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

                  Trade-offs, stated plainly:

                  • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
                  • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
                  • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

                  Acceptance

                  Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

                  Notes

                  Related but distinct: #41 (autobuild relaunch race).

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

                      Description

                      @radroid

                      Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

                      Diagnosis

                      Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

                      $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
                      Identifier=Electron # <- not com.t3tools.t3code
                      CodeDirectory flags=0x20002(adhoc,linker-signed)
                      Signature=adhoc
                      Info.plist=not bound # <- signature does not cover our Info.plist
                      TeamIdentifier=not set
                      Sealed Resources=none
                      

                      Compare upstream's official build, installed alongside:

                      $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
                      Identifier=com.t3tools.t3code
                      CodeDirectory flags=0x10000(runtime)
                      Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
                      $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
                      

                      TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

                      Two things that are not the cause, so we do not chase them:

                      • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
                      • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

                      Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

                      We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

                      1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
                      2. Mark it Always Trust for code signing.
                      3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
                      4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

                      The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

                      Trade-offs, stated plainly:

                      • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
                      • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
                      • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

                      Acceptance

                      Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

                      Notes

                      Related but distinct: #41 (autobuild relaunch race).

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

                          Description

                          @radroid

                          Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

                          Diagnosis

                          Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

                          $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
                          Identifier=Electron # <- not com.t3tools.t3code
                          CodeDirectory flags=0x20002(adhoc,linker-signed)
                          Signature=adhoc
                          Info.plist=not bound # <- signature does not cover our Info.plist
                          TeamIdentifier=not set
                          Sealed Resources=none
                          

                          Compare upstream's official build, installed alongside:

                          $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
                          Identifier=com.t3tools.t3code
                          CodeDirectory flags=0x10000(runtime)
                          Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
                          $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
                          

                          TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

                          Two things that are not the cause, so we do not chase them:

                          • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
                          • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

                          Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

                          We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

                          1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
                          2. Mark it Always Trust for code signing.
                          3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
                          4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

                          The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

                          Trade-offs, stated plainly:

                          • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
                          • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
                          • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

                          Acceptance

                          Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

                          Notes

                          Related but distinct: #41 (autobuild relaunch race).

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              macOS re-prompts for every permission after each update, because the fork build is ad-hoc signed with a new identity every time #70

                              Description

                              @radroid

                              Every time the app updates itself, macOS asks for all its permissions again. This is not the updater misbehaving — it is a code-signing problem, and it is fixable for $0.

                              Diagnosis

                              Our build is completely unsigned. scripts/build-desktop-artifact.ts:1996 sets CSC_IDENTITY_AUTO_DISCOVERY=false when --signed is not passed, so electron-builder ships the stock prebuilt Electron binary with its original linker signature untouched:

                              $ codesign -dv --verbose=4 "/Applications/T3 Code (Alpha).app"
                              Identifier=Electron # <- not com.t3tools.t3code
                              CodeDirectory flags=0x20002(adhoc,linker-signed)
                              Signature=adhoc
                              Info.plist=not bound # <- signature does not cover our Info.plist
                              TeamIdentifier=not set
                              Sealed Resources=none
                              

                              Compare upstream's official build, installed alongside:

                              $ codesign -dv --verbose=4 "/Applications/T3 Code (Nightly).app"
                              Identifier=com.t3tools.t3code
                              CodeDirectory flags=0x10000(runtime)
                              Authority=Developer ID Application: T3 Tools, Inc. (ARK85ZXQ4Z)
                              $ spctl -a -vvv ... -> accepted, source=Notarized Developer ID
                              

                              TCC (the permissions database) keys each grant to the app's designated requirement, derived from its code signature. A signed app's DR is stable — bundle ID plus signing cert — so grants survive updates. An ad-hoc bundle has no cert and no sealed resources, so the DR degrades to the binary's cdhash, which is different on every single build. Each update is therefore a brand-new app as far as macOS is concerned, and every Screen Recording / Accessibility / Microphone / Files-and-Folders / Local Network grant is re-requested from scratch.

                              Two things that are not the cause, so we do not chase them:

                              • Quarantine / Gatekeeper. Already handled — installCommands.ts:62 and :78 strip com.apple.quarantine from the dmg and recursively from the staged app. The installed bundle carries only com.apple.provenance.
                              • Over-broad usage descriptions.Info.plist does declare camera, microphone, Bluetooth, and audio-capture strings we may not need, but those only prompt on first use of the feature, not on update. Worth trimming separately, not the fix here.

                              Proposed fix: a self-signed code-signing certificate ($0, no Developer Program)

                              We do not need the $99 Developer Program to get a stable identity — only to get a trusted one. A self-signed cert gives us stability, which is the whole problem.

                              1. Keychain Access -> Certificate Assistant -> Create a Certificate: self-signed, type Code Signing. Name it something like T3X Local Build.
                              2. Mark it Always Trust for code signing.
                              3. Teach build-desktop-artifact.ts a third signing mode between --signed and unsigned — call it local/self-signed — that sets CSC_NAME to that cert instead of forcing CSC_IDENTITY_AUTO_DISCOVERY=false, and enables hardened runtime with the existing entitlements.
                              4. Point the autobuild pipeline (the t3code-build worktree watcher) at that mode.

                              The resulting designated requirement is identifier "com.t3tools.t3code" and certificate leaf = <our cert> — identical across every rebuild, so TCC grants persist. The user grants each permission once, ever.

                              Trade-offs, stated plainly:

                              • The app still is not notarized, so a freshly downloaded copy would hit the unidentified-developer wall. Irrelevant to the update path, which already de-quarantines.
                              • The cert lives in one Mac's keychain. Anyone else installing this fork would see the prompts again. Acceptable for a personal fork; a blocker if this is ever distributed.
                              • Existing grants will be re-prompted one final time when the first self-signed build lands, since the identity changes from cdhash to cert. After that they stick.

                              Acceptance

                              Build twice with the new mode, install the second over the first via the normal update path, and confirm no permission dialog reappears. codesign -dv should report our bundle ID as Identifier, a TeamIdentifier, and sealed resources.

                              Notes

                              Related but distinct: #41 (autobuild relaunch race).

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                bugSomething isn't workingready-for-agentSpecified enough for an agent to pick up

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions