Tech 8 min read

Claude CLI Auth Breaking Silently in cron After a macOS Update

I run a batch job that automatically collects RSS feeds and pipes them through Claude CLI to generate articles. The setup is cron → shell script → tmux → claude -p, running several times a day.

One day, new articles stopped appearing on the blog. I noticed about a day and a half later and started investigating. The culprit was a macOS update.

Symptoms

Looking at the cron logs, the batch script was starting normally:

RSS batch started: rss-pipeline:rss-batch (mode: scoring)
Log: /Users/.../rss-batch/last-run.log
Attach: tmux attach -t rss-pipeline:rss-batch

But the contents of last-run.log were just this:

Not logged in · Please run /login

Claude CLI couldn’t authenticate and was exiting immediately.

The Misleading Part

Running claude auth status directly from the terminal showed it was logged in fine:

{
  "loggedIn": true,
  "authMethod": "claude.ai",
  "apiProvider": "firstParty",
  "email": "...",
  "subscriptionType": "max"
}

Same machine, same user, same binary — but running it from inside tmux returned loggedIn: false.

tmux Sessions Can’t Access the Keychain

Claude CLI stores authentication credentials in macOS Keychain.

$ security dump-keychain login.keychain-db | grep claude
    0x00000007 <blob>="Claude Code-credentials"
    "acct"<blob>="gakugakumanjp"
    "svce"<blob>="Claude Code-credentials"

Access from a normal terminal works fine:

# From terminal → OK
$ security find-generic-password -s "Claude Code-credentials" -a "gakugakumanjp"
keychain: "/Users/.../Library/Keychains/login.keychain-db"
class: "genp"

# From inside tmux → Fails
$ security find-generic-password -s "Claude Code-credentials" -a "gakugakumanjp"
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.

Same command, same arguments, different result.

macOS’s Security Framework checks the process’s “security session.” Processes under a GUI login session can freely access the login keychain, but processes launched by cron or launchd run in a different session context where keychain access is restricted.

When macOS restarts after an update, existing tmux sessions are gone. The next time cron runs run.sh, it creates a new tmux session within cron’s session context — a context that doesn’t have GUI session Keychain permissions.

Timeline

  1. macOS update → restart
  2. After restart, cron executes run.sh
  3. run.sh creates a new tmux session (inside cron’s security session)
  4. Claude CLI launches inside tmux → can’t access Keychain → Not logged in
  5. The same tmux session keeps getting reused, so every subsequent batch run fails

Recreating the tmux Session Fixes It

Kill the old tmux session from a GUI terminal, then recreate it:

# Delete the old session
tmux kill-session -t rss-pipeline

# Create a new session from the terminal (inherits GUI session Keychain permissions)
tmux new-session -d -s rss-pipeline -c ~/Projects/rss-batch

That’s all it takes. The new tmux session can access the Keychain. When cron runs next time, run.sh just creates a new window inside the existing session, preserving Keychain permissions.

Does This Happen Every Time?

Yes, with any update that requires a restart. Though to be precise, it’s the restart — not the update itself — that triggers it.

macOS Keychain access is managed through a three-layer “bootstrap namespace” structure:

  1. System namespace — created at boot, exists until shutdown
  2. Per-user namespace — created when the user logs in
  3. Per-session namespace — created per GUI session (Aqua) or SSH session

The key point: the login keychain is unlocked when the user enters their password at GUI login. cron operates in the System namespace and simply can’t reach the user’s login keychain.

This means all of the following will break:

  • After an OS update restart: tmux sessions disappear, cron recreates them
  • Power outage or kernel panic restart: same
  • Manual restart: same
  • Keychain auto-lock: if the login keychain locks after sleep or a timeout (depending on settings)

Conversely, if the tmux session created from a GUI terminal stays alive, there’s no problem. tmux sessions don’t disappear without a restart, so in normal operation you can ignore this.

A Structural macOS Keychain Problem

Looking into it, this turned out to have nothing to do with Claude CLI specifically. Any CLI tool that stores credentials in macOS Keychain hits the same issue:

ToolSymptomReference
aws-vaulterrSecInteractionNotAllowed (-25308) in SSH/tmuxIssue #925
git credential-osxkeychainAuth fails in SSH sessions or LaunchDaemonVS Code #3872
Python keyring-25308 error or None returned from cronIssue #188
fastlaneCan’t access code signing certs from cronIssue #1014
VS Code Remote SSHCan’t access GitHub auth tokensIssue #152964
Jenkins on macOSKeychain items invisible from LaunchDaemonApple Developer Forums

The aws-vault Issue #925 was closed with: “macOS appears to enforce a security model assuming all Keychain actions should be attached to an app that has an interactive GUI.” The design assumes GUI apps — automated CLI invocation is out of scope.

Getting Stricter With Each Version

Apple has been tightening Keychain security across versions, and non-GUI session access gets harder and harder:

macOS versionChange
Sierra (10.12)Partition list (private ACL mechanism) introduced. codesign started showing a modal dialog every time
Sequoia (15.0)Keychain Access.app moved. Auth flow changes caused unlock failures in some environments
Tahoe (26.0)security find-generic-password -w hangs or returns exit code 36 regression. SecurityAgent sessions are no longer inherited by child processes

The Tahoe (26.0) change is especially bad: when bash launches as ProgramArguments[0] in a LaunchAgent, bash itself has a SecurityAgent session, but child processes it spawns (Python, node, etc.) don’t inherit it. So bash -c "python3 script.py" and running python3 script.py directly give different results.

Reference: macOS Tahoe Broke Keychain CLI Reads

Multiple Reports in the Claude CLI GitHub Issues

The same problem has been reported repeatedly on the Claude Code side:

  • #9403 — OAuth credentials saved with restrictive ACL permissions, unreadable from new sessions. Resetting ACL with security unlock-keychain fixes it, but it’s not a permanent solution
  • #10127/login hangs or fails inside tmux on macOS. Authentication succeeds but the token doesn’t persist
  • #5957 — Can’t authenticate from an SSH session. OAuth auth succeeds in the browser but credentials don’t reach the SSH session
  • #22144 — When OAuth tokens refresh (every ~8 hours), the design deletes and recreates the Keychain item, resetting third-party tool ACL permissions. Password prompts 5–10 times a day

Status on all of these is NOT_PLANNED or Open. Since the root cause is macOS architecture, there’s not much Claude CLI can do about it fundamentally.

Why This Is Hard to Notice

This problem is easy to miss because all of the following are true simultaneously:

  • It’s a background process: You don’t normally look at tmux sessions
  • cron itself is running fine: Logs say “started” so the launch looks successful
  • The CLI itself isn’t broken: Works perfectly from your terminal
  • The error message is generic: “Not logged in” alone doesn’t tell you whether it’s an expired session or a Keychain problem

Permanent Fix Options

Recreating the tmux session is a stopgap. The next OS update restart will cause the same thing again.

Check Keychain reachability at the start of run.sh

if ! security find-generic-password -s "Claude Code-credentials" -a "$USER" &>/dev/null; then
    echo "ERROR: Keychain access failed. Recreate tmux session from GUI terminal." >&2
    # Send a notification here (Slack webhook, macOS notification, etc.)
    exit 1
fi

Not a permanent fix, but at least you’ll know when it fails. Right now the only evidence is “Not logged in” buried in a log file while cron’s log just says “started.”

Add security unlock-keychain to run.sh

security unlock-keychain -p "$(cat ~/.keychain-pass)" ~/Library/Keychains/login.keychain-db

This requires storing the password in a plaintext file, which involves a security tradeoff. That said, this pattern is effectively the industry standard in CI/CD — fastlane and others use it routinely.

Switch from cron to a launchd LaunchAgent

LaunchAgents run within the user session, so they can access the login keychain. Apple officially recommends LaunchAgents:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.rss-batch</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/username/Projects/rss-batch/run.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <array>
        <dict>
            <key>Minute</key><integer>30</integer>
            <key>Hour</key><integer>1</integer>
        </dict>
    </array>
</dict>
</plist>

However, due to the Tahoe (26.0) issue where SecurityAgent sessions aren’t inherited by child processes, you need to specify the target binary directly as ProgramArguments[0] rather than going through bash.

A Keychain-Free Credential Store Would Be Ideal

The real solution would be “storing credentials without depending on macOS Keychain.” Other tools handle this as follows:

  • git: credential.helper store switches to a file-based backend (plaintext, but works from cron)
  • 1Password CLI: Service Accounts mode (operates without interactive auth)
  • Python keyring: KEYRING_PROPERTY_FILE_PATH switches to a file backend

Switching to API key authentication is an option, but running batch jobs on API billing while already paying for the Max plan seems wasteful. A file-based credential storage option for Claude CLI (like Linux has) would solve this cleanly. There’s a request for exactly that in #22144, but as of February 2026 it’s unaddressed.


Background processes dying silently is the worst. For now I’m adding the Keychain reachability check plus notifications, and planning to migrate to launchd in the medium term.