Skip to main content
skill CX Codex

kotlin-multiplatform — Claude Code skill: KMP and Gradle

A skill that tells the agent which Amethyst module and source set a file belongs to, so the KMP build and its purity gate stay green.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a claude-code
Open on GitHub

What kotlin-multiplatform does

The skill answers the two things agents get wrong in a Kotlin Multiplatform repo: where a file goes, and how the Gradle wiring should look. It maps the modules listed in settings.gradle.kts and explains when to reach for expect/actual, when for a port interface, and when for a composition-local slot. Then comes verifyKmpPurity, the gate applied by commons and commonsUI. It scans every iOS and native-compiled source set and fails on okhttp3, com.fasterxml.jackson, System.currentTimeMillis, Thread.sleep, java.util.UUID and synchronized(.

Build traps get their own section. Versions belong in gradle/libs.versions.toml and nowhere else, test resolves to each module's jvmTest, and testFixtures needs a top-level dependencies block with the configuration name as a string. The secp256k1 JNI artifacts differ per source set, so the Android variant on a JVM classpath throws UnsatisfiedLinkError at load.

Expect it to point at AGENTS.md, commons/ARCHITECTURE.md and tools/r8-verify/. Those files live in Amethyst and nowhere else, so dropping the skill into an unrelated repository leaves the agent with nothing to read — forking it and rewriting the references is the honest option. After touching commonMain, the author suggests running ./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity.

What it helps with

  • place a new class in commonMain instead of jvmAndroid
  • pick expect/actual over a port for a platform call
  • fix a verifyKmpPurity failure on java.util.UUID
  • add a dependency through libs.versions.toml
  • move an Android composable into commonsUI

Example prompts for the agent

  • Where should this class live: commonMain or jvmAndroid?
  • Run ./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity and explain the failures.
  • Extract this Android composable into commonsUI following references/extract-composable.md.

How to install kotlin-multiplatform

1Skills CLI (Vercel)

npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a claude-code

2Manually: put the files in place

~/.claude/skills/kotlin-multiplatform/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/kotlin-multiplatform/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about kotlin-multiplatform

How to install kotlin-multiplatform in Claude Code?

Run npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a claude-code. The skill lands in ~/.claude/skills/kotlin-multiplatform/SKILL.md.

What does the kotlin-multiplatform skill do?

It covers module and source set placement in Amethyst, the choice between expect/actual, ports and slots, plus the Gradle and R8 traps that break KMP builds.

Does the skill work outside the Amethyst repository?

It leans on AGENTS.md, commons/ARCHITECTURE.md, commonsUI/ARCHITECTURE.md and the verifyKmpPurity task. Without those files the agent has no project rules to follow.

Source text

SKILL.md · MIT
---
name: kotlin-multiplatform
description: KMP placement and Gradle in Amethyst: which module or source set, expect/actual vs ports, verifyKmpPurity, extracting Android composables to commonsUI, build.gradle.kts/libs.versions.toml edits, secp256k1 JNI variants, R8.
---

# Kotlin Multiplatform and Gradle in Amethyst

Module roles, the "share vs keep native" rule and the CLI-safe boundary are in
`AGENTS.md` (Architecture, Feature Workflow). Package placement inside `commons` /
`commonsUI` is in `commons/ARCHITECTURE.md` ("Where does my code go?", "Source sets")
and `commonsUI/ARCHITECTURE.md` ("Platform slots"). Read those first; this skill
adds the source-set mechanics and the build traps.

## Modules

`settings.gradle.kts` includes 13: `amethyst`, `nappletHost`, `benchmark`,
`baselineprofile`, `geode`, `commons`, `commonsUI`, `nestsClient`,
`marmotQuic`, `desktopApp`, `cli`, `relayBench`, `marmotBench`. Quartz is the
external `libs.quartz` (vitorpamplona/quartz) and QUIC the external `libs.quic`
(vitorpamplona/quic-kmp); `-PquartzDir=../quartz` / `-PquicKmpDir=../quic-kmp` build a
local checkout instead (an included build, so quic's own quartz dependency follows).

## Source sets

| Source set | Holds | Targets |
|---|---|---|
| `commonMain` | Pure Kotlin. Compiled for iOS/native too, so no JVM APIs | all |
| `jvmAndroid` | JVM libraries shared by Android + Desktop: OkHttp, `nestsClient`, NWC/LNURL | android, jvm |
| `androidMain` / `jvmMain` | Platform actuals (Keystore/DataStore vs keyring, Coil bridges) | one each |
| `iosMain`, `appleMain`, `nativeMain`, `linuxMain`, `macosMain` | quartz only (plus `iosMain` in commons/commonsUI) | Apple / native |
| `skikoMain` | commonsUI code shared by Desktop and iOS | jvm, ios |

Targets: `quartz` builds android, jvm, iosArm64, iosSimulatorArm64, linuxX64,
macosArm64. `commons` and `commonsUI` build android, jvm, iosArm64,
iosSimulatorArm64. `nestsClient`, `marmotQuic` are android + jvm. There is
no `iosX64`.

`jvmAndroid` is a custom intermediate set created with `create("jvmAndroid")`
before `androidMain`/`jvmMain` depend on it (see `commons/build.gradle.kts`).
Prefer the most common set that compiles: `commonMain` → `jvmAndroid` → platform.

kotlinx.serialization is the house JSON stack and OkHttp the house HTTP stack
(`AGENTS.md` › Tech Stack). Jackson is left only inside quartz's JVM code and reaches
the app modules transitively; don't add new Jackson use here. Don't migrate JVM HTTP
to Ktor. Jackson and OkHttp are banned in iOS-compiled sets, which the purity gate
enforces.

## expect/actual vs ports and slots

- **expect/actual** for a small platform verb or value with no app state:
  `KmpLock`, `ConcurrentSet`, `SecureKeyStorage`, `isDebug`, `formattedDateTime`
  (commons), `rememberTextSharer`, `rememberShortNotice` (commonsUI). Put the actual
  in `jvmAndroid` when Android and Desktop share it.
- **A port** (interface the shell implements) when the platform side needs app
  state or services: `AccountViewModelHost`, `UserNoticeSink`, `LocalCacheHost`.
- **A slot** (CompositionLocal the front end installs) for things only a front end
  can draw: `LocalNotePlatform`, `LocalRichTextPlatform`.
- Not every quartz singleton is expect: `Secp256k1Instance`, `Log` and
  `LibSodiumInstance` are plain `object`s in `quartz/.../utils/`.
- Don't `expect` a whole screen or navigation; share it and put only the leaf
  behind a port or slot.

## Purity gate: `verifyKmpPurity`

`gradle/kmp-purity.gradle.kts`, applied by `commons` and `commonsUI` (quartz keeps a copy) and
wired into `check`, scans every iOS/native-compiled source set (`commonMain`,
`iosMain`, `skikoMain`, …, and their tests) and fails on `com.fasterxml.jackson`,
`okhttp3`, `System.currentTimeMillis` (use `TimeUtils.now()`), `Thread.sleep`,
`java.util.UUID` (use `kotlin.uuid.Uuid`), `kotlin.jvm.Synchronized` or bare
`synchronized(` (use `KmpLock`), `kotlin.jvm.Volatile` (use
`kotlin.concurrent.Volatile`). CI runs it on Linux in the lint job; the macOS
`test-commons-ios` / `test-commonsui-ios` jobs are authoritative. Run
`./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity`
after touching `commonMain`. Quartz has its own copy of the task and its own iOS CI.

## Gradle traps

- **Versions live only in `gradle/libs.versions.toml`.** Add the library there and
  reference `libs.*`; never hardcode a version or copy one into docs. Check the
  license first (`AGENTS.md` › Dependency Licensing).
- **`test` is an alias for each KMP module's `jvmTest`.** Other targets
  (`:commonsUI:testAndroidHostTest`, the native ones) run only when named; see
  `AGENTS.md` › Build Commands.
- **testFixtures in KMP:** the source-set `dependencies { }` block
  (`KotlinDependencyHandler`) has no `testFixtures(...)`. Use a top-level block with
  the configuration name as a string:
  `dependencies { "jvmAndroidTestImplementation"(testFixtures(project(":geode"))) }`.
- **secp256k1 variants:** `secp256k1-kmp-common` in `commonMain`,
  `secp256k1-kmp-jni-android` in `androidMain`, `secp256k1-kmp-jni-jvm` in `jvmMain`
  and JVM tests. All three share one `version.ref`. A JVM classpath with the
  android variant fails at load with `UnsatisfiedLinkError`.
- **R8:** library keep rules ship in `quartz/consumer-rules.pro`. Never add a blanket
  `-keep class com.vitorpamplona.quartz.** { *; }`. R8 can't see reflection, so a
  moved/renamed reflective class is a silent release break: before tagging, run
  `python3 tools/r8-verify/verify_reflection_contract.py amethyst/build/outputs/mapping/<variant>/`
  (the release workflow checks `playRelease` and `fdroidRelease`) and update
  `tools/r8-verify/reflection-contract.txt` when you move a listed class.

## Extracting Android code to shared modules

Steps and the Android → multiplatform replacement table:
[references/extract-composable.md](references/extract-composable.md).

© vitorpamplona · MIT · vitorpamplona/amethyst

Similar skills

All skills for Programming languages →

From the same set2

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor