uxskill
Star on GitHub
Language
English 简体中文 繁體中文 日本語 한국어 हिन्दी Bahasa Indonesia Tiếng Việt ไทย العربية Español Français Deutsch Português Русский Türkçe Italiano
Blog · CLI · 2026-05-28

# The anti-slop CLI for vibe coders — 145 rules, no LLM, runs in CI.

The vibe coder's tool belt is a hand-rolled set of CLI primitives. Cursor, Claude Code, Cline, Aider, Codex — the model writes; the developer reviews. The missing piece is a deterministic gate that catches the AI design fingerprint before it merges. Not an LLM judge. Not a SaaS dashboard. A command-line linter that runs in milliseconds, plugs into pre-commit, and exits non-zero on a finding. Here is the engineering write-up.

By the ux-skill team · 8 min read · MIT-licensed open source

## Why a CLI is the right shape for vibe coding

Vibe coding sits inside an existing developer toolchain: git, a package manager, an IDE that shells out, a hook runner. The toolchain is text in and text out. A SaaS dashboard or a Figma plugin is the wrong shape — it sits next to the toolchain instead of inside it. Anything that interrupts the keyboard-only loop costs more than the time the loop saved.

The CLI shape gets four things for free: composability with shell pipelines, native pre-commit integration, native CI integration, and no auth handshake. A command that reads files and exits non-zero is the most boring API surface on a developer laptop, which is exactly why it is the right one.

ux-skill's `ux lint` command is built on those constraints. One binary entry point. JSON or human-readable output. Configurable threshold. No network call. No background daemon.

## The architecture in one paragraph

The package is a Python wheel published to PyPI under the name `uxskill`, with an npm wrapper that vendors a self-contained Python runtime so JavaScript teams without Python tooling still get the same CLI. Inside, the engine loads `data/anti-patterns.json` — 100 entries today, each a regex with a severity, category, scope, and fix string. The scanner walks the target directory, filters by file extension against each rule's scope, runs each regex against the matching files, and emits a structured findings array. The CLI prints the array as a JSON document or a human-readable list. Findings at or above the threshold cause a non-zero exit.

Nothing about the architecture is novel. The engineering work is in the catalog, the test suite, and the cross-IDE distribution — not in the runner. 75 unit tests cover the rule loader, the scope filter, the regex compiler, the threshold gate, and the JSON output format. Every release ships a green test matrix on Python 3.10, 3.11, 3.12, and 3.13.

## The command surface

The lint subcommand is the most-used. Three modes cover every real use case:

    # Mode 1 — default: scan ./, threshold high, exit non-zero on findings
    $ uxskill lint
    [OK] Scanned 142 files in 412ms · 0 findings at threshold high

    # Mode 2 — scan a subtree, lower the threshold
    $ uxskill lint apps/web/src --threshold medium

    # Mode 3 — JSON output, scan only staged files (used by pre-commit)
    $ uxskill lint --json --staged | jq '.findings | length'

Flags that matter day to day:

- `--threshold <low|medium|high|critical>` — only count findings at or above this severity. Default is `high`.
- `--json` — emit a JSON document instead of human-readable output. Used by CI and editor plugins.
- `--staged` — scan only files staged in git. The right mode for pre-commit.
- `--rules <id,id,id>` — restrict the scan to a comma-separated list of rule ids. Useful for targeted CI jobs.
- `--ignore <id,id>` — suppress specific rule ids. Each ignore should land with a comment in the repo's `.uxskillignore` file explaining why.
- `--report <path>` — write the JSON report to a file. Used by CI artifact upload.

## The JSON shape

The output schema is stable. Editor plugins, CI dashboards, and IDE integrations rely on it.

    {
      "files_scanned": 142,
      "rules_loaded": 100,
      "exit_code": 1,
      "findings": [
        {
          "rule_id": "font-stretched-display",
          "rule_name": "Body face used at display size",
          "severity": "high",
          "category": "Typography",
          "file": "src/components/Hero.tsx",
          "line": 18,
          "column": 12,
          "excerpt": "font-['Inter'] text-7xl leading-none",
          "fix": "Pair the body face with a distinct display face"
        }
      ]
    }

Every field is documented in `docs/schema/findings.md`. The schema is versioned via the `schema_version` field in `data/anti-patterns.json` — consumers can pin to a major version and trust that minor versions are additive.

## Pre-commit, GitHub Actions, GitLab CI, Husky, Lefthook

Five integration recipes, all built on the same exit-code contract. Pick the one that matches your hook runner; the rest is identical.

### pre-commit (Python ecosystem)

    # .pre-commit-config.yaml
    repos:
      - repo: local
        hooks:
          - id: uxskill-lint
            name: ux-skill anti-pattern lint
            entry: uxskill lint --threshold high --staged
            language: python
            additional_dependencies: [uxskill]
            files: \.(tsx|jsx|vue|css|scss|html|astro|blade)$
            pass_filenames: true

### GitHub Actions

    # .github/workflows/ux-lint.yml
    name: ux-lint
    on: {pull_request: {}, push: {branches: [main]}}
    jobs:
      lint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-python@v5
            with: {python-version: '3.11'}
          - run: pip install uxskill
          - run: uxskill lint --threshold high --report lint.json
          - if: failure()
            uses: actions/upload-artifact@v4
            with: {name: ux-lint-report, path: lint.json}

### GitLab CI

    # .gitlab-ci.yml
    ux-lint:
      image: python:3.11-slim
      stage: test
      script:
        - pip install uxskill
        - uxskill lint --threshold high --report lint.json
      artifacts:
        when: on_failure
        paths: [lint.json]

### Husky (Node ecosystem)

    # .husky/pre-commit
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"

    npx uxskill@latest lint --threshold high --staged || exit 1

### Lefthook (multi-language)

    # lefthook.yml
    pre-commit:
      commands:
        ux-lint:
          glob: '*.{tsx,jsx,vue,css,scss,html,astro,blade}'
          run: uxskill lint --threshold high --staged {staged_files}

Every recipe uses the same flags. The contract is the exit code; everything else is decoration.

## Performance, measured honestly

Numbers from a 2023 MacBook Air, M2, scanning a Next.js repo with 200 components, 84 stylesheets, and 12 megabytes of source under `apps/web/src`:

Scenario | Files scanned | Rules loaded | Wall time
--- | --- | --- | ---
Cold start, full repo | 342 | 100 | 412 ms
Warm, full repo | 342 | 100 | 96 ms
Warm, staged diff (8 files) | 8 | 100 | 18 ms
Warm, single file | 1 | 100 | 7 ms
GitHub Actions, cold runner | 342 | 100 | ~7.2 s (incl. pip install)

The pre-commit path is the one developers feel. Eighteen milliseconds for the staged-diff case is comfortably under the perceptual threshold — the hook is invisible.

> Make the slow path good and the fast path invisible. The CLI is the fast path.

## What the 145 rules cover

The catalog is grouped by category. The counts are current as of v2.1:

Category | Count | What it catches
--- | --- | ---
A11y | 28 | Alt text, focus styles, click target size, color-only state, ARIA on interactive elements, viewport that blocks pinch-zoom.
Content | 16 | Generic placeholder names, sample copy leaks, marketing verbs in headlines, fake testimonial stars, placeholder pricing.
Typography | 14 | Body face at display sizes, hairline body weights, rainbow gradient on text, all-caps body, missing font-feature-settings.
Color | 12 | Default AI gradient over white, chrome-y multi-stop gradients, neon on light surfaces, low-contrast text on cards.
Quality | 10 | Inline style attribute on shipping components, console.log leaks, dead imports, hardcoded pixels where tokens exist.
Layout | 8 | Three equal cards in a row, full-bleed gradient hero, centered-everything axis, 100vh without 100dvh fallback.
Motion | 6 | Fade-in-up on every direct child, no reduced-motion branch, animating layout properties instead of transform.
Visual | 4 | Drop shadows on flat designs, blur backdrops without fallback, gratuitous neumorphism, glassmorphism on white.
Performance | 2 | Animating layout properties, animating filters in scroll handlers.
Total | 100 | From 35 at v2.0.0 launch to 100 in the v2.1 cycle.

## The recommender is the upstream half

The linter is the downstream half of the loop. The upstream half is `ux recommend` — five parallel lookups over a 1,243-entry catalog merged into a single structured design system. The recommender narrows the model's choices before the prompt; the linter punishes the model when the choices slip anyway.

Both halves share one source-of-truth document at `.ux/design-system/MASTER.md`. Every IDE's rules file (`.cursorrules`, `CLAUDE.md`, `.windsurfrules`) is generated from that document. There is one place to look when something is wrong, one place to edit when something needs to change.

Read more in the recommender write-up and the regex-linter engineering post.

Honest scope

#### The CLI is the floor, not the ceiling.

The linter catches structural fingerprints — literal tokens, class names, hex values, DOM shapes. It does not catch "the section rhythm is wrong" or "the copy reads cold." Those need a review pass, by a human or a stronger model. The CLI is the deterministic floor; `/ux-critique` ships in the same package for the taste pass.

The reason the CLI never calls an LLM is the same reason a unit test never calls an LLM. Reproducibility is the entire value proposition. If the lint result depends on a model's mood, the lint is no longer a lint — it is a recommendation, and recommendations belong in a different layer.

## What lives in `.uxskillignore`

Every codebase has legitimate edge cases. A design tool that needs to render an emoji literal. A docs page that quotes a regex pattern. A demo file that shows the wrong-way example. The `.uxskillignore` file is a per-repo list of rule-and-path pairs, each with a required reason:

    # .uxskillignore
    # Demo page documents what NOT to do; the lint trips on its own examples.
    docs/anti-patterns.html                font-stretched-display, default-ai-gradient

    # Inline SVG icon stamps are legitimate in this design system.
    src/components/Stamp.tsx               icon-emoji-stamp

    # Story file for Chromatic visual diffs.
    src/**/*.stories.tsx                   fade-in-up-everywhere

The format is two columns: a glob and a comma-separated rule id list. Anything to the right of a `#` is treated as a comment. The required-reason convention is enforced by code review, not by the parser.

## Cross-IDE distribution

The CLI is one package; every IDE that shells out reaches the same engine. Specifically:

- Claude Code — `/plugin install ux@ux-skill`; slash commands map to the CLI subcommands.
- Cursor — `npx uxskill@alpha init`; emits `.cursorrules` and registers a pre-commit hook.
- Windsurf — same npx initializer; emits `.windsurfrules`.
- Cline · Continue · Aider · Codex · Roo Code — read whatever rules file is at the repo root. The CLI emits all three formats from the same recommendation.
- JetBrains · Zed · GitHub Copilot · Gemini Code Assist — same story. The CLI does not care which editor is on the other side of the file.

Walkthroughs for each: Cursor, Windsurf, Zed, JetBrains, GitHub Copilot.

## The vibe coder's tool belt, one slot at a time

Every serious vibe coder ends up with the same five things in their belt by month two: a strong IDE (Cursor or Claude Code), a hook runner (pre-commit, Husky, or Lefthook), an LLM router (Aider, Continue, or Codex), an MCP server or two (sequential thinking, GitHub, filesystem), and a deterministic gate for the output. ux-skill is built for the last slot — the gate.

14 stars on GitHub at time of writing, against 84k for the largest competitor in the marketplace. The fight is not for the headline number, it is for the slot. Once the gate is in the belt, the rest of the loop compounds.

## Related reading

- Regex linter for AI coding — the engineering write-up on the rule format itself.
- Vibe coding is real — but your AI still ships the same defaults — the workflow framing.
- Python design system generator — the upstream half of the loop.
- Anti-AI-slop tools for Claude Code — where ux-skill sits in the toolset.

Install the gate

## One CLI. Three install paths. No LLM.

145 deterministic regex rules, a 1,243-entry catalog, 25 commands across 17 IDEs. Plugs into pre-commit, Husky, Lefthook, GitHub Actions, GitLab CI. MIT-licensed. No telemetry. No account.

$ /plugin marketplace add Laith0003/ux-skill
$ /plugin install ux@ux-skill
— or —
$ pip install uxskill
— or —
$ npx uxskill@alpha init

## Related reads

- Regex linter for AI coding
- Vibe coding + ux-skill
- Python design system generator
- Anti-AI-slop tools for Claude Code