Ktlint with pre-commit Hook: Git Hooks in Kotlin Made Easy

Image presents Git logo in the foreground and a blurred desk in the background

In this article, we will learn how easily we can automate our Kotlin project with git hooks on the example of Ktlint pre-commit check.

At the end of this tutorial, you will know precisely:

  • what are Git client-side Hooks,
  • what types of client-side hooks Git offers,
  • how to implement an automated code check before we commit our changes,
  • and how to combine that together with Kotlin and Gradle.

And although this guide may seem a bit specific, you can trust me that after reading it you will be able to adjust this knowledge to many other cases in your real-life scenarios.

Video Tutorial

If you prefer video content, then check out my video:

If you find this content useful, please leave a subscription  😉

Git Hooks And Client-Side Types

Before we see how to do a Ktlint check as a pre-commit hook, let’s learn a bit about Git Hooks.

Git Hooks, in simple words, are a way to trigger custom scripts, whenever a particular action happens in our repository. An action can be a rebase, a checkout, a merge, etc.

And in order to run our script, the only thing we must do is to put it inside the hooks directory, which we can find in the .git directory.

Moreover, when we navigate there, we will see that it already contains a bunch of useful examples:

Image is a screenshot presenting a directory where Git Hooks are put and a list of sample, already predefined hooks. This is the directory where we will put our ktlint pre-commit hook.

If we would like to try any of the predefined samples, then we must simply remove the .sample suffix from the filename.

And what client-side Git Hooks can we work with? Well:

  • pre-commit – run before creating a commit,
  • prepare-commit-msg – run before the commit message editor is opened,
  • commit-msg – triggered after the commit message is created but before the commit is finalized,
  • post-commit – run after a commit is made,
  • applypatch-msg – invoked during the git apply operation to edit patch messages,
  • pre-applypatch / post-applypatch – run before/after applying changes from a patch,
  • pre-rebase – run before starting a rebase operation,
  • post-rewrite – triggered after commands that rewrite commit history,
  • post-checkout – invoked after a successful git checkout operation,
  • post-merge – run after a successful git merge operation,
  • pre-push – run before a push to a remote repository.

The pre-receive, update, and post-receive are server-side hooks, which we won’t cover here.

Import Ktlint


At this point, we know what we’re dealing with today and we can start the practice part of the Ktlint pre-commit hook implementation.

As the first thing, let’s navigate to the build.gradle.kts and import the library:

plugins {
    kotlin("jvm") version "1.9.10"

    id("org.jlleitschuh.gradle.ktlint") version "11.6.1"

At the moment of writing, the most recent version is 11.6.1 and you can always figure out what’s the current one here.

Following, let’s sync the project.

When the synchronization process finishes, we should see plenty of new tasks added, among which, these we will use the most:

  • ktlintFormat– to format according to the code style all SourceSets Kotlin files and project Kotlin script files,
  • ktlintCheck– to check all SourceSets and project Kotlin script files

Add Script

With that done, let’s create the scripts directory and put the pre-commit file there:


echo "Running git pre-commit hook"

./gradlew ktlintCheck


# return 1 if check fails
if [[ $ktlintCheckStatus -ne 0 ]]; then
     exit 1
     exit 0

But why don’t we put that directly to the .git/hooks directory?

Well, the problem is that by default, the .git directory is not tracked. It contains all the information about the repository, including the repository’s configuration, commit history, branches, and other metadata. And if the .git directory were tracked, it would create a kind of recursive loop, leading to potential issues and conflicts.

So, when working with Gradle and Kotlin, we will simply put our script in the scripts destination, and later configure Gradle to copy it to the desired folder.

And what is our script?

Well, it is a simple script, which will run the ktlint check command using the gradle wrapper. If it is successful, the command returns 0 and we return 0, too. In other case, we return 1 and the commit will simply fail.

Update build.gradle.kts

Following, let’s navigate to the build.gradle.kts file and add a new task- the copyPreCommitHook:

tasks.register<Copy>("copyPreCommitHook") {
    description = "Copy pre-commit git hook from the scripts to the .git/hooks folder."
    group = "git hooks"
    outputs.upToDateWhen { false }

As we can see, this task is responsible for copying the pre-commit file from the scripts/pre-commit to the .git/hooks/.

Moreover, we make a small workaround- outputs.upToDateWhen { false } – so that our task will never be cached.

When we sync our Gradle project, we should see that our task is available from now on inside the git hooks group:

Image is a screenshot and presents the Gradle tool window in IntelliJ and our copyPreCommitHook task, which will copy the pre-commit script with ktlint check from the scripts tirectory to the desired directory- .git/hooks

At this point, we can run the task and verify that the script was moved successfully.

But is that all?

Well, we could stop right here, but if we automate things, it would be good to avoid manual run of the copyPreCommitHook task, right? And we can easily achieve that this way:

tasks.build {

With this setting, we simply instruct Gradle to run copyPreCommitHook before the build task.


With all of that done, we can finally verify if our ktlint check will be run as a pre-commit hook.

To do so, let’s make some changes that will fail ktlintCheck and try to commit. In IntelliJ, we should see the following:

And when we check the details, we will see a meaningful message:

Running git pre-commit hook
Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details
> Task :loadKtlintReporters UP-TO-DATE
> Task :runKtlintCheckOverKotlinScripts UP-TO-DATE
> Task :runKtlintCheckOverTestSourceSet NO-SOURCE
> Task :ktlintTestSourceSetCheck SKIPPED
> Task :ktlintKotlinScriptCheck
> Task :runKtlintCheckOverMainSourceSet
> Task :ktlintMainSourceSetCheck FAILED
C:\Users\Piotr\src\main\kotlin\Main.kt:2:1 Unexpected indentation (10) (should be 4)
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':ktlintMainSourceSetCheck'.
> A failure occurred while executing org.jlleitschuh.gradle.ktlint.worker.ConsoleReportWorkAction
   > KtLint found code style violations. Please see the following reports:
     - C:\Users\Piotr\build\reports\ktlint\ktlintMainSourceSetCheck\ktlintMainSourceSetCheck.txt
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
5 actionable tasks: 3 executed, 2 up-to-date

And this snippet proves that our pre-commit hook was triggered and works as expected.

Moreover, when we correct our code, we will see that we can commit without any message!


And that’s all for this article about implementing ktlint check as a pre-commit Git Hook.

I hope you enjoyed this tutorial and that this will be a great start for adding more automation to your project. If you would like to get a ready-to-go project, then please navigate to my GitHub repo here.

Lastly, I would like to invite you to my free newsletter, so that you will never miss any important updates from both my blog and Kotlin world!

Share this:

Picture of Hi there! 👋

Hi there! 👋

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

Related content


Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy

Free tutorials and courses on Kotlin & backend

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.