CLI Usage#

This document describes how to invoke scafctl from the command line. The CLI follows a kubectl-style structure where verbs, kinds, and names are explicit and positional.

The general pattern is:

scafctl <verb> <kind> <name[@version(or constraint)]> [flags]
  • <verb> describes what you want to do
  • <kind> identifies the type of object
  • <name> identifies the object
  • @version is optional and resolved via the catalog ( or constraint)

Implementation Status#

CommandStatusNotes
run solution✅ ImplementedRequires workflow (errors if no workflow defined; use run resolver for resolver-only)
run resolver✅ ImplementedResolver-only execution for debugging and inspection
render solution✅ ImplementedIncludes action-graph and snapshot modes
get solution/provider/resolver✅ Implemented
explain solution/provider✅ Implemented
config *✅ Implementedview, get, set, unset, add-catalog, remove-catalog, use-catalog, init, schema, validate
snapshot show/diff✅ Implemented
solution diff✅ ImplementedStructural comparison of two solution files
secrets *✅ Implementedlist, get, set, delete, exists, export, import, rotate
auth *✅ Implementedlogin, logout, status, token
resolver graph❌ RemovedUse run resolver --graph instead
build solution✅ ImplementedCatalog feature
catalog list/inspect/delete/prune✅ ImplementedCatalog management
catalog save/load✅ ImplementedOffline distribution
eval cel✅ ImplementedEvaluate CEL expressions from CLI
eval template✅ ImplementedEvaluate Go templates from CLI
eval validate✅ ImplementedValidate solution files from CLI
new solution✅ ImplementedScaffold a new solution from template
lint rules✅ ImplementedList all available lint rules
lint explain✅ ImplementedExplain a specific lint rule
examples list✅ ImplementedList available example configurations
examples get✅ ImplementedGet/download an example file
push solution/plugin📋 PlannedRemote catalog feature
pull solution/plugin📋 PlannedRemote catalog feature
tag solution/plugin📋 PlannedCatalog feature
--catalog flag📋 PlannedCatalog feature
Version constraints (@^1.2)📋 PlannedRequires catalog

Core Concepts#

Kinds#

  • solution
  • provider
  • resolver
  • catalog (planned)

Names and Versions#

Names identify an object within a kind.

Versions are optional and may be:

  • an exact version (1.0.0)
  • a constraint (^1.2, >=1.0 <2.0) (planned - requires catalog)
  • omitted (default resolution rules apply)

Shell escaping: Complex version constraints with special characters should be quoted:

scafctl run solution "example@>=1.0 <2.0"  # planned
scafctl run solution "example@^1.2"         # planned

File Paths vs. Catalog References#

When a command accepts both a positional name argument and a -f/--file flag, the CLI distinguishes catalog references from local file paths:

InputInterpretationExample
Bare nameCatalog referencemy-app, deploy
Versioned nameCatalog referencemy-app@1.0.0, deploy@^1.2
Registry referenceCatalog referenceghcr.io/org/sol:v1, localhost:5000/sol
URLRemote referencehttps://example.com/sol.yaml
Starts with / or .Rejected — use -f/tmp/sol.yaml, ./sol.yaml
Ends with .yaml, .yml, .jsonRejected — use -fsolution.yaml
Path with separators, non-hostname first segmentRejected — use -fconfigs/solution, relative/path/sol
Windows pathRejected — use -fC:\dir\sol, dir\sol

To pass a local file path, always use the -f/--file flag:

# Correct: file via -f flag
scafctl run solution -f ./solution.yaml

# Correct: catalog reference as positional arg
scafctl run solution my-app@1.0.0

# Error: local file path as positional arg
scafctl run solution solution.yaml   # rejected with helpful error

This separation keeps the CLI unambiguous — positional arguments are always catalog/registry lookups while -f is always a file path.


Running a Solution#

Execute a solution’s resolvers and perform its workflow actions. The solution must define a workflow section with actions — if no workflow is defined, the command errors and suggests using scafctl run resolver instead.

scafctl run solution example

Run a specific version:

scafctl run solution example@1.0.0

Run with a version constraint:

scafctl run solution example@^1.2

Getting a Solution#

Show metadata of the latest example solution:

scafctl get solution example

Show metadata of version 1.0.0 of the example solution:

scafctl get solution example@1.0.0

Listing Resources#

Following kubectl conventions, use singular or plural forms:

# List all solutions in the catalog
scafctl get solutions

# List all providers
scafctl get providers

# Get a specific solution
scafctl get solution example

Both singular and plural forms without a name will list all resources of that kind.#

Rendering a Solution#

Render executes resolvers and renders actions but does not perform side effects.

From Catalog (by name)#

scafctl render solution example

From File#

Use -f or --file to specify a file path:

scafctl render solution -f mysolution.yaml

From stdin:

cat solution1.yaml | scafctl render solution -f -

Note: The -f flag is used consistently across commands (run, render, publish) to indicate a file source rather than a catalog lookup.

Render a specific version:

scafctl render solution example@1.0.0

Typical uses:

  • dry runs
  • snapshot testing
  • delegating execution to another system
  • reviewing generated artifacts

Passing Resolver Parameters#

Resolver parameters are passed using -r or --resolver.

scafctl run solution example -r env=prod

Multiple parameters:

scafctl run solution example \
  -r env=prod \
  -r region=us-east1

Parameters participate in normal resolver resolution via the parameter provider.

Key-Value Format#

Resolver parameters (and other similar flags) use a key=value format where:

  • Key: Simple string identifier (no spaces or newlines allowed)
  • Value: Supports ALL characters including newlines, special characters, quotes, etc.

Basic Usage#

The flag can be repeated for each key-value pair:

scafctl run solution example \
  -r someKey=sk_live_abc123 \
  -r config='{"nested": "json"}' \
  -r script="line1
line2
line3"

CSV Support#

New: You can also pass multiple comma-separated key=value pairs in a single flag:

# Multiple pairs in one flag
scafctl run solution example \
  -r "env=prod,region=us-east1,region=us-west1"

To include commas in values, use quotes:

# Quoted values preserve commas as literal characters
scafctl run solution example \
  -r "msg=\"Hello, world\"" \
  -r "data='item1,item2,item3'"

Escaped quotes are supported within quoted values:

scafctl run solution example \
  -r "json=\"{\\\"key\\\":\\\"value\\\"}\""

Multiple Values for Same Key#

Values for the same key are automatically combined, whether using CSV or repeated flags:

# Using repeated flags
scafctl run solution example \
  -r region=us-east1 \
  -r region=us-west1 \
  -r region=eu-west1

# Using CSV in one flag
scafctl run solution example \
  -r "region=us-east1,region=us-west1,region=eu-west1"

# Combining both approaches
scafctl run solution example \
  -r "region=us-east1,region=us-west1" \
  -r region=eu-west1

# All three produce: region = [us-east1, us-west1, eu-west1]

Usage Patterns#

Separate flags (traditional):

scafctl run solution example \
  -r key1=value1 \
  -r key2=value2

CSV in single flag (convenient for multiple pairs):

scafctl run solution example \
  -r "key1=value1,key2=value2"

Mixed approach:

scafctl run solution example \
  -r "env=prod,region=us-east1" \
  -r region=us-west1 \
  -r apiKey=secret

Technical Note: The CLI uses StringArrayVarP with custom CSV parsing (via pkg/flags.ParseKeyValueCSV) to avoid Cobra’s built-in CSV issues while still supporting comma-separated values with proper quote handling

URI Scheme Support#

To simplify passing complex data like JSON or YAML without escaping, use URI scheme prefixes:

Supported schemes: json://, yaml://, base64://, http://, https://, file://

# JSON without quote escaping
scafctl run solution example \
  -r "config=json://{\"key\":\"value\",\"count\":42}"

# JSON with commas in CSV context
scafctl run solution example \
  -r "env=prod,data=json://[1,2,3],region=us-east1"

# YAML configuration
scafctl run solution example \
  -r "config=yaml://items: [a, b, c]"

# Base64 encoded data
scafctl run solution example \
  -r "token=base64://SGVsbG8sIFdvcmxkIQ=="

Important: The scheme prefix is preserved and should be processed by your solution logic.

Validation: Values with URI schemes are automatically validated:

  • json:// - Validated as well-formed JSON
  • yaml:// - Validated as well-formed YAML
  • base64:// - Validated as proper base64 encoding
  • file:// - Verified that file exists and is not a directory
  • http://, https:// - Validated as properly formatted URLs

Validation errors are reported immediately with helpful messages.

Parameter File References (@file)#

Load all parameters from a YAML or JSON file using the @ prefix:

# Load from a YAML file
scafctl run resolver -f solution.yaml -r @params.yaml

# Load from a JSON file
scafctl run resolver -f solution.yaml -r @params.json

# Mix file and inline parameters
scafctl run resolver -f solution.yaml -r @defaults.yaml -r env=prod

Stdin Parameters (@-)#

Read all parameters from stdin as YAML or JSON using @-, following the same convention as curl:

# Pipe JSON from echo
echo '{"env": "prod", "region": "us-east1"}' | scafctl run resolver -f solution.yaml -r @-

# Pipe from a file via cat
cat params.yaml | scafctl run solution example -r @-

# Use as positional argument
echo '{"env": "prod"}' | scafctl run resolver -f solution.yaml @-

Per-Key Stdin and File References (key=@- / key=@file)#

Assign raw content from stdin or a file directly to a single key. Unlike standalone @- (which parses YAML/JSON into multiple keys), key=@- reads stdin as a raw string value:

# Pipe raw text into a single parameter
echo hello | scafctl run provider message message=@-
echo hello | scafctl run resolver -f solution.yaml -r message=@-

# Read a file's raw content into a parameter
scafctl run provider http url=https://example.com body=@request.json
scafctl run resolver -f solution.yaml -r config=@defaults.txt

# Mix with other parameter forms
scafctl run resolver -f solution.yaml -r name=Alice body=@template.txt

A single trailing newline is trimmed automatically (matching shell echo behavior).

Restrictions:

  • @- can only appear once (stdin is consumed on first read) — this applies across both standalone @- and key=@-
  • @- cannot be combined with -f - (both read from stdin)

Rendering With Parameters#

scafctl render solution example \
  -r env=staging \
  -r dryRun=true

Render Options#

The render command supports additional modes for debugging and testing:

Execution Snapshots#

Capture resolver execution state for testing and comparison:

# Save snapshot after rendering
scafctl render solution -f solution.yaml --snapshot output.json

# Redact sensitive values
scafctl render solution -f solution.yaml --snapshot output.json --redact

Snapshots can be analyzed with dedicated commands:

# Display a saved snapshot
scafctl snapshot show output.json

# Compare two snapshots
scafctl snapshot diff before.json after.json

Working With the Catalog#

Status: ✅ Implemented - Local catalog with build, list, inspect, delete, prune, save, and load. Remote push/pull planned for Phase 2.

Run a solution directly from the catalog:

scafctl run solution example@1.7.0

Building Artifacts#

Status: ✅ Implemented

Build a solution for the local catalog (analogous to docker build):

# Build a solution from file
scafctl build solution -f ./solution.yaml --version 1.0.0

# Build using version from metadata
scafctl build solution -f ./solution.yaml

# Overwrite existing version
scafctl build solution -f ./solution.yaml --version 1.0.0 --force

The build process validates, resolves dependencies, bundles local files, vendors catalog dependencies, and packages artifacts into the local catalog. See catalog-build-bundling.md for the full bundling design.

Additional build flags:

# Dry-run: show what would be bundled without building
scafctl build solution -f ./solution.yaml --dry-run

# Skip file bundling (legacy single-layer artifact)
scafctl build solution -f ./solution.yaml --no-bundle

# Skip vendoring catalog dependencies
scafctl build solution -f ./solution.yaml --no-vendor

# Set max bundle size
scafctl build solution -f ./solution.yaml --bundle-max-size 100MB

# Re-resolve and update the lock file
scafctl build solution -f ./solution.yaml --update-lock

Publishing Artifacts#

Status: 📋 Planned

Push artifacts to a remote catalog (analogous to docker push):

# Push a solution
scafctl push solution my-solution@1.7.0

# Push a plugin
scafctl push plugin aws-provider@1.5.0

# Push to a specific catalog
scafctl push solution my-solution@1.7.0 --catalog=production

Pulling Artifacts#

Status: 📋 Planned

Pull artifacts from a remote catalog to local (analogous to docker pull):

# Pull a solution
scafctl pull solution example@1.7.0

# Pull a plugin
scafctl pull plugin aws-provider@1.5.0

Inspecting Artifacts#

Status: ✅ Implemented

View artifact metadata, dependencies, and structure:

# Inspect a solution (latest version)
scafctl catalog inspect example

# Inspect specific version
scafctl catalog inspect example@1.7.0

# JSON output
scafctl catalog inspect example -o json

Tagging Artifacts#

Status: 📋 Planned

Create version aliases:

# Tag a solution
scafctl tag solution my-solution@1.2.3 my-solution:latest

# Tag a plugin
scafctl tag plugin aws-provider@1.5.0 aws-provider:stable

Offline Distribution#

Status: ✅ Implemented

Export and import artifacts for air-gapped environments (analogous to docker save/load):

# Save a solution (exports latest version by default)
scafctl catalog save my-solution -o solution.tar

# Save specific version
scafctl catalog save my-solution@1.2.3 -o solution.tar

# Load from archive
scafctl catalog load --input solution.tar

# Force overwrite if artifact already exists
scafctl catalog load --input solution.tar --force

The archive uses OCI Image Layout format, making it compatible with OCI registry tools.

Deleting Artifacts#

Status: ✅ Implemented

Remove an artifact from the local catalog:

# Delete specific version (version required)
scafctl catalog delete example@1.7.0

# Prune orphaned blobs after deletion
scafctl catalog prune

Catalog Resolution#

Status: 📋 Planned

By default, scafctl uses the local filesystem as the default catalog. Use --catalog to target a specific configured catalog:

scafctl run solution example --catalog=internal
scafctl get solutions --catalog=production

Explaining Resources#

Get detailed metadata and documentation for solutions and providers:

Explain Solution#

# From file
scafctl explain solution -f solution.yaml

# From catalog
scafctl explain solution example
scafctl explain solution example@1.0.0

Outputs:

  • Name, version, description
  • List of resolvers with their providers
  • List of actions with types
  • Required parameters
  • Dependency summary

Explain Provider#

scafctl explain provider github
scafctl explain provider static

Outputs:

  • Provider description
  • Configuration schema with types and validation
  • Example configurations
  • Supported features

Global Flags#

These flags are available on all commands. Run scafctl options to see them:

FlagShortDescriptionStatus
--cwd-CChange the working directory before executing the command (similar to git -C)✅ Implemented
--quiet-qSuppress non-essential output✅ Implemented
--no-colorDisable colored output✅ Implemented
--configPath to config file (default: ~/.scafctl/config.yaml)✅ Implemented
--log-levelSet log level (none, error, warn, info, debug, trace, or numeric V-level)✅ Implemented
--debug-dEnable debug logging (shorthand for –log-level debug)✅ Implemented
--log-formatLog format: console (default) or json✅ Implemented
--log-fileWrite logs to a file path✅ Implemented
--catalogTarget a specific configured catalog📋 Planned

Note: The -o/--output flag is available per-command (not global) on commands that support structured output.

Output format support:

  • get, render, explain, config view: Full support for -o flag
  • run: Supports -o flag for result output
  • auth status, secrets list: Support -o flag

Configuration#

scafctl uses a configuration file at ~/.scafctl/config.yaml managed via Viper . Configuration can also be set via environment variables with the SCAFCTL_ prefix.

Config File Structure#

catalogs:
  - name: default
    type: filesystem
    path: ./
  - name: internal
    type: oci
    url: oci://registry.example.com/scafctl
settings:
  defaultCatalog: default
action:
  # Default output directory for action file operations
  # CLI --output-dir flag overrides this setting
  outputDir: "/path/to/output"

Config Commands#

View the current configuration:

scafctl config view

Get a specific setting:

scafctl config get settings.defaultCatalog

Set a configuration value:

scafctl config set settings.defaultCatalog=internal

Unset a configuration value:

scafctl config unset settings.defaultCatalog

Catalog Management#

Convenience commands for catalog configuration:

# Add a catalog
scafctl config add-catalog internal --type=oci --url=oci://registry.example.com/scafctl

# Remove a catalog
scafctl config remove-catalog internal

# Set the default catalog
scafctl config use-catalog internal

Environment Variables#

All configuration can be overridden via environment variables:

export SCAFCTL_SETTINGS_DEFAULTCATALOG=internal
export SCAFCTL_CONFIG=/path/to/custom/config.yaml

Managing Secrets#

Securely manage encrypted secrets for authentication and configuration:

# List all secrets
scafctl secrets list

# List all secrets including internal (auth tokens, etc.)
scafctl secrets list --all

# Get a secret value
scafctl secrets get my-api-key

# Get an internal secret (e.g. auth token metadata)
scafctl secrets get scafctl.auth.entra.metadata --all

# Set a secret (prompts for value)
scafctl secrets set my-api-key

# Set with value directly
scafctl secrets set my-api-key --value="secret-value"

# Delete a secret
scafctl secrets delete my-api-key

# Check if secret exists
scafctl secrets exists my-api-key

# Export secrets (encrypted)
scafctl secrets export -o secrets.enc

# Import secrets
scafctl secrets import -i secrets.enc

# Rotate encryption key
scafctl secrets rotate

Secrets are encrypted with AES-256-GCM and stored in platform-specific locations:

  • macOS: ~/.local/share/scafctl/secrets/
  • Linux: ~/.local/share/scafctl/secrets/
  • Windows: %APPDATA%\scafctl\secrets\

Authentication#

Manage authentication for accessing protected resources:

# Login with an auth handler
scafctl auth login entra

# Check authentication status
scafctl auth status
scafctl auth status entra

# Get a token (for debugging)
scafctl auth token entra --scope "https://graph.microsoft.com/.default"

# Logout
scafctl auth logout entra

Supported auth handlers:

  • entra - Microsoft Entra ID (formerly Azure AD)

Resolver Commands#

Note: The standalone scafctl resolver graph command has been removed. Use scafctl run resolver --graph instead.

Running Resolvers#

The run resolver command executes resolvers from a solution without running actions. This is designed for debugging and inspecting resolver execution.

# Run all resolvers
scafctl run resolver -f solution.yaml

# Run specific resolvers (with their transitive dependencies)
scafctl run resolver db config -f solution.yaml

# JSON output (includes __execution metadata by default)
scafctl run resolver -f solution.yaml -o json

# JSON output without __execution metadata
scafctl run resolver -f solution.yaml -o json --hide-execution

# Skip transform and validation phases
scafctl run resolver --skip-transform -f solution.yaml

# Dependency graph (ASCII, DOT, Mermaid, or JSON)
scafctl run resolver --graph -f solution.yaml
scafctl run resolver --graph --graph-format=dot -f solution.yaml

# Snapshot execution state
scafctl run resolver --snapshot --snapshot-file=out.json -f solution.yaml
scafctl run resolver --snapshot --snapshot-file=out.json --redact -f solution.yaml

# Interactive TUI for exploring results
scafctl run resolver -f solution.yaml -i

Aliases: res, resolvers


Help and Discovery#

List available verbs:

scafctl help

List supported kinds for a verb:

scafctl run --help

Get help for a specific kind:

scafctl run solution --help

Because kinds are registered dynamically, help output always reflects what is available at runtime.


Summary#

The scafctl CLI follows a structured, extensible pattern:

  • Verbs describe intent
  • Kinds identify object types
  • Names and versions identify concrete artifacts

This design enables dynamic extension, clear UX, and long-term scalability without breaking existing commands.


Evaluating Expressions#

The eval command group lets you test CEL expressions, Go templates, and validate solution files without running a full solution.

Evaluate CEL#

# Simple expression
scafctl eval cel "1 + 2"

# With JSON data
scafctl eval cel '_.name == "test"' --data '{"name": "test"}'

# From a data file
scafctl eval cel '_.items.size() > 0' --data-file data.json

# Output as JSON
scafctl eval cel '_.items.filter(i, i.active)' --data-file data.json -o json

Evaluate Go Template#

# Simple template
scafctl eval template '{{.name}}' --data '{"name": "hello"}'

# Template from file
scafctl eval template --template-file template.txt --data-file data.json

# With output file
scafctl eval template --template-file template.txt --data-file data.json --output result.txt

Validate Solution#

# Validate a solution YAML file
scafctl eval validate -f solution.yaml

# Output as JSON
scafctl eval validate -f solution.yaml -o json

Creating New Solutions#

Scaffold a new solution from a built-in template:

# Interactive — prompts for name, description, providers
scafctl new solution

# With flags
scafctl new solution --name my-solution --description "My new solution" --output my-solution.yaml

# With specific providers
scafctl new solution --name my-solution --providers static,exec,cel

Exploring Lint Rules#

List Rules#

List all available lint rules with severity, category, and descriptions:

# List all rules
scafctl lint rules

# Output as JSON
scafctl lint rules -o json

Explain a Rule#

Get a detailed explanation of a specific lint rule:

# Show rule details, examples, and fix guidance
scafctl lint explain <rule-id>

# Output as JSON
scafctl lint explain <rule-id> -o json

Browsing Examples#

Discover and download built-in example configurations:

List Examples#

# List all examples
scafctl examples list

# Filter by category
scafctl examples list --category solutions
scafctl examples list --category resolvers
scafctl examples list --category actions

# Output as JSON
scafctl examples list -o json

# Output as YAML
scafctl examples list -o yaml

Get an Example#

# Print example to stdout
scafctl examples get resolvers/hello-world.yaml

# Save to file
scafctl examples get resolvers/hello-world.yaml -o output.yaml

Aliases: ls for list


Command Grammar: Verb-Noun vs Noun-Verb#

Current State of scafctl#

scafctl uses two distinct command grammar patterns:

Verb-Noun (kubectl-style) — the verb is the top-level command, the noun follows:

CommandVerbNoun
scafctl run solutionrunsolution
scafctl run resolverrunresolver
scafctl get solutiongetsolution
scafctl render solutionrendersolution
scafctl explain solutionexplainsolution
scafctl build solutionbuildsolution
scafctl new solutionnewsolution
scafctl push solutionpushsolution
scafctl pull solutionpullsolution
scafctl tag solutiontagsolution

Noun-Verb — the noun is the top-level command, the verb follows:

CommandNounVerb
scafctl secrets getsecretsget
scafctl secrets setsecretsset
scafctl secrets listsecretslist
scafctl secrets deletesecretsdelete
scafctl auth loginauthlogin
scafctl auth logoutauthlogout
scafctl auth statusauthstatus
scafctl config viewconfigview
scafctl config setconfigset
scafctl config getconfigget
scafctl catalog listcataloglist
scafctl catalog inspectcataloginspect
scafctl snapshot showsnapshotshow
scafctl snapshot diffsnapshotdiff
scafctl solution diffsolutiondiff
scafctl lint ruleslintrules
scafctl lint explainlintexplain
scafctl eval celevalcel
scafctl eval templateevaltemplate
scafctl examples listexampleslist
scafctl examples getexamplesget
scafctl cache cleancacheclean
scafctl plugins listpluginslist
scafctl bundle createbundlecreate
scafctl vendor syncvendorsync

Standalone (no sub-noun or sub-verb):

CommandNotes
scafctl versioninformational
scafctl mcplaunches MCP server
scafctl testruns solution tests

What Major CLIs Do#

Most successful CLIs converge on the same hybrid pattern scafctl already uses:

CLICore Domain ObjectsService/InfrastructureExample
kubectlverb-noun: get pods, delete svc, apply -fnoun-verb: config use-context, auth can-iDomain is verb-noun; plumbing is noun-verb
dockerverb-noun: run, build, pull, pushnoun-verb: network create, volume ls, system pruneTop-level verbs act on images/containers; subsystems are noun-verb
gitverb-first: clone, commit, push, pullnoun-verb: remote add, branch delete, stash popCore workflow is verbs; ancillary resource management is noun-verb
gh (GitHub CLI)verb-noun: pr create, issue listnoun-verb: auth login, config set, secret setDomain objects verb-noun; infrastructure noun-verb
az (Azure CLI)noun-verb: az vm create, az storage blob uploadnoun-verb throughoutPurely noun-verb (resource-group style)
gcloudnoun-verb: gcloud compute instances createnoun-verb throughoutPurely noun-verb (resource hierarchy)
terraformverb-first: plan, apply, destroyNo sub-resources, single verb layer
helmverb-noun: install, upgrade, rollbacknoun-verb: repo add, plugin installCharts are verb-noun; supporting systems noun-verb

Observation: The most widely-used developer CLIs (kubectl, docker, git, gh, helm) all use a hybrid model. Only cloud-provider CLIs (az, gcloud) that model deep resource hierarchies go fully noun-verb. Purely verb-noun CLIs (terraform) tend to have a flat, single-resource domain.


Best Practice: The Delineation Rule#

The hybrid pattern is not arbitrary — it follows a clear principle:

Use verb-noun for domain operations on core business objects. Use noun-verb for infrastructure, plumbing, and service management.

The deciding question: “Is this a core workflow action the user came here to do, or is it managing supporting infrastructure?”

CategoryPatternRationalescafctl examples
Core domain operations<verb> <kind>The user thinks in terms of what they want to do: run, get, render, build. The kind is just a target.run solution, get provider, render solution, build solution, new solution
Infrastructure / services<noun> <action>The user thinks in terms of which subsystem they need to manage. The subsystem is the anchor; actions within it are secondary.config set, secrets get, auth login, catalog list, cache clean
Standalone utilities<verb> or <noun>Single-purpose commands that don’t need a sub-resource.version, mcp, test

Why this works:

  1. Discoverability — Users can type scafctl and immediately see the core verbs (run, get, render) alongside the subsystems (config, secrets, auth). The top-level command list reads like a table-of-contents.
  2. Composability — Verb-noun allows the same verb to apply to multiple kinds (run solution, run resolver). Noun-verb allows subsystems to have independent, self-documenting action sets.
  3. Scalability — New kinds slot into existing verbs. New subsystem features slot into their noun group. Neither pollutes the other.
  4. Precedent — kubectl, docker, git, gh, and helm all draw the same line, so users already have muscle memory for the split.

Verdict on scafctl#

The current hybrid structure is correct. No changes needed.

scafctl already follows the established delineation:

  • Verb-noun for core domain operations — run, get, render, explain, build, new, push, pull, tag all act on domain kinds (solution, provider, resolver).
  • Noun-verb for infrastructure/services — config, secrets, auth, catalog, snapshot, lint, eval, examples, cache, plugins, bundle, vendor are all subsystem groups with their own actions.
  • Standalone for single-purpose utilities — version, mcp, test.

This matches how kubectl, docker, git, gh, and helm structure their CLIs. Attempting to “unify” to pure verb-noun or pure noun-verb would:

  • Break user expectations from other tools.
  • Create awkward commandsscafctl manage secrets get (verb-noun forced) or scafctl solution run (noun-verb forced for domain ops) reads worse in both cases.
  • Lose discoverability — a flat verb-only top level hides subsystem structure; a flat noun-only top level hides the core workflow.

The rule of thumb going forward: if adding a new top-level command, ask “Is the user performing a core domain action on a kind, or managing a subsystem?” — verb-noun for the former, noun-verb for the latter.