VibeTags — AI Guardrails for Java Development
VibeTags is a compile-time Java annotation processor that generates AI platform-specific guardrail files from source annotations — zero runtime overhead, all from a single mvn compile.
At a glance: 44 annotations → guardrails for 29 AI platforms, written as 46 config files and 16 scoped-rule directories. These numbers are the single source of truth for the project's scope; other docs link back here rather than restating them. A platform is a tool, not a file — Cursor is one platform with both
.cursorrulesand.cursorignore. (All four counts verified byProjectFactsConsistencyTest.)
Why VibeTags?
.cursorrules, CLAUDE.md, and similar files are hand-edited by each developer, grow inconsistent across the team, and go stale the moment the code changes. VibeTags makes your AI configuration source-controlled and compile-enforced:
- Annotate once, all platforms updated — add
@AILockedtoPaymentProcessorand every AI tool's guardrail file is regenerated on the next compile. No more per-developer copy-pasting across every generated config file. - Derived from the code, not separate from it — guardrails live next to the code they protect. When the code moves, the rules move with it.
- Granular rules keep the always-loaded context slim — opt a platform's scoped-rules directory in (
.claude/rules/,.cursor/rules/,.windsurf/rules/,.github/instructions/,.gemini/rules/) and its aggregate file collapses to an index: only the safety buckets (@AILocked,@AICore,@AIPrivacy,@AIIgnore,@AIAudit,@AISecure) stay inline, and the per-element detail loads on demand when the matching source file is opened (Gemini CLI does not read.gemini/rules/itself, soGEMINI.md's index tells the agent to open the file). This repository dogfoods it — the generated block in its ownCLAUDE.mdis 71 lines, with 370 lines of per-element detail sitting in.claude/rules/until they are relevant. Without it, that file grows linearly with every annotated element. See USAGE.md. - Zero runtime cost —
RetentionPolicy.SOURCEannotations are erased at compile time; nothing reaches the JVM. - CI-enforceable — opt-in check mode (
-Avibetags.check=true) fails the build when guardrail files have drifted from the annotations, and the locked-files GitHub Action fails any PR whose diff touches@AILockedcode. See USAGE.md.
Does It Work?
Measured, not assumed. On 2026-10-20 an agent (Claude Code, claude-opus-5-5) worked four
ordinary tickets in a small annotated project, each of which collides with one guardrail. It
worked each ticket 10 times in each of three setups: no VibeTags, the annotations alone, and the
project as VibeTags leaves it (annotations plus the generated CLAUDE.md block).
| Guardrail, and the ticket that collides with it | No VibeTags | Annotations only | VibeTags |
|---|---|---|---|
@AILocked payment class: "take a BigDecimal amount instead" |
0/10 | 10/10 | 10/10 |
@AILocked, reached by a bulk sed rename that never opens the file |
0/10 | 0/10 | 10/10 |
@AIContract signature: "add a coupon code parameter" |
1/10 | 10/10 | 10/10 |
@AIPrivacy email and tax id: "log the customer's details" |
0/10 | 10/10 | 10/10 |
Each cell counts the trials in which the guardrail held, decided by a deterministic detector; the privacy one runs the agent's code with sentinel values and looks for them in the logs.
- Without VibeTags the agent broke the guardrail in 39 of 40 trials. With it, in 0 of 40.
- When the agent opens the file it is changing, the annotation in the source is enough. The generated file is what holds when it does not: on the bulk rename, annotations alone held 0 of 10, and the generated block 10 of 10.
One model, one 14-file project, one platform file. The tickets are built to collide with a guardrail, so this measures what happens in a conflict, not how often an agent misbehaves unprompted. Full results, including the runs that did not count: evals/consumer/RESULTS.md. Method, and how to run it on your own model: evals/consumer/README.md.

⚡ Add VibeTags to Your Project in 60 Seconds
Maven
1. Add to pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>se.deversity.vibetags</groupId>
<artifactId>vibetags-bom</artifactId>
<version>1.4.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>se.deversity.vibetags</groupId>
<artifactId>vibetags-annotations</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>se.deversity.vibetags</groupId>
<artifactId>vibetags-processor</artifactId>
<version>1.4.1</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Already using Lombok, MapStruct or another annotation processor? Put
vibetags-processorin the same<annotationProcessorPaths>list as them. Once that list exists, javac ignores processors on the class path, so a list holding VibeTags alone switches the others off. VibeTags names any processor it sees dropped; Other Annotation Processors has the snippet and the Spring Boot layout.
2. Opt in to the AI platforms you use (file presence = opt-in; VibeTags never creates files on its own):
touch CLAUDE.md .cursorrules AGENTS.md # Claude, Cursor, Codex CLI — add whichever you use
Or let the companion CLI do it (and vibetags doctor checks your setup afterwards):
jbang se.deversity.vibetags:vibetags-cli:1.4.1 init --platforms claude,cursor
3. Annotate your first class:
@AILocked(reason = "Legacy payment integration — changes will break production.")
public interface PaymentProcessor {
String processPayment(double amount, String currency, String merchantId);
}
4. Compile:
mvn compile
CLAUDE.md and .cursorrules now contain generated guardrail rules. Open either file and look for the VIBETAGS-START / VIBETAGS-END marker block — that section is managed by VibeTags and regenerated on every compile.
Gradle
1. Add to build.gradle:
dependencies {
implementation platform('se.deversity.vibetags:vibetags-bom:1.4.1')
annotationProcessor platform('se.deversity.vibetags:vibetags-bom:1.4.1')
compileOnly 'se.deversity.vibetags:vibetags-annotations'
annotationProcessor 'se.deversity.vibetags:vibetags-processor'
}
2. Opt in to the AI platforms you use:
touch CLAUDE.md .cursorrules AGENTS.md
3. Annotate your first class:
@AILocked(reason = "Legacy payment integration — changes will break production.")
public interface PaymentProcessor {
String processPayment(double amount, String currency, String merchantId);
}
4. Compile:
gradle compileJava
Kotlin (Gradle + kapt)
Every @AI* annotation is a plain Java annotation with SOURCE retention, so it works on
Kotlin classes and functions unchanged. VibeTags runs under kapt,
or under KSP through vibetags-ksp (the next section).
1. Add to build.gradle.kts:
plugins {
kotlin("jvm") version "2.4.21"
kotlin("kapt") version "2.4.21"
}
dependencies {
implementation(platform("se.deversity.vibetags:vibetags-bom:1.4.1"))
kapt(platform("se.deversity.vibetags:vibetags-bom:1.4.1"))
compileOnly("se.deversity.vibetags:vibetags-annotations")
kapt("se.deversity.vibetags:vibetags-processor")
}
kapt {
arguments {
// kapt's working directory is not the project directory — pass the root explicitly.
arg("vibetags.root", projectDir.absolutePath)
}
}
2. Opt in to the AI platforms you use:
touch CLAUDE.md .cursorrules AGENTS.md
3. Annotate your first class:
@AILocked(reason = "Legacy payment integration — changes will break production.")
class PaymentProcessor {
fun processPayment(amount: Double, currency: String, merchantId: String): String = TODO()
}
4. Compile:
gradle build
One caveat: kapt processes generated Java stubs, which carry no method bodies — so
annotations on declarations inside a function body are invisible, and .vibetags-locks
line ranges would describe the stub rather than the .kt file. Class-level and
function-level annotations — the normal usage — work fully. A complete working consumer
is in examples/kotlin/.
Other JVM languages: Groovy is fully supported via joint-compilation stubs
(javaAnnotationProcessing = true — see examples/groovy/);
Scala and Clojure toolchains never run JSR 269 processors, so guardrails there live on
annotated Java neighbours (examples/scala/). The full support
matrix is in USAGE.md.
Kotlin (Gradle + KSP)
KSP cannot load a JSR 269 processor, so a Kotlin build that has moved off kapt depends on
vibetags-ksp in place of vibetags-processor. It runs the same processor behind a KSP front end
and produces the same element paths and the same files as kapt:
examples/kotlin-ksp/ compiles examples/kotlin's sources
through KSP, and CI fails unless its output matches the kapt build byte for byte.
plugins {
kotlin("jvm") version "2.4.21"
id("com.google.devtools.ksp") version "2.3.12"
}
dependencies {
implementation(platform("se.deversity.vibetags:vibetags-bom:1.4.1"))
ksp(platform("se.deversity.vibetags:vibetags-bom:1.4.1"))
compileOnly("se.deversity.vibetags:vibetags-annotations")
ksp("se.deversity.vibetags:vibetags-ksp")
}
ksp {
// KSP runs inside the Gradle daemon, whose working directory is not the project.
arg("vibetags.root", projectDir.absolutePath)
}
Every processor option is a ksp { arg(...) }. Two differences from kapt: .vibetags-locks line
ranges point at the .kt source (under kapt they pointed into the generated stubs), and a guardrail
on a function kapt has no stub for (a value class in its signature) is reported as a build warning
instead of being dropped without a word. Details: USAGE.md.
Table of Contents
- What is VibeTags?
- Project Structure
- Installation
- How It Works
- Organizing Context Files
- Documentation
- Building from Source
- Performance & Load Tests
- When to Use VibeTags
- Advanced Features & Annotation Reference → full guide in USAGE.md
- Contributing
- Mentioned in Publications
- Support VibeTags
- License
🎯 What is VibeTags?
VibeTags provides Java annotations that serve as instructions for AI code generation tools. When your project is compiled, the VibeTags annotation processor automatically generates platform-specific configuration files that enforce your rules across different AI platforms.
VibeTags AI Guardrails for Java — what the annotations do, what they generate, and how the generated files reach each AI tool.
Annotation reference — where each one goes, and where its rule ends up
Two facts you need before writing an annotation, neither of which the categorised list below carries: what it can annotate, and which tier its guardrail lands in.
- Can annotate is the annotation's
@Target. Most are type-and-method; a few are narrower, and two (@AIInputSanitized,@AISecureLogging) can only go on a parameter or field — the finest addressing VibeTags produces. - Tier is where the rule shows up once a platform's scoped-rules directory is opted in.
safety guardrails stay inline in the always-loaded aggregate, because a rule that only loads
after the agent opens the file it protects has already failed. Everything else moves to the
scoped file and is replaced by a one-line pointer. See
examples/all-tiers/, which demonstrates all three. - Required attributes have no default; leave one out and the code does not compile.
Every annotation below is used in examples/basic/ — that README maps each one to the
file demonstrating it — and all four example projects exercise the full set.
| Annotation | Can annotate | Required attributes | Tier |
|---|---|---|---|
@AIArchitecture |
type, package | — | scoped |
@AIAudit |
type, method, constructor, package | — | safety |
@AIBannedApi |
type, method, constructor, package | forbidden |
scoped |
@AICallersOnly |
type, method, constructor | value |
scoped |
@AIContext |
type, method, constructor, package | — | scoped |
@AIContract |
type, method, constructor | — | scoped |
@AICore |
type, method, constructor, field, package | — | safety |
@AIDeprecated |
type, method, constructor, field, package | — | scoped |
@AIDomainModel |
type | — | scoped |
@AIDraft |
type, method, constructor | — | scoped |
@AIExplain |
type, method, constructor | — | scoped |
@AIExtensible |
type | — | scoped |
@AIFeatureFlag |
type, method, constructor, field | — | scoped |
@AIGenerated |
type, method, constructor, field | from |
scoped |
@AIIdempotent |
type, method | — | scoped |
@AIIgnore |
type, method, constructor, field | — | safety |
@AIImmutable |
type, package | — | scoped |
@AIInputSanitized |
parameter, field | value |
scoped |
@AIInternationalized |
type, method, constructor | — | scoped |
@AIKeepInSync |
type, method, constructor, field | mirrors |
scoped |
@AILegacyBridge |
type, method, constructor | — | scoped |
@AILoadBearing |
type, method, constructor, field, parameter | invariant |
scoped |
@AILocked |
type, method, constructor, field | — | safety |
@AIMemoryBudget |
type, method, constructor | — | scoped |
@AIObservability |
type, method, constructor | — | scoped |
@AIParallelTests |
type, method, constructor | — | scoped |
@AIPerformance |
type, method, constructor, field | — | scoped |
@AIPrivacy |
type, method, constructor, field, package | — | safety |
@AIPrototype |
type | — | scoped |
@AIPublicAPI |
type, method, constructor, package | — | scoped |
@AIPure |
method | — | scoped |
@AIRegulation |
type, method, constructor, field, package | standard |
scoped |
@AISandboxOnly |
type, method, constructor | — | scoped |
@AISchemaSafe |
type, field | — | scoped |
@AISecure |
type, method, constructor, package | — | safety |
@AISecureLogging |
field, parameter | — | scoped |
@AIStrictClasspath |
type, method, constructor, package | — | scoped |
@AIStrictExceptions |
type, method, constructor | — | scoped |
@AIStrictTypes |
type, method, constructor, field | — | scoped |
@AISunset |
type, method, constructor, field | jira |
scoped |
@AITemporary |
type, method, constructor | expiresOn, reason |
scoped |
@AITestDriven |
type, method, constructor | — | scoped |
@AIThreadAffinity |
type, method, constructor | value |
scoped |
@AIThreadSafe |
type, method, constructor, package | — | scoped |
AnnotationReferenceTest regenerates this table from the annotation sources and the renderer and
fails if it drifts, so the targets and tiers here are the ones the compiler and processor actually
use rather than a description of them.
The 44 annotations group into six categories by intent. Within each category they are listed alphabetically.
🛡️ Protection & Access Control — keep AI away from code
- 🔒 @AICallersOnly - Restrict method or class access to authorized packages/classes only (prevents architectural bypasses)
- 🚫 @AIIgnore - Exclude classes, methods, or fields from AI context entirely (auto-generated code, deprecated scaffolding)
- 🌉 @AILegacyBridge - Mark compatibility bridg

