Know your unknowns · Pre-implementation
← All examples

Blindspot pass

Before touching unfamiliar code, ask Claude to surface what you don't know you don't know — and to tell you how to prompt around each gap.

The prompt
I'm adding a new SSO auth provider to Acme but I've never touched the auth module. Do a blindspot pass: find my unknown unknowns in this part of the codebase, explain each one, and tell me how to prompt you better for the implementation.
What Claude produced
blindspot-pass · acme/services/auth · 47 files scanned

Unknown unknowns: the Acme auth module

Scanned services/auth/ (47 files), 214 commits touching it since Jan 2025, 3 migrations in flight, 2 reverted attempts. Seven things you'd have learned the hard way.

What you asked for

"Add a new SSO provider" — which sounds like: implement an interface, register it, write the redirect handlers, done. On a clean codebase, a two-day task.

What you're actually walking into

A module mid-migration on session storage, with one provider that quietly bypasses the middleware everyone assumes runs, a reverted previous attempt at exactly this task, and an identity-linking model that isn't visible from the provider interface at all.

4 landmines 2 unwritten conventions 1 missing concept 1 reverted attempt at this exact task (PR #2841)
The blindspots
Landmine 01

Sessions are double-written — the Redis store you'll find first is the wrong one

Acme is mid-migration from Postgres-backed sessions to Redis (MIG-118, stalled since March). Every session write currently goes through SessionBridge.write(), which fans out to both RedisSessionStore and the legacy pg_sessions table. The migration was never finished, so reads still come from Postgres.

Why it bitesGrepping for "session" surfaces RedisSessionStore first — it looks canonical, has the newest code, and the best docs. Write to it directly and your logins will appear to work in dev (Redis reads are enabled there) and silently fail in prod, where reads still hit Postgres.
When you create sessions for the new provider, route everything through SessionBridge — never RedisSessionStore or pg_sessions directly. Explain the MIG-118 read/write split before writing any session code.
Landmine 02

The SAML provider — your obvious template — bypasses the auth middleware

Every provider except SAML mounts under authPipeline, which runs rate limiting, audit logging, and the device-trust check. saml/handler.ts mounts its ACS endpoint directly on the router because SP-initiated POST callbacks broke the CSRF layer in 2024, and nobody circled back. There's a // TODO(priya): fold back into pipeline from 14 months ago.

Why it bitesSAML is the most recently added provider, so it's the natural file to copy. Copy it and your new provider ships with no rate limiting and no audit trail — which won't fail any test, because the contract tests don't assert middleware presence.
Use oauth/google.ts as the structural template, not saml/handler.ts — SAML intentionally bypasses authPipeline. Confirm the new provider's routes are mounted inside authPipeline and show me the mount point.
History 03

Someone already tried this — and it was reverted for a reason that still holds

PR #2841 ("Add Okta OIDC provider", Sept 2025) was merged and reverted nine days later. The revert commit explains why: Acme callback URLs must carry a tenant suffix (/auth/callback/:provider/:workspaceSlug) so workspace-scoped invite links survive the redirect round-trip. Okta's strict redirect-URI matching rejected the dynamic suffix, and invited users landed in the wrong workspace. The fix everyone agreed on — a state-parameter workspace claim signed with AUTH_STATE_SECRET — was designed in the revert thread but never built.

Why it bitesNothing in the current code hints at this. You'd re-implement #2841's approach, pass every local test (single-workspace dev env), and rediscover the bug in production the first time an invited user signs in.
Read the revert of PR #2841 first. Implement the signed state-parameter workspace claim it proposed instead of tenant-suffixed callback URLs, and add a test for the workspace-scoped invite flow.
Missing concept 04

Identities and accounts are separate things — and the provider interface hides it

Acme splits users from identities: one account can have several linked identities (email+password, Google, SAML), joined in the identities table. The provider interface only returns a profile; the linking decision happens in identity/linker.ts, keyed on verified email. Providers that can't guarantee verified emails (many SSO IdPs don't) must set requiresManualLink: true or the linker will happily merge two different people who share a corporate alias.

Why it bitesImplement the interface naively and everything works — until a customer's IdP sends an unverified or reassigned email and your provider silently attaches a stranger's identity to an existing account. This is an account-takeover bug, not a login bug.
Walk me through identity/linker.ts before implementing. Decide explicitly whether this IdP guarantees verified emails; if not, set requiresManualLink and build the manual-link confirmation path.
Landmine 05

Token refresh is behind a flag that's on in dev and off in prod

Refresh-token rotation is gated by auth.refresh_rotation in flags.yaml. It defaults to true in dev and staging, false in prod (a paused rollout after a clock-skew incident in May). With the flag off, prod sessions live exactly ACCESS_TOKEN_TTL (12h) and then hard-expire — there is no refresh path.

Why it bitesAny refresh logic you write will be tested against dev behavior and dead code in prod. Worse: if your provider assumes a refresh happens, prod users get logged out mid-session every 12 hours and the bug report will say "SSO randomly logs me out," pointing at your code.
Implement against auth.refresh_rotation=false (prod reality). Make the provider work with hard 12h expiry, and note separately what changes if the rotation rollout resumes.
Convention 06

Registering a provider takes three steps, and only two are discoverable

A provider isn't live when it compiles. It must be (1) registered in providers/registry.ts, (2) inserted into the auth_providers table via a migration — the admin UI reads the DB, not the registry — and (3) added to the fixture list in __tests__/provider-contract.spec.ts, which runs the shared contract suite (state validation, replay protection, logout fan-out) against every registered provider. CI fails if the registry and fixture list disagree.

Why it bitesSkip the migration and the provider compiles, passes tests, and simply never appears in any workspace's SSO settings. Two engineers have independently lost half a day to this; it's in nobody's docs.
Do all three registration steps: registry.ts, an auth_providers migration, and the provider-contract fixture list. Show me the contract suite passing for the new provider before we call it done.
Landmine 07

Logout isn't a function call — it's an event, and the review player listens

Session revocation publishes auth.session.revoked on the internal event bus. The realtime review player holds its own long-lived playback grants and only releases them when it consumes that event. logout.ts handles this for existing providers, but SSO single-logout (SLO) callbacks arrive on a separate path and must publish the event themselves.

Why it bitesMiss the event and logout "works" — the session row is gone — but anyone with an open review tab keeps full playback access to private video until the grant TTL expires (up to 24h). For a company selling review workflows to studios, that's a security incident, not a bug.
Handle SLO: any revocation path in the new provider must publish auth.session.revoked. Add a test asserting the review player's grant is released on IdP-initiated logout.
Your improved prompt

Everything above, folded into the prompt you should actually give me

Add a new SSO auth provider to Acme. Constraints from the blindspot pass:

1. Sessions: route all session writes through SessionBridge (MIG-118 is
   unfinished; prod still reads from pg_sessions). Never touch
   RedisSessionStore directly.
2. Template: base the structure on oauth/google.ts, NOT saml/handler.ts —
   SAML bypasses authPipeline. Mount all new routes inside authPipeline
   and show me the mount point.
3. History: read the revert of PR #2841 first. Implement the signed
   state-parameter workspace claim (AUTH_STATE_SECRET) instead of
   tenant-suffixed callback URLs, and test the workspace-invite flow.
4. Identity linking: walk me through identity/linker.ts before coding.
   If this IdP doesn't guarantee verified emails, set requiresManualLink
   and build the confirmation path — do not auto-link.
5. Flags: implement against auth.refresh_rotation=false (prod). Sessions
   hard-expire at 12h; no refresh path. Note what changes if rotation
   ships.
6. Registration: all three steps — providers/registry.ts, an
   auth_providers migration, and the provider-contract fixture list.
   Show the contract suite green.
7. Logout: every revocation path must publish auth.session.revoked, and
   add a test that the review player's playback grant is released on
   IdP-initiated logout.

Work in this order: linker walkthrough → provider skeleton → callback +
state claim → registration → SLO + events → contract tests. Stop and
show me the plan after the walkthrough before writing code.

Seven sentences you couldn't have written this morning — each one bought with someone else's half-day. That's the point of the pass.