macOSアプデ後にcronのClaude CLIが認証切れで死んでた話
RSSフィードを自動収集してClaude CLIで記事化するバッチ処理を動かしている。cron → シェルスクリプト → tmux → claude -p という構成で、1日に何度か自動実行される。
ある日、ブログに新しい記事が上がってこなくなった。1.5日ほど放置してから気づいて調査したところ、原因はmacOSのアップデートだった。
症状
cronのログを見ると、バッチスクリプトは正常に起動していた。
RSS batch started: rss-pipeline:rss-batch (mode: scoring)
Log: /Users/.../rss-batch/last-run.log
Attach: tmux attach -t rss-pipeline:rss-batch
しかし last-run.log の中身はこれだけ。
Not logged in · Please run /login
Claude CLIが認証できず、即座に終了していた。
紛らわしいところ
ターミナルから直接 claude auth status を叩くと、ちゃんとログイン済みと表示される。
{
"loggedIn": true,
"authMethod": "claude.ai",
"apiProvider": "firstParty",
"email": "...",
"subscriptionType": "max"
}
同じマシン、同じユーザー、同じバイナリなのに、tmuxの中から叩くと loggedIn: false になる。
tmuxセッションからKeychainにアクセスできない
Claude CLIはmacOSのKeychainに認証情報を保存している。
$ security dump-keychain login.keychain-db | grep claude
0x00000007 <blob>="Claude Code-credentials"
"acct"<blob>="gakugakumanjp"
"svce"<blob>="Claude Code-credentials"
通常のターミナルからは問題なくこのエントリにアクセスできる。
# ターミナルから → OK
$ security find-generic-password -s "Claude Code-credentials" -a "gakugakumanjp"
keychain: "/Users/.../Library/Keychains/login.keychain-db"
class: "genp"
# tmux内から → 失敗
$ security find-generic-password -s "Claude Code-credentials" -a "gakugakumanjp"
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
同じコマンド、同じ引数で結果が違う。
macOSのSecurity Frameworkはプロセスの「セキュリティセッション」を見ている。GUIログインセッション配下のプロセスはlogin keychainに自由にアクセスできるが、cronやlaunchdから起動されたプロセスは別のセッションコンテキストで動くため、keychainへのアクセスが制限される。
OSアップデート後にMacが再起動すると、以前のtmuxセッションは消える。次にcronがrun.shを実行したとき、新しいtmuxセッションがcronのセッションコンテキスト内で作成される。このセッションにはGUIセッションのKeychain権限がない。
時系列
- macOSアップデート → 再起動
- 再起動後、cronがrun.shを実行
- run.shがtmuxセッションを新規作成(cronのセキュリティセッション内で)
- tmux内でClaude CLIが起動 → Keychainにアクセスできない →
Not logged in - 以後、同じtmuxセッションが使い回されるため、すべてのバッチ実行が失敗し続ける
tmuxセッションを作り直せば直る
GUIセッション(ターミナル)から古いtmuxセッションを殺して、新しく作り直す。
# 古いセッションを削除
tmux kill-session -t rss-pipeline
# ターミナルから新しいセッションを作成(GUIセッションのKeychain権限を継承)
tmux new-session -d -s rss-pipeline -c ~/Projects/rss-batch
これだけで、新しいtmuxセッション内からKeychainにアクセスできるようになる。次のcron実行時にはrun.shが既存セッション内に新しいウィンドウを作るだけなので、Keychain権限は維持される。
毎回起きるのか
再起動を伴うアップデートでは毎回起きる。 ただし正確にはアップデートが原因ではなく、再起動がトリガーになる。
macOSのKeychainアクセスは「ブートストラップ名前空間」という3層構造で管理されている。
- System名前空間 — ブート時に作られ、シャットダウンまで存在する
- Per-user名前空間 — ユーザーのログイン時に作られる
- Per-session名前空間 — GUIセッション(Aqua)やSSHセッションごとに作られる
重要なのは、login keychainがアンロックされるのは GUIログイン時にユーザーパスワードが入力されたとき だということ。cronはSystem名前空間で動くため、ユーザーのlogin keychainにはそもそも到達できない。
つまり以下のすべてのケースで壊れる。
- OSアップデート後の再起動: tmuxセッションが消え、cronが新しいセッションを作り直す
- 停電やカーネルパニックによる再起動: 同上
- 手動の再起動: 同上
- Keychainの自動ロック: スリープ後や一定時間経過でlogin keychainがロックされた場合(設定による)
逆に言えば、GUIセッションのターミナルから作ったtmuxセッションが生き続けている限りは問題ない。再起動しない限りtmuxセッションは消えないので、通常の運用では意識しなくて済む。
macOS Keychainの構造的な問題
調べてみると、これはClaude CLI固有の問題ではなかった。macOS Keychainに認証情報を保存するCLIツールは軒並み同じ問題を踏んでいる。
| ツール | 症状 | 参考 |
|---|---|---|
| aws-vault | SSH/tmuxで errSecInteractionNotAllowed (-25308) | Issue #925 |
| git credential-osxkeychain | SSHセッションやLaunchDaemonで認証失敗 | VS Code #3872 |
| Python keyring | cronから呼ぶと -25308 エラーまたはNone返却 | Issue #188 |
| fastlane | cronからコード署名証明書にアクセスできない | Issue #1014 |
| VS Code Remote SSH | GitHubの認証トークンにアクセスできない | Issue #152964 |
| Jenkins on macOS | LaunchDaemonからkeychainアイテムが見えない | Apple Developer Forums |
aws-vaultのIssue #925は「macOS appears to enforce a security model assuming all Keychain actions should be attached to an app that has an interactive GUI」という一文で閉じられている。GUIアプリ前提の設計だから、CLIの自動実行は想定外ということだ。
バージョンごとに悪化している
Appleはバージョンを重ねるごとにKeychainのセキュリティを強化しており、非GUIセッションからのアクセスはどんどん厳しくなっている。
| macOSバージョン | 変更 |
|---|---|
| Sierra (10.12) | パーティションリスト(非公開のACL機構)導入。codesign が毎回モーダルダイアログを出すようになった |
| Sequoia (15.0) | Keychain Access.appが移動。認証フローの変更で一部環境でアンロックが失敗 |
| Tahoe (26.0) | security find-generic-password -w がハングまたはexit code 36を返す回帰バグ。SecurityAgentセッションが子プロセスに継承されなくなった |
特にTahoe (26.0) の変更は深刻で、bashがLaunchAgentの ProgramArguments[0] として起動した場合、bash自体はSecurityAgentセッションを持っているのに、bashから起動した子プロセス(Pythonやnodeなど)にはそのセッションが 継承されない という挙動になった。つまり bash -c "python3 script.py" と直接 python3 script.py を叩くのとで結果が変わる。
参考: macOS Tahoe Broke Keychain CLI Reads
Claude CLIのGitHub Issueにも複数報告がある
Claude Code側でも同じ問題が何度も報告されている。
- #9403 — OAuth認証情報が制限的なACL権限で保存され、新しいセッションから読めなくなる。
security unlock-keychainでACLがリセットされると直るが、根本解決ではない - #10127 — macOSのtmux内で
/loginがハングまたは失敗する。認証は成功するがトークンが永続化されない - #5957 — SSHセッションから認証できない。OAuth認証はブラウザで成功するが、SSHセッションにクレデンシャルが届かない
- #22144 — OAuthトークンのリフレッシュ時(約8時間ごと)にKeychainアイテムを削除・再作成する設計のため、サードパーティツールのACL許可がリセットされる。1日5〜10回パスワードプロンプトが出る
いずれもステータスは NOT_PLANNED か Open のまま。macOSのアーキテクチャに起因する問題なので、Claude CLI側で根本的に解決するのは難しいのだと思う。
厄介なポイント
この問題は以下の条件が揃うと発見が遅れる。
- バックエンド処理である: 普段tmuxのセッションを見に行かない
- cronは正常に動いている: ログには「started」と出るので、起動自体は成功しているように見える
- CLI自体は壊れていない: 手元のターミナルからは問題なく動く
- エラーメッセージが一般的すぎる: 「Not logged in」だけでは、セッション期限切れなのかKeychainの問題なのか判別できない
恒久対策の選択肢
tmuxセッションの再作成は応急処置に過ぎない。次にOSアップデートで再起動したら同じことが起きる。
run.shの先頭でKeychain到達性をチェックする
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
# ここで通知を飛ばす(Slack webhook, macOS notification等)
exit 1
fi
根本解決ではないが、少なくとも失敗に気づける。今の状態だと「Not logged in」がログに1行残るだけで、cronのログには「started」としか出ないのが一番の問題。
security unlock-keychain をrun.shに追加する
security unlock-keychain -p "$(cat ~/.keychain-pass)" ~/Library/Keychains/login.keychain-db
パスワードをファイルに平文保存する必要があるため、セキュリティ上のトレードオフがある。ただしCI/CDの世界ではfastlaneをはじめこのパターンが事実上の標準になっている。
cronからlaunchd LaunchAgentに切り替える
LaunchAgentはユーザーセッション内で動くため、login keychainにアクセスできる。Appleも公式にLaunchAgentの使用を推奨している。
<?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>
ただしTahoe (26.0) ではSecurityAgentセッションが子プロセスに継承されない問題があるため、bashを経由せずターゲットバイナリを直接 ProgramArguments[0] に指定する必要がある。
Keychainを使わないCredential Storeがほしい
根本的には「macOS Keychainに依存しない認証情報の保存」が理想。他のツールでは以下のアプローチが取られている。
- git:
credential.helper storeでファイルベースに切り替え(平文だがcronで動く) - 1Password CLI: Service Accountsモード(対話的認証なしで動作)
- Python keyring:
KEYRING_PROPERTY_FILE_PATHでファイルバックエンドに切り替え
APIキー認証に切り替えるという手もあるが、Maxプランで使っているのにバッチ処理のためだけにAPI課金するのは馬鹿らしい。Claude CLIにもLinuxのようなファイルベースの認証情報保存オプションがあれば、この問題は解消される。#22144でそのようなリクエストが出ているが、2026年2月時点で未対応。
バッチ処理が静かに死ぬのは一番困る。とりあえずKeychain到達性チェック+通知を入れて、中期的にはlaunchd化を検討するつもり。