Skip to content

/kotlin-tooling-kotlin-toolchain-plugin-authoring

Load when authoring, writing, or designing a Kotlin Toolchain local plugin to extend the declarative build with code generation, build-time processing, custom verification, or packaging that module.yaml cannot express, or when referencing @TaskAction, @Configurable, plugin.yaml,

From plugin
kotlin-agent-skills
1k10 skills
Install
$ npx -y skills add kotlin/kotlin-agent-skills --skill kotlin-tooling-kotlin-toolchain-plugin-authoring --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/kotlin-tooling-kotlin-toolchain-plugin-authoring

Context preview

The summary Claude sees to decide when to auto-load this skill.

Load when authoring, writing, or designing a Kotlin Toolchain local plugin to extend the declarative build with code generation, build-time processing, custom verification, or packaging that module.yaml cannot express, or when referencing @TaskAction, @Configurable, plugin.yaml,

SKILL.md

kotlin-tooling-kotlin-toolchain-plugin-authoring.SKILL.md
name: kotlin-tooling-kotlin-toolchain-plugin-authoring
description: >
  Load when authoring, writing, or designing a Kotlin Toolchain local plugin to
  extend the declarative build with code generation, build-time processing,
  custom verification, or packaging that module.yaml cannot express, or when
  referencing @TaskAction, @Configurable, plugin.yaml, or jvm/amper-plugin. Skip
  for porting an existing Gradle plugin.
license: Apache-2.0
metadata:
  author: github:@singleton11
  version: "0.1.0"

Kotlin Toolchain Plugin Authoring

Local plugins are the official escape hatch from declarative YAML: a `jvm/amper-plugin` module shipping task actions, settings, and generated sources/resources alongside your project. Code patterns to adapt are in [references/examples.md](references/examples.md).

When to write a plugin

Write one when you need:

  • A build-time step a library's workflow expects (code generation, schema compilation, resource

transformation, version stamping).

  • Custom verification wired into the build (pre-release checks, schema validation, contract tests).
  • A build-time value published into the JAR classpath or downstream tasks — the closest analog to Gradle's

`project.version`.

  • A named CLI command for a repeated workflow (`./kotlin do release`).

Don't write one when `module.yaml` already covers it (dependencies, JDK provisioning, source layouts, basic packaging), and never to reuse a Gradle plugin — the Kotlin Toolchain cannot consume them.

Layout

repo-root/
├── kotlin, kotlin.bat            # wrappers (from `kotlin init`)
├── project.yaml                  # registers the plugin
├── plugins/<name>/
│   ├── module.yaml               # product: jvm/amper-plugin
│   ├── plugin.yaml               # tasks: + commands: + generated:
│   └── src/
│       ├── Settings.kt           # @Configurable interface
│       ├── tasks/                # one @TaskAction per file
│       │   ├── Foo.kt
│       │   └── FooSteps.kt       # internal shared helpers (not @TaskAction)
│       └── <domain logic>/
└── <consumer-module>/
    └── module.yaml               # plugins: { <name>: enabled: true, ... }

Keep at least one consumer module in the repo — it is the only way to exercise the plugin end-to-end, and plugins cannot be published to any public registry yet.

`project.yaml`

modules:
  - consumer-app
  - plugins/<name>

plugins:
  - ./plugins/<name>

Without the top-level `plugins:` block the plugin id is unresolvable from any consumer.

`module.yaml` — the plugin module

product: jvm/amper-plugin          # marks the module as a plugin

dependencies:
  - <coordinate>:<version>
  - <coordinate>:<version>: runtime-only   # required only at runtime
  - <coordinate>:<version>: compile-only

pluginInfo:
  id: <plugin-id>                                  # what consumers write under `plugins:`
  settingsClass: <fully.qualified.Settings>        # the @Configurable interface

settings:
  jvm:
    jdk:
      version: 21
  kotlin:
    languageVersion: 2.1

`@Configurable interface Settings`

Defaults go in interface property getters; nested blocks become nested `@Configurable` interfaces.

@Configurable
interface Settings {
    val someValue: String get() = "default"
    val checks: ChecksSettings
}

@Configurable
interface ChecksSettings {
    val strict: Boolean get() = true
}

Consumers override what they need in `module.yaml`; omitted values fall back to the getter default:

plugins:
  <plugin-id>:
    enabled: true
    someValue: "override"
    checks:
      strict: false

`@TaskAction`

Task actions are top-level `fun`s, called when the matching `plugin.yaml` entry executes.

@TaskAction
fun foo(
    @Input moduleRootDir: Path,
    @Output outputDir: Path,
    settings: Settings,
) {
    // body
}
  • `@Input path: Path` — declared input; Kotlin Toolchain snapshots its contents for execution avoidance.
  • `@Output path: Path` — declared output directory; Kotlin Toolchain creates it and passes the path in. Write

to the exact `Path` you received, or downstream references won't find the result.

  • `settings: Settings` (or any `@Configurable`) — typed configuration, wired in `plugin.yaml`.
  • Plain `Path` / primitives — passed literally from `plugin.yaml`.
  • `println(...)` is the output channel; Kotlin Toolchain captures stdout.

Execution avoidance

A `@TaskAction` is skipped when its declared inputs are unchanged. Tasks whose real inputs are Git history, the network, or environment variables cannot be fingerprinted, so opt out:

@TaskAction(executionAvoidance = ExecutionAvoidance.Disabled)
fun foo(@Output outputDir: Path, settings: Settings) { /* ... */ }

Tasks with no `@Output` are never cached and always re-run — correct for purely side-effecting tasks (releases, deployments, pushes).

`plugin.yaml`

tasks:
  foo:
    action: !<fully.qualified.foo>
      moduleRootDir: ${module.rootDir}
      outputDir: ${taskOutputDir}
      settings: ${pluginSettings}

  bar:
    action: !<fully.qualified.bar>
      input: ${tasks.foo.action.outputDir}/result.txt
      settings: ${pluginSettings}

generated:
  resources:
    - directory: ${tasks.foo.action.outputDir}

commands:
  - foo

| Reference | Resolves to | |---|---| | `${module.rootDir}` | Directory containing the consumer's `module.yaml`. Pass as `@Input` to inspect the consumer's tree. | | `${taskOutputDir}` | Toolchain-managed per-task output directory. Pass as `@Output`. | | `${pluginSettings}` | The `@Configurable` object built from the consumer's `module.yaml`. | | `${tasks.<task>.action.<param>}` | Another task's parameter — used in `generated.*` and to wire one task's `@Input` to another's `@Output`. |

`generated.resources` / `generated.sources`

Both register a directory (usually a task's `@Output`) as a contribution to the consumer's build, and both auto-wire the producing task to run first:

  • `
Read more
Ships withkotlin-agent-skills

A collection of AI agent skills useful for projects using the Kotlin language. Skills are following the Agent Skills standard, see agentskills.io for more information.

Get the whole plugin, auto-invoked

Other skills on kotlin-agent-skills.