# atmos scaffold generate

Generate a component, configuration, or project shape from a template. The template's versioned
manifest owns its fields, conditions, files, hooks, and update provenance.

## Usage

```shell
atmos scaffold generate [template] [target] [flags]
```

## Examples

```shell
# Prompt for active fields and generate into a new directory.
atmos scaffold generate terraform-component ./components/terraform/vpc

# Supply values for automation; defaults satisfy required fields.
atmos scaffold generate terraform-component ./components/terraform/vpc \
  --defaults \
  --set component_name=vpc \
  --set environments=dev,staging

# Preview a template without writing files or running generation hooks.
atmos scaffold generate terraform-component ./preview --dry-run --skip-hooks

# Bring a recorded project forward after its template changes.
atmos scaffold generate terraform-component ./components/terraform/vpc \
  --update --merge-strategy=manual

# Force line-oriented text merging so YAML formatting (e.g. blank lines) survives an update.
atmos scaffold generate terraform-component ./components/terraform/vpc \
  --update --merge-driver=text

# Update without relying on the target's own Git history for the merge base.
atmos scaffold generate terraform-component ./components/terraform/vpc \
  --update --update-strategy=rendered
```

## Template Sources

Select an embedded template, a template declared under `scaffold.templates` in `atmos.yaml`, or a
local/remote source — git, HTTPS, S3, or an OCI registry reference. A git source can be pinned
with `--ref` to make a release, tag, or commit explicit; `--ref` has no effect on OCI/S3/local
sources, which address a specific version through the source string itself.

```shell
atmos scaffold list
atmos scaffold generate ./scaffolds/terraform-component ./components/terraform/vpc
atmos scaffold generate https://github.com/example/platform-templates.git ./output --ref v1.2.0
atmos scaffold generate oci://ghcr.io/example/templates:v1.0.0 ./components/terraform/vpc
```

An OCI source is pulled the same way `atmos vendor pull` fetches OCI-hosted components —
authentication uses the same precedence (Docker keychain, then `ATMOS_GITHUB_TOKEN` for
`ghcr.io`, then anonymous); see [Vendor URL Syntax](/vendor/url-syntax#oci-syntax) for
details.

## Template Configuration

Use the versioned manifest below; the former top-level `prompts:` key is not valid.

```yaml title="scaffold.yaml"
apiVersion: atmos/v1
kind: AtmosScaffoldConfig
metadata:
  name: terraform-component
spec:
  fields:
    - name: component_name
      label: Component name
      type: input
      required: true
      validation:
        pattern: "^[a-z0-9-]+$"
    - name: create_monitoring
      type: confirm
      default: false
    - name: alert_email
      type: input
      when: "answers.create_monitoring == true"
  files:
    - path: monitoring.tf
      when: "answers.create_monitoring == true"
```

Fields render into template content and paths through `{{ .Config.<field> }}`. Required, option,
boolean, and regular-expression validation is enforced after all answer sources merge: interactive
answers, defaults, saved `spec.values`, and `--set`. `select` and `multiselect` require values from
their declared `options`; `false` remains a valid answer for a required boolean.

`when:` accepts predicate words, CEL, or an implicit-`all` list. Conditions can inspect only
earlier field answers through `answers`; use CEL (`&&`, `||`, `!`) for compound logic because the
map-style `{all, any, not}` form is not accepted by scaffold manifests.

## Stack-compatible scaffolds

A scaffold used by [`stack create`](/cli/commands/stack/stack-create) declares
`spec.stack.manifest`, the rendered manifest path relative to the generation root.
Use `{{ .Config.stack_file }}` in both that declaration and the corresponding
file's `target` to respect the caller's `--file` selection. The stack command uses
the same template catalog and renderer, then validates the stack and all output
paths before running hooks or creating files. General scaffolding and project
initialization remain available for the same recipe.

## Computed Fields

A `type: computed` field is never prompted for and can't be set with `--set` — its `value:` is
either a Go-template expression deriving it from other fields' answers (using the same
`answers.*` binding `options:`'s dynamic form uses below), or a literal of any type (string,
number, boolean, list, or map), used as-is with no rendering at all. A string is only treated as
an expression when it actually contains a template action (`{{ ... }}`, or your configured
delimiters); a plain string with none, like `value: hello`, is a literal too:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: regions
      type: multiselect
      options: [us-east-1, us-west-2, eu-west-1]
    - name: primary_region_select
      type: select
      options: answers.regions
      when: "size(answers.regions) > 1"
    - name: primary_region
      type: computed
      value: "{{ ternary answers.primary_region_select (index answers.regions 0) (gt (len answers.regions) 1) }}"
    - name: provider_version_pins
      type: computed
      value:
        aws: "~> 5.0"
        azurerm: "~> 3.0"
        google: "~> 5.0"
```

Without a computed field, every file that needs the "primary region, defaulting to the only
region when there's just one" value has to re-derive it with the same `{{ if .Config.primary_region_select }}...{{ end }}`
snippet. `primary_region` derives it once, and `.Config.primary_region` is then usable anywhere
`.Config` is: file content, `target:`, and `matrix:` axes. `provider_version_pins` is a plain
literal — no template rendering happens for it at all, it's just stored and exposed at
`.Config.provider_version_pins` as-is, useful for a small hand-authored reference table.
`options:` is the one exception —
it's resolved before computed fields have a value, so a `select`/`multiselect` field's
`options:` referencing a computed field is a load-time error rather than a silently
non-functional constraint.

`value:` is required on a `computed` field (and rejected on every other field type); `required:`
and `default:` are both rejected on a `computed` field, since it's always self-supplied. Computed
fields evaluate once, in `spec.fields[]` declaration order, after every regular field's answer is
already final — a computed field can reference any regular field regardless of order, but only an
**earlier-declared** computed field's own result. Referencing itself or a later-declared computed
field fails at scaffold-load time rather than silently rendering as `<no value>`. Because that
evaluation happens after the interactive form completes, a regular field's own `when:` can never
depend on a computed field — only the reverse works.

A computed field's `value:` must be **pure** — deterministic given the same
answers, with no reliance on outside state that can change between calls (the current time, an
environment variable, a remote fetch that isn't guaranteed to return the same content twice).
Interactive generation with no target directory given evaluates every computed field twice: once
to suggest a target directory name, again against the final answers to actually generate files.
An impure expression can compute a different value each time, so the suggested directory and the
generated files can end up reflecting two different results for the same field.

## Dynamic and Label/Value Options

`select` and `multiselect` fields declare their choices through `options:`, which accepts four
shapes. A plain list of strings is the common case, where each choice's label and underlying
value are the same:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: environment
      type: select
      options: [dev, staging, production]
```

An option can instead be a `{label, value}` object, for when the displayed choice should read
differently from the value stored in `answers` and passed to templates. `value` is required;
`label` defaults to `value` when omitted:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: environment
      type: select
      options:
        - label: Development
          value: dev
        - label: Staging
          value: staging
        - value: production # No label -- displays as "production".
```

`options:` can also be sourced dynamically, using exactly the two forms `spec.files[].matrix`
axes support below: a dot-path into `answers.*`, or a Go-template expression:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: environments
      type: multiselect
      options: [dev, staging, production]

    # Sourced from the prior multiselect answer -- only offers environments
    # the user actually selected above.
    - name: default_environment
      type: select
      options: "answers.environments"
```

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: csv_owners
      type: input
      label: Comma-separated list of component owners (e.g. GitHub teams)

    - name: primary_owner
      type: select
      options: '{{ splitList "," answers.csv_owners }}'
```

A dot-path source must resolve to an already list-shaped answer -- a `multiselect` answer, a
`spec.values` preset, or a `--set`-supplied value never declared as a field at all -- while a
template expression computes the list the same way a matrix axis expression does. Both dynamic
forms resolve correctly against whatever the earlier field was ultimately answered, whether that
answer came from the interactive prompt (fields are prompted one at a time, so a later field is
only ever shown after the ones before it) or from `--set`/`--defaults`.

When a later field's dot-path sources directly from a field using `{label, value}` options,
labels are recovered for the filtered subset of values present in the referenced answer:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: environments
      type: multiselect
      options:
        - label: Development
          value: dev
        - label: Staging
          value: staging
        - label: Production
          value: prod

    # environments answer is [dev, staging] -> default_environment offers
    # "Development" and "Staging" -- not "Production", and not the raw
    # "dev"/"staging" values.
    - name: default_environment
      type: select
      options: "answers.environments"
```

Only values ever reach `answers` and templates; labels are presentation-only.

Two limitations to keep in mind: there's no field-declaration-order validation at load time, so a
forward reference, self-reference, or typo'd dot-path loads successfully and degrades to an empty
option list (no constraint, any value accepted) at runtime instead of erroring; and label recovery
looks back only one hop -- it does not chase labels through a chain of dynamic references, nor
through the template-expression form, both of which fall back to `label == value`.

## Loading External Data with `!include` and Other YAML Functions

The [`!include`](/functions/yaml/include) YAML function, already available in stack manifests, also
works in `scaffold.yaml` -- resolving before schema validation, so it can be used anywhere a literal
YAML value is otherwise accepted: `options:`, a computed field's `value:`, and a `matrix:` axis.

A YQ-filtered `!include` shapes external data into `{label, value}` options directly, so a list
doesn't have to be duplicated across fields that need the same choices:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: license
      type: select
      options: !include "./lib/licenses.yaml '. | to_entries | map({\"label\": .value.full_name, \"value\": .key})'"
```

An unfiltered `!include` on a `type: computed` field's `value:` lands the raw included structure at
`.Config.<name>` instead -- see [Computed Fields](#computed-fields) above -- useful for a small
reference table read as data rather than a list of choices:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: license_lookup
      type: computed
      value: !include ./lib/licenses.yaml
```

`lib/licenses.yaml` here is a normal template file, resolved relative to `scaffold.yaml`'s own
directory. A local file that exists solely to be included this way is automatically excluded from
generation output -- it's never copied into the generated project, the same way `scaffold.yaml`
itself never is. `!include` also accepts everything it does in a stack manifest: a remote `git::`,
`oci://`, or `https://` source works the same way, with no such exclusion needed since a remote
source is never part of the local template directory to begin with.

Included content is not processed transitively: a literal `!include` tag written inside an
included file's own content is never resolved, and stays as inert, unprocessed text. This matches
stack manifests, which share the same underlying `!include` implementation and the same
limitation.

Besides `!include`/[`!include.raw`](/functions/yaml/include.raw), `scaffold.yaml` resolves a fixed
set of other YAML functions that need no stack, component, or backend context --
[`!env`](/functions/yaml/env), [`!random`](/functions/yaml/random), [`!cwd`](/functions/yaml/cwd),
the `!git.*`/[`!repo-root`](/functions/yaml/repo-root) family
([`!git.root`](/functions/yaml/git.root), [`!git.sha`](/functions/yaml/git.sha),
[`!git.ref`](/functions/yaml/git.ref), [`!git.branch`](/functions/yaml/git.branch),
[`!git.repository`](/functions/yaml/git.repository), [`!git.owner`](/functions/yaml/git.owner),
[`!git.name`](/functions/yaml/git.name), [`!git.host`](/functions/yaml/git.host),
[`!git.url`](/functions/yaml/git.url)), and [`!literal`](/functions/yaml/literal) (useful for a
field value that looks like a Go template expression but should be taken as-is). Any other YAML
function -- one that needs real stack, component, or backend context Atmos doesn't have while
loading `scaffold.yaml`, such as `!terraform.state`, `!store`, or `!secret` -- is rejected with a
clear error naming the tag, rather than silently left unresolved.

[`!exec`](/functions/yaml/exec) is deliberately **not** supported, even though it needs no stack
context: a template's `scaffold.yaml` is resolved for every template configured in `atmos.yaml`
just to populate `atmos scaffold list` and the interactive template picker, not only the one a
user actually selects or generates. Allowing shell execution here would let any configured
template -- including a shared or vendored one -- run arbitrary code merely by being listed.

## Dynamic File Generation

`matrix:` expands a single discovered file into one generated file per resolved combination — the
Cartesian product of one or more axes, using the same axis shape the workflow `matrix:` step uses.
Axis values share the same `answers.`-prefix dot-path and template-expression convention as a
dynamic `options:` source (see "Dynamic and Label/Value Options" above).

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: environments
      type: multiselect
      options: [dev, staging, production]
  files:
    - path: environment.yaml
      target: "stacks/{{ .matrix.environment }}.yaml"
      matrix:
        environment: answers.environments
```

An axis's value is a literal list declared directly in `scaffold.yaml` (e.g.
`region: [us-east-1, us-west-2]`), a dot-path into `answers.*` referencing an already
list-shaped answer, such as a `multiselect` field, or a Go-template expression (any string
containing `{{`) that computes the list. `--set` values for a `multiselect` field are split on
commas automatically, so `--set environments=dev,staging` works non-interactively.

Declaring more than one axis expands their full Cartesian product; add `when:` to prune
combinations that don't apply, using the `matrix` CEL variable alongside `answers`:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: deploy.yaml
      target: "deploy/{{ .matrix.environment }}/{{ .matrix.region }}.yaml"
      matrix:
        environment: [dev, staging, production]
        region: [us-east-1, us-west-2]
      when: "matrix.region in answers.environments[matrix.environment].regions"
```

`target:` is required whenever `matrix:` is set. The resolved combination is available in
`target:` and the file's own content — not just the output path — as `.matrix.<axis>`, matching
Go template's leading-dot field access. `when:` is CEL, not Go template, so it reads the same
value as `matrix.<axis>` instead, without the leading dot (see the `when:` example above). Two
files (matrixed or not) rendering to the same output path is a hard error, never a silent
overwrite.

An axis doesn't need to come from a `multiselect` at all — a plain free-text answer is just a
string, and any Sprig/Gomplate function can split it into a list:

```yaml title="scaffold.yaml"
spec:
  fields:
    - name: environments_csv
      type: input
      label: Comma-separated list of environments
  files:
    - path: deploy.yaml
      target: "deploy/{{ .matrix.environment }}.yaml"
      matrix:
        environment: '{{ splitList "," answers.environments_csv }}'
```

Typing `dev,staging,production` at the prompt generates the same three files a `multiselect`
with those three options would — except the values aren't limited to a fixed,
template-author-declared list.

When an axis's values aren't already list-shaped anywhere in `answers` — e.g. an answer is itself
a map of structured values rather than a flat `multiselect` — compute the list with
`collectKeys`, a template function unconditionally available to axis expressions, alongside every
Sprig/Gomplate function. Scaffold templating always has both available and is independent of the
`templates.settings.sprig.enabled`/`templates.settings.gomplate.enabled` settings, which only gate
stack manifest templating. `collectKeys(m)` returns `m`'s top-level keys, sorted; `collectKeys(m,
"nestedKey")` collects `nestedKey`'s own keys from every value in `m`, flattened and deduplicated:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: deploy.yaml
      target: "deploy/{{ .matrix.environment }}/{{ .matrix.region }}.yaml"
      matrix:
        environment: '{{ collectKeys answers.environments }}'
        region: '{{ collectKeys answers.environments "regions" }}'
      when: "matrix.region in answers.environments[matrix.environment].regions"
```

Given an `environments` answer shaped like this:

```yaml
environments:
  dev:
    regions:
      us-east-1: {}
  production:
    regions:
      us-east-1: {}
      us-west-2: {}
```

`environment` resolves to `dev` and `production`, and `region` to every region used by any
environment (`us-east-1` and `us-west-2`) — `when:` then prunes the Cartesian product down to each
environment's actual regions.

## Glob Paths and Directory-Level Matrix

`spec.files[].path` may be a glob pattern instead of a literal path — `*`, `?`, `[...]`, `**` (any
depth, including zero), and `{a,b}` (brace expansion), matched against every discovered file's
path. This lets one entry gate or multiply an entire directory at once, instead of listing every
file individually. A backslash in the pattern is always treated as a forward-slash directory
separator, regardless of which OS authored or evaluates it — discovered paths are always
forward-slash-normalized. A malformed pattern (an unclosed `[` or `{`) fails `atmos scaffold
validate` and scaffold generation immediately, rather than silently and permanently matching
nothing.

Skip (or gate) an entire directory, recursively, with one `when:`-gated entry:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: "docs/legacy/**"
      when: "answers.include_legacy_docs"
```

When more than one entry's `path:` matches the same discovered file, the **last** matching entry
in declaration order wins — the same precedence convention `.gitignore`/`CODEOWNERS` use. Order
entries broad-to-specific, and place a more specific override _after_ a broad glob it should
shadow:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: "docs/legacy/**"
      when: "answers.include_legacy_docs"
    - path: "docs/legacy/keep-this.md"
      when: "always"
```

Combine a glob `path:` with `matrix:` and `target:` to duplicate an entire directory's files once
per matrix combination, exactly like a single-file matrix entry — every matched file gets the same
`.matrix.<axis>` values a single-file entry would (resolved once per `path:`, not once per matched
file, so every matched file sees the identical combination even if an axis expression uses a
non-deterministic function). Since a glob can match many files, `target:` must differentiate
_which_ matched file an output came from, using two new template variables available everywhere
`.matrix.<axis>` is (`target:` and the file's own content), regardless of whether `path:` is a
glob or `matrix:` is even set:

- **`.file.Path`** — the currently matched file's own discovered path.
- **`.file.RelPath`** — `.file.Path` with the matching entry's glob literal prefix stripped.
  Equal to `.file.Path` when the entry's `path:` has no glob metacharacter.

```yaml title="scaffold.yaml"
spec:
  files:
    - path: "components/**"
      target: "environments/{{ .matrix.env }}/{{ .file.RelPath }}"
      matrix:
        env: [dev, staging, production]
```

A `components/` directory containing `vpc/main.tf` and `eks/main.tf` produces six generated files:
`environments/dev/vpc/main.tf`, `environments/dev/eks/main.tf`, and the same pair under `staging/`
and `production/`. A `target:` that doesn't differentiate matched files (e.g. omitting
`.file.RelPath`) fails immediately, before any file in the run is written — this is checked
deterministically, not just caught after a collision happens mid-run.

`.file.Path`/`.file.RelPath` are available in Go templates only — they aren't exposed to CEL
`when:` alongside `answers`/`matrix`. To exclude one specific file from a glob+matrix entry's own
`when:`, declare a second, more specific entry after the broad one instead, per the precedence
rule above.

## Template Delimiters

A `spec.files` entry can set `delimiters` to a two-element list, overriding `spec.delimiters` for
every file it matches. The pair applies to the file's content, its discovered path, `target:`, and
Go-template `matrix:` axis expressions, so the same entry can combine all of them:

```yaml title="scaffold.yaml"
spec:
  files:
    - path: deploy.yaml
      delimiters: ["[[", "]]"]
      target: "deploy/[[ .matrix.environment ]].yaml"
      matrix:
        environment: '[[ collectKeys answers.environments ]]'
```

See [Template Delimiters](/cli/commands/scaffold/usage#template-delimiters) for the precedence
rules and a Helm and GitHub Actions example.

## Generation Hooks

Generation hooks run after answers are validated and before or after files are written:

```yaml title="scaffold.yaml"
spec:
  hooks:
    prepare:
      events: [before.scaffold.generate]
      kind: step
      type: shell
      with:
        command: mkdir -p generated
    validate:
      events: [after.scaffold.generate]
      kind: steps
      with:
        - type: shell
          command: terraform fmt -recursive
        - type: shell
          command: terraform validate
```

Scaffold hooks run in stable name order and support only `kind: step` and `kind: steps`. A single
step hook uses its envelope `type:` plus step-specific `with:` data; a steps hook executes the
ordered `with:` list. The shared envelope supplies `events`, `when`, `env`, `retry`, and
`on_failure`. Use `answers` in hook CEL and `{{ .Answers.<field> }}` inside a step template.

An unset or bare-relative `working_directory:` on a step defaults to (or resolves under) the
scaffold's target directory -- also available as `{{ .TargetPath }}` -- so `terraform fmt
-recursive` above formats the generated project, not wherever `atmos` happened to be launched
from. Use `working_directory: "."` to opt back into the directory `atmos` was launched from. A
`type: atmos` step is exempt from this default: it keeps running in the directory `atmos` was
launched from when `working_directory:` is unset, since the nested `atmos` invocation must resolve
its own config there, but an explicit `working_directory:` on that step is still honored.

Use `--skip-hooks` to skip all hooks or `--skip-hooks=prepare,validate` to skip named hooks. The
stack-level [hooks reference](/stacks/hooks) documents additional stack-only kinds such as scanners,
stores, Git, and CI integrations.

## Update and Safety Flags

- **`--defaults`** — Use defaults and `--set` values without prompting.
- **`--dry-run`** — Render a preview without generated-file writes.
- **`--force`** — Permit generation into a non-empty target without update merging.
- **`--update`** — Apply an optimistic three-way merge using the recorded source/base revision.
- **`--base-ref`** — Override the recorded merge base (used with `--update`; uses the initial-generation commit
  when recorded, otherwise `HEAD`). Only applies to
  `--update-strategy=tracked` — `rendered`'s base comes from the target's own recorded
  `.atmos/scaffold.yaml`, not `--base-ref`. Combining `--base-ref` with
  `--update-strategy=rendered` is rejected; drop `--base-ref` when using `rendered`.
- **`--update-strategy` (default `tracked`)** — Choose where `--update`'s three-way merge base comes from. `tracked` reads it from the
  target's own Git history at `--base-ref`. `rendered` instead re-renders the template at the
  ref that produced what's currently on disk, using that generation's recorded answers, with no
  dependency on the target being a Git repository at all. This needs the template itself to
  define a `scaffold.yaml` manifest (so the old ref's fields can be resolved) and the target to
  already carry a prior generation's `.atmos/scaffold.yaml` record (so the original answers are
  recoverable) — two separate files, not one. Under `rendered`, `--update` also deletes a file
  the template stopped generating between refs, unless you've edited it locally — in that case
  the update fails with an unresolved conflict instead of silently deleting or keeping it.
  `tracked` doesn't support this: there's no safe way to know which files a historical commit
  actually belonged to the template.
- **`--merge-driver` (default `auto`)** — Choose `auto` (YAML-aware for `.yaml`/`.yml`, text otherwise) or `text` to force every file
  through the line-oriented text merge driver, preserving formatting (e.g. blank lines) that a
  YAML-aware re-encode would otherwise collapse.
- **`--merge-strategy` (default `manual`)** — Choose `manual`, `ours`, or `theirs` for merge conflicts. Unresolved manual conflicts return a nonzero exit code.
- **`--max-changes` (default `50`)** — Maximum percentage of changed lines allowed in a `--update` three-way merge before it fails
  instead of applying. `0` disables this check entirely — the merge is never rejected for having
  too many changes (conflicts still write markers for `--merge-strategy=manual` to resolve). Any
  other value is compared against a computed change percentage that has no upper bound, so no
  positive value is a guaranteed bypass the way `0` is — raising it only makes a hard failure
  less likely, not impossible. Configurable via `ATMOS_SCAFFOLD_MAX_CHANGES`.
- **`--recreate-deleted` (default `false`)** — By default, `--update` leaves in place a file you deleted that the template still generates,
  instead of silently recreating it. Pass `--recreate-deleted` to always recreate it with the
  template's current content. This is independent of `--force`/`--merge-strategy`: `--force`
  already means "on conflict, the template's version wins," so tying recreation to it would make
  manual conflict resolution and recreating a deleted file mutually exclusive.
- **`--skip-hooks`** — Skip all hooks or a comma-separated set of hook names.
- **`--git` / `--no-git`** — Control initial Git setup; generation defaults to no Git initialization.

## Try it out

### scaffolding

Copy the example into a new directory and make it your own:

```shell
atmos init examples/scaffolding
cd scaffolding
```

Generate a project from the copied scaffold.

```shell
atmos scaffold generate example ./my-project --set project_name=my-project --set enable_vendoring=false
```

- [Installation guide](/install)
- [Explore the example](/examples/scaffolding)
- [Initialization and repository settings](/cli/commands/init#examples-and-source-directories)

## Related Commands

- [`atmos scaffold validate`](/cli/commands/scaffold/validate)
- [`atmos scaffold list`](/cli/commands/scaffold/list)
- [`atmos init`](/cli/commands/init)
- [Lifecycle hooks](/stacks/hooks)
