mcpbeat Sign in

Restricted Marker Is Not An Opt In Agent Skill

Tell an opt-in marker from a restricted-to marker before adding suppressions — the first is enforced by the compiler and demands acknowledgement, the second demands nothing and means something different and worse; includes how to read which one an API carries straight out of the cached artifact, and why both conventions coexist in one library. Use when an `@OptIn` looks necessary but the same API compiles without it elsewhere, when the IDE offers a suppression for an annotation you have not read, or when deciding whether a library call is safe to depend on.

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 restricted-marker-is-not-an-opt-in

The instruction itself

3 sections, as written by the author

A restricted-to marker is not an opt-in

Two annotations in one library package, differing by a prefix, are enforced by completely different

machinery. One is @RequiresOptIn: the compiler refuses the call until you acknowledge it. The other

is @RestrictTo: the compiler does not care at all — it is a lint-visible statement that the API is

internal to the library's own group.

Guessing between them produces two opposite mistakes: suppressions that do nothing, and a

false sense that an API is public because it compiled.

Traps

Compiling without a suppression proves the marker is *not* an opt-in — nothing more. In this tree

two such APIs are called with no acknowledgement at all, while a sibling file wraps two further calls

to one of them in an @OptIn naming the similarly-spelled *experimental* marker — which acknowledges

nothing, because that is not the marker either API carries. All of it ships, so the suppression is

not what makes any of it compile; the file that has one is a live specimen of the trap below.

An unnecessary @OptIn is a warning, and warnings do not survive review. Once written it looks

exactly like a needed one. Worse, it names a real marker, so a genuinely experimental API added to

that file later is silently pre-suppressed and nobody is asked the question the mechanism exists to

ask.

**"The file next door has one" is not evidence, because both conventions coexist inside one

artifact.** Here the design-system artifact declares both markers, and different components carry

different ones — a loading component is genuinely experimental and *does* require the opt-in, while

the theme wrapper and the wavy indicator carry only the restricted-to marker. A per-file convention

cannot be inferred from a neighbour; it has to be read per API.

Check the build files before concluding anything from an absent @OptIn. A module-wide

optIn(...) in the Kotlin compiler options makes every file look clean and destroys the evidence.

Confirm there is none before reasoning from file-level annotations.

Restricted-to is the more serious finding, not the lesser one. An experimental API is public and

expected to change with a deprecation path. A restricted-to API is not public at all: the library

group reserves the right to rename or delete it in a patch release, and no deprecation cycle is

promised. "It compiles and needs no opt-in" therefore reads as *safer* while meaning *less* stable.

Decide deliberately, and leave a comment at the call site saying you did.

Whether lint enforces it depends on the module. The restricted-to check is a lint rule, so a

multiplatform or non-Android module frequently never runs it — which is why such a call can sit in a

codebase for months with no diagnostic anywhere.

These facts are per version, not permanent. APIs graduate: a marker that is restricted-to today

becomes public tomorrow, and an experimental one loses its marker when it stabilises. Everything

above is true as of the versions currently pinned in this tree — re-run the census below after any

bump rather than trusting a note like this one.

An acknowledgement covers a marker, not an API. Every experimental API sharing that marker inside

the annotated scope is silenced at once, including ones added later. That is why an unnecessary

acknowledgement is worse than noise and why the scope should be as small as the call.

Prefer a function-level acknowledgement to a file-level one. A @file:OptIn covers everything in

the file forever, including code added months later that nobody meant to exempt — the same disease as

an unnecessary suppression, spread across a whole file. Annotate the function that makes the call.

Do not "fix" it by opting in to both — and expect no help from the compiler if you do.

Acknowledging a marker that is not an opt-in marker compiles. It raises

OPT_IN_ARGUMENT_IS_NOT_MARKER, a *warning* whose own text says the annotation is ignored, so the

line fixes nothing, stops nothing, and then sits in the file looking exactly like a real

acknowledgement — this skill's own trap, arriving through the door you assumed was locked. Only the

wrong-*target*, wrong-*retention* and subclass-argument variants are errors; the plain one is not,

unless the build promotes warnings. Acknowledging the experimental marker to silence a restricted-to

lint warning suppresses nothing either. If lint objects, the honest options are to stop using the API

or to suppress the specific lint check with a reason.

Verifying it

  • Every marker the tree currently acknowledges, with counts — the list you are about to audit:
   grep -rhoE "@(file:)?OptIn\([^)]*\)" --include='*.kt' --exclude-dir=build . \
     | tr ',' '\n' | grep -oE "[A-Za-z0-9_]+::class" | sort | uniq -c | sort -rn

The file: alternation is not optional: a regex anchored on @OptIn( misses every file-level

acknowledgement, which is the broadest kind and therefore the one you most want in the census.

  • Which kind each marker in the library actually is. Read-only, no build required — the annotation

class carries its own answer:

   JAR=$(find ~/.gradle/caches/modules-2 -name '*material3*.jar' ! -name '*sources*' | sort -V | tail -1)
   echo "$JAR"; D=$(mktemp -d); unzip -oq "$JAR" -d "$D"
   for C in "$D"/androidx/compose/material3/*Api.class; do B=$(basename "$C" .class)
     printf '%-42s %s\n' "$B" "$(javap -v -p -cp "$D" "androidx.compose.material3.$B" 2>/dev/null \
       | grep -oE 'kotlin/RequiresOptIn|androidx/annotation/RestrictTo' | head -1)"
   done

Expect a mixed listing: several kotlin/RequiresOptIn markers and at least one

androidx/annotation/RestrictTo. That mix is the point — find both before deciding anything.

sort -V picks the newest *cached* artifact, not necessarily the one your build resolves; read

the echoed path, and swap the artifact name to audit a different library.

  • Which marker specific APIs carry — reusing $D from step 2, run against the file classes that

declare them (top-level Kotlin functions live in a …Kt class named after their source file).

Pick components you actually call; two of these three differ from the third:

   for K in WavyProgressIndicatorKt MaterialThemeKt LoadingIndicatorKt; do
     printf '%-26s %s\n' "$K" "$(javap -v -p -cp "$D" "androidx.compose.material3.$K" 2>/dev/null \
       | grep -oE 'androidx[/.]compose[/.]material3[/.][A-Za-z0-9]*Api' | sed 's|.*[/.]||' | sort -u | paste -sd' ')"
   done
  • No module-wide opt-in is hiding the evidence. Every hit needs reading:
   grep -rn "optIn\|freeCompilerArgs" --include="build.gradle.kts" --exclude-dir=build .
  • That a wrong acknowledgement really is only a warning here — the claim above rests on it, and it

is a property of your compiler and your build, not of the language. Same technique as step 2: the

diagnostic table is a class in the compiler artifact, and each entry names its own severity.

   J=$(find ~/.gradle/caches/modules-2 -name 'kotlin-compiler-embeddable*.jar' ! -name '*sources*' | sort -V | tail -1)
   echo "$J"; C=$(mktemp -d); unzip -oq "$J" -d "$C"
   javap -c -p -cp "$C" org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors \
     | awk '/ldc(_w)? +#[0-9]+ +\/\/ String OPT_IN/ {n=$NF; getline
            if ($0 ~ /Severity\./) {split($0,a,"Severity."); split(a[2],b,":"); printf "%-52s %s\n", n, b[1]}}'
   grep -rn "allWarningsAsErrors\|-Werror" --include='*.gradle.kts' --exclude-dir=build .

Expect a mixed listing: the argument-is-not-marker row WARNING, the wrong-target and

wrong-retention rows ERROR. The second command must print nothing — a build that promotes

warnings turns the whole family into errors and the reasoning above no longer applies.

How to use it

Copy the folder

Take maxrave-dev/restricted-marker-is-not-an-opt-in 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.