Skip to content

cli-scaffold ​

Generates a production-grade command-line application in one of 12 languages, built against one five-pillar architecture doctrine and checked against it before you see it.

The name. A scaffold is the temporary frame a building goes up on. Here it is the first working skeleton of a CLI — layout, help text, exit codes, tests, distribution — which you then own and grow.

  • Use when you are starting a new CLI and want its structure, --help, exit codes and packaging to follow the language's real idioms from the first commit.
  • Not when the code is already written and you want it audited for drift or weak assertions — use zeugnis; to check it against a doctrine you wrote, use lehre.

Use case ​

Situation. You need a small inventory tool for widgets that composes in a pipeline (--json on stdout, diagnostics on stderr) and ships as a Python package. No code exists yet. Call. /cli-scaffold python called widgetctl inspect and list widgetsWrites. generated-clis/widgetctl/ under the current directory: source package, pyproject.toml, tests/, docs/, README.md, LICENSE. The path is validated by scripts/write_scope.py before anything is written. Next. Read the verification result; fixable gaps are repaired inside a bounded loop, the rest are listed for you. Then build and run the CLI yourself, since the verifier never does.

Usage: widgetctl [OPTIONS] [WIDGET_ID]

  Inspect and list widgets.

Options:
  --json        Emit machine-readable output on stdout.
  --no-input    Never prompt; fail with exit code 2 instead.
  --verbose     Write diagnostics to stderr.
  --help        Show this message and exit.

Exit codes:
  0  success
  1  runtime error
  2  usage error

Excerpt from plugins/cli-scaffold/scripts/fixtures/widgetctl/docs/help.golden.txt, the reference scaffold the plugin's tests check against.

Why this exists ​

CLI scaffolding usually means one of two things: hand-rolled boilerplate that goes stale as a language's idioms move on, or a generator locked to whatever templates it shipped with. cli-scaffold does neither — every scaffold is freeform-generated against one frozen five-pillar doctrine (UX, backend/core separation, stability, idiomatic distribution, Unix composability) and each target ecosystem's actual current idioms, then verified against that doctrine before you ever see it. The same architectural bar applies whether you ask for Rust, Bash, or PowerShell.

What it is not ​

  • Not a template library. Every scaffold is generated freeform from the current per-language reference and doctrine — never from stored boilerplate, and never reused across requests.
  • Not a guesser. An ambiguous or unsupported language name is refused outright and lists the 12 supported languages; it is never silently resolved to a "close enough" match.
  • Not a builder or a publisher. Verification is static and read-only — the verifier never builds, runs, publishes, or installs the generated CLI (no cargo publish, npm publish, gem push, dotnet nuget push, pip upload, etc.).
  • Not a fixer of everything. Only fixable gaps are auto-fixed inside a bounded loop; anything that needs human judgment is surfaced to you unchanged, never resolved on your behalf.

Install ​

/plugin marketplace add Anselmoo/werkstoff
/plugin install cli-scaffold@werkstoff

Nothing runs on install: cli-scaffold registers no hook, so no background process starts — the plugin only acts when you invoke /cli-scaffold or describe the CLI you want in plain language.

Generated scaffolds land under generated-clis/<app-name>/; verification reports under .cli-scaffold-reports/ (both git-ignored).

Local development ​

Point Claude Code at a checkout without registering the marketplace:

bash
claude --plugin-dir /path/to/werkstoff/plugins/cli-scaffold

Example Prompts ​

Say any of these to Claude Code once the plugin is installed — they're plain-language prompts, not exact phrasing Claude has to match. Claude routes them to the skill below by intent.

Use the slash command directly ​
prompt
/cli-scaffold rust called myapp

The slash command itself — skips straight to generation for a named language and app name.

Ask in plain language ​
prompt
"scaffold a Python CLI named foo that fetches weather data"

Triggers scaffold-cli (interpreted paradigm) — natural-language equivalent of the slash command: resolves the language, loads the doctrine, generates, then verifies.

Scaffold a shell CLI ​
prompt
"scaffold a bash CLI called backup-tool"

Triggers scaffold-cli (shell paradigm, cli-scaffold-shell) — same five-pillar doctrine, plus POSIX-sh bashism checks.

Scaffold a compiled-language CLI ​
prompt
"scaffold a CLI in Go called deploy-bot"

Triggers scaffold-cli (compiled paradigm, cli-scaffold-compiled) — produces a lib+binary split with zero CLI-framework imports in the core library, packaging metadata for Go's idiomatic channel, and a --help snapshot test.

An unsupported or ambiguous language is refused, not guessed ​
prompt
"scaffold a CLI in some scripting language, whatever's easiest"

scaffold-cli refuses outright and lists the 12 supported languages rather than picking one for you — ambiguity is never silently resolved.

Ambiguous or unsupported language names are refused outright, never guessed — see ## What it does below for the full generate-then-verify sequence.

What it does ​

Ask for a CLI (/cli-scaffold rust called myapp, or "scaffold a Python CLI named foo") and the plugin:

  1. Resolves the language to one of three paradigms — in code, refusing ambiguous or unsupported names instead of guessing.
  2. Loads the doctrine (cli-architecture) that defines what "production-grade" means for every language.
  3. Generates the scaffold freeform from the per-language reference (never from stored boilerplate) — a core/library with zero CLI-framework imports, a thin entry point, packaging metadata for the ecosystem's idiomatic channel, and a snapshot test for --help.
  4. Verifies it read-only against the doctrine, fixes every fixable gap and re-verifies (bounded), and surfaces only needs-human-judgment gaps to you.

The 12 languages / 3 paradigms ​

ParadigmLanguagesSkill
compiledRust, Go, .NETcli-scaffold-compiled
interpretedPython, TypeScript, JavaScript, Ruby, PHP, Perlcli-scaffold-interpreted
shellBash, Zsh, PowerShell, POSIX sh*cli-scaffold-shell

* POSIX sh is a shell dialect routed to the shell paradigm; it is not one of the 12 counted languages.

The five pillars ​

Every generated CLI satisfies all five: UX/discoverability, backend/core separation, stability, idiomatic distribution, and Unix composability. The full doctrine lives in skills/cli-architecture/SKILL.md.

The frozen exit-code contract ​

Identical in all 12 languages: 0 success, 1 runtime error, 2 usage error.

Components ​

.claude-plugin/plugin.json
skills/
  cli-architecture/            # the doctrine (single source of truth)
  scaffold-cli/                # /cli-scaffold dispatcher (user-invoked)
  cli-scaffold-compiled/       # + references/{rust,go,dotnet}.md
  cli-scaffold-interpreted/    # + references/{python,typescript,javascript,ruby,php,perl}.md
  cli-scaffold-shell/          # + references/{bash,zsh,powershell,posix-sh}.md
agents/
  cli-scaffold-verifier.md     # read-only doctrine conformance check
scripts/
  constants.py                 # frozen registry + numeric bounds (asserted on import)
  lang_router.py               # resolve language -> paradigm; refuse unknown/ambiguous
  write_scope.py               # reject traversal/absolute/out-of-scope targets
  verify_scaffold.py           # the verification engine (all doctrine rules)
  report_validator.py          # validate report/ledger on read AND write
  check_doctrine_isolation.py  # fail if a paradigm skill duplicates the doctrine
  selftest.py                  # runnable proof the guards refuse what they must
  build_architecture_tree.py   # renders ARCHITECTURE.html from the manifest + a real walk
  fixtures/widgetctl/          # committed demo scaffold behind the screenshot in "The report"

Skills (5) ​

SkillPurpose
scaffold-cli/cli-scaffold dispatcher — the user-invoked entry point.
cli-architectureThe doctrine (single source of truth); not invoked directly, loaded by the three paradigm skills below.
cli-scaffold-compiledRust, Go, .NET — plus references/{rust,go,dotnet}.md.
cli-scaffold-interpretedPython, TypeScript, JavaScript, Ruby, PHP, Perl — plus per-language references.
cli-scaffold-shellBash, Zsh, PowerShell, POSIX sh — plus per-language references.

Agents (1) ​

cli-scaffold-verifier — read-only doctrine conformance check; has no Write/Edit tool and the verification engine itself refuses to write anywhere under the scaffold.

Commands (1) ​

/cli-scaffold <language> called <app-name> [functionality...] — loads the scaffold-cli skill and relays its arguments as the language/dialect, the app name, and any requested functionality; the skill then does the resolve, load, generate and verify steps above.

What is enforced, and what is not ​

cli-scaffold registers no PreToolUse hook, so nothing here is denied at the tool-call layer — a model that decided to skip a skill's steps could, in principle, write a scaffold without ever running lang_router.py, write_scope.py, or verify_scaffold.py. What actually holds is narrower but real: every rule below is a conditional that exits non-zero or raises when its script is actually invoked, and the cli-scaffold-verifier agent's read-only boundary is enforced at the tool-permission layer rather than by prose — it is declared with tools: Read, Glob, Bash and simply has no Write or Edit tool to misuse.

Enforcement is in code, not prose ​

Every rule that says MUST NOT / MUST refuse / MUST halt is enforced by a conditional that actually refuses:

RuleWhere it is enforced
Exactly 12 languages / 3 paradigms / 5 pillarsconstants.py asserts these at import time
Frozen 0/1/2 exit contractEXIT_* constants reused everywhere; verify_scaffold.py checks each scaffold
Language routing — never a silent fallbacklang_router.py exits non-zero on ambiguous/unsupported
Write scope — no traversal/absolute/outsidewrite_scope.py raises before any write
Core-library isolation, help sections, --json, --no-input, NO_COLOR, stdout/stderr, snapshot, distribution, completionone check function each in verify_scaffold.py, recording a fail finding with a first-class disposition
POSIX-sh bashismsverify_scaffold.py sweeps against FORBIDDEN_BASHISMS (mirrored in posix-sh.md)
Verifier must not write generated filesagent has no Write/Edit tool and the engine refuses to write anywhere under the scaffold
Fixable-gap loop is boundedMAX_FIX_ITERATIONS constant + a per-scaffold ledger that HALTs when exceeded
Persisted state has no invented gating valuesreport_validator.py rejects any finding missing disposition, any report missing language/paradigm/verdict — on read and on write
Doctrine never duplicated into paradigm skillscheck_doctrine_isolation.py fails the build on restatement

Run the proof:

bash
python3 scripts/selftest.py

The report ​

Generation also writes ARCHITECTURE.html inside the scaffold itself: a self-contained report viewer rendering the scaffold's real directory tree, each file tagged with the five-pillar role(s) its manifest declares for it.

It answers one question rather than merely drawing a tree — does every pillar the doctrine names have a file behind it? — and states the answer in a sentence at the top, derived from the manifest's own gating keys. Four of the five pillars are file-backed and therefore countable here; the fifth, Unix composability, is behavioural (exit codes 0/1/2, data on stdout, diagnostics on stderr), so no file can carry its badge and the viewer says so instead of scoring a pillar it cannot see.

Architecture tree filling the first screen: the widgetctl scaffold as an icicle on the left -- the root, then directory bands (docs/, src/, src/widgetctl/, tests/), then one evenly sized cell per file, each tinted and named with the doctrine role it plays or marked no role -- joined by coloured ribbons to a pillar ledger on the right that groups one slot per role under its pillar (backend/core separation, idiomatic distribution, stability, UX/discoverability); the test and completion slots are empty and hatched with no ribbon reaching them, and the stability pillar is framed in a dashed red border saying no file stands behind it; a verdict that 4 of 6 roles are claimed, a one-row role legend, and a right-hand drawer open on the overview listing the gaps worst first with the manifest line, fix and evidence for the top one

That image is reproducible rather than a one-off capture. The scaffold it shows is committed at scripts/fixtures/widgetctl/ — a real directory tree plus its cli-scaffold.manifest.json, which is exactly what the builder consumes (the manifest supplies the roles, the walk supplies the tree). It is deliberately incomplete: it declares no --help snapshot test and no shell completion, so the report shows a partial-coverage verdict and two hollow legend chips rather than a clean sweep that would prove nothing. To rebuild it:

bash
mkdir -p /tmp/cli-scaffold-demo
cp -R plugins/cli-scaffold/scripts/fixtures/widgetctl /tmp/cli-scaffold-demo/widgetctl
python3 plugins/cli-scaffold/scripts/build_architecture_tree.py /tmp/cli-scaffold-demo/widgetctl \
    --template plugins/cli-scaffold/assets/architecture-tree-viewer.html \
    --d3 plugins/cli-scaffold/assets/inline-d3.html \
    --tokens plugins/cli-scaffold/assets/tokens.css

Design decisions ​

(spec was silent here)

Where the specification was silent, these defaults were chosen and are noted here:

  • Scaffold manifest (cli-scaffold.manifest.json). The verifier must branch on structured file-role data, never on prose, so each generated scaffold carries a small JSON manifest declaring core_files, entry_file, distribution_file, snapshot_test, flags, positional_args, and completion. These are the gating keys the engine reads. This is the concrete form of "fields that gate a decision are first-class keys."
  • Output scope. All writes are confined to generated-clis/ under the current directory; reports/ledgers to .cli-scaffold-reports/. Reports are deliberately kept outside any scaffold so the verifier structurally cannot touch generated files.
  • MAX_FIX_ITERATIONS = 5. The spec bounds the fix/re-verify loop but does not give a number; 5 attempts before halting-to-human was chosen as a safe default and lives as a named constant.
  • Static verification. The verifier is read-only and does not build/run the generated CLI, so runtime-behavior rules (exit codes, stdout/stderr, no-hang) are checked by static signals in the source. Signals that cannot be confirmed statically are reported as needs-human-judgment rather than force-fixed.
  • One idiomatic distribution channel per language. crates.io (Rust), go install module (Go), NuGet .NET tool (.NET), PyPI (Python), npm (TS/JS), RubyGems (Ruby), Packagist/Composer (PHP), CPAN (Perl), Homebrew (Bash/Zsh), PowerShell Gallery (PowerShell), make install (POSIX sh).
  • Completion honesty. Where an ecosystem has a first-party mechanism it is used (cobra, clap_complete, argcomplete, yargs .completion(), zsh #compdef, Register-ArgumentCompleter, symfony completion). Where none exists (Perl, POSIX sh, Ruby), the manifest declares completion.supported: false with a note rather than inventing one.
  • Snapshot tool per ecosystem. insta/trycmd (Rust), golden go test (Go), Verify (.NET), syrupy (Python), vitest/jest (TS/JS), rspec-snapshot (Ruby), spatie snapshot (PHP), Test::Snapshot (Perl), bats-core (Bash/Zsh), shellspec (POSIX sh), Pester (PowerShell).
  • Author metadata in plugin.json defaults to the invoking user's identity; adjust before publishing.

Verifying a change to this plugin ​

bash
python3 test/plugins/lint-frontmatter.py plugins/cli-scaffold
claude plugin validate plugins/cli-scaffold --strict
python3 plugins/nacharbeit/scripts/nacharbeit_lint.py plugins/cli-scaffold --docs-root docs
python3 plugins/cli-scaffold/scripts/selftest.py

Behavioural cases ​

bash
bash test/plugins/verify-clean-box.sh
bash test/plugins/run.sh new-cli-doctrine