mcpbeat Sign in

Derive The Flag Dont Store And Correct It Agent Skill

A boolean that is a pure function of state already being collected gets stored as its own `mutableStateOf` anyway, seeded with a guess and corrected a frame later by a `LaunchedEffect` — so the first frame renders the guess, and later changing only the seed value does nothing once `rememberSaveable` has already saved the old one. Use when a UI element visibly flashes shown-then-hidden-then-shown on cold start, or when editing a `remember`/`rememberSaveable` initializer doesn't change what a warm app already shows.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill derive-the-flag-dont-store-and-correct-it

The instruction itself

3 sections, as written by the author

Derive it, don't store a guess and patch it

A value fully determined by state the composable already collects sometimes still gets its own slot —

remember or rememberSaveable { mutableStateOf(...) } — written to by a LaunchedEffect that

watches the real source and copies a derived answer into it. That slot is strictly worse than reading

the source directly, for the two reasons below: an initial guess renders before anything can correct

it, and the "obvious" fix to the guess does not reach a process that already saved the old one.

// adapted — trimmed to the two relevant declarations; both are otherwise verbatim
// before
var isShowMiniPlayer by rememberSaveable { mutableStateOf(true) }
// ...
LaunchedEffect(nowPlayingData) {
    isShowMiniPlayer = !(nowPlayingData?.mediaItem == null || nowPlayingData?.mediaItem == GenericMediaItem.EMPTY)
}

// after — nothing to be wrong independently of its source
val isShowMiniPlayer by remember {
    derivedStateOf {
        val item = nowPlayingData?.mediaItem
        item != null && item != GenericMediaItem.EMPTY
    }
}

Traps

**A value nothing but its own derivation should ever write does not need a slot anything could write

to.** isShowMiniPlayer has exactly one legitimate value at every instant: whatever nowPlayingData

says. A separate mutableStateOf means the compiler no longer enforces that — it holds whatever the

last write left it holding, from whichever code path wrote last. Every trap below is a way that

separate value ends up disagreeing with its source.

The effect meant to correct the guess cannot run before the first frame that already used it.

Composition happens, then effects run — never the reverse. rememberSaveable { mutableStateOf(true) }

renders true on frame one, before LaunchedEffect(nowPlayingData) has run even once, so a cold

start shows the element on a screen that has not connected to any data yet. The effect then runs, sees

the real (absent) state, and flips it off; real data arrives shortly after and flips it back on. Three

renders — shown, hidden, shown — for a value that should have had one answer the whole time. This is

not a race: it is the ordering LaunchedEffect always uses, so it reproduces on every single launch.

**Changing the seed does nothing for any process that already saved the old value — a rotation, or a

return from the background after the system killed it.** The tempting one-line fix — seed

mutableStateOf(false) instead of true — changes nothing there, because rememberSaveable

*restores* a previously saved value across that recreation rather than re-running its initializer. A

warm app that already saved true under the old code goes on reading true regardless of what the

literal now says. This survival is narrower than "persists": a swiped-away task, a force-stop, or a

fresh install/update all discard the saved bundle and pick up the new seed immediately, same as a

cold start. When a fix to a remember/rememberSaveable default appears to do nothing, check it on

a warm app first — rotate the device, or background it and let the system reclaim the process — and

only trust "the fix works" once it also survives that, not just a cold reinstall.

The same derived flag can exist as two separate copies, one per composable that draws the element.

Here it did: a second, platform-specific composable rendered its own version of the same bar with the

same rememberSaveable + LaunchedEffect pair, verbatim. Fixing the first composable changed nothing

on the configuration that renders through the second, which reads as "the fix doesn't work" rather

than "the fix works everywhere except the file that still has the old copy." Grep for the pattern

project-wide, not just in the file the bug report points at.

**A nullable holder compared against both null and an empty sentinel is a sign the holder should

carry the sentinel too.** nowPlayingData?.mediaItem == null || nowPlayingData?.mediaItem == EMPTY is

two questions because nowPlayingData itself is nullable on top of mediaItem having its own empty

value — see empty-sentinel-instance for why that doubling happens and when it is avoidable.

Verifying it

  • Find the shape: a remember/rememberSaveable { mutableStateOf(...) } boolean, and check

whether its only assignment anywhere in the file sits inside a LaunchedEffect body. Match the

declaration alone — a literal rememberSaveable { mutableStateOf on one line finds only 57 of the

144 by rememberSaveable sites in this codebase, silently skipping every declaration that wraps

its initializer onto its own line, which is exactly the shape the worked example above starts from:

   grep -rn "by rememberSaveable" --include="*.kt" . | grep -v "/build/"

For each hit, look for a LaunchedEffect nearby assigning the same name from a value already in

scope — that pairing, not the declaration alone, is the trap; plenty of legitimate rememberSaveable

booleans are written from a click handler instead and are not this bug. Running this against the

whole tree, not one file, is also the check for a second copy: a platform-specific composable can

carry its own copy of the exact same pair, fixed by touching neither.

  • Cold start, watch one frame. Launch onto the screen that shows the element from a fully killed

process. A value flashing present-absent-present in the first second is frame one using a guess.

  • Warm-state check. With app state already saved from a build that shipped the old seed, change

only the mutableStateOf literal, then rotate the device — or enable Developer Options → Don't

keep activities and background/foreground the app — instead of a cold relaunch, which would go

through the swiped-away-task path and clear the saved value regardless of which fix is right. No

visible change confirms the seed was never the bug — the field holding it is.

How to use it

Copy the folder

Take maxrave-dev/derive-the-flag-dont-store-and-correct-it from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.