A collection of Vaadin tools that AI agents (and humans) can run to inspect and validate Vaadin projects.
It ships two ways, both from this one repository:
- A self-contained native CLI — no Node.js and no JVM required at runtime, and it never searches the machine for one.
- A Claude Code / Codex plugin that exposes the tools as agent skills.
A Vaadin machine is not guaranteed to have Node.js, and reaching into
~/.vaadin/node to borrow Vaadin's copy makes a launcher look like it's hunting
for something to execute. The tools are therefore compiled ahead of time, per
platform, into small self-contained binaries (Go, ~2 MB each). The plugin bundles
those binaries and a tiny selector that picks the right one by uname — it only
ever runs code inside this repository, and needs no runtime install, no network,
and no $HOME probing.
This repository is the plugin checkout, with Claude Code and Codex manifests, the Go sources, and the shared test fixtures alongside each other:
.
├── .codex-plugin/
│ └── plugin.json # Codex plugin manifest
├── .claude-plugin/
│ ├── plugin.json # Claude Code plugin manifest
│ └── marketplace.json # self-marketplace, for trying it from a checkout
├── bin/
│ ├── vaadin-agent-tools # POSIX arch selector (run via ${CLAUDE_PLUGIN_ROOT})
│ ├── vaadin-agent-tools.bat # Windows selector
│ └── platform/ # native binaries (built by go/build.sh, committed)
├── skills/ # one SKILL.md per tool (the agent surface)
│ ├── vaadin-check-aura-usage/SKILL.md
│ ├── vaadin-check-theme-mixing/SKILL.md
│ └── vaadin-create-project/SKILL.md
├── hooks/ # Claude Code / Codex hooks shipped by the plugin
│ └── hooks.json # PostToolUse: styling checks after styling edits
├── go/ # the CLI implementation (source of the binaries)
│ ├── main.go build.sh go.mod
│ └── internal/{cli,tool,tools,lib}/…
├── scripts/ # maintenance scripts
│ └── derive-aura-properties.sh # refresh the Aura property tables per release
└── test/fixtures/ # sample projects, shared by the Go tests
For real distribution the plugin is published through
vaadin/agent-marketplace
alongside vaadin-skills. To try it from a local checkout, this repo doubles as
a one-plugin dev marketplace:
/plugin marketplace add /path/to/agent-tools
/plugin install vaadin-agent-tools@vaadin-agent-tools-devThen just ask — the skills trigger by description (e.g. "create a new Vaadin project" or "check this project for theme mixing") and the agent runs the tool.
The prebuilt binaries are committed under bin/platform/, so a fresh checkout is
ready to run — no build step needed to try it.
For real distribution, publish the plugin through
vaadin/agent-marketplace
alongside vaadin-skills. After the vaadin-agent-tools entry is published
there, install it from that marketplace:
codex plugin marketplace add vaadin/agent-marketplace --ref main
codex plugin add vaadin-agent-tools@vaadin-marketplaceTo pick up later changes, refresh Git-backed marketplace snapshots:
codex plugin marketplace upgradeCodex reads this repository's .codex-plugin/plugin.json and loads the shared
skills/ directory from the installed plugin. The prebuilt binaries are
committed under bin/platform/, so a fresh install is ready to run without a
build step.
The plugin is a thin wrapper around a normal command line, which you can also run by hand or in CI:
bin/vaadin-agent-tools <tool> [args] [--json]
bin/vaadin-agent-tools listEvery tool supports --json for machine-readable output (recommended for agents)
and exits non-zero when it finds a problem, so it slots into CI and agent
workflows.
Bootstraps a new Vaadin project by downloading a skeleton from
start.vaadin.com and extracting it — the core of
create-vaadin (npm init vaadin), minus the interactive prompts and IDE launch.
bin/vaadin-agent-tools create-project ./my-app
bin/vaadin-agent-tools create-project ./my-app --example=flow --pre --jsonFlags:
--name=<id>— Maven artifactId (default: sanitized basename of the target dir)--example=flow|none— include the "Task List" Flow example view (default:flow)--pre— use the pre-release Vaadin platform version--overwrite— if the target exists and is non-empty, replace its contents
This tool reaches the network and writes files. Exit codes: 0 created · 1
download or extraction failed · 2 usage error.
Detects whether a Vaadin project mixes the Aura and Lumo themes, which leads to conflicting styles and CSS custom properties that fail to resolve.
bin/vaadin-agent-tools check-theme-mixing ./my-project
bin/vaadin-agent-tools check-theme-mixing ./my-project --jsonDetection signals:
@StyleSheet(Aura.STYLESHEET)/@StyleSheet(Lumo.STYLESHEET)in Java sources@importofaura/aura.cssorlumo/lumo.cssin reusable-theme stylesheets- Usage of
--aura-*vs--lumo-*CSS custom properties - Usage of the
LumoUtilityclass in Java, whose utility CSS classes only work under the Lumo theme (not Aura)
If no base theme is explicitly loaded, the active theme cannot be determined (it
may be a custom or base-styles theme). In that case the theme-dependent checks
are a no-op — the tool reports a THEME_INDETERMINATE info finding and exits
0 rather than guessing. Ensuring correctness there is outside the tool's scope.
Exit codes: 0 no error-level findings · 1 mixing detected · 2 usage error.
Validates how a project uses the Aura theme's custom properties — in its CSS,
and in the inline styles its Java sources set through Style. Doc search can
tell an agent the rule; this tells it that the stylesheet, or the Flow view, it
just generated breaks the rule.
bin/vaadin-agent-tools check-aura-usage ./my-project
bin/vaadin-agent-tools check-aura-usage ./my-project --json| Code | Level | Scans | What it catches |
|---|---|---|---|
AURA_READONLY_PROPERTY_ASSIGNED |
error | CSS + Java | A property Aura computes is assigned. Most are defined with light-dark(…), so assigning one value discards both color-scheme values and dark mode stops working. The message names the customizable property to set instead. The two font stacks (--aura-font-family-system, --aura-font-family-instrument-sans) are a warning: redefining them is wrong usage rather than a dark-mode bug. |
AURA_SURFACE_PROPERTY_WITHOUT_SURFACE_CLASS |
warning | CSS | --aura-surface-level / --aura-surface-opacity set on a selector that carries neither aura-surface nor aura-surface-solid and is not one of the built-in components that use the surface color. Silently does nothing. |
AURA_ACCENT_SURFACE_WITHOUT_ACCENT_CLASS |
warning | CSS | --aura-accent-color-light / -dark set on a selector Aura does not recompute the accent-derived colors on, so --aura-accent-surface keeps its inherited tint. Silently does nothing. |
AURA_UNITLESS_LENGTH |
error | CSS + Java | An Aura length property given a bare number (--aura-app-layout-inset: 0). It feeds a calc(), so it needs a unit even at zero. |
AURA_UNKNOWN_PROPERTY |
warning | CSS | var() reads an --aura-* property that neither the theme nor the project defines — a typo or a hallucinated token. Fires on reads only; a project may define its own --aura--prefixed properties. |
Exit codes: 0 no error-level findings · 1 error-level findings · 2 usage
error.
The same mistakes are as reachable from a Flow view as from a stylesheet, so the
two checks that need no selector context also run over what .java sources
assign through com.vaadin.flow.dom.Style:
box.getStyle().set("--aura-background-color", "#fff"); // dark mode silently breaks
box.getStyle().set("--aura-app-layout-inset", "0"); // calc() silently breaksStyle.set(String, String) is the only Style member that can assign a custom
property — the rest are typed setters for standard properties — so the matcher
keys on a .set("--…", …) / .bind("--…", …) call. What that means at the edges:
- Non-literal value (
set("--aura-accent-color", accent),bind(…, signal),set(…, "0" + unit)) — the property name is still checked, soAURA_READONLY_PROPERTY_ASSIGNEDstill fires;AURA_UNITLESS_LENGTHhas nothing to read and stays quiet. Style.remove("--aura-…")— not an assignment, not reported.- A snippet quoted in a text block (
"""…""") — documentation, not code, so its contents are blanked before the scan. The name harvesting above still sees them: a token named in a CSS string the project injects is one it knows. setAttribute("style", "--aura-x: 0")— a raw style string, deliberately not parsed. Mistakes written that way go unreported.- A non-
Style.set("--…", …)— reported, knowingly. Requiring the--prefix makes it rare, and the line is worth a look either way.
The three remaining codes stay CSS-only, and not by omission:
AURA_SURFACE_PROPERTY_WITHOUT_SURFACE_CLASS and
AURA_ACCENT_SURFACE_WITHOUT_ACCENT_CLASS need the class names on the same
element, which the documented Java idiom puts in a different statement
(addClassNames(…) on a local variable, then getStyle().set(…)); correlating
those means tracking a variable through a method body, and getting it wrong means
false positives on exactly the correct code. AURA_UNKNOWN_PROPERTY fires on
reads, and Style.set is a write.
Hilla/React views are a third surface with the same failure mode
(<div style={{ '--aura-surface-level': 2 }} />); .tsx / .ts are not walked
yet.
The property names come from @vaadin/aura and are embedded in
go/internal/tools/auradata.go —
77 names measured from @vaadin/aura@25.3.0-rc1 (74 the theme defines plus 3 it
only reads through a fallback). Re-derive them and diff against the embedded
table with:
sh scripts/derive-aura-properties.sh [version]The names are derivable; the write-safety classification is not. Nine
properties are computed from other properties and are still the documented,
intended override points — --aura-neutral-light / -dark,
--aura-accent-text-color-light / -dark,
--aura-accent-contrast-color-light / -dark,
--aura-overlay-outline-color, --aura-overlay-inner-outline-color and
--aura-shadow-color. A naive "is it computed?" scan would flag exactly the
properties an agent needs most, so the split follows the Read-only and
light-dark() badges in the Vaadin Aura reference pages instead, and is
maintained by hand. test/fixtures/aura-customizable is a regression test for
that false-positive trap.
The plugin ships one hook for Claude Code and Codex, wired up in
hooks/hooks.json, which both load from the plugin root by
default.
PostToolUse → styling checks. After the agent edits files (Edit, Write,
or MultiEdit in Claude Code, apply_patch in Codex), the hook runs the
styling checks — check-theme-mixing and check-aura-usage — and, only when
one of them finds an error-level problem, feeds the findings back to the agent
so it can fix them. The scope is deliberately narrow so the hook stays quiet — it
speaks only when all three gates pass:
- An edited file is
.cssor.java(anything else is ignored). - For
.java, the text the edit introduced mentions a Vaadin styling API (getStyle(,*ClassName(s),@CssImport/@StyleSheet/@Theme,LumoUtility,--lumo-/--aura-,*ThemeName(s),getThemeList(). Any.cssedit qualifies. - A check reports an error-level finding for that file's project. Clean projects, warnings, and indeterminate results produce no output, and the edit is never blocked. A Codex patch can touch several projects. Each one is checked once, and its findings are labelled with its directory.
The hook logic lives in the native binary (vaadin-agent-tools hook post-tool-use, reading the PostToolUse event on stdin), so it runs identically
on macOS, Linux, and Windows with no bash, jq, or PowerShell dependency. The
plugin isn't on PATH, so hooks/hooks.json invokes the
selector through ${CLAUDE_PLUGIN_ROOT}, the plugin's install directory:
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/vaadin-agent-tools\" hook post-tool-use"Codex loads the same hooks/hooks.json from the plugin root and sets
CLAUDE_PLUGIN_ROOT for compatibility, so the command needs no changes. Codex
has no field that names a hook, so its /hooks browser uses the handler's
statusMessage as the title (Codex 0.162 and later; older releases show
Hook 1). Claude Code shows the same text as the spinner while the hook runs.
Codex makes edits through apply_patch. Its Edit / Write matcher aliases
fire this hook, but the payload carries the whole patch in tool_input.command
instead of a file_path. The hook reads that patch the way Codex does. Each
added or updated file counts as edited, and a moved file counts at its new path.
A deleted file is skipped. The file's + lines are the text the edit
introduced. Relative paths in the patch resolve against the payload's cwd.
You can test it by hand:
echo '{"tool_input":{"file_path":"/abs/path/App.java","new_string":"btn.getStyle().set(\"color\",\"red\");"}}' \
| bin/vaadin-agent-tools hook post-tool-useOr with a Codex apply_patch payload (printf '%s' keeps the \n escapes for
the JSON parser, where some shells' echo would expand them):
printf '%s' '{"cwd":"/abs/path/my-app","tool_name":"apply_patch","tool_input":{"command":"*** Begin Patch\n*** Update File: src/main/java/com/example/App.java\n@@\n+btn.getStyle().set(\"color\",\"red\");\n*** End Patch"}}' \
| bin/vaadin-agent-tools hook post-tool-useThe command-line surface is the stable interface — treat it as the contract, not the implementation behind it:
- Invocation:
vaadin-agent-tools <tool> [args] [--json] [-h|--help] [-v|--version] - JSON output (
--json):{ "tool", "ok", ...result }, or{ "tool", "ok": false, "usageError" }for a usage error. - Exit codes:
0no error-level findings ·1findings/error ·2usage error.
The implementation is Go today, reached only through this contract (the command
name, args, JSON shape, and exit codes) so a future reimplementation could drop
in behind the same vaadin-agent-tools command without changing how agents or CI
call it.
Cross-compile the binaries for every supported platform into bin/platform/:
sh go/build.shTargets: linux/amd64, linux/arm64, windows/amd64, and a universal macOS
binary (darwin, arm64 + amd64 via lipo; falls back to per-arch binaries when
lipo is unavailable). Binaries are built with CGO_ENABLED=0 and stripped
(-ldflags "-s -w" -trimpath) — small and self-contained, with no packing.
- Create
go/internal/tools/<name>.godefining atool.Descriptor(Name,Summary,Usage, and aRun(tool.Args) tool.Resultfunction). - Register it in the
registryslice ingo/internal/cli/cli.go. - Add a
skills/vaadin-<name>/SKILL.mdso agents discover and run it by description. Prefix the skillnamewithvaadin-so it does not collide with skills from other plugins (the CLI subcommand stays unprefixed).
Use the shared helpers in go/internal/lib — Walk for
scanning project files, NewFinding / NewEvidence for the standard finding
shape — to keep output consistent across tools.
cd go
go test ./...
go vet ./...The tests reuse the sample projects in test/fixtures.
The native binaries in bin/platform/ are committed to the repository so that
a git-source install (a fresh clone, or the marketplace) has a runnable plugin
with no build step. When you change anything under go/, rebuild and commit the
binaries in the same change so they stay in sync with the source:
sh go/build.sh && git add bin/platformBecause the plugin manifest is at the repository root, a marketplace entry can use
a plain url source pointing at this repo — the same pattern vaadin-skills
uses. (If the committed binaries ever become too heavy for the repo, the
alternative is to keep them out and ship a release archive referenced from the
marketplace entry with { "source": "archive", "url": "…", "sha256": "…" }.)
Apache-2.0