COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
POCKET

Keychain Unlock UX for Claude Code

Continuing from a previous session that fixed font rendering (Nerd Fonts, emoji width). The next item was improving UX when Claude Code can't access its API key due to a locked macOS keychain.

2026-01-15 // RAW LEARNING CAPTURE
PROJECTPOCKET

Starting Point

Continuing from a previous session that fixed font rendering (Nerd Fonts, emoji width). The next item was improving UX when Claude Code can't access its API key due to a locked macOS keychain.

The problem: Claude Code stores API keys in macOS Keychain. When connecting via SSH (especially after reboot), the keychain is locked. Users see:

Missing API key · Run /login

SSH sessions can't prompt for GUI keychain unlock — the session has no GUI context.

Understanding the Problem Space

First question: what signals do we have from the iOS client to detect keychain state?

From iOS (pre-SSH): Nothing. We're just an SSH client connecting remotely.

During SSH session, we could probe:

security show-keychain-info ~/Library/Keychains/login.keychain-db 2>&1

Returns:

  • Unlocked: Keychain "login.keychain-db" lock-on-sleep timeout=300s
  • Locked: SecKeychainCopySettings: The user name or passphrase you entered is not correct.

But probing adds latency and feels intrusive. Also, user might have ANTHROPIC_API_KEY exported in shell profile — keychain state wouldn't matter.

Decision: Don't try to detect. Make unlock convenient to trigger. Fresh SSH sessions probably need it; once done, you're good for that session.

Three UX Approaches Considered

A) Shortcut button in toolbar — "🔐 Unlock Keychain" always visible, one tap

B) First-connect banner — dismissable hint on new sessions

C) Detect-on-demand — user taps help when Claude fails, we offer unlock

Started with Option A — simplest, no detection logic, user learns the pattern.

Implementation: Adding the Button

The shortcuts row lives in BottomPanelView.swift. Current shortcuts:

private let shortcuts: [(label: String, key: String?)] = [
    ("esc", "\u{1b}"),
    ("tab", "\t"),
    ("ctrl", nil), // Special handling
    ("^C", "\u{03}"),
    ("copy", nil),
    ("paste", nil),
    ("del", "\u{7f}"),
    // ...
]

Added keychain unlock command and button:

// Keychain unlock command for Claude Code API key access
private let keychainUnlockCommand = "security unlock-keychain ~/Library/Keychains/login.keychain-db"

private let shortcuts: [(label: String, key: String?)] = [
    ("esc", "\u{1b}"),
    ("tab", "\t"),
    ("ctrl", nil),
    ("^C", "\u{03}"),
    ("copy", nil),
    ("paste", nil),
    ("🔐", nil), // Keychain unlock - special handling
    ("del", "\u{7f}"),
    // ...
]

Handler in the ForEach:

} else if shortcut.label == "🔐" {
    // Type and run the keychain unlock command
    onKey(keychainUnlockCommand + "\r")
}

The + "\r" auto-submits — tap button, command runs immediately, macOS prompts for password.

Added accessibility label:

case "🔐": return "Unlock keychain for Claude Code"

First Test: Manual Submit

Initially tried without \r:

onKey(keychainUnlockCommand)

User would see the command, then press Enter manually. Tested it — worked but felt like unnecessary friction.

Iteration: Auto-Submit

Changed to include carriage return:

onKey(keychainUnlockCommand + "\r")

Now: tap 🔐 → command runs → macOS password prompt appears → enter password → keychain unlocked → Claude Code works.

One-tap fix.

Side Discovery: Missing Space After Glyphs

While testing, noticed the Nerd Font tree icon was eating the space after it:

Ctx:0.0%|🌲nogit

Should be:

Ctx:0.0%|🌲 nogit

Similar class of bug to the globe emoji we fixed earlier (SwiftTerm character width calculation). Logged to docs/missing-space-glyph-issue.md for later investigation.

Cleaning Up Previous Session's Changes

Git status showed uncommitted changes from theme refactoring work:

M  Pocket/Pocket/Core/Models.swift
M  Pocket/Pocket/Core/PocketStore.swift
M  Pocket/Pocket/UI/Settings/SettingsView.swift
M  Pocket/Pocket/UI/TerminalSessionOverlay.swift
M  Pocket/Pocket/UI/TerminalView.swift

These were:

  1. Theme migration — Old names (Pocket Light/Dark, Termius, Matrix) → New (Ember, Slate, Deep, Verdant)
  2. Removed per-project theme override — Simplified to global theme only
  3. Simplified MiniTerminalPreview — Removed overengineered contrast adjustment logic

Committed in logical chunks:

  • 277b872 - docs: Update build commands for iPhone 17 Pro simulator
  • 48979e3 - refactor: Migrate to new theme system
  • 3c0647f - feat: Add keychain unlock shortcut button

Where We Landed

Keychain unlock is now a one-tap operation:

  1. User connects to Mac via Pocket
  2. Runs Claude Code, sees "Missing API key"
  3. Taps 🔐 in shortcuts row
  4. Enters macOS password when prompted
  5. Claude Code works

Commits:

  • 3c0647f feat: Add keychain unlock shortcut button

Files changed:

  • Pocket/Pocket/UI/BottomPanelView.swift — Added 🔐 button + handler

Future UX Improvements

Documented in docs/keychain-unlock-ux.md:

  1. Detection — Watch terminal output for "Missing API key" pattern, surface hint proactively
  2. Placement — Could move button or make more prominent for new users
  3. Alternative fix — Suggest export ANTHROPIC_API_KEY=... in shell profile to bypass keychain entirely

For now, the simple shortcut button solves the immediate friction.

LOG.ENTRY_END
ref:pocket
RAW