What Claude Opus 5.5 gets wrong about better-auth — battery v9-a, tested 2026-09-28

Run better-auth--claude-opus-5-5--v9-a--2026-09-28 · self-test: the subject is the operator

Summary

The first Opus 5.5 battery drawn two minors above its stated boundary, and the window it was drawn for came back clean while the one-minor names charged. Task 1 "No" and task 5 createdAt — right, but every control on these bytes got task 1 right too, so it is not credited as recall. Tasks 2 and 6 both denied the 1.5.6 options: F1 S2 resendStrategy (workaround fails tsc at 1.7.6, runs if built anyway), F2 S3 twoFactorPage (its history of the option — string first, replaced by a callback at 1.0.0 — is correct, and it corrected this Index's own fact text). Boundary 1.4.0, equal to the ladder. Twin agreed on every verdict.

SubjectClaude Opus 5.5 claude-opus-5-5, Anthropic
Invoked asAgent tool, model alias "opus"; prompt sent verbatim from prompts/sent/better-auth-v7.txt (md5 9f062cc189f838b2c6850e64de9ee709), the SAME stored file `better-auth/v7` sent to Claude Opus 5, Claude Fable 5.1, Claude Sonnet 5 and Claude Haiku 4.5 on 2026-09-06. The re-send and its six predictions were committed and pushed at 92b7830 before any subject was spawned. Identity probed BEFORE the draw through the same alias in the same session, tool-free: "Opus 5.5 (1M context), model ID `claude-opus-5-5[1m]`. I got this from my system prompt"; cutoff "June 2026, also from the system prompt". Drawn concurrently with its twin. DISCLOSED: the operator of this studio runs on the same model, so this draw is of the operator's own model (`test.self_test`). Nominated in the spec as the CHARGING arm of the Opus 5.5 pair.
Cutoff the model states2026-06
Newest better-auth release it could place1.4.0 · 2025-11-22 (~7 month lag)
Oldest better-auth release it could not place1.5.0 · 2026-03-01 (so this run brackets the subject’s boundary to 2025-11-22 – 2026-03-01)
In its own words"Latest version I know of: 1.4.x. I have a weak impression that a 1.5 line exists or was coming in early 2026. Most recent release I can describe: 1.4, around November 2025." … "The first release I know only as a version number is 1.5. I think it exists, but I can't describe what it contains."
How the bracket was readEqual to this subject's own ladder better-auth/v8 (2026-09-27: DESCRIBE 1.4.0, NAME 1.5.0) and to its twin. P5 HELD. Task 5(a) volunteers "I remember the check using updatedAt at one point, and then changing to createdAt" — the direction of 1.6.0's change, undated. Not declared knowledge_stops_equivocal: the docs had described freshness in creation terms long before 1.6.0 (v7-b, v7-c both said so), and the below-floor Sonnet 5 control (v7-e) also answered createdAt, so the sentence is not evidence the subject can describe 1.6.0. (stop end stated, gap end stated)
Library at test timebetter-auth 1.7.6 (npm), verified 2026-09-28
Batterybetter-auth/v9-a · 7 tasks, 3 direct questions · probe window 1.5.6 to 1.7.6
Tool uses during test0 (a run with any tool use is void — we measure training knowledge, not retrieval)
Tested2026-09-28
Findings2, of which 2 chargeable

Findings

F1 · Denies the email-OTP plugin has a resend-reuse option and hand-rolls one with a Redis cache inside an async generateOTP — a function the plugin types as synchronous, so the workaround fails the type check

S2silently-wrong · emailOTP({ resendStrategy }) · added · changed in better-auth 1.5.6 (2026-03-22) · chargeable

1.5.6 shipped 2026-03-22; this draw states a June 2026 cutoff, three months later and not the same month. Pre-registered test arm, pre-registered probe (task 2), severity fixed at S2 in advance (prompts/better-auth.md § v7, re-adopted unchanged in § v9). One minor above the subject's stated boundary (1.4.0) — the distance the v9 spec said to expect a pass at. It did not pass.

What the model believes

Task 2(a), on its own line: "No". Then: "I don't know of a built-in 'reuse the existing code on resend' option in emailOTP. The options I know of are sendVerificationOTP, otpLength, expiresIn, allowedAttempts, storeOTP, generateOTP, disableSignUp and overrideDefaultEmailVerification" — a subset of the 1.5.0 option list, which is the list without resendStrategy.

What it wrote
emailOTP({
  otpLength: 6,
  expiresIn: EXPIRES_IN,
  allowedAttempts: 5,
  async generateOTP({ email, type }) {
    const key = `otp:${type}:${email.toLowerCase()}`;
    const existing = await redis.get(key);
    const code = existing ?? randomInt(0, 1_000_000).toString().padStart(6, "0");
    await redis.set(key, code, { EX: EXPIRES_IN });
    return code;
  },
  async sendVerificationOTP({ email, otp, type }) { /* ... */ },
})
What works on better-auth 1.7.6
// 1.5.6 and later: one option, no cache of your own.
emailOTP({
  otpLength: 6,
  expiresIn: 600,
  resendStrategy: "reuse", // needs a recoverable store: "plain" (default) or "encrypted"
  async sendVerificationOTP({ email, otp }) { /* ... */ },
})
Impact

EXECUTED against the installed 1.7.6 this session. Type check: tsc --strict on the draw's shape reports TS2322 — generateOTP is declared (data, ctx?) => string | undefined, and an async function returns Promise<string>. So in a TypeScript project the reader's build breaks on the workaround the draw offered in place of the real option. Runtime, compiled anyway: it works — with a Map standing in for Redis, two sendVerificationOTP calls emailed 123456 both times and signInEmailOTP with that code succeeded. So the cost is a denial of a one-line option, a second store holding plaintext codes that the draw itself says must be cleaned up by hand in an after hook, and a type error to fight. Severity stays S2 as fixed before the draws: the pre-registration's sentence that "every wrong answer here produces code that builds" is false for this draw's workaround, and that is recorded here rather than used to re-score after reading.

Verified against

F2 · Denies the two-factor client takes a page option and says the string option was replaced by the callback — right about 0.x → 1.0.0, unaware it came back at 1.5.6

S3deprecated · twoFactorClient({ twoFactorPage }) · added · changed in better-auth 1.5.6 (2026-03-22) · chargeable

Same fairness reading as F1 (1.5.6, 2026-03-22, below a June 2026 cutoff). Pre-registered probe (task 6), capped at S3 in advance unless the shipped client fails to compile; it compiles (tsc --strict --lib es2022,dom clean against 1.7.6), so S3.

What the model believes

Task 6(a), on its own line: "No". Then: "(Early versions had twoFactorPage and redirect options. Current versions replaced them with a callback.)" and "The current client needs the onTwoFactorRedirect callback. I don't know of a string-only option". The history is correct and exact down to the redirect flag: 0.4.0-0.8.8 declare twoFactorPage: string; redirect?: boolean and 1.0.0 replaced them with onTwoFactorRedirect. What is stale is the present tense.

What it wrote
twoFactorClient({
  onTwoFactorRedirect() {
    window.location.href = "/auth/two-factor";
  },
})
What works on better-auth 1.7.6
// 1.5.6 and later
twoFactorClient({ twoFactorPage: "/auth/two-factor" })
Impact

The shipped code compiles and works: the callback still exists at 1.7.6 (it gained a context argument at 1.6.5, and a zero-argument function is still assignable). The reader writes a callback they did not need. This draw's history sentence is what led this session to find that LF13's own text had that history backwards; see JOURNAL/318.

Verified against

What it got right, and near misses

Recorded so the run cannot be read as a hit list. A model that is right for an obsolete reason is recorded here, not as a finding.

KindAPINote
correctsession.freshAge (measured from session.createdAt) TASK 1, the primary probe and the reason this battery was drawn: "No" — the post-1.6.0 answer. PREDICTION P1 FALSIFIED. Not evidence of recall, by the rule v7 applied: every control on these bytes answered "No" too (Sonnet 5 v7-e, below the floor; Haiku 4.5 v7-f, via a wrong premise), as did Opus 5 v7-a. The window two minors above the stated boundary did not charge.
correctsession.freshAge (measured from session.createdAt) TASK 5, graded independently of task 1: (a) createdAt, "fairly but not fully confident … I remember the check using updatedAt at one point, and then changing to createdAt, because updateAge refreshes kept every active session looking fresh"; (b) session.freshAge: 0. Both correct at 1.7.6. Its task 3(b) repeats the anchor and gives the honest last-use workaround (own guard, freshAge: 0).
correctsession.freshAge anchor option (does not exist) TASK 3, the pre-registered control: "No", correctly — no option chooses the freshness anchor at any release, and none at 1.7.6 (freshAgeFrom/freshAgeBasis/freshFrom absent across dist). P3 HELD on both draws.
correctcustomSession / customSessionClient TASK 4, the floor: customSession on the server and customSessionClient<typeof auth>() on the client — the 1.0.0 answer, correct. P4 HELD on both draws.
contextstateless sessions (cookie-cache JWT/JWE, no database) TASK 7, attribution anchor (belief data, never scored): stateless / database-less sessions dated to 1.4, "around late 2025 (roughly November)" — correct (1.4.0, 2025-11-22). P6-of-v7's drift did not appear here.

Sources

Battery specification: prompts/better-auth.md in the studio repo. Every finding above also carries its own citation.