Skill for AI coding agents + static analyzer CLI for developing with StarHTML.
StarHTML is a Python-first reactive web framework using Datastar. This repo helps AI agents (Claude Code, OpenCode, Cursor, etc.) write correct StarHTML code and catch framework-specific bugs before runtime.
This project follows the Bash + Code philosophy: agents already know Bash and Pythonβleverage that instead of adding protocol overhead.
| Approach | Token Cost | Composable | Extensible | Agent Knowledge |
|---|---|---|---|---|
| starhtml-skill | ~5k (core) + ~12k (ref, on-demand) | β Output to files, chain commands | β Single Python file | β Bash + Python |
| MCP Server | High (protocol + tools) | β Must pass through agent context | β Full codebase to understand | β MCP-specific protocol |
| LSP | High (config + capabilities) | β Tied to editor/IDE | β Language server protocol | β LSP-specific protocol |
1. Token Efficient
- Core skill: ~5k tokens (SKILL.md is 14.7KB) β loaded once per session
- Reference files: ~12k tokens total β loaded contextually when needed
demos.mdβ index of 30 official demo fileshandlers.mdβ plugins: persist, scroll, motion, drag, canvas...icons.mdβ Icon() component referencejs.mdβ js(), f(), value(), regex() referenceslots.mdβ slot system reference
- Checker output: concise, structured, designed for LLM loops
- No protocol overheadβjust Python and CLI
2. Composable
# Chain commands, save to files, integrate anywhere
starhtml-check component.py --summary > issues.txt
starhtml-check --fix f.py && git commit -m "fix: $(cat issues.txt)"3. Extensible
- Single Python file (
starhtml_check.py) β no dependencies - Add new rules in minutes, not hours
- Agents can read and modify the checker itself
4. Agent-Agnostic
- Works with any coding agent (Claude Code, Cursor, OpenCode, Qwen, etc.)
- No MCP host required
- No LSP server configuration
- Load skill via
npx skills addor manual install
5. Framework-Specific Intelligence
- Catches StarHTML-specific bugs (reactivity, signal naming, f-string traps)
- Locality of Behavior checks (W028, W030) β generic linters (pylint, flake8) miss these
- HTTP action validation, plugin registration, SSE handler resets
- Designed for the way StarHTML actually works, not generic Python linting
For StarHTML development with AI agents, CLI + Skill is simpler, faster, and more flexible.
# Install the skill using the skills manager
npx skills add renatocaliari/starhtml-skillThis will:
- Download this repository
- Find
skills/starhtml/SKILL.md(wherename: starhtml) - Install to
~/.agents/skills/starhtml/(or project-level.agents/skills/)
If you prefer manual installation:
# Clone to skills directory
git clone https://github.com/renatocaliari/starhtml-skill.git ~/.agents/skills/starhtml-temp
# Move the skill to correct location (name must match directory)
mv ~/.agents/skills/starhtml-temp/skills/starhtml ~/.agents/skills/starhtml
rm -rf ~/.agents/skills/starhtml-tempThen load the skill based on your agent:
- Claude Code: Already loads from
~/.claude/skills/or.claude/skills/ - OpenCode: Add to
opencode.json:{"instructions": ["~/.opencode/skills/starhtml/SKILL.md"]} - Cursor: Create
.cursor/rules/starhtml.mdcwith SKILL.md content - Other agents: Load
~/.agents/skills/starhtml/SKILL.md+reference/*.mdas context
Option A β Global install (recommended):
# macOS / Linux - system-wide (may require sudo)
curl -L https://raw.githubusercontent.com/renatocaliari/starhtml-skill/main/starhtml_check.py -o /usr/local/bin/starhtml-check && chmod +x /usr/local/bin/starhtml-check
# Or user-local (no sudo required)
curl -L https://raw.githubusercontent.com/renatocaliari/starhtml-skill/main/starhtml_check.py -o ~/.local/bin/starhtml-check && chmod +x ~/.local/bin/starhtml-checkOption B β pip install:
pip install git+https://github.com/renatocaliari/starhtml-skill.gitOption C β Local download (per-project):
curl -O https://raw.githubusercontent.com/renatocaliari/starhtml-skill/main/starhtml_check.pyOnce installed globally, update to the latest version anytime:
# Check for updates and install if available
starhtml-check --updateThis will:
- Fetch the latest version from GitHub
- Compare with your current version
- Create a backup of your current file (
.bak) - Update to the latest version automatically
# If installed globally:
starhtml-check component.py # full analysis
starhtml-check --summary f.py # compact output
starhtml-check --update # check for updates and update
# If downloaded locally:
python starhtml_check.py component.py
python starhtml_check.py --summary f.py
python starhtml_check.py --updateLoop: write β check β fix ERRORs β re-run β β no issues
starhtml-skill/
βββ starhtml_check.py # static analyzer CLI (zero dependencies)
βββ pyproject.toml # for pip install
βββ skills/ # skills directory (for npx skills add)
β βββ starhtml/
β βββ SKILL.md # core skill β load this first
β βββ reference/ # sub-references (load on demand)
β βββ demos.md # index of 30 official demo files
β βββ icons.md # Icon() component reference
β βββ js.md # js(), f(), value(), regex() reference
β βββ handlers.md # plugins: persist, scroll, motion, drag, canvas...
β βββ slots.md # slot system reference
βββ README.md # this file
from starhtml import *
# Define reactive state (walrus := in outer parens)
(counter := Signal("counter", 0))
(name := Signal("name", ""))
(visible := Signal("visible", True))
# Reactive attributes
data_show=visible # show/hide
data_text=name # display value
data_bind=name # two-way binding
data_class_active=visible # toggle class
# Events
data_on_click=counter.add(1)
data_on_input=(search, {"debounce": 300})
data_on_submit=(post("/api/save"), {"prevent": True})
# Signal operations
counter.add(1) # increment
counter.set(0) # assign
visible.toggle() # boolean flip
name.upper() # string method
count.default(0) # nullish fallback
theme.one_of("light", "dark") # enum guard- No f-strings in reactive attrs β use
+orf()helper - data_show needs flash prevention β
style="display:none" - Positional args BEFORE keywords β
Div("Hello", cls="container") - Signal names must be snake_case β
my_count, notmyCount - Walrus
:=in outer parens β(name := Signal("name", "")) - Signals are reactive state, NOT data containers β use Python variables for data
| Code | Issue |
|---|---|
| E001 | Positional arg after keyword β Python SyntaxError |
| E002 | f-string in reactive attr β static, won't update in browser |
| E003 | f-string URL in HTTP action β Python-static, not reactive |
| E004 | Special chars (: / [ ]) in data_class_* β parse error |
| E005 | camelCase Signal name β must be snake_case |
| E006 | f() helper used without import β NameError at runtime |
| E007 | data_attr_class and data_attr_cls on same element β different behaviors |
| E008 | Walrus := Signal without outer parentheses β breaks reactivity |
| E009 | data_show without flash prevention β element flashes before JS loads |
| E010 | Form submit without is_valid guard β submits invalid data |
| E011 | data_on_scroll/data_on_input without throttle/debounce β performance bug |
| E012 | @sse endpoint without yield signals() reset β client state not cleaned |
| E013 | Icon() without explicit size β inherits 1em from font-size |
| E014 | js() raw JavaScript β potential security risk |
| E015 | Plugin data attribute used without plugin registration |
| E016 | data_on_submit with post() without {"prevent": True} β page reloads |
| E017 | Signal.value access β Signals don't have .value attribute |
| E018 | len(signal) β Signals don't support len() |
| E019 | signals() with positional arguments β use keyword arguments |
| Code | Issue |
|---|---|
| W003 | 3+ signals with & operator β prefer all() for readability |
| W008 | Signal name too short β prefer descriptive snake_case names |
| W012 | Signal with empty name β use descriptive snake_case names |
| W015 | delete() HTTP action without confirmation β data loss risk |
| W016 | Signal used but not defined β will cause runtime error |
| W017 | Computed Signal detected β auto-updates on dependencies |
| W018 | _ref_only=True Signal β excluded from data-signals (correct) |
| W019 | f-string in elements() selector β verify selector is static |
| W020 | elements() replace-mode without explicit id β may not be targetable |
| W021 | switch() used for CSS classes β use collect() to combine |
| W022 | collect() used for exclusive logic β use switch() or if_() |
| W023 | .then() without conditional signal β verify boolean signal is used |
| W024 | data_effect without .set() β use signal.set(expression) |
| W025 | Component function without **kwargs β limits pass-through attributes |
| W026 | f() helper with < 3 signals β prefer + operator for 1-2 signals |
| W027 | File > 400 lines β consider splitting into smaller modules |
| W028 | Deep nesting (>3 levels) β extract to sub-component for better LoB |
| W029 | Signal not used in backend without _ prefix β indicate frontend-only |
| W030 | js() that StarHTML can handle β Locality of Behavior violation |
Issues and PRs welcome.
For new checker rules, include:
- The bug it prevents (real example)
GOT:example of wrong codeFIX:example of corrected code
For skill improvements, test with at least one LLM agent before submitting.
MIT License β see LICENSE for details.