macOS Keychain credential corruption: security -i 4096-byte line buffer causes OAuth token refresh failures

Resolved 💬 3 comments Opened Feb 25, 2026 by jean-humann Closed Mar 1, 2026

Summary

When MCP OAuth plugins (Slack, GitLab, etc.) store discovery metadata in the credential blob, the combined payload exceeds the macOS security -i 4096-byte line buffer limit, causing silent credential corruption. This results in persistent 401 "OAuth token has expired" errors and 400 responses from the refresh endpoint, requiring frequent /login to recover.

Root Cause

Claude Code stores OAuth credentials in macOS Keychain via the security CLI in interactive mode:

security -i <<< 'add-generic-password -U -a "USER" -s "Claude Code-credentials" -X "HEX_DATA"\n'

The security -i command has a hard 4096-byte line buffer limit. The command structure is:

| Component | Size |
|-----------|------|
| add-generic-password -U -a "USER" -s "Claude Code-credentials" -X " | ~67 bytes |
| Hex-encoded credential data | variable |
| "\n | 2 bytes |

Since hex encoding doubles the size, the maximum credential JSON payload is:

  • (4096 - 67 - 2) / 2 = 2013.52,013 bytes max (practically ~2,014 with alignment)

Credential Size Growth with MCP Plugins

MCP OAuth plugins store discovery metadata (authorization endpoints, token endpoints, supported grant types, etc.) in the same credential blob. Measured sizes:

| Configuration | Credential Size | Status |
|---------------|----------------|--------|
| claudeAiOauth only | ~467 bytes | OK |
| + Atlassian plugin | ~483 bytes | OK (minimal metadata) |
| + Slack plugin | ~2,343 bytes | OVER LIMIT |
| + GitLab plugin | ~4,361 bytes | WAY OVER |

Failure Chain

  1. User enables MCP plugins with OAuth (Slack, GitLab, etc.)
  2. Plugin OAuth discovery metadata is added to the credential JSON
  3. Credential exceeds 2,014 bytes → security -i write silently fails (exit code 1)
  4. Keychain retains old/truncated data; plaintext fallback (.credentials.json) may or may not catch the write
  5. On next token refresh: POST /v1/oauth/token with grant_type=refresh_token400 Bad Request (because the refreshed token was never persisted, the old revoked token is reused)
  6. On next API call: cached access token expired → 401 "OAuth token has expired"
  7. User must /login every few hours to get fresh tokens

Evidence

Experimental proof of the 4096-byte limit

# Size 2014 (command = 4096 bytes) → SUCCESS
security -i <<< "add-generic-password -U -a test_user -s test-svc -X \"$(python3 -c "print('41' * 2014)")\""
echo $?  # 0

# Size 2015 (command = 4098 bytes) → FAILURE
security -i <<< "add-generic-password -U -a test_user -s test-svc -X \"$(python3 -c "print('41' * 2015)")\""
echo $?  # 1

Debug log pattern (zero successful refreshes across 4,300+ sessions)

grep -c "status=400" debug_logs   # Hundreds of refresh failures
grep -c "status=200" debug_logs   # Zero successful refreshes

Every single token refresh attempt returned 400. Manual curl to the refresh endpoint with a valid token returned 200, confirming the server-side refresh works correctly.

Keychain "Failed to delete" errors

Every /login since enabling MCP plugins shows "Failed to delete keychain entry" in debug logs, suggesting the keychain entry is in a corrupted state that prevents normal deletion.

Suggested Fixes

Short-term

  1. Use security add-generic-password directly instead of security -i (avoids the line buffer limit entirely):

``bash
security add-generic-password -U -a "USER" -s "SERVICE" -w "PASSWORD"
``
Or pass the hex data via a temp file/pipe instead of inline.

  1. Chunk the credential across multiple keychain entries if a single entry is too large.

Long-term

  1. Store MCP OAuth discovery metadata separately from the main credential blob. Discovery metadata is cacheable and doesn't need to be in the credential store.
  1. Add credential size validation before writing — log a warning and gracefully degrade (e.g., skip persisting non-essential metadata) rather than silently failing.
  1. Add write verification — read back after writing to ensure the credential was persisted correctly.

Environment

  • macOS 15.4 (Darwin 25.3.0)
  • Claude Code 2.1.56
  • MCP plugins: Atlassian, Slack, GitLab (all with OAuth)

Workaround

Disable MCP plugins that use OAuth (Slack, GitLab, etc.) to keep the credential payload under the 2,014-byte limit, then /login to regenerate a clean credential.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗