Picture opening an old chat-enabled app of yours and searching the source for "claude". The model name turns up in more places than you expected: one environment variable, one screen component, one server-side function. Which of them is actually live is not obvious at a glance.
According to Anthropic's deprecations page, Claude Sonnet 4.5 was announced as deprecated on September 30, 2026, with retirement on November 30, 2026 (marked tentative in the status table). The suggested replacement is claude-sonnet-5-5. That is 61 days of notice, in line with the stated policy of at least 60 days for publicly released models.
Here is a script that finds the model IDs left in your project, and how I would order the fixes depending on where each one turns up.
First, what is and is not in scope
Two things are easy to mix up.
- The model menu inside Rork chooses the AI that helps you build the app. It is separate from the model your app calls at runtime. For the menu side, see Sonnet 5.5・GPT-6.1 Sol・Opus 5.5 のどれに頼むかを、修正の種類で先に決めておきます (Japanese title; the article has an English version).
- What needs checking is any Claude API call you added to the app yourself: chat, summaries, caption generation, query rewriting, and so on.
The only row I could confirm on the deprecations page was the dated ID claude-sonnet-4-5-20250929. The undated alias claude-sonnet-4-5 is not listed, so the script treats it as the same family and flags it. claude-haiku-4-5-20251001 is Active with no retirement date, only a "not sooner than October 15" estimate, so it goes in a watch column rather than the action column.
The scan script
No dependencies, just Node.js. It collects every string starting with claude-, and skips node_modules and generated ios / android folders.
// scan-claude-models.mjs — find every Claude model ID in a project
// usage: node scan-claude-models.mjs ./my-app
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative, extname } from "node:path";
const root = process.argv[2] ?? ".";
const SKIP_DIRS = new Set(["node_modules", ".git", ".expo", "ios", "android", "dist", "build"]);
const EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".json", ".env", ".toml", ".yml", ".yaml", ".sql", ".md"]);
// Update these from Anthropic's model deprecations page (dates are as checked on 2026-10-25)
const RETIRING = {
"claude-sonnet-4-5": "retires 2026-11-30 (suggested replacement: claude-sonnet-5-5)",
};
const WATCH = {
"claude-haiku-4-5": "no retirement announced (only a 'not sooner than 2026-10-15' estimate)",
};
const idPattern = /claude-[a-z0-9]+(?:-[a-z0-9]+)*/g;
const hits = [];
function walk(dir) {
for (const name of readdirSync(dir)) {
if (SKIP_DIRS.has(name)) continue;
const path = join(dir, name);
const st = statSync(path);
if (st.isDirectory()) { walk(path); continue; }
const isEnv = name === ".env" || name.startsWith(".env.");
if (!isEnv && !EXTS.has(extname(name))) continue;
if (st.size > 1_000_000) continue;
readFileSync(path, "utf8").split("\n").forEach((line, i) => {
for (const m of line.matchAll(idPattern)) {
hits.push({ id: m[0], file: relative(root, path), line: i + 1 });
}
});
}
}
walk(root);
// Treat dated IDs (claude-sonnet-4-5-20250929) and the undated alias as the same family
const base = (id) => id.replace(/-\d{8}$/, "");
let danger = 0;
for (const h of hits) {
const b = base(h.id);
const label = RETIRING[b] ? "ACTION" : WATCH[b] ? "WATCH" : "OK";
const note = RETIRING[b] ?? WATCH[b] ?? "";
if (RETIRING[b]) danger++;
console.log(`${label} | ${h.id} | ${h.file}:${h.line} | ${note}`);
}
console.log(`\n${hits.length} found / ${danger} need action`);
process.exit(danger > 0 ? 1 : 0);Run it like this:
node scan-claude-models.mjs ./my-appIt exits with code 1 when anything needs action, so you can later wire it into a pre-build check. Edit RETIRING and WATCH from the deprecations page whenever you run it; the status table changes over time.
This is the output on a dummy project with model IDs placed in four spots (plus one inside node_modules, to confirm it is skipped):
ACTION | claude-sonnet-4-5-20250929 | .env:1 | retires 2026-11-30 (suggested replacement: claude-sonnet-5-5)
ACTION | claude-sonnet-4-5 | src/chat.ts:1 | retires 2026-11-30 (suggested replacement: claude-sonnet-5-5)
WATCH | claude-haiku-4-5-20251001 | src/chat.ts:2 | no retirement announced (only a 'not sooner than 2026-10-15' estimate)
ACTION | claude-sonnet-4-5-20250929 | supabase/functions/chat/index.ts:1 | retires 2026-11-30 (suggested replacement: claude-sonnet-5-5)
4 found / 3 need action
Where it was found decides the order
Every ACTION line looks the same, but the time each takes to fix is very different.
| Where it was found | What a fix requires | Start |
|---|---|---|
| Server-side function (Supabase Edge Functions, etc.) | Change the ID and redeploy. No store review | By the second week of November |
App code, or an EXPO_PUBLIC_ environment variable | The value is baked into the build, so you need a new release and users who update | This week |
Expo inlines EXPO_PUBLIC_ variables into the app at build time. Editing .env on your machine does nothing for an app already in users' hands. Add store review time and the days it takes people to update, and the runway to November 30 is shorter than it looks.
As an indie developer, the rule I draw from this: keep the model ID on the server, not in the app. It follows the same logic as keeping the API key out of the app, and the next time a model is swapped out, a redeploy is all it takes. If the scan shows IDs baked into the app, moving them server-side during this migration saves you the same work next time.
Compare outputs before swapping
The suggested replacement is claude-sonnet-5-5, but I would not treat this as a find-and-replace. Response length and tone can shift, and that can affect layouts or whatever processes the output downstream.
When I swap a model in my own apps, my order is:
- Pick three real inputs from your app and save the current model's outputs.
- Run the same inputs with the new ID and compare length, format (if a feature returns JSON, does it still parse?), and tone.
- Check pricing and rate limits on the official pricing page before switching. I am deliberately not quoting numbers here, since they may have changed.
- If it all holds up, change the server-side ID and redeploy.
In the same spirit of checking before you upgrade, I also wrote a pre-upgrade scan for Expo: SDK 58 で削除された React Native の API を、アップグレード前に洗い出すスクリプト (Japanese title).
One thing to do today
Run the script once and count the ACTION lines. Zero means nothing to do this time. Even one means finding out whether it sits on the server or in the app, and that answer settles the order of everything else.
Dates on the deprecations page can change, so look at the Sonnet 4.5 row once more right before you make the switch.