mcpbeat Sign in

Settings Value Round Tripped Through Its Label Agent Skill

A selection dialog that maps the chosen localized label back to a stored value breaks the day two labels translate identically — the write is skipped or lands on the wrong option, with no error; covers carrying the id instead of the text, making the miss loud, and the sibling hazard of a default declared both in the store and as the collector's initial value. Use when a settings choice does not stick in one language only, when a picker writes a neighbouring option, or when a screen renders the wrong variant for a moment on entry.

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 settings-value-round-tripped-through-its-label

The instruction itself

3 sections, as written by the author

Do not round-trip a setting through its own label

The shape is everywhere: pair each stored value with its translated caption, show the captions, get

back the chosen caption, look the value up again by string equality.

// adapted — the reverse lookup that is the bug; list and setter renamed, body inlined
styleLabels.firstOrNull { it.second == selected }?.first?.let { setStyle(it) }

Forward is fine — value to caption, for the subtitle under the row. It is the return trip that

depends on every caption being unique in every language, which is not a property anyone maintains.

Traps

Two identical translations silently disable the write. Many languages have no distinct wording

for two neighbouring options, and translators are not shown the constraint. When it happens the

lookup either fails — the ?.let swallows it, the dialog closes, the setting is unchanged, nothing

is logged — or, worse, matches the *first* pair with that caption and writes the wrong value. The

same failure arrives from a stray trailing space, a differing apostrophe, or a caption that renders

one way in the list and another in the dialog.

Carry the id, not the text. The selection API should hand back an index or the value itself. If

the dialog is yours, that is a small change and it removes the whole class. If it is not, map by

position against the *same list instance* you built the captions from.

Positional mapping only works if the list is built once. Option lists are frequently conditional

— an entry added only on a platform that supports it — so a list rebuilt in the dialog callback can

have different indices from the one that was displayed. One val built once, read by both the

display and the callback.

Make the miss loud. ?.let { } is the wrong terminator for a lookup that must succeed. Log a

warning, or throw in debug builds. A settings write that quietly does nothing is reported months

later as "it doesn't save", by one user, in one language.

A default declared in the store and again at the collector is two places to drift. The store

falls back to a default when the key is absent; the screen collecting that flow supplies an initial

value for the frames before the first emission. When they disagree, the screen renders the *other*

option for a beat on every entry — and a "reset to defaults" path can write the value the store

believes while the UI showed the collector's. Reference one named constant from both, and prefer a

store API that never needs a second default (a flow with a start value, or a state flow).

The boolean version of that drift is the same bug, just cheaper to spot. A screen full of false

collector defaults is a list of duplicated defaults; each is a wrong first frame if the store's

fallback is true. Audit them the same way — but note that a store keeping its flags as named string

constants writes its false as a dotted name, not as a literal, so the two halves of one screen are

spelled differently and a filter that knows only false silently keeps every one of the others.

The forward direction has its own quiet failure. The row's subtitle looks the caption up from the

stored value and falls back to an empty string when nothing matches — so a value written by an older

build, or by the wrong-option bug above, renders as a settings row with a blank subtitle rather than

as an error. Fall back to the default option's caption instead, and log the miss.

Watch what else that click lambda does. Dialog titles and buttons in this pattern are frequently

fetched with a blocking resource read. Inside an event handler that is tolerable; the same call from

composition is not — see compose-multiplatform-viewmodel-base.

Verifying it

  • Every reverse lookup by caption. Each hit is either a bug or needs a comment explaining why the

captions are guaranteed unique:

   grep -rn --include='*.kt' --exclude-dir=build "it.second == \|\.second == selected" .
  • Every lookup that ends in a silent ?.let, which is how the miss stays invisible:
   grep -rn --include='*.kt' --exclude-dir=build "firstOrNull {.*}?\..*?\.let {" .
  • Collector defaults that are not plain booleans — these are the ones whose drift is visible as a

wrong first frame. Each hit must name the same constant the store falls back to in step 4:

   grep -rnE --include='*.kt' --exclude-dir=build \
     "collectAsState[A-Za-z]*\((initialValue *= *)?[A-Za-z_][A-Za-z0-9_.]*\)" . \
     | grep -vE "\((initialValue *= *)?(true|false|null|[A-Za-z0-9_.]*\.(TRUE|FALSE))\)"

Both halves of that pattern are load-bearing. The argument position has to be optional, because

the default is as legally passed positionally as by name — and a codebase mixes the two, so an

audit anchored on initialValue = reports a clean screen while the twin declaration of the very

setting you are auditing sits in another file, in the other spelling. The exclusion has to cover

both spellings of a boolean for the reason in the trap above, or widening the match simply refills

the listing with the flags it was meant to drop. null is excluded on different grounds: it is a

not-yet-loaded sentinel rather than a default anyone declared twice.

  • The store's own fallbacks, to compare against step 3:
   grep -rn --include='*.kt' --exclude-dir=build "preferences\[.*\] ?: " .
  • By hand, and this is the test that finds it: switch the app to a language where two options of one

setting read the same, choose the second of them, reopen the dialog. If the first is ticked — or

the old value is — the round trip is going through the caption.

How to use it

Copy the folder

Take maxrave-dev/settings-value-round-tripped-through-its-label 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.