v4.5.0 — Keychain-only credential storage
Keychain-only credential storage — zero plaintext on disk, every failure loud
Closes #162. Slack credentials can now live exclusively in the macOS Keychain — no plaintext token file is ever written, while --setup, slack_refresh_tokens, and automatic refresh keep working unchanged.
Three storage modes (SLACK_MCP_TOKEN_STORAGE, or choose once in --setup)
auto(default) — token file (chmod 600) + Keychain backup, exactly the previous behaviorkeychain-only— macOS Keychain only. Writes verified by read-back; a legacy token file is migrated in and removed only after both entries verify. Failures are loud and structured (keychain_write_failed,plaintext_removal_failedwith the exact cleanup command) — never a silent fallback to plaintextfile— token file only; the Keychain is never touched (no prompts — shared machines, CI)
On macOS, --setup asks where credentials should live and remembers the answer in ~/.slack-mcp-meta.json, so the server, CLI, and any LaunchAgent follow the same choice with no env-var plumbing. Unrecognized mode values fail closed at startup — a typo can never downgrade to plaintext.
Reliability hardening (from the adversarial review on #163)
- In-memory last-known-good tokens — a refresh that extracts working tokens but can't persist them (locked Keychain) keeps them available in-process, so the auth retry uses the fresh pair instead of the stale stored credentials. Surfaced as
storage.unpersisted_fresh_tokens. - Honest auto-heal — persistence failures are recorded in telemetry (stuck-detection can trip) and reported as
persisted: false, never as a clean heal. - Cross-process write lock — token file + metadata writes are serialized across server/CLI/LaunchAgent (O_EXCL lock, stale-holder takeover), ending silent lost-updates; proven by a two-process regression test.
- Keychain writes use
-Uupdate-in-place (all modes), removing the delete-then-add window that could lose the entry. - Hardened setup wizard (platform gate, strict prompts, friendly save errors) and token CLI (
tokens:clearexits non-zero on incomplete cleanup;tokens:statusflags a lingering plaintext file even when no credentials load).
Observability
slack_token_status now reports the active backend under storage: { mode, mode_source, keychain_available, plaintext_file_present, unpersisted_fresh_tokens }. --doctor and npm run tokens:status print the storage mode with its origin and warn on pending migration.
Tests
44 tests (20 new since 4.4.3), including a two-process metadata-race regression, undeletable-plaintext fail-loud coverage, memory-fallback lifecycle, and five end-to-end tests that boot the real MCP server over stdio.
Credit: adversarial test pass by @TyceHerrman surfaced four of the hardening items; the pre-existing Chrome extraction timeout found during that pass is tracked in #168.
Full changelog: https://github.com/jtalk22/slack-mcp-server/blob/main/CHANGELOG.md