ktrs is a native replacement for ktfmt and ktlint. It produces the same output and ships as small binaries with no runtime. Try the formatter in the playground, which runs it as WebAssembly.
- ⚡ Fast. 10-200x faster than the JVM tools and 2-16x faster than ktfmt's published GraalVM native image (Performance).
- 🎯 Identical output. Byte-identical to ktfmt 0.65 on 6,121 of 6,123 real-world files (both
tools reject the other two). Lint violations and
--formatoutput match ktlint 2.0 in all three code styles, and ktlint 1.8 in a compatibility mode (How we test). - 🔌 Drop-in. The
ktfmtandktlintbinaries accept the originals' flags, messages and exit codes, so existing scripts, hooks and CI keep working. - 🧩 Fits your setup. GitHub Actions, pre-commit, editors, Gradle, Maven and Spotless, Bazel, reviewdog and Danger.
- 🌳 Built on a faithful parser. The lossless Kotlin parser's tree matches the compiler's PSI node for node, and you can use it as a library.
curl -fsSL https://raw.githubusercontent.com/Hexay/ktrs/master/install.sh | sh # prebuilt binaries
cargo binstall ktrs # prebuilt, via cargo-binstall
cargo install ktrs # from crates.io
brew tap hexay/ktrs https://github.com/Hexay/ktrs && brew install hexay/ktrs/ktrs # Homebrew
scoop bucket add ktrs https://github.com/Hexay/ktrs && scoop install ktrs/ktrs # Scoop (Windows)
npm install --save-dev @ktrs/cli # npm (npx ktlint, npx ktfmt)
docker run --rm -v "$PWD:/src" ghcr.io/hexay/ktrs ktlint "**/*.kt" # Docker (linux amd64/arm64)One install puts three binaries on your PATH: ktrs, ktfmt and ktlint. On Windows, the
install script also works under Git Bash, or download a zip from
Releases. The Docker image takes the binary name as its first argument,
works in /src, and has no Java, so ktlint -R with a rule set other than compose-rules doesn't run there; on
Linux, add --user "$(id -u):$(id -g)" to keep fixed files owned by you.
ktrs fmt # format every .kt/.kts under the current directory
ktrs fmt --style kotlinlang src/ # styles: meta (default), google, kotlinlang
ktrs fmt --check # CI: list files that would change, exit 1 if any
ktrs fmt --changed-since main # only files changed since the merge base with main
ktrs fmt - < Foo.kt # stdin to stdout, for editors
ktrs lint # check every .kt/.kts under the current directory
ktrs lint --format src/ # autocorrect what can be fixed, report the rest
ktrs lint --changed-since main # only files changed since the merge base with main
ktrs lint --reporter json - < Foo.kt # reporters: plain, json, checkstyle, sarif, html, ...The ktfmt and ktlint binaries accept the original command lines exactly, so anything that runs
the jars can run them instead:
ktfmt --kotlinlang-style --set-exit-if-changed src/
ktlint --relative "src/**/*.kt" "!src/**/generated/**"ktfmt formats like ktfmt 0.65. ktlint behaves like 2.0.0-ALPHA-4 with all 105 standard rules; for 1.8.0, pass
--ktlint-version=1.8 or set it in .editorconfig. compose-rules runs
natively, and any other -R rule set runs through the real ktlint jar at JVM speed. Details and limits:
docs/compatibility.md.
Each tool runs from the command line with the same flags and files. Times are the median wall time of 5 runs of a fresh process, on one Linux machine (Ryzen 7 9700X, 16 CPUs, JDK 21).
JVM warm-up is included. Every run starts a new JVM, so JVM times include startup, class loading and a cold JIT, which is what a CLI, hook or CI step pays. The last table shows the JVM fully warmed up.
| Scenario | ktrs | ktfmt 0.65 (GraalVM) | ktfmt 0.65 (JVM) | ktrs vs GraalVM | ktrs vs JVM |
|---|---|---|---|---|---|
| Editor: one 8 KB file on stdin | <5 ms | 7.1 ms | 376 ms | ~5x | >70x |
| Pre-commit: 10 changed files | <5 ms | 6.4 ms | 365 ms | ~2x | >70x |
| Format okhttp (617 files) | 73 ms | 1.13 s | 1.74 s | 16x | 24x |
| Format okhttp, 1 core | 250 ms | 2.48 s | 6.81 s | 10x | 27x |
| Format 7 projects (6,123 files) | 298 ms | 4.68 s | 4.70 s | 16x | 16x |
GraalVM here is the native image ktfmt publishes, which has no PGO. A PGO build should do better, and we couldn't build one to measure.
| Scenario | ktrs | ktlint (JVM) | ktrs speedup |
|---|---|---|---|
| 2.0: lint one file | <5 ms | 402 ms | >80x |
| 2.0: lint okhttp | 119 ms | 4.41 s | 37x |
| 2.0: lint 7 projects | 640 ms | 19.65 s | 31x |
2.0: autocorrect okhttp (-F) |
219 ms | 42.73 s | 195x |
| 1.8 mode: lint okhttp | 220 ms | 3.25 s | 15x |
| 1.8 mode: lint 7 projects | 1.29 s | 13.05 s | 10x |
ktlint has no GraalVM column because its published 2.0.0-ALPHA-4 Linux native image crashes on every lint or format.
| JVM warmed up, okhttp | ktrs | JVM, fresh process | JVM, warmed up | ktrs vs warmed up |
|---|---|---|---|---|
| Format (ktfmt) | 73 ms | 1.74 s | 276 ms | 4x |
| Lint (ktlint 2.0) | 119 ms | 4.41 s | 2.96 s | 25x |
"Warmed up" is the median of the last 5 of 30 runs inside one JVM. Command-line users never get this; a long-lived process can approach it.
Binary size: ktrs's ktfmt is 1.6 MB and its ktlint 4.3 MB, against 56.5 MB for ktfmt's native image and
71-75 MB plus a JRE for the jars.
Every scenario, memory use, Spotless numbers, the method and how to reproduce it all: docs/performance.md.
Parity with the original tools is the spec: ktrs runs next to the real jars, and a difference fails the gate unless
it is on a reviewed list in tools/parity/known-diffs/. A release can't be tagged
without a passing parity run on its commit.
| What | Compared with | On | When |
|---|---|---|---|
| Unit and ported upstream tests (ktfmt's suite, ktlint's rule tests) | Upstream's expected results | Linux, macOS, Windows | Every push and PR |
| Parser | Kotlin compiler's PSI tree (DebugUtil.psiToString) |
Compiler's fixtures, every corpus file | Fixtures every push, corpus nightly |
| Formatter | ktfmt jar, byte for byte | Corpus in meta, google and kotlinlang styles | Nightly |
| Linter | ktlint jar: violations and --format output |
Corpus in ktlint_official, intellij_idea, android_studio, with and without experimental rules |
Nightly |
| ktlint 1.8 mode, compose-rules | Their jars | Corpus | Nightly |
| CLIs | Jars: stdout, stderr, exit code, written files | Scenario suite, including editor invocations | Nightly |
| Gradle and Maven drop-ins | The original plugins | Fixture builds | Nightly |
| Held-out corpus | Same as the formatter, linter and CLIs | 15,287 files from 20 other projects, which never drove a fix | On demand |
| Fuzzing | Jars, plus crash and hang checks | Mutated real-world files | On demand |
The corpus is ~6,000 files from 7 projects, pinned in tools/corpus/REVISIONS. The
nightly parity run is public, and a weekly workflow opens
an issue when ktfmt, ktlint, compose-rules or Kotlin publishes a newer release. On 2026-10-30, cargo test --workspace
on Linux gave 922 passed, 0 failed, 3 ignored; that counts ktrs's test functions, not upstream's tests one for one.
- Held-out corpus. It checks that corpus work didn't overfit. In its first run, all 2.2M lint violations and
every ktfmt output matched in all three styles, and
--formatdiffered on 3 files from one rule, since fixed (research/21;tools/holdout/run.sh). - Fuzzing. The first runs found 4 bugs, now fixed, including exponential parser memory on deeply nested
generic-looking input (research/24;
fuzz/,tools/fuzz/diff.sh).
Run them yourself: cargo test, then cargo corpus-diff, cargo fmt-diff and cargo lint-diff. The full list of
gates is in CLAUDE.md.
- Why not just count warm-up out? CLIs, hooks and CI start a new process each time, so that cost is real, and ktrs needs no daemon to avoid it. It also isn't the whole gap. Warming up closes most of ktfmt's (24x to 4x on okhttp) but little of ktlint's (37x to 25x).
- Why not a GraalVM native image? It removes startup for far less work, and for single files the published ktfmt image is close enough that you won't notice the difference. See ktfmt#584 and KT-82373. ktrs stays 10-16x ahead on whole projects, in a much smaller binary. We haven't measured PGO builds, which will narrow that.
- Why not Kotlin/Native? There is no port to measure, so we have no numbers either way.
- What does it cost? A second codebase to maintain. ktrs has about 103,000 lines of Rust including tests, against
about 12,000 in ktfmt's core and 33,000 in ktlint, excluding their tests. That isn't like for like: those tools reuse
the Kotlin compiler's parser, and ktrs reimplements it. About 20 lines use
unsafe, in FFI, WASM and the tree builder; the parser, formatter and lint crates have none.
Stay on the originals if a PGO native image or a warm daemon is fast enough for you, if you rely on rule sets other than compose-rules (they run on the JVM anyway), or if you need new upstream releases the day they ship.
ktrs migrate switches a build's ktfmt-gradle, ktlint-gradle, kotlinter, ktlint-maven-plugin and
Spotless setup to the ktrs drop-ins, editing only ids, coordinates and versions in place. Setups it can't rewrite, such
as a task that runs ktlint from its jar, get a note: saying what to change by hand.
ktrs migrate # print the edits as a diff; exit 1 if there are any
ktrs migrate --write # apply themCheck a pull request's Kotlin files, with each finding shown as an annotation on the diff:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # changed-only needs the history back to the base commit
- uses: Hexay/ktrs@v0.5.3 # Linux, macOS and Windows
with:
check: fmt lint # ktrs fmt --check and ktrs lint (or just one of them)
changed-only: true # only the files the pull request changes
fmt-args: --style kotlinlang # lint-args likewiseSARIF upload, changed-only rules and running the binaries yourself:
docs/ci-integrations.md.
No Rust needed: on first run, the hook downloads the release binaries for its rev.
- repo: https://github.com/Hexay/ktrs
rev: v0.5.3
hooks:
- id: ktrs-fmt # also: ktrs-fmt-check, ktfmt (with ktfmt's flags in `args`)
args: [--style, kotlinlang]
- id: ktrs-lint # also: ktrs-lint-format, ktlint (with ktlint's flags in `args`)| Integration | Guide |
|---|---|
| ktfmt-gradle, ktlint-gradle, kotlinter (plugin drop-ins), Spotless, Maven, JVM API | docs/build-tools.md |
| VS Code, IntelliJ, Android Studio, Neovim, Helix, Zed, Emacs | docs/editors.md |
| Bazel, reviewdog, Danger | docs/ci-integrations.md |
The parser is available on its own as the Rust crate kt-syntax: a parser for
Kotlin that builds the compiler's PSI tree (same kinds, same nesting, same error elements) with no JVM.
It gives you navigation, byte ranges and line/column positions, syntax errors as nodes, comments and
parsed KDoc, and text edits for codemods. kt-syntax is the stable API; the ktrs-* crates are internals. See its
README for examples.
ktrs is maintained by @Hexay. It tracks ktfmt 0.65, ktlint 2.0.0-ALPHA-4 (plus 1.8.0 in compatibility mode), compose-rules 0.6.8 and the Kotlin 2.4.21 parser. New upstream releases are ported and re-checked against the parity gates before a ktrs release. If ktrs output ever differs from ktfmt or ktlint on your code, that's a bug: please open an issue with the file, or a snippet that reproduces it, and the command line you used.
tools/sync-kotlin.sh # pinned upstream sources + vendored test fixtures
tools/psi-dump/psi-dump.sh one X.kt # reference PSI tree from the real compiler (needs a JDK)
cargo xtask codegen # regenerate SyntaxKind from crates/ktrs-syntax/kinds.tsv
cargo testCLAUDE.md lists the crate layout and every parity gate. Design notes are in
research/.
Dual-licensed under MIT or Apache-2.0, at your option. ktrs contains code and test data ported from the Kotlin compiler, the IntelliJ Platform, ktfmt, google-java-format, ec4j (Apache-2.0), ktlint and ktfmt-gradle (MIT). See NOTICE.