CodingOpen SourceFreeActiveMachine-verified· beginner · ~10 min setup

anti-slop: fail delivery on low-contrast normal text

Use anti-slop's UI review guidance, then run its keyless local checker on every solid sRGB normal-text/background pair and stop delivery when the unrounded WCAG AA ratio is below 4.5:1.

FlowStacks verification by Shilpa Mitra·Submitted by @miqdadbadjuber· verified today· v1.0.0

Run this workflow

CI-verified, 6/6 fixtures passing.

Build this with your agent

One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.

Intended Use

Teams using an AI coding agent to build or revise an interface who want a simple fail-closed gate for known solid foreground/background colors used by normal text. Use anti-slop's broader rules as review guidance, obtain the actual rendered sRGB colors for every state and theme, and invoke the pinned contrast-check.py separately for each pair before delivery.

Not for

  • Claiming that one passing pair makes a page accessible, WCAG-conformant, well-designed, or free of generic AI patterns; keyboard behavior, focus, semantics, reflow, content, states, and human judgment remain separate checks
  • Text over transparency, gradients, photographs, video, patterns, or colors altered by blending; the checker accepts two opaque hex values and does not inspect rendered pixels
  • Large-text-only or non-text 3:1 gating by process exit code: v3.2.9 exits successfully only when the pair also passes the normal-text 4.5:1 threshold
  • Following the bundled prose instruction to round a borderline ratio before comparison; WCAG says threshold comparisons are unrounded, and this recipe independently validates the CLI's unrounded verdict
  • Using the bundled v3.2.9 MCP contrast server for borderline pass/fail decisions: that separate implementation rounds the ratio before comparing and is not executed or attested here
  • Running the interactive installer in CI or overwriting existing skill folders and agent instruction files without first reviewing the pinned source, choosing the target scope, and approving the mutation

The Stack

Tested Against

miqdadbadjuber/anti-slop@6eb854fb82c2b5eb9ed970669e59da037627c433anti-slop v3.2.9 source archive@sha256:3f210c3b661b04e76d153bc12ba2e366c73b5aebf55d1e2677617f46e3903eb8Python@3.12Node.js@20WCAG 2.2 Success Criterion 1.4.3

Side effects & data flow

Network
codeload.github.com only while CI fetches the immutable source archive, the user's selected model or coding-agent provider during real UI work; the contrast checker itself has no network path
Writes
./anti-slop.tar.gz, ./source/, ./repo-check.txt, and ./selftest.txt inside the disposable CI directory, user-approved project files during a real agent-assisted UI revision
Credentials
A model-provider or coding-agent credential only for the optional AI-assisted UI work; none for the contrast gate

Data privacy

  • GitHub codeload CI requests only the public immutable anti-slop source archive (retention: per GitHub's published policies)
  • nowhere The pinned contrast checker receives two color strings locally and has no network dependency (retention: process lifetime only)
  • the model provider selected by the user Project code, design context, and prompts during real AI-assisted UI work; none is sent by FlowStacks CI (retention: per the selected provider and account policy)

Prerequisites

  • Python 3 and the pinned anti-slop v3.2.9 antislop-human skill folder for a real contrast check
  • The actual opaque sRGB foreground and background hex values for every normal-text state and theme being shipped
  • Human review of the rendered interface and the rest of the accessibility and product-quality requirements
  • curl, tar, Node.js 20+, and Python 3 for the independent FlowStacks verification gate

Steps

  1. 1

    Pin the release and prove the local normal-text gate

    Fetch the immutable v3.2.9 commit archive with a strict size cap, verify its exact byte size and SHA-256 digest, reject traversal and every member type except regular files and directories, then extract it into the disposable CI directory. Audit the checker boundary as source, run the repository's 11 structural guardrails and eight-row self-test, and independently compare six color pairs with the WCAG 2.2 formula while proving exit 0 for normal-text passes, exit 1 for contrast failures, and exit 2 for invalid input. No Agent Skill, installer, MCP server, model, browser, project, or credential is invoked.

    set -eu
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 2097152 -fsSL   https://codeload.github.com/miqdadbadjuber/anti-slop/tar.gz/6eb854fb82c2b5eb9ed970669e59da037627c433   -o anti-slop.tar.gz
    node <<'NODE'
    const crypto = require("crypto");
    const fs = require("fs");
    const { execFileSync } = require("child_process");
    
    function bad(message) {
      console.error("BAD: " + message);
      process.exit(1);
    }
    const archive = fs.readFileSync("anti-slop.tar.gz");
    if (archive.length !== 920495) bad("source archive size changed");
    const digest = crypto.createHash("sha256").update(archive).digest("hex");
    if (digest !== "3f210c3b661b04e76d153bc12ba2e366c73b5aebf55d1e2677617f46e3903eb8") {
      bad("source archive digest changed");
    }
    const root = "anti-slop-6eb854fb82c2b5eb9ed970669e59da037627c433/";
    const names = execFileSync("tar", ["-tzf", "anti-slop.tar.gz"], { encoding: "utf8" })
      .trim()
      .split("\n");
    if (names.length !== 71) bad("source archive member count changed");
    if (names.some((name) => !name.startsWith(root) || name.startsWith("/") || name.split("/").includes(".."))) {
      bad("source archive contains an unsafe path");
    }
    const verbose = execFileSync("tar", ["-tvzf", "anti-slop.tar.gz"], { encoding: "utf8" })
      .trim()
      .split("\n");
    if (verbose.some((line) => !["-", "d"].includes(line[0]))) {
      bad("source archive contains a non-file/directory member");
    }
    console.log("anti-slop source pinned OK: v3.2.9 at 6eb854f, 71 members, sha256 " + digest);
    NODE
    mkdir source
    tar -xzf anti-slop.tar.gz -C source --strip-components=1
    cd source
    node <<'NODE'
    const fs = require("fs");
    function bad(message) { console.error("BAD: " + message); process.exit(1); }
    function has(text, value, message) { if (!text.includes(value)) bad(message); }
    
    const license = fs.readFileSync("LICENSE", "utf8");
    const checker = fs.readFileSync("skills/antislop-human/contrast-check.py", "utf8");
    const manifest = JSON.parse(fs.readFileSync(".codex-plugin/plugin.json", "utf8"));
    has(license, "MIT License", "MIT license missing");
    has(license, "Copyright (c) 2026 Miqdad Badjuber", "copyright holder changed");
    if (manifest.name !== "antislop" || manifest.version !== "3.2.9" || manifest.license !== "MIT") {
      bad("plugin identity changed");
    }
    const skillDirs = fs.readdirSync("skills", { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .map((entry) => entry.name)
      .sort();
    if (skillDirs.length !== 6 || !skillDirs.includes("antislop-human")) bad("skill set changed");
    for (const forbidden of ["subprocess", "socket", "requests", "urllib", "http.client", "ftplib", "os.system", "os.popen"]) {
      if (checker.includes(forbidden)) bad("checker gained a forbidden capability: " + forbidden);
    }
    has(checker, "return 0 if normal and large else 1", "normal-text exit gate changed");
    has(checker, "return 2", "invalid-input exit gate changed");
    console.log("checker boundary OK: MIT; six skills; contrast-check.py local, keyless, and normal-text fail-closed");
    NODE
    node scripts/check-repo.mjs > ../repo-check.txt
    python3 skills/antislop-human/contrast-check.py --selftest > ../selftest.txt
    node <<'NODE'
    const fs = require("fs");
    function bad(message) { console.error("BAD: " + message); process.exit(1); }
    const repo = fs.readFileSync("../repo-check.txt", "utf8");
    const selftest = fs.readFileSync("../selftest.txt", "utf8");
    if ((repo.match(/^ok\s+/gm) || []).length !== 11 || !repo.includes("all 11 checks passed")) {
      bad("repository guardrails did not all pass");
    }
    if (!selftest.includes("selftest: 8 reference pairs OK")) bad("upstream contrast table self-test failed");
    console.log("repository guardrails OK: 11/11");
    console.log("upstream contrast selftest OK: 8 reference pairs");
    NODE
    python3 <<'PY'
    import importlib.util
    import math
    from pathlib import Path
    import subprocess
    import sys
    
    checker_path = Path("skills/antislop-human/contrast-check.py")
    spec = importlib.util.spec_from_file_location("antislop_contrast", checker_path)
    checker = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(checker)
    
    def reference(hex_color):
        value = hex_color.lstrip("#")
        if len(value) == 3:
            value = "".join(ch * 2 for ch in value)
        rgb = [int(value[i:i + 2], 16) / 255 for i in (0, 2, 4)]
        linear = [c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in rgb]
        return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
    
    def ratio(a, b):
        l1, l2 = sorted((reference(a), reference(b)), reverse=True)
        return (l1 + 0.05) / (l2 + 0.05)
    
    cases = [
        ("#000000", "#FFFFFF", 0),
        ("#FFFFFF", "#000000", 0),
        ("#767676", "#FFFFFF", 0),
        ("#777777", "#FFFFFF", 1),
        ("#949494", "#FFFFFF", 1),
        ("#FFFFFF", "#FFFFFF", 1),
    ]
    for foreground, background, expected_exit in cases:
        expected_ratio = ratio(foreground, background)
        actual_ratio = checker.contrast_ratio(checker.parse_hex(foreground), checker.parse_hex(background))
        if not math.isclose(actual_ratio, expected_ratio, rel_tol=0, abs_tol=1e-12):
            raise SystemExit(f"formula mismatch for {foreground} on {background}")
        run = subprocess.run(
            [sys.executable, str(checker_path), foreground, background],
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            check=False,
        )
        if run.returncode != expected_exit:
            raise SystemExit(f"wrong exit for {foreground} on {background}: {run.returncode}")
        expected_normal = "PASS" if expected_ratio >= 4.5 else "FAIL"
        if f"normal text (4.5:1): {expected_normal}" not in run.stdout:
            raise SystemExit(f"wrong normal-text verdict for {foreground} on {background}")
    
    invalid = subprocess.run(
        [sys.executable, str(checker_path), "#GGGGGG", "#FFFFFF"],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        check=False,
    )
    if invalid.returncode != 2 or "expected a hex color" not in invalid.stdout:
        raise SystemExit("invalid input did not fail with exit 2")
    print("contrast boundary OK: 6 independent WCAG 2.2 pairs; normal-text exit 0/1 and invalid exit 2")
    PY
  2. 2

    Build or revise the UI, then identify every actual color pair

    Use the pinned anti-slop rules as review guidance in During or After mode, with explicit user approval before installing or changing files. Inspect the rendered UI in every supported state and theme. Record the opaque sRGB foreground and background hex values for each normal-text treatment; do not substitute design tokens without proving their resolved values, and do not reduce a gradient, image, transparency, or blended background to a guessed single color.

  3. 3

    Run the local checker for every normal-text pair and stop on failure

    Invoke the pinned skills/antislop-human/contrast-check.py with Python 3 once per recorded pair. Exit 0 means that pair clears the normal-text 4.5:1 gate; exit 1 means delivery stops until the color is corrected and rechecked; exit 2 means the input or invocation is invalid. Preserve the checker output in the review evidence, then separately complete keyboard, focus, semantics, state, responsive-layout, content, and human visual checks. Do not describe the result as whole-page WCAG certification.

Eval, 6 fixtures

Last passed: verified today
  • source-pinnedcontainstimeout 120s · max $0

    Expected: anti-slop source pinned OK: v3.2.9 at 6eb854f, 71 members, sha256 3f210c3b661b04e76d153bc12ba2e366c73b5aebf55d1e2677617f46e3903eb8

  • checker-boundarycontainstimeout 120s · max $0

    Expected: checker boundary OK: MIT; six skills; contrast-check.py local, keyless, and normal-text fail-closed

  • repo-guardrailscontainstimeout 120s · max $0

    Expected: repository guardrails OK: 11/11

  • upstream-selftestcontainstimeout 120s · max $0

    Expected: upstream contrast selftest OK: 8 reference pairs

  • independent-boundarycontainstimeout 120s · max $0

    Expected: contrast boundary OK: 6 independent WCAG 2.2 pairs; normal-text exit 0/1 and invalid exit 2

  • clean-exitexit_codetimeout 120s · max $0

    Expected: 0

Results

FlowStacks pins the complete anti-slop v3.2.9 source archive, rejects path traversal and unsafe archive members before extraction, verifies the MIT license and local/keyless checker boundary, runs all 11 upstream repository guardrails and the eight-pair upstream self-test, then independently checks six color pairs plus invalid input against the current WCAG 2.2 formula and the CLI's 0/1/2 exit semantics. The verified command is deliberately limited to solid sRGB normal text: it does not inspect a rendered page, infer font size, sample gradients or images, or certify the subjective anti-slop Delivery Gate.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Related workflows

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).