<?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>Forem: Euda1mon1a</title>
    <description>The latest articles on Forem by Euda1mon1a (@euda1mon1a).</description>
    <link>https://forem.com/euda1mon1a</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3780682%2F36022045-36a5-4703-9714-05a14d0fef18.png</url>
      <title>Forem: Euda1mon1a</title>
      <link>https://forem.com/euda1mon1a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/euda1mon1a"/>
    <language>en</language>
    <item>
      <title>macOS Tahoe Broke Keychain CLI Reads: Novel Findings from an AI Agent Deployment</title>
      <dc:creator>Euda1mon1a</dc:creator>
      <pubDate>Thu, 19 Feb 2026 07:47:04 +0000</pubDate>
      <link>https://forem.com/euda1mon1a/macos-tahoe-broke-keychain-cli-reads-novel-findings-from-an-ai-agent-deployment-2p3o</link>
      <guid>https://forem.com/euda1mon1a/macos-tahoe-broke-keychain-cli-reads-novel-findings-from-an-ai-agent-deployment-2p3o</guid>
      <description>&lt;p&gt;If you're running automated scripts on macOS Tahoe that read from the Keychain, you've probably noticed something broke.&lt;/p&gt;

&lt;p&gt;I run a 24/7 Mac Mini M4 Pro as a local AI agent deployment — 12+ API keys, 25 scripts, 15 cron jobs, all orchestrated through OpenClaw. When I upgraded to Tahoe 26.x, my entire secrets pipeline died.&lt;/p&gt;

&lt;p&gt;This post documents six Tahoe keychain regressions I found, including one that appears to be genuinely novel (I couldn't find it documented anywhere). If you're a macOS sysadmin or developer hitting keychain issues on Tahoe, this should save you a week of debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This worked on Sequoia. On Tahoe, it hangs forever:&lt;/span&gt;
security find-generic-password &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"my-api-key"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"myaccount"&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not "fails with an error." &lt;strong&gt;Hangs.&lt;/strong&gt; Indefinitely. Even after &lt;code&gt;security unlock-keychain&lt;/code&gt; with the correct password. Even from a LaunchAgent with GUI session context. Exit code 36 if you're lucky, infinite hang if you're not.&lt;/p&gt;

&lt;p&gt;This is a Tahoe 26.x regression. It works fine on Sequoia 15.x and earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: The security CLI is broken on Tahoe
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;security&lt;/code&gt; command-line tool's &lt;code&gt;-w&lt;/code&gt; flag (which outputs just the password value) appears to have a regression in Tahoe's SecurityAgent integration. It either hangs waiting for a dialog that never appears, or returns exit code 36.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; Python's &lt;code&gt;keyring&lt;/code&gt; library bypasses the broken CLI entirely. It calls the Security framework's C API via ctypes, never touching the &lt;code&gt;security&lt;/code&gt; binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;
&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works from terminal sessions and from LaunchAgent context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: Keychain ACLs are per-binary
&lt;/h2&gt;

&lt;p&gt;This one bit me hard. I stored a secret using &lt;code&gt;/usr/bin/python3&lt;/code&gt; (Python 3.9 on macOS), then tried to read it from &lt;code&gt;/opt/homebrew/bin/python3.14&lt;/code&gt;. Access denied.&lt;/p&gt;

&lt;p&gt;macOS Keychain Access Control Lists record the &lt;em&gt;specific binary path&lt;/em&gt; that created the item. A different binary — even a different version of the same language — gets rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; When storing secrets, inject from &lt;em&gt;every&lt;/em&gt; Python version on the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="n"&gt;pythons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/homebrew/bin/python3.14&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pythons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import keyring; keyring.set_password(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each invocation adds that binary to the ACL. Now both versions can read the item.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: The novel one — bash subprocess loses SecurityAgent
&lt;/h2&gt;

&lt;p&gt;This is the one I couldn't find documented anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt; A bash script runs as a LaunchAgent. It calls &lt;code&gt;python3 get_secret.py&lt;/code&gt; as a subprocess to read a keychain item.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected:&lt;/strong&gt; Python reads the secret via &lt;code&gt;keyring&lt;/code&gt;, returns it to bash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actual:&lt;/strong&gt; The Python process hangs forever. No error, no timeout, just... stuck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; When bash is the LaunchAgent's ProgramArguments[0], it has a SecurityAgent session attachment (this is what allows keychain access in a GUI-less context). When bash spawns a Python subprocess, that SecurityAgent session attachment is &lt;em&gt;not inherited&lt;/em&gt;. The child Python process has no SecurityAgent context, so &lt;code&gt;keyring.get_password()&lt;/code&gt; blocks waiting for a GUI dialog that will never appear.&lt;/p&gt;

&lt;p&gt;This doesn't happen on Sequoia. It's specific to Tahoe's SecurityAgent session handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Either make Python the direct LaunchAgent program (not a subprocess of bash), or use a "file bridge" pattern — have a Python-native process read from keychain and write to a chmod 600 file that bash can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: SSH sessions can't read keychain at all
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;keyring.get_password()&lt;/code&gt; from an SSH session returns None or raises errSecInteractionNotAllowed (-25308). The keychain requires a SecurityAgent GUI session that SSH doesn't provide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Run keychain reads from a LaunchAgent (which has GUI context). The commonly recommended &lt;code&gt;security unlock-keychain -p&lt;/code&gt; also fails on Tahoe (see Finding 6 below).&lt;/p&gt;

&lt;p&gt;For SSH writes, you can unlock via the Security framework's C API (ctypes). The unlock + read/write &lt;strong&gt;must happen in a single Python process&lt;/strong&gt; — the unlock is process-scoped and doesn't persist across invocations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;

&lt;span class="n"&gt;Security&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cdll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LoadLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;keychain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;c_void_p&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Users/USERNAME/Library/Keychains/login.keychain-db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SecKeychainOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keychain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_LOGIN_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SecKeychainUnlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keychain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;c_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Now keyring works — but ONLY within this same process
&lt;/span&gt;&lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; This ctypes unlock only works with &lt;code&gt;/usr/bin/python3&lt;/code&gt; (Apple's system Python). Homebrew Pythons still get -25308 even after the unlock (see Finding 6).&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 5: keyring must be installed per-Python
&lt;/h2&gt;

&lt;p&gt;Each Python binary has its own site-packages. Running &lt;code&gt;pip3 install keyring&lt;/code&gt; only installs it for whichever Python pip3 points to. If you have system Python 3.9 and Homebrew Python 3.14, you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;keyring
/opt/homebrew/bin/python3.14 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--break-system-packages&lt;/span&gt; keyring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finding 6: The security CLI is &lt;em&gt;entirely&lt;/em&gt; broken — not just reads
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Update (March 2026):&lt;/strong&gt; My original post said &lt;code&gt;security find-generic-password -w&lt;/code&gt; was broken. After further testing, the damage is broader than that. The entire &lt;code&gt;security&lt;/code&gt; CLI is unreliable on Tahoe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;security find-generic-password -w&lt;/code&gt; — hangs or exits 36 (original finding)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security unlock-keychain -p&lt;/code&gt; — returns "incorrect passphrase" with a known-correct password&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security show-keychain-info&lt;/code&gt; — exits 36&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workaround for SSH is to use the Security framework C API directly via Python ctypes (see Finding 4 above). But even that has a quirk: the unlock only works with &lt;code&gt;/usr/bin/python3&lt;/code&gt; (Apple's system Python 3.9). Homebrew Python binaries (3.12, 3.14) still get errSecInteractionNotAllowed (-25308) after the same ctypes unlock, even in the same process. Root cause unknown — likely an entitlement or codesigning difference between Apple-signed and Homebrew-built binaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical impact:&lt;/strong&gt; If you need to write secrets from an SSH session, do the ctypes unlock + &lt;code&gt;keyring.set_password()&lt;/code&gt; from &lt;code&gt;/usr/bin/python3&lt;/code&gt;, then write a Group B bridge file in the same process for immediate SSH access. For multi-Python ACL coverage, inject from Homebrew Pythons via a VNC/GUI Terminal session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Group A/B Architecture
&lt;/h2&gt;

&lt;p&gt;After a week of debugging, I landed on an architecture that handles all five issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group A:&lt;/strong&gt; Python scripts that are direct LaunchAgent programs read from keychain via keyring. No files on disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group B:&lt;/strong&gt; For bash scripts (which can't reliably call Python keyring as a subprocess), a boot-time Python LaunchAgent reads secrets from keychain and writes them to chmod 600 files. Bash scripts read from files.&lt;/p&gt;

&lt;p&gt;A shared get_secret() helper tries keychain first, falls back to file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moltbot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="c1"&gt;# Fallback to Group B file
&lt;/span&gt;    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECRETS_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Before: 12 API keys stored as plaintext files. Any process with file read access could exfiltrate them.&lt;/p&gt;

&lt;p&gt;After: 7 secrets are Group A (keychain only, no file on disk). 5 are Group B (keychain + boot-time file bridge for bash tools). All 12 are in macOS Keychain with proper ACLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Skill
&lt;/h2&gt;

&lt;p&gt;I packaged all of this — migration tool, audit checker, file bridge, diagnostic tools — as an OpenClaw skill called &lt;strong&gt;Keychain Bridge&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free on ClawHub:&lt;/strong&gt; &lt;code&gt;clawhub install keychain-bridge&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium on ClawMarket:&lt;/strong&gt; &lt;a href="https://claw-market.xyz/skills/keychain-bridge" rel="noopener noreferrer"&gt;claw-market.xyz/skills/keychain-bridge&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration tool auto-detects all Python versions, injects secrets from each for ACL coverage, verifies the round-trip, and optionally deletes the original plaintext files. The audit tool checks for leaked files and keychain health.&lt;/p&gt;

&lt;p&gt;100% local. Zero network calls. No telemetry. All code open for inspection.&lt;/p&gt;

&lt;p&gt;Even if you're not using OpenClaw, the key takeaways apply to any macOS Tahoe deployment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use Python &lt;code&gt;keyring&lt;/code&gt; instead of the &lt;code&gt;security&lt;/code&gt; CLI — the &lt;em&gt;entire&lt;/em&gt; CLI is broken, not just reads&lt;/li&gt;
&lt;li&gt;Inject keychain items from every Python version that needs to read them&lt;/li&gt;
&lt;li&gt;Never spawn Python keychain reads as a subprocess from bash LaunchAgents&lt;/li&gt;
&lt;li&gt;Use a file bridge for bash scripts that need secrets&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;keyring&lt;/code&gt; separately for each Python binary&lt;/li&gt;
&lt;li&gt;For SSH access, use ctypes &lt;code&gt;SecKeychainUnlock&lt;/code&gt; from &lt;code&gt;/usr/bin/python3&lt;/code&gt; — Homebrew Pythons can't unlock even via the C API&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>macos</category>
      <category>security</category>
      <category>python</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
