ssh-claude-auth · git:20260717.e3de0e7 · 2026-07-17 · sha256 f8a236692e7c1dd4

ssh-claude-auth git:20260717.e3de0e7A

Immutable. This exact content is served forever at /api/v1/blob/f8a236692e7c1dd4.

---
name: ssh-claude-auth
description: Fix Claude Code appearing logged-out over SSH on a headless macOS machine — its credentials sit in the login keychain, which stays locked in SSH/headless sessions. Offers two fixes and asks the user to choose: a keychain-free long-lived OAuth token (recommended), or auto-unlocking the login keychain with a stored password. Use when Claude Code shows unauthenticated over SSH on a Mac, when setting up a Mac mini or headless Mac for remote/CI Claude Code use, or when `security show-keychain-info` reports the login keychain locked.
---

# Claude Code auth on a headless Mac over SSH

## Problem

Claude Code (subscription / OAuth login) stores its credentials in the macOS **login keychain**, encrypted with the user's macOS login password. A GUI login unlocks it automatically; an SSH / headless session does **not**, so Claude Code looks logged out until the keychain is unlocked. Two facts shape the fix:

- `sudo` cannot help — unlocking is *decryption*, and root has privilege but not the password.
- macOS Claude Code cannot use a plaintext credential file the way Linux does — the credential either lives in an unlocked keychain, or is replaced by a token.

## Step 1 — Confirm the diagnosis

```bash
security show-keychain-info ~/Library/Keychains/login.keychain-db   # locked/timeout error => this skill applies
security find-generic-password -s "Claude Code-credentials" ~/Library/Keychains/login.keychain-db >/dev/null 2>&1 \
  && echo "creds are in the keychain"
```

## Step 2 — Ask the user which approach (do NOT choose for them)

Use **AskUserQuestion** with the trade-offs below. List Approach A first, labeled "(Recommended)" — unless the user drives this machine's Claude Code remotely (see caveat), in which case recommend B.

| | A. Long-lived OAuth token | B. Auto-unlock keychain |
|---|---|---|
| Secret stored on disk | A scoped, revocable OAuth token | The **macOS login password**, plaintext |
| Blast radius if leaked | Inference only; revoke anytime | Unlocks the **entire** keychain |
| Touches the keychain? | No — bypasses it | Yes |
| Maintenance | Re-mint ~yearly (token expires) | Re-edit file when Mac password changes |
| Remote Control sessions | ❌ token can't establish them | ✅ works |

**Caveat to surface before they pick:** a `setup-token` credential is inference-only and **cannot establish Remote Control sessions** (driving this machine's Claude Code from claude.ai or mobile). Users who rely on that need Approach B.

## Step 3 — Run the chosen setup script

Invoke scripts by **absolute path**: `bash <skill-base-dir>/scripts/<name>.sh`, where `<skill-base-dir>` is the base directory printed when this skill loads. All scripts read secrets interactively with no echo — **never** pass a password or token on the command line, and never have the user paste one into the conversation.

**Approach A** (requires a Claude Pro / Max / Team / Enterprise subscription):
1. The **user** mints the token themselves: `claude setup-token` (interactive browser OAuth; prints a ~1-year token, does not save it).
2. `bash <skill-base-dir>/scripts/setup-oauth-token.sh` — prompts for the token, stores it `600`, and sources it from `~/.zshrc` as `CLAUDE_CODE_OAUTH_TOKEN`, which Claude Code uses instead of the keychain.
3. If Approach B was ever set up, remove it so the macOS password stops living on disk: `bash <skill-base-dir>/scripts/teardown-keychain-unlock.sh`

**Approach B**:
- `bash <skill-base-dir>/scripts/setup-keychain-unlock.sh` — prompts for the macOS password, then installs the password file (`600`), unlock script (`700`), a login LaunchAgent, and a `~/.zshrc` SSH hook.
- For a lighter variant that stores no password and prompts once per SSH session instead, see [REFERENCE.md](REFERENCE.md).

## Step 4 — Verify

Open a **fresh** SSH session and run `claude` — it should be logged in with no prompt.
- Approach A: `[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && echo "token set"`. Note `claude --bare` ignores this variable — use `ANTHROPIC_API_KEY` there.
- Approach B: `bash ~/.claude/unlock-keychain.sh && echo ok`

## Scripts

| `scripts/` | Purpose |
|---|---|
| `setup-oauth-token.sh` | Install Approach A (token file + `~/.zshrc` block) |
| `teardown-oauth-token.sh` | Undo Approach A (falls back to the keychain) |
| `setup-keychain-unlock.sh` | Install Approach B (password file, unlock script, LaunchAgent, `~/.zshrc` hook) |
| `teardown-keychain-unlock.sh` | Undo Approach B, **deleting the stored macOS password** |
| `lib.sh` | Shared helpers; single source of truth for the `# >>> … >>>` block markers |

Switching approaches = run the new setup, then the old teardown. Manual walkthrough, no-stored-password variant, command quick-reference, and common mistakes: [REFERENCE.md](REFERENCE.md)