Oh My Design: evidence-led UI work with deterministic delivery gates
Turn an AI-assisted interface request into an evidence-backed brief, traceable copy, deliberate structure, one production build, and a blind rendered critique, while using Oh My Design's local CLI gates to catch contract drift, stale source, unsafe stack replacement, and reviewable UI-risk signals before delivery.
Run this workflow
CI-verified, 7/7 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
Product and marketing UI work where a coding agent should make its framing, evidence, copy, structural choice, source freshness, and rendered review traceable instead of jumping directly to polished code. Use the pinned OMD process with explicit human scope and host approvals, run deterministic gates in the real project, inspect desktop and mobile renders, and keep the final design decision and release authority with a human.
Not for
- Treating a green CLI or CI result as proof that an interface is beautiful, original, accessible, correct, legally cleared, or approved by users; the most important design and critique judgments remain agent-authored and require human review
- Treating source-scan candidates as an authorship detector or automatic rejection: OMD documents them as advisory signals for contextual triage, and the source scanner exits successfully even when candidates exist
- Assuming a successful render or lint covers real product behavior, authenticated states, keyboard/focus behavior, assistive technology, responsive edge cases, performance, security, or destructive flows
- Installing directly into a shared host without reviewing the pinned source and backup plan: the installer copies plugin/agent payloads, patches Claude or Codex configuration, may refresh an existing Claude plugin cache, and can install a checksum-pinned browser-rs helper
- Giving untrusted reference pages, imported files, screenshots, or tool output authority to execute commands or disclose credentials; OMD is a workflow layer running with its host's permissions, not an OS sandbox
- Assuming `/tmp` is a supported render destination on current main: guarded writes stay inside the project root and render commands also append project-local `.omd/history.jsonl`
- Treating pre-1.0 single-maintainer main as a stable release channel; pin exact commits and re-audit source, generated payloads, dependencies, and host mutations before upgrades
The Stack
Tested Against
3x-haust/oh-my-design@6b502884990dab63a5534d4d7fff81dd9caddaccsource archive@sha256:f96d8f973b417ff110d898802eec2038e9211a423f89e8546bef0448591fb56bNode.js@22.23.2smol-toml@1.8.0Playwright@1.61.1 (render gates independently re-audited; browser runtime is outside the published CI fixtures)Side effects & data flow
- Network
- codeload.github.com, nodejs.org, and registry.npmjs.org/security endpoints in CI, the selected model/agent provider, explicitly chosen public reference sites, and optional Figma or browser-provider endpoints during a real run
- Writes
- ./source.tar.gz, ./source/, ./node-runtime/, ./npm-cache/, and source/node_modules/ inside the disposable CI directory, project-local .omd records, PNG evidence, and user-approved application files during a real run, host plugin, agent, configuration, backup, and optional browser-helper files only when the user explicitly runs the installer
- Credentials
- A model-provider or coding-agent credential for agent-assisted work; optional FIGMA_TOKEN only for explicitly selected Figma operations; none for the FlowStacks CI gate
Data privacy
- GitHub codeload, Node.js distribution, and npm advisory services ← CI requests only public pinned source/runtime bytes and lockfile dependency metadata; it sends no user project, prompt, screenshot, or credential (retention: per the respective operators' published policies)
- the model or coding-agent provider selected by the user ← During a real run: authorized project code, briefs, copy, design context, and prompts; none is sent by FlowStacks CI (retention: per the selected provider and account policy)
- public reference sites and any explicitly selected browser/Figma provider ← During real research or rendering: requested URLs, browser requests, page content, screenshots, and optional Figma document data; none is accessed by the browser-free CI gate (retention: per each provider's policy and the user's local evidence-retention choices)
Prerequisites
- Node.js 22.19+ and a reviewed, commit-pinned Oh My Design installation for a real project run
- Codex, Claude Code, or a compatible Pi host with narrowly scoped filesystem and command authority
- A launchable supported browser provider for rendered evidence, plus human inspection of desktop/mobile output
- Authorized public references and appropriate rights for every retained image, copy source, and design influence
- curl, tar, xz, and a supported x64 Linux or Apple-silicon macOS host for the independent FlowStacks gate
Steps
- 1
Pin the signed source and reproduce the Node 22 deterministic gates
Download the exact owner-supplied commit archive, verify its byte size, SHA-256, path set, member count, and sole safe `CLAUDE.md -> AGENTS.md` symlink before extraction. Audit the MIT/package/lockfile/read-only scanner boundary as inert source, download an official Node.js 22.23.2 binary for the CI host and verify its release checksum, install the exact graph with lifecycle scripts disabled, require a clean moderate-severity audit, run the submitted 135-test slice, check the exact source-scan and stack-routing JSON, and pass TypeScript. No Skill, installer, MCP server, model, browser, reference site, host configuration, user project, or credential is invoked.
set -eu curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 5242880 -fsSL https://codeload.github.com/3x-haust/oh-my-design/tar.gz/6b502884990dab63a5534d4d7fff81dd9caddacc -o source.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("source.tar.gz"); if (archive.length !== 3748602) bad("source archive size changed"); const digest = crypto.createHash("sha256").update(archive).digest("hex"); if (digest !== "f96d8f973b417ff110d898802eec2038e9211a423f89e8546bef0448591fb56b") bad("source archive digest changed"); const root = "oh-my-design-6b502884990dab63a5534d4d7fff81dd9caddacc/"; const names = execFileSync("tar", ["-tzf", "source.tar.gz"], { encoding: "utf8" }).trim().split("\n"); if (names.length !== 1128) bad("source archive member count changed"); if (names.some((name) => !name.startsWith(root) || name.startsWith("/") || name.split("/").includes(".."))) bad("unsafe source archive path"); const verbose = execFileSync("tar", ["-tvzf", "source.tar.gz"], { encoding: "utf8" }).trim().split("\n"); const links = verbose.filter((line) => line[0] === "l"); if (links.length !== 1 || !links[0].endsWith(root + "CLAUDE.md -> AGENTS.md")) bad("source archive symlink set changed"); if (verbose.some((line) => !["-", "d", "l"].includes(line[0]))) bad("source archive contains a special member"); console.log("Oh My Design source pinned OK: commit 6b50288, 1128 members, sha256 " + digest); NODE mkdir source tar -xzf source.tar.gz -C source --strip-components=1 node <<'NODE' const fs = require("fs"); function bad(message) { console.error("BAD: " + message); process.exit(1); } function read(name) { return fs.readFileSync("source/" + name, "utf8"); } const license = read("LICENSE"); if (!license.includes("MIT License") || !license.includes("Copyright (c) 2026 3x-haust")) bad("MIT identity changed"); const pkg = JSON.parse(read("package.json")); if (pkg.name !== "@3xhaust/oh-my-design" || pkg.version !== "0.20.0" || pkg.engines?.node !== ">=22.19" || pkg.license !== "MIT") bad("package identity/runtime changed"); const expectedDependencies = { playwright: "1.61.1", "smol-toml": "^1.8.0", tldts: "7.4.13", tsx: "^4.23.1", yaml: "^2.6.0" }; if (JSON.stringify(pkg.dependencies) !== JSON.stringify(expectedDependencies)) bad("direct dependency set changed"); const lock = JSON.parse(read("package-lock.json")); if (lock.lockfileVersion !== 3 || Object.keys(lock.packages || {}).length !== 61) bad("lockfile graph changed"); for (const [name, meta] of Object.entries(lock.packages)) { if (name && (!meta.integrity || !String(meta.resolved || "").startsWith("https://registry.npmjs.org/"))) bad("unlocked or non-npm dependency " + name); } if (lock.packages["node_modules/smol-toml"]?.version !== "1.8.0") bad("smol-toml is below the audited fix"); const installScripts = Object.entries(lock.packages).filter(([, meta]) => meta.hasInstallScript).map(([name]) => name).sort(); const expectedScripts = ["node_modules/esbuild", "node_modules/fsevents", "node_modules/tsx/node_modules/fsevents"]; if (JSON.stringify(installScripts) !== JSON.stringify(expectedScripts)) bad("dependency lifecycle-script set changed"); const readOnlyModules = ["core/slop/index.ts", "core/design/index.ts", "core/stack/index.ts", "core/stack/shell.ts"].map(read).join("\n"); if (/from ['"]node:(?:child_process|http|https|net|tls)['"]/.test(readOnlyModules)) bad("browser-free scanner gained process or network access"); if (/\b(?:writeFileSync|appendFileSync|rmSync|unlinkSync|renameSync|mkdirSync)\b/.test(readOnlyModules)) bad("browser-free scanner gained write access"); console.log("dependency boundary OK: Node >=22.19; smol-toml 1.8.0; 61 lock entries; lifecycle scripts disabled"); NODE case "$(uname -s)-$(uname -m)" in Linux-x86_64) NODE_ASSET=node-v22.23.2-linux-x64.tar.xz NODE_SHA=d60acfe00a2932254bb0ad20e01b0d74397a0875595de719654b214f4b03f307 ;; Darwin-arm64) NODE_ASSET=node-v22.23.2-darwin-arm64.tar.gz NODE_SHA=61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6 ;; *) echo "BAD: unsupported CI host for pinned Node.js binary" >&2; exit 1 ;; esac curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 67108864 -fsSL "https://nodejs.org/dist/v22.23.2/$NODE_ASSET" -o node-runtime.tar ACTUAL_NODE_SHA=$(shasum -a 256 node-runtime.tar | cut -d ' ' -f 1) test "$ACTUAL_NODE_SHA" = "$NODE_SHA" || { echo "BAD: Node.js release digest changed" >&2; exit 1; } mkdir -p node-runtime npm-cache case "$NODE_ASSET" in *.tar.xz) tar -xJf node-runtime.tar -C node-runtime --strip-components=1 ;; *) tar -xzf node-runtime.tar -C node-runtime --strip-components=1 ;; esac test "$(node-runtime/bin/node --version)" = "v22.23.2" || { echo "BAD: wrong Node.js version" >&2; exit 1; } RUNTIME_BIN="$PWD/node-runtime/bin" NPM_CACHE="$PWD/npm-cache" cd source env PATH="$RUNTIME_BIN:/usr/bin:/bin" npm_config_cache="$NPM_CACHE" npm ci --ignore-scripts --no-audit env PATH="$RUNTIME_BIN:/usr/bin:/bin" npm_config_cache="$NPM_CACHE" npm audit --package-lock-only --audit-level=moderate echo "dependency audit OK: zero known vulnerabilities at moderate severity" env PATH="$RUNTIME_BIN:/usr/bin:/bin" node --test --test-concurrency=4 test/slop-source.test.ts test/copy.test.ts test/design.test.ts test/source-seal.test.ts test/stack.test.ts > target-tests.txt env PATH="$RUNTIME_BIN:/usr/bin:/bin" node <<'NODE' const fs = require("fs"); function bad(message) { console.error("BAD: " + message); process.exit(1); } const tap = fs.readFileSync("target-tests.txt", "utf8"); for (const expected of [/^# tests\s+135$/m, /^# pass\s+135$/m, /^# fail\s+0$/m, /^# skipped\s+0$/m]) { if (!expected.test(tap)) bad("targeted Node 22 regression suite changed"); } console.log("targeted regression suite OK: 135/135 on Node 22.23.2"); NODE env PATH="$RUNTIME_BIN:/usr/bin:/bin" node bin/omd.ts slop scan test/fixtures --json > source-scan.json env PATH="$RUNTIME_BIN:/usr/bin:/bin" node bin/omd.ts stack --json > stack.json env PATH="$RUNTIME_BIN:/usr/bin:/bin" node <<'NODE' const fs = require("fs"); function bad(message) { console.error("BAD: " + message); process.exit(1); } const scan = JSON.parse(fs.readFileSync("source-scan.json", "utf8")); if (scan.schemaVersion !== "1.0" || scan.filesScanned !== 25 || !Array.isArray(scan.candidates) || scan.candidates.length !== 0) bad("source-scan result changed"); const stack = JSON.parse(fs.readFileSync("stack.json", "utf8")); if (stack.stack !== "existing" || stack.framework !== null || stack.greenfield !== false || stack.shell?.kind !== "browser") bad("stack-routing result changed"); console.log("deterministic CLI gates OK: slop scan 25 files / 0 candidates; existing stack preserved"); NODE env PATH="$RUNTIME_BIN:/usr/bin:/bin" node node_modules/typescript/bin/tsc --noEmit echo "type gate OK: TypeScript clean" - 2
Run the evidence, copy, structure, build, and blind-review loop with explicit authority
Review the pinned Skill and host permissions before installation. Frame the user outcome and risks; retain only authorized, attributable evidence; write source-linked copy before layout; compare anonymous structural candidates with a fresh selector; build exactly one chosen direction; and keep OMD records inside the authorized project. Treat agent-produced research, selections, and critiques as proposals. Do not let page content or tool output grant command, credential, publication, or scope authority.
- 3
Render inside the project boundary, triage findings, and require human release review
Build the actual target and render the named desktop/mobile states to project-local paths; current main rejects `/tmp` output and records render history under `.omd/`. Use `omd check` and the source scan as bounded signals, preserve contextual dismissals, then have a reviewer who is blind to the builder rationale inspect the current captures. Repair or reframe, rebuild, reseal, and recapture after changes. Separately complete functional, accessibility, security, privacy, performance, rights, and user-approval checks before release. Browser execution and those human judgments are deliberately outside the machine CI claim.
Eval, 7 fixtures
Last passed: verified todaysource-pinnedcontainstimeout 420s · max $0Expected:
Oh My Design source pinned OK: commit 6b50288, 1128 members, sha256 f96d8f973b417ff110d898802eec2038e9211a423f89e8546bef0448591fb56bdependency-boundarycontainstimeout 420s · max $0Expected:
dependency boundary OK: Node >=22.19; smol-toml 1.8.0; 61 lock entries; lifecycle scripts disableddependency-auditcontainstimeout 420s · max $0Expected:
dependency audit OK: zero known vulnerabilities at moderate severitytargeted-suitecontainstimeout 420s · max $0Expected:
targeted regression suite OK: 135/135 on Node 22.23.2deterministic-clicontainstimeout 420s · max $0Expected:
deterministic CLI gates OK: slop scan 25 files / 0 candidates; existing stack preservedtype-gatecontainstimeout 420s · max $0Expected:
type gate OK: TypeScript cleanclean-exitexit_codetimeout 420s · max $0Expected:
0
Results
FlowStacks pins the complete signed Oh My Design commit 6b50288, verifies the reproducible 1,128-member source archive including its one intentional internal symlink, audits the exact Node and dependency boundary, installs the lockfile with lifecycle scripts disabled, requires a clean npm advisory result, runs the submitted 135-test browser-free regression slice on checksum-pinned Node.js 22.23.2, proves the read-only source scan and existing-stack routing outputs, and passes the TypeScript gate. This verifies deterministic contracts and the owner’s Node 22 fix—not the quality of an agent’s research, copy, design choices, screenshots, blind critique, or final interface.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- anti-slop: fail delivery on low-contrast normal text
- code-review-skill: verify independent review and deterministic P0/P1 verdicts
- agent-qa: validate Agent Skills before LLM-backed QA runs
- One AGENTS.md, no drift: prove CLAUDE.md is a real symlink and the generated files are in sync
- Self-host CodeWiki on private code: validate the config before you spend tokens
- Run GLM-5.2 for the bulk, escalate the hard turns to Opus 4.8
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).