New
Apps 816Makers 815This week 816
Promote your app−30%
VibeTags  AI Guardrails for Java Development screenshot 1
2views in
the last 30 days

VibeTags AI Guardrails for Java Development

VibeTags is a Java annotation processor that acts as AI guardrails for code generation tools...

Built with Claude Code

by pisberg · from SESweden · added Oct 29, 2026
no ratings0 comments

VibeTags — AI Guardrails for Java Development

OpenSSF Scorecard OpenSSF Best Practices

Lines of Code PMD Analyzed with codekoll SpotBugs Find Security Bugs Error Prone NullAway Checkstyle Donate

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 .cursorrules and .cursorignore. (All four counts verified by ProjectFactsConsistencyTest.)

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 @AILocked to PaymentProcessor and 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, so GEMINI.md's index tells the agent to open the file). This repository dogfoods it — the generated block in its own CLAUDE.md is 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.SOURCE annotations 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 @AILocked code. 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.

VibeTags demo — annotate, compile, all platforms update

⚡ 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-processor in 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?

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 — a walkthrough

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

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#java · #annotations · #guardrails