# Licensee: a Gradle plugin that fails the build on a forbidden dependency licence

> Licensee is a Gradle plugin from Cash App that walks your resolved dependency graph, compares the SPDX licence of every artifact against an allow list you configure, and fails the build when a transitive dependency brings in a licence you did not expect. It writes a JSON artifact report and a plain text validation report on every run, allowed or not.

**cashapp/licensee** — Gradle plugin which validates the licenses of your dependency graph match what you expect

- Repository: https://github.com/cashapp/licensee
- Website: https://cashapp.github.io/licensee/docs/1.x/
- Stars: 756 · Forks: 42
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/cashapp-licensee

## The failure it prevents arrives through a patch version

Licensee is a Gradle plugin that validates the licences of your dependency graph match what you expect, and fails the build when they do not. The scenario the README opens with is worth reading in full, because it explains the plugin's reason for existing.

You depend on an Apache 2 licensed artifact in a closed-source product. The developer releases a new version and you upgrade:

```diff
-implementation 'com.example:example:1.0'
+implementation 'com.example:example:1.1'
```

That new version has a transitive dependency on a GPL licensed artifact, which is incompatible with a closed-source product. The README asks whether you caught it, and points out that you may have seen the new dependency in a dependency tree diff without checking its licence:

```diff
-+--- com.example:example:1.0
++--- com.example:example:1.1
+     \--- com.other:other:2.5
```

The failure mode is specific. The transitive dependency appears in the tree, so a dependency review process would catch it, but the licence of that new node is the thing that is easy to skip, because the tree diff is about coordinates and not about terms.

With the plugin applied, the check is mechanical. You allow the licences you expect, and the upgrade fails:

```groovy
licensee {
  allow('Apache-2.0')
}
```

And the build tells you exactly which artifact caused it:

```text
> Task :app:licensee FAILED
com.other:other:2.5
 - SPDX identifier 'GPL-3.0-or-later' is NOT allowed
```

The plugin is written in Kotlin, comes from Cash App, is Apache-2.0 licensed, and the last push was on 2026-10-21 with the newest listed release being 1.14.1 from 2025-10-09.

## The task walks the whole transitive graph, so leaf modules are where it belongs

The usage section is explicit about where the plugin should be applied. The plugin's tasks walk the entire transitive dependency graph, so it only needs to be applied to leaf modules, those without downstream dependencies, such as applications and services. If you are building a library, you apply it to each library module instead.

That distinction follows from what a dependency graph is. An application module has no downstream consumers, so its graph is complete at the point of resolution. A library module's graph is partial at build time, because the consumers add their own dependencies later, and checking a partial graph of a library is a partial check.

The plugin also requires a supported language or platform plugin on the same module. The listed options are java-library, java, com.android.application, com.android.library, org.jetbrains.kotlin.jvm, org.jetbrains.kotlin.js and org.jetbrains.kotlin.multiplatform. That list is the coverage map of the plugin, and it is a Gradle-only design with no equivalent for Maven or Bazel.

The task integration is automatic. The licensee task is added to the build and automatically added as a dependency of the check task, so it runs as part of the normal verification cycle rather than as a separate command someone has to remember. For Android and Kotlin multiplatform modules the plugin adds variant-specific tasks such as licenseeDebug and licenseeJs, with the plain licensee task aggregating them together.

The one operational consequence of this design is the failure mode of forgetting a module. A module without the plugin applied is a module whose graph is not checked, and the check passes silently. There is no aggregate that covers the whole project by default, which is a trade the README makes explicit.

## Two report files are written whether or not the build passes

Beyond failing the build on a disallowed licence, the plugin always generates report files. That word always is the important part, since a report you only get on failure is a report you cannot diff.

The first is artifacts.json, a JSON file with licence information on every artifact in the dependency graph. The second is validation.txt, a plain text report listing each artifact and whether its licences are allowed or disallowed. Both go into the reports/licensee directory under the build directory, or for Android modules into a variant subdirectory.

The pair has different uses. The JSON artifact report is machine readable and describes the whole graph, so it can be archived as a build output, uploaded to an artifact store, or consumed by another tool that wants to know what licences are in a release. The text validation report is the human-facing one, and it is also the file that carries the reason strings you attach to allow and ignore rules, which is what makes the configuration auditable rather than a bare list of identifiers.

The reporting is also why the plugin is useful even in a project where every licence is allowed. A build that never fails still produces a record of the licence composition of the release, which is the input to whatever legal review process a company has. The check and the inventory come from the same traversal, so they cannot drift apart.

There is also a step for keeping the licence data itself current. The README says to run the updateLicenses task to update the embedded SPDX licence file in the resources of the source tree. That embedded copy is what the plugin matches identifiers against, and an out-of-date copy means a newly issued SPDX identifier is unknown to the tool.

## Four escape hatches, and the reason strings are the point

The configuration DSL offers four ways to say yes to something, and each one exists for a different real situation.

The allow function takes SPDX identifiers, so Apache-2.0 or MIT, with the full list of supported identifiers at spdx.org/licenses. The allowUrl function covers artifacts with an unknown or non-SPDX licence, matching on a URL, which is what you need when a dependency ships its own licence text rather than an identifier. The allowDependency function targets a specific groupId, artifactId and version, described as useful for artifacts that contain no licence data or have invalid or incorrect licence data. The ignoreDependencies function removes something from consideration entirely during graph resolution, so the artifact is not analysed and does not appear in any report, and it exists for internal closed-source libraries and commercial libraries you have paid for.

What makes this configuration usable as an audit record is the optional reason string. allowUrl, allowDependency and ignoreDependencies all accept a block where you can supply a because clause, and those reasons are included in the validation reports. So the report says not just that an artifact was allowed but why, in words, in the file you attach to the build.

The ignore path has a stronger form and it is guarded. An ignore can be marked as transitive, which ignores the target artifact's entire branch of the dependency tree, ignoring its dependencies regardless of coordinates or licence information. The README calls this especially dangerous and requires a reason string for it. That guard is a good one, because a transitive ignore is the configuration that can hide a GPL dependency without anyone noticing.

The overloads matter too. ignoreDependencies accepts a groupId or a groupId and artifactId pair, so you can exclude a whole namespace or a single artifact.

## Reporting from inside the Android app is a switch, not a feature

One option deserves its own mention because it is unusual. The bundleAndroidAsset property enables asset generation and copies the licensee report into the Android asset directory, making it available in assets/app/cash/licensee/artifacts.json by default. The androidAssetReportPath property overrides the default path.

The purpose is straightforward once you see it. A mobile app that ships a licence report as an asset can display its own third-party licence acknowledgements to the user, which is a requirement on both major mobile stores. Rather than maintaining that list by hand and letting it drift from the actual dependency graph, the build produces it from the graph.

That is the pattern worth generalising from this plugin. A licence check that produces a report as a by-product can feed more than a build gate, and the Android asset path is the clearest example of a report becoming a user-facing artefact.

The plugin is applied the same way as any other Gradle plugin, through the buildscript block with the plugin version on the classpath:

```groovy
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'app.cash.licensee:licensee-gradle-plugin:1.14.1'
  }
}

apply plugin: 'app.cash.licensee'
```

Development snapshots are published to the Central Portal snapshots repository under a 1.15.0-SNAPSHOT version, with the extra maven block adding the snapshot URL to the repositories list. The repository also carries a RELEASING.md, a CHANGELOG.md and a build-logic directory, so the release process is written down rather than living in a maintainer's head.

## Conclusion

Licensee is worth adding to any Gradle build that ships a closed-source product, because the failure it prevents is the one that arrives silently through a patch version bump, and the allow list plus the report files give you both a gate and an audit trail. It is the wrong tool for a project that ships only permissive licences by policy, since the check will never fire, and it is the wrong tool for a build system other than Gradle. Before you turn it on, verify three things yourself: that the allow list covers every licence already present in your graph, because the first run will fail on anything you have not explicitly allowed, that the ignore rules you add for commercial or internal libraries carry a reason string, since the transitive form requires one, and that the plugin is applied to every leaf module, because the task walks the entire transitive graph and a module you forget is a module that is not checked.

## FAQ

### What does the Licensee Gradle plugin do?

It walks your resolved dependency graph, compares the SPDX licence of every artifact against the allow list you configure in the licensee DSL, and fails the build when a dependency carries a licence you did not allow. It also always writes an artifacts.json and a validation.txt report.

### How do I add the Licensee plugin to a Gradle build?

Add the classpath 'app.cash.licensee:licensee-gradle-plugin:1.14.1' dependency in a buildscript block with mavenCentral in the repositories, then apply plugin: 'app.cash.licensee' to the module. A supported platform plugin such as java, java-library, com.android.application or org.jetbrains.kotlin.jvm must be applied to the same module.

### Where should I apply the Licensee plugin in a multi-module build?

To leaf modules without downstream dependencies, such as applications and services, because the task walks the entire transitive graph. If you are building a library, the README says to apply it to each library module instead.

### How do I allow a dependency that has no usable SPDX identifier?

Use allowUrl for a licence that matches a URL, or allowDependency for a specific groupId, artifactId and version. Both accept a because clause with a reason string, and those reasons appear in the validation reports.

### Can I exclude a commercial SDK from the Licensee check?

Yes, ignoreDependencies skips an artifact during graph resolution so it is not analysed and does not appear in reports, and it is documented as intended for internal closed-source and commercial libraries. Marking an ignore as transitive skips the whole branch of the tree, and the README says a reason string is required for that form because it is especially dangerous.

## Sources

- [cashapp/licensee on GitHub](https://github.com/cashapp/licensee)
- [License: Apache-2.0](https://github.com/cashapp/licensee/blob/trunk/LICENSE)
- [Project website](https://cashapp.github.io/licensee/docs/1.x/)
- [README](https://github.com/cashapp/licensee/blob/trunk/README.md)
- [Releases](https://github.com/cashapp/licensee/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cashapp-licensee
