TL;DR — Svelte 5's runes are powerful and quiet about their failure modes. Generic AI review bots (CodeRabbit, Greptile, the default Copilot reviewer) don't model them, so they approve secret leaks, SSR crashes, and broken reactivity without a word. This post catalogues 10 Svelte 5 / SvelteKit footguns, with safe rewrites, and ships a free GitHub Action (Marketplace) that flags them in the PR.
One import that shipped an API secret to every browser
You needed a feature flag in a component, so you reached for the env:
import { STRIPE_SECRET_KEY } from '$env/static/private'; // ❌
const flagsOn = STRIPE_SECRET_KEY.length > 0;
It type-checked. It worked locally. And in a universal component, $env/static/private is reachable on the client — so SvelteKit either refuses to build, or (in the subtler dynamic/server-data variants below) your secret rides along to the browser bundle.
The reviewer approved it. The generic AI bot approved it. Nothing in the standard flow says "this value is server-only and you just pulled it into client-reachable code."
This is one of ten footguns I want to put on the table.
Why generic review bots miss all of these
A general-purpose bot reviews your diff as TypeScript. It doesn't know:
- Rune semantics. That a $effect writing state it derives should be a $derived. That destructuring $state snapshots the value and drops reactivity. These are valid TS — the bug is in the framework contract.
- SvelteKit's server/client boundary. Which files run where. A +page.ts runs on the server and the client; a +page.server.ts never reaches the browser. The bot can't see that distinction in a diff.
- SSR. That touching window at module top level runs during server render and crashes the page, not just the browser.
So the review passes. The bug ships.
The catalogue — 10 patterns to never merge
Runes & reactivity
1. $effect used to compute derived state
let total = $state(0);
$effect(() => { total = price * qty; }); // ❌
This creates an extra render pass and can loop. If a value is a pure function of other state, it's derived.
Safe rewrite:
const total = $derived(price * qty); // ✅
2. An effect that loops by writing state it reads
$effect(() => { count = count + items.length; }); // ❌ reads + writes count
Schedules itself forever.
Safe rewrite: derive it, or read with untrack() if a side effect is truly intended.
3. Destructured $state loses reactivity
let user = $state({ name: 'Ada' });
const { name } = user; // ❌ name is now a plain string snapshot
Safe rewrite: keep the access live — user.name in the template, or wrap with $derived.
4. Non-reactive let in a .svelte.ts module
// store.svelte.ts
let count = 0; // ❌ exported and mutated, but nothing re-renders
Safe rewrite: let count = $state(0); — runes work in .svelte.ts, plain let doesn't react.
5. Mutating a non-$bindable prop
let { value } = $props();
function reset() { value = ''; } // ❌ won't propagate to parent
Safe rewrite: declare let { value = $bindable() } = $props(); if two-way is intended, or emit an event/callback.
SvelteKit server/client boundary
6. Secrets returned from a universal load
// +page.ts — runs on client too
export const load = async () => ({ apiKey: process.env.SECRET }); // ❌
Safe rewrite: move it to +page.server.ts. Universal loads serialize their return value into the HTML.
7. $env/static/private (or $env/dynamic/private) in client-reachable code
Covered above. Private env belongs only in *.server.ts, server hooks, or +server.ts.
Safe rewrite: read it server-side, pass only the derived, non-secret result to the client.
8. window / document / localStorage at module top level
const theme = localStorage.getItem('theme'); // ❌ runs during SSR → crash
Safe rewrite: guard with import { browser } from '$app/environment', or read inside onMount (client-only).
Security & accessibility
9. {@html} on unsanitized input
{@html comment.body}
Safe rewrite: sanitize server-side before render, or don't use {@html} for user content.
10. Missing a11y basics
Images without alt, inputs without an associated , click handlers on non-interactive elements with no keyboard equivalent. The compiler warns on some; a reviewer should catch the rest.
What about ESLint / the Svelte compiler warnings?
Both are great and you should run both. Neither is enough:
- eslint-plugin-svelte — excellent for style and some rune misuse, but doesn't model the SvelteKit server/client boundary across files, and won't reason about whether a returned value is secret.
- Svelte compiler a11y warnings — cover a subset of accessibility, nothing on reactivity or leaks.
- Generic AI review bots (CodeRabbit, Greptile, default Copilot) — review the diff as TypeScript; they'll happily approve a $effect-as-$derived or a private-env leak.
There's a gap: a reviewer that actually knows Svelte 5 + SvelteKit semantics. So I built it.
Catch them in the PR — svelte-autopilot
Svelte Autopilot is a free, MIT-licensed GitHub Action that reads each PR's diff and leaves a single, focused review of the Svelte 5 / SvelteKit issues that matter — and keeps it updated as you push.
Two minutes to set up:
# .github/workflows/svelte-review.yml
name: Svelte Autopilot
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: isabellehuecloser-ctrl/svelte-autopilot@v0
with:
api-key: ${{ secrets.OPENAI_API_KEY }}
No checkout needed — the diff is read through the GitHub API. Bring your own OpenAI key (gpt-4o for accuracy, gpt-4o-mini to cut cost). A hosted Pro version runs with no API key and no CI setup.
What I'd love your feedback on
- Which Svelte 5 footgun is missing? Especially around $derived.by, snippets, and await in load.
- False positives on your real SvelteKit app? Install it on a branch and tell me what fires wrongly.
- Is the single-comment format right, or do you want inline?
Source, issues, contributions:
https://github.com/isabellehuecloser...elte-autopilot
If it catches a real footgun on a PR of yours — a ⭐ on the repo tells me it's landing.
References & further reading:
More...