# Manage Lifecycle Events with Hooks

Atmos hooks run automated actions around Terraform, Kubernetes, Helm, and Helmfile component
operations. Built-in kinds cover cost analysis, security scanning, stores, Git, and CI; `kind: step`
and `kind: steps` make the complete typed step library available at lifecycle events.

## Hooks Schema

Each hook has a `kind` that selects an engine and a set of defaults. The
generic `command` kind runs any binary; named kinds like `infracost`,
`trivy`, and `tflint` ship sane defaults so you don't need to write any wiring.

```yaml
components:
  terraform:
    vpc:
      hooks:
        # Cost diff after every plan — zero config.
        cost:
          events: [after.terraform.plan]
          kind: infracost

        # Security scan after every plan.
        security:
          events: [after.terraform.plan]
          kind: trivy

        # Store outputs after apply (the original hook use case).
        outputs:
          events: [after.terraform.apply]
          kind: store
          name: prod/ssm
          outputs:
            vpc_id: .id
```

Hooks can be declared at the global level, the `terraform` section, an
individual component, or in the `overrides` section. Partial config at
each level merges into the final result so you can keep configuration
[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

## Supported Lifecycle Events

| Family | Events |
| --- | --- |
| Terraform | `before`/`after.terraform.init`, `plan`, `apply`, and `test`; `deploy` normalizes to `apply`. Aggregate events fire once for multi-component runs: `after.terraform.plan.aggregate`, `after.terraform.apply.aggregate`, and `after.terraform.destroy.aggregate`. |
| Kubernetes | `before`/`after.kubernetes.render`, `diff`/`plan`, `apply`/`deploy`, `delete`, and `validate`. `plan` normalizes to `diff`; `deploy` normalizes to `apply`. |
| Native Helm | `before`/`after.helm.template`, `diff`, `apply`/`deploy`, and `delete`. `deploy` normalizes to `apply`. |
| Helmfile | `before`/`after.helmfile.template`, `diff`, `apply`/`sync`/`deploy`, and `destroy`. `sync` and `deploy` normalize to `apply`. |
| Scaffold generation | `before.scaffold.generate` and `after.scaffold.generate`. These are declared in `scaffold.yaml`, not stack manifests, and support only `kind: step` and `kind: steps`. |

Hooks with no `events` list fire on every supported event for backward
compatibility with configs written before event filtering existed. You
should use the dotted form (`after.terraform.plan`) in new configs. The
legacy hyphenated form (`after-terraform-plan`) is still accepted and
equivalent for backward compatibility.

The `before.terraform.init` and `after.terraform.init` events fire around the
explicit `atmos terraform init` command. They do not fire for the implicit
`init` that `plan`, `apply`, and `deploy` run automatically — use
`before.terraform.plan` / `before.terraform.apply` for those.

## Supported Kinds

Stack hooks support `command`, `store`, `git`, `infracost`, `trivy`, `checkov`, `kics`, `step`,
and `steps`.
The named kinds below provide purpose-built defaults; `kind: command` is the general binary
runner. Scaffold hooks reuse the `when:` vocabulary and step engine but reject every kind except
`step` and `steps` because they do not have stack/component execution context.

The legacy `ci.check`, `ci.output`, `ci.summary`, `ci.upload`, and `ci.download` kinds still parse
for compatibility but are deprecated no-ops. Use current CI provider bindings rather than adding
new `ci.*` hook configuration.

## Built-in Kinds

Each named kind below ships defaults for `command`, `args`, `env`, and
`on_failure` so the minimal config (just `events` and `kind`) does the
right thing. Every default is overridable — set the field on your hook
and your value wins. See [Overriding Kind Defaults](#overriding-kind-defaults)
for the override rules (notably: `args` and `env` are full replacement,
not merge).

### `kind: infracost`

Runs [Infracost](https://www.infracost.io/) to compute monthly cloud cost
estimates from your Terraform plan. Renders a cost diff card with the
top resources by monthly cost.

```yaml
hooks:
  cost:
    events: [after.terraform.plan]
    kind: infracost
```

Requires `INFRACOST_API_KEY` to be set in the shell or on the hook
(free at [infracost.io](https://www.infracost.io)).

### `kind: trivy`

[Trivy](https://trivy.dev/) is an open-source security and misconfiguration
scanner from [Aqua Security](https://www.aquasec.com/). The `trivy` kind
runs Trivy in `config` mode against the component's Terraform sources and
feeds the SARIF output through Atmos's shared scanner result handler.

```yaml
hooks:
  security:
    events: [after.terraform.plan]
    kind: trivy
```

- **`command` (default)**
  `trivy`
  .
- **`args` (default)**
  `config --format sarif --output $ATMOS_OUTPUT_FILE --quiet $ATMOS_COMPONENT_PATH`
  .
- **Output**
  Single SARIF file written directly to 
  `$ATMOS_OUTPUT_FILE`
  .
- **`on_failure` (default)**
  `warn`
   — a scanner finding does not block your plan unless you override this.
- **Runtime requirements**
  The 
  `trivy`
   binary on PATH. Declare it in 
  `dependencies.tools`
   to have Atmos auto-install through the toolchain.
- **Authentication**
  None required for 
  `config`
   scans.

### `kind: checkov`

[Checkov](https://www.checkov.io/) is an open-source policy-as-code scanner
maintained by [Bridgecrew / Prisma Cloud](https://www.paloaltonetworks.com/prisma/cloud)
(Palo Alto Networks). The `checkov` kind runs Checkov over the component's
Terraform sources and feeds the SARIF output through Atmos's shared scanner
result handler.

```yaml
hooks:
  policy:
    events: [after.terraform.plan]
    kind: checkov
```

- **`command` (default)**
  `checkov`
  .
- **`args` (default)**
  `-d $ATMOS_COMPONENT_PATH -o sarif --output-file-path $ATMOS_OUTPUT_DIR --quiet --soft-fail`
  . Note: Checkov's 
  `--output-file-path`
   takes a 
  _directory_
   and writes 
  `results_sarif.sarif`
   inside it.
- **Output**
  SARIF file written to 
  `$ATMOS_OUTPUT_DIR/results_sarif.sarif`
  ; the kind's result handler reads from that nested path.
- **`on_failure` (default)**
  `warn`
  .
- **Runtime requirements**
  The 
  `checkov`
   binary on PATH. Declare it in 
  `dependencies.tools`
   to have Atmos auto-install through the toolchain.
- **Default env additions**
  Atmos sets 
  `SSL_CERT_FILE`
   from the host CA bundle when one is detected. The PyInstaller-packaged Checkov release ships a frozen 
  `certifi`
   bundle that often cannot validate the TLS chain serving 
  `api0.prismacloud.io`
   (the API Checkov hits at startup to fetch guideline mappings); pointing 
  `SSL_CERT_FILE`
   at the host bundle is the official PyInstaller workaround. On hosts where Atmos cannot detect a CA bundle, the kind makes no env change and Checkov falls back to its bundled 
  `certifi`
  .

### `kind: kics`

[KICS](https://kics.io/) (Keeping Infrastructure as Code Secure) is an
open-source IaC scanner from [Checkmarx](https://checkmarx.com/). The
`kics` kind runs KICS over the component's Terraform sources and feeds
the SARIF output through Atmos's shared scanner result handler.

```yaml
hooks:
  iac-scan:
    events: [after.terraform.plan]
    kind: kics
```

- **`command` (default)**
  `kics`
  .
- **`args` (default)**
  `scan -p $ATMOS_COMPONENT_PATH -o $ATMOS_OUTPUT_DIR --report-formats sarif --no-progress`
  .
- **Output**
  SARIF file written to 
  `$ATMOS_OUTPUT_DIR/results.sarif`
  .
- **`on_failure` (default)**
  `warn`
  .
- **Runtime requirements**
  The 
  `kics`
   binary on PATH. Declare it in 
  `dependencies.tools`
   to have Atmos auto-install through the toolchain — Atmos ships a curated registry override so 
  `kics: "<version>"`
   resolves correctly even though the upstream Aqua entry uses an unsupported 
  `go_build`
   type.
- **`KICS_QUERIES_PATH`**
  Required at runtime. The KICS release tarball does not include the query library, so Atmos toolchain installs provide the binary only. Homebrew install: 
  `export KICS_QUERIES_PATH=$(brew --prefix kics)/share/kics/assets/queries`
  . For toolchain installs, supply a trusted KICS query library separately and point 
  `KICS_QUERIES_PATH`
   at its 
  `assets/queries`
   directory.

### `kind: tflint`

[tflint](https://github.com/terraform-linters/tflint) is an open-source
Terraform/OpenTofu linter. Where `terraform validate` only proves your config
parses, tflint enforces **code consistency and conventions** and catches
**hygiene** problems — unused declarations, deprecated syntax, missing version
constraints, and invalid provider-specific arguments — so those standards hold
uniformly across every component instead of relying on reviewers. The `tflint`
kind runs tflint over the component's sources and feeds the SARIF output
through Atmos's shared scanner result handler.

Wire it on `before.terraform.init` so a lint failure stops you before any
init/plan work — a linter should fail fast:

```yaml
hooks:
  lint:
    events: [before.terraform.init]
    kind: tflint
```

If your CI job only runs `atmos terraform plan`, `apply`, or `deploy` and does
not run an explicit `atmos terraform init` step, attach the hook to that command
instead (for example `before.terraform.plan`) so the lint summary appears in the
same job.

- **`command` (default)**
  `tflint`
  .
- **`args` (default)**
  `--chdir=$ATMOS_COMPONENT_PATH --format=sarif`
  .
- **Output**
  SARIF on 
  **stdout**
   — tflint has no file-output flag. The kind opts into stdout capture, so Atmos redirects tflint's stdout into 
  `$ATMOS_OUTPUT_FILE`
   before parsing (transparent to you; the result is identical to the file-output scanners).
- **`on_failure` (default)**
  `warn`
   — a lint finding does not block your plan unless you override this. (tflint exits non-zero when it finds issues; the SARIF is still captured and summarized regardless.)
- **Runtime requirements**
  The 
  `tflint`
   binary on PATH. Declare it in 
  `dependencies.tools`
   (e.g. 
  `tflint: "0.59.1"`
  ) to have Atmos auto-install through the toolchain. The builtin 
  `terraform`
   ruleset runs with no configuration; provider rulesets (aws/google/azurerm) require a 
  `.tflint.hcl`
   plus a one-time 
  `tflint --init`
   (run it via a separate 
  `kind: command`
   hook if you need plugins).

All four SARIF scanner kinds (`trivy`, `checkov`, `kics`, `tflint`) share a
result handler that renders findings as a compact markdown table — severity,
rule ID (linked to the upstream remediation guide when available), short
description, and `file:line` location. The same markdown body flows to your
terminal, the Atmos Pro run page, and PR comments. In CI it additionally
becomes a step summary, inline PR annotations (`::error`/`::warning` on the
diff), and a GitHub Code Scanning upload — all automatic, gated by your `ci.*`
config.

### `kind: store`

Reads Terraform outputs and writes them to an external store such as
`kind: aws/ssm`, `kind: aws/asm`, Azure Key Vault, or Google Secret Manager.
Use this to share state between components without backend wiring — paired with
the [`!store` function](/stacks/sharing-state/stores) for reading.

```yaml
hooks:
  outputs:
    events: [after.terraform.apply]
    kind: store
    name: prod/ssm
    outputs:
      vpc_id: .id
      private_subnet_ids: .private_subnet_ids
```

See [Store function reference](#store-function-reference) below for
configuration details.

### `kind: git`

Publishes component artifacts to a Git repository on lifecycle events —
commit (and optionally push) generated files through the shared
[Atmos Git service](/cli/commands/git/usage), with its safety rules
(no force push, path-scoped commits, bounded push retry) and
authentication built in.

By default the hook targets the **current repository** — the repo the
component already lives in — matching the common case of committing
generated files back to the working repo after `terraform apply`:

```yaml
components:
  terraform:
    app:
      hooks:
        publish-artifacts:
          events:
            - after.terraform.apply
          kind: git
          # repository omitted -> the current repository
          commit:
            message: "Update generated artifacts for {{ .component }} in {{ .stack }}"
            paths:
              - generated/{{ .stack }}/{{ .component }}
          push: true
```

Setting `repository: <name>` targets a managed repository configured
under the top-level [`git.repositories`](/cli/configuration/git) section
instead — Atmos clones/reconciles its workdir, commits, and pushes with
the repository's configured identity, signing, and retry settings:

```yaml
components:
  terraform:
    app:
      hooks:
        publish-artifacts:
          events:
            - after.terraform.apply
          kind: git
          repository: generated-terraform
          commit:
            message: "Update generated artifacts for {{ .component }} in {{ .stack }}"
          push: true
```

Behavior notes:

- **Clean no-op:** when there are no changes to commit, the hook
  succeeds without creating a commit — no empty commits.
- **Push only on commit:** `push: true` pushes only when a commit was
  actually created.
- **Provenance trailers:** published commits carry `Atmos-Stack`,
  `Atmos-Component`, and `Atmos-Source-SHA` trailers, making
  deployment-repo commits traceable back to exactly what produced them.
- **Failure mode:** unlike the advisory tool kinds, `kind: git` defaults
  to `on_failure: fail` — a silently dropped commit or push loses
  artifacts. Override with `on_failure: warn` or `ignore` if publishing
  is best-effort.
- **Templating:** `commit.message` and `commit.paths` support the
  standard hook template context (`{{ .component }}`, `{{ .stack }}`,
  `{{ .vars.* }}`).

`kind: git` hooks are separate from _local_ Git hooks (`pre-commit`,
`commit-msg`, ...) — those are configured under `git.hooks` and managed
by [`atmos git hooks`](/cli/commands/git/hooks/).

### `kind: command` (generic engine)

The escape hatch — runs any toolchain-resolved binary with Atmos's
standard `ATMOS_*` env-var contract. Use when no built-in kind fits
your tool.

```yaml
hooks:
  custom-scan:
    events: [after.terraform.plan]
    kind: command
    command: my-internal-scanner
    args:
      - "--component"
      - "{{ .atmos_component }}"
      - "--source"
      - "$ATMOS_COMPONENT_PATH"
      - "--out"
      - "$ATMOS_OUTPUT_FILE"
    format: markdown     # optional; tells Atmos to render the output file as markdown
    on_failure: warn     # warn | fail | ignore
```

The subprocess receives these environment variables:

- **`ATMOS_COMPONENT_PATH`**
  On-disk path to the component's Terraform module (honors workdir resolution).
- **`ATMOS_PLANFILE`**
  Planfile path on after-plan events (when threaded — currently a stub).
- **`ATMOS_OUTPUT_DIR`**
  A fresh temp directory created by Atmos before the subprocess starts (via 
  `os.MkdirTemp("", "atmos-hook-*")`
  ). 
  **Unique per hook invocation**
   — two hooks firing on the same event get separate directories and do not share state through these env vars. The directory and everything inside it is deleted automatically when the hook returns. Use this for tools that write to a directory rather than a single file (e.g., KICS, Checkov).
- **`ATMOS_OUTPUT_FILE`**
  `$ATMOS_OUTPUT_DIR/output`
  . Write structured output here for the kind's result handler to read. Same lifetime as 
  `ATMOS_OUTPUT_DIR`
  .
- **`ATMOS_STACK`**
  The stack name.
- **`ATMOS_COMPONENT`**
  The component name.

Atmos creates a fresh temp directory per hook invocation, points
`ATMOS_OUTPUT_DIR` and `ATMOS_OUTPUT_FILE` at it, and deletes the entire
directory after the hook returns. Custom commands can write any
additional files they need into `$ATMOS_OUTPUT_DIR` — sibling hooks won't
collide, and cleanup is automatic.

Tool stdout/stderr stream through Atmos's I/O layer so you see them in
real time — same UX as Terraform plan output.

### `kind: step` (run a step type)

Runs any registered [workflow / custom-command step type](/workflows/steps/type)
— `container`, `toast`, `log`, `markdown`, `http`, and the rest — on a
lifecycle event. Instead of growing the hook-kind list one tool at a
time, `kind: step` makes the whole step library available as hooks, so a
step you already use in a workflow runs identically as a hook.

```yaml
hooks:
  notify-slack:
    kind: step
    type: http                 # any registered step type
    events: [after.terraform.apply]
    on_failure: warn           # envelope policy: warn | fail | ignore
    retry:                     # envelope policy: same schema as a workflow step's retry
      max_attempts: 3
    with:                      # the step's own parameters
      url: https://hooks.slack.com/services/XXX
      method: POST
      body: '{"text": "Deployed {{ .atmos_component }} to {{ .stack }}"}'
```

:::note The `http` step ships separately
The Slack example above needs the `http` step type. If your Atmos build
doesn't have it yet, use a step type that is registered (e.g. `container`,
`toast`, `log`, `markdown`) — the bridge itself works with every
registered step type.
:::

The split between the envelope and `with:` is deliberate:

- The **envelope** (`kind`, `type`, `events`, `on_failure`, `retry`,
  `env`) is what the _hook runner_ interprets.
- **`with:`** is what the _step_ interprets — it is the step's own
  parameter block, exactly as you'd write it in a workflow.

`on_failure` and `retry` sit at the envelope because Atmos applies them
_around_ the step (retry the whole step, then decide warn/fail/ignore) —
the step never sees them.

```yaml
# Build and push an image after apply, retried, non-blocking.
hooks:
  publish-image:
    kind: step
    type: container
    events: [after.terraform.apply]
    on_failure: warn
    with:
      action: build
      image: example:latest
      build:
        context: .
        tags: [example:latest]
```

A common pairing is the [`emulator` step type](/workflows/steps/type/emulator) on the
`terraform test` events: bring a local cloud [emulator](/stacks/components/emulator) up before the tests
and tear it down after (`when: always` guarantees teardown even when a test fails), so
`atmos terraform test` runs against a local sandbox with no cloud account:

```yaml
hooks:
  start-emulator:
    kind: step
    type: emulator
    events: [before.terraform.test]
    with: { component: aws, action: up }
  stop-emulator:
    kind: step
    type: emulator
    events: [after.terraform.test]
    when: always
    with: { component: aws, action: down }
```

Another common pairing is the [`archive` step type](/workflows/steps/type/archive)
on `before.terraform.*` events — rebuild a Lambda-style `handler.zip` fresh
before every plan/apply/destroy, replacing a Terragrunt-style `before_hook`
shell-out to `zip`:

```yaml
hooks:
  package:
    kind: step
    type: archive
    events: [before.terraform.plan, before.terraform.apply, before.terraform.destroy]
    with:
      source: src/
      destination: handler.zip
```

- **`type`**
  The step type to run — any registered workflow/custom-command step type. A typo fails the 
  **preflight**
   check before Terraform runs.
- **`with`**
  The step's parameters. These are rendered with the standard hook template context and YAML functions (so 
  `{{ .atmos_component }}`
   and 
  `!store ...`
   work inside 
  `with:`
  ), then handed to the step. The step's own validation is the gate — the schema keeps 
  `with:`
   open.
- **`when` (default `success`)**
  Declarative condition controlling whether this hook runs. See 
  [Reacting to success or failure](#reacting-to-success-or-failure)
  .
- **`retry`**
  Optional. Wraps the step execution in Atmos's retry policy — same schema as a 
  [workflow step's `retry`](/workflows/steps/retry)
  . Applied by the hook runner, not the step.
- **`env`**
  Extra environment variables layered on top of the standard 
  `ATMOS_*`
   variables (
  `ATMOS_STACK`
  , 
  `ATMOS_COMPONENT`
  , 
  `ATMOS_COMPONENT_PATH`
  , …) the step receives.
- **`on_failure` (default `warn`)**
  `warn`
   | 
  `fail`
   | 
  `ignore`
  , applied to the 
  **step's own**
   outcome (not the terraform operation's). Use 
  `when:`
   to react to the operation outcome.

All step types are available, including interactive ones — Atmos does not
restrict them. Whether an interactive step makes sense on a (usually
headless / CI) lifecycle event is the step's responsibility, not the
hook's.

### `kind: steps` (run ordered steps)

Runs an ordered list of registered workflow/custom-command step types on one
lifecycle event. Use this when a lifecycle needs sequencing but should still
stay local to the component being operated on — for example, start an emulator,
apply a fixture component, run the test, then destroy the fixture and stop the
emulator.

`kind: steps` uses the same hook envelope as every other hook (`events`, `when`,
`on_failure`, `retry`, `env`). The ordered payload lives under `with:`, the same
conventional payload key used by `kind: step`.

```yaml
hooks:
  test-fixtures-up:
    events: [before.terraform.test]
    kind: steps
    on_failure: fail
    with:
      - type: emulator
        component: aws
        stack: fixtures
        action: up

      - type: atmos
        command: terraform apply vpc -s fixtures -auto-approve

  test-fixtures-down:
    events: [after.terraform.test]
    when: always
    kind: steps
    with:
      - type: atmos
        command: terraform destroy vpc -s fixtures -auto-approve

      - type: emulator
        component: aws
        stack: fixtures
        action: down
```

Steps run in list order. A failing step stops the list and the hook's
`on_failure` policy decides whether the Terraform operation fails, warns, or
continues. A hook-level `retry` retries the whole ordered list.

### Reacting to success or failure

By default an `after-*` hook runs **only when the operation succeeds** — a
`store` hook must not write outputs after a failed apply. Set `when:` to change
that. Hooks support the same declarative predicates as workflow and custom
command steps, plus the hook-specific lifecycle status predicates:

- **`when: success` (default)**
  Run only when the operation succeeded.
- **`when: failure`**
  Run only when the operation failed.
- **`when: always`**
  Run on both outcomes.
- **`when: ci`**
  Run in CI after a successful operation. Hooks apply 
  `success`
   implicitly when the condition does not mention 
  `success`
  , 
  `failure`
  , or 
  `always`
  .
- **`when: [ci, always]`**
  Run in CI after both successful and failed operations.

Built-in predicates are `ci`, `local`, `always`, `never`, `success`, and
`failure`. Atmos evaluates those keywords first; any other scalar string is a
CEL expression. Use `!cel` when you want to make that explicit. Compose
predicates and CEL expressions with list shorthand, `all`, `any`, and `not`:

```yaml
hooks:
  ci-summary:
    kind: command
    events: [after.terraform.plan]
    command: ./scripts/write-ci-summary.sh
    when: [ci, always]

  local-cleanup:
    kind: command
    events: [after.terraform.apply]
    command: ./scripts/cleanup.sh
    when:
      all:
        - local
        - failure

  skip-in-ci:
    kind: command
    events: [after.terraform.plan]
    command: ./scripts/local-report.sh
    when:
      not: ci

  prod-failure-summary:
    kind: command
    events: [after.terraform.apply]
    command: ./scripts/write-prod-failure-summary.sh
    when: !cel 'stack == "prod" && status == "failure"'
```

Condition filtering also applies during hook preflight. For example,
`when: ci` hooks are skipped outside CI before Atmos validates or installs the
hook's tools. Hook CEL expressions can read `ci`, `status`, `stack`,
`component`, `hook`, `event`, and `env`.

The outcome is available to the step both as **template context** and as
**environment variables**, alongside the component and stack — so a hook can
announce exactly what happened:

```yaml
hooks:
  announce:
    kind: step
    type: say
    events: [after.terraform.apply]
    when: always
    with:
      print: always
      content: >-
        The {{ .atmos_component }} component in the {{ .stack }} stack
        {{ if eq .status "failure" }}was not successful{{ else }}was successful{{ end }}
```

- **`{{ .status }}`**
  `success`
   or 
  `failure`
  . Also 
  `ATMOS_HOOK_STATUS`
  .
- **`{{ .exit_code }}`**
  The operation's exit code (
  `0`
   on success). Also 
  `ATMOS_HOOK_EXIT_CODE`
  .
- **`{{ .error }}`**
  The error message on failure, empty on success. Also 
  `ATMOS_HOOK_ERROR`
  .
- **`{{ .atmos_component }}` / `{{ .stack }}`**
  The component and stack (already part of the hook template context). Also 
  `ATMOS_COMPONENT`
   / 
  `ATMOS_STACK`
  .

Steps that read the environment instead of templates (e.g. a `shell` or `say`
step) get the same values via `ATMOS_HOOK_STATUS`, `ATMOS_HOOK_EXIT_CODE`,
`ATMOS_HOOK_ERROR`, `ATMOS_COMPONENT`, and `ATMOS_STACK`.

:::note Outcome reaches all hook kinds
`when:` and the `ATMOS_HOOK_*` variables apply to every hook kind, not just
`kind: step` — e.g. a `kind: command` cleanup hook with `when: failure`. The
outcome is only meaningful on `after-*` events; `before-*` events always run as
`success`.
:::

## Overriding Kind Defaults

Every named kind (`infracost`, `trivy`, `checkov`, `kics`, `tflint`, `store`)
ships defaults for `command`, `args`, `env`, and `on_failure`. Any
field you set on the hook overrides the kind's default for that field.

:::caution Override is replace, not merge
`args` and `env` are **full replacement** — if you set `args` on the
hook, the kind's default arg list is discarded entirely (you must
restate every arg you want). Same for `env`. If you only need to tweak
one flag, use `kind: command` directly and pass the full command line.
:::

A common case is bumping a scanner's severity threshold or adding a
config file. Because override replaces the entire arg list, copy the
default args from the [kind's reference section above](#kind-trivy) and
add your overrides:

```yaml
hooks:
  security:
    events: [after.terraform.plan]
    kind: trivy
    args:
      - config
      - --format
      - sarif
      - --output
      - $ATMOS_OUTPUT_FILE
      - --severity
      - HIGH,CRITICAL          # added
      - --quiet
      - $ATMOS_COMPONENT_PATH
```

You can also override just `on_failure` (independent of args/env) to
make a scanner finding block the run:

```yaml
hooks:
  security:
    events: [after.terraform.plan]
    kind: trivy
    on_failure: fail            # default is warn
```

Or override `env` to inject a tool-specific config path:

```yaml
hooks:
  policy:
    events: [after.terraform.plan]
    kind: checkov
    env:
      CHECKOV_CONFIG_FILE: $ATMOS_COMPONENT_PATH/.checkov.yaml
```

Setting `env` on the hook replaces the kind's `DefaultEnv` map
entirely. For `kind: checkov` specifically, that means losing the
default `SSL_CERT_FILE` workaround (see [`kind: checkov`](#kind-checkov))
— restate it if you still need it.

## Tool Auto-Install via `dependencies.tools`

Declare which tools a hook needs and Atmos installs them automatically
through the toolchain before the hook fires. Same syntax used by
[component dependencies](/stacks/dependencies/components):

```yaml
components:
  terraform:
    vpc:
      dependencies:
        tools:
          infracost: "0.10.44"
          checkov: "3.2.529"
      hooks:
        cost:     { events: [after.terraform.plan], kind: infracost }
        security: { events: [after.terraform.plan], kind: checkov }
```

A **pre-flight check** runs once per command invocation:

1. Resolve every tool the component declares.
2. Install any that aren't already present.
3. Build a PATH augmentation that points at the installed pinned versions.
4. Verify each hook's binary is resolvable.

If any binary is missing, the run aborts **before** Terraform with a
clear error including a hint to declare it in `dependencies.tools`. You
won't lose a 90-second plan to find out a hook is misconfigured.

Pin a concrete version (`"3.2.529"`) or use a SemVer constraint
(`"~> 3.2"`). `"latest"` is accepted but the resulting PATH may not match
what the toolchain installed — pin a version for reliable behavior.

### Curated Atmos Registry

Atmos ships a small curated registry baked into the binary that
overrides upstream Aqua registry entries for tools that don't model
cleanly with Aqua's defaults. Today this covers **KICS** (the upstream
entry uses `type: go_build` which the Atmos installer doesn't support
yet). The override is invisible to users — you just write `kics:
"2.1.20"` in `dependencies.tools` and it works.

You can override even the built-in overrides with your own registry
entry at higher priority — see [Toolchain
Configuration](/cli/configuration/toolchain) for the registry syntax.

## Skipping Hooks at Runtime

Set `--skip-hooks` (or the `ATMOS_SKIP_HOOKS` environment variable) to
bypass hooks for a single invocation without editing stack config.
Useful for emergency operations, local iteration, or when a scanner is
producing noisy false positives you want to ignore for one run.

```bash
atmos terraform plan vpc -s prod --skip-hooks                  # skip all
atmos terraform plan vpc -s prod --skip-hooks=cost,security    # skip specific hooks by name
ATMOS_SKIP_HOOKS=true atmos terraform apply vpc -s prod        # via env var
```

Skipped hooks are logged at `INFO` level so it stays visible in CI
output. The flag is **per-invocation only** — it doesn't propagate to
nested commands or workflows. Because hooks only run on terraform
operations, `--skip-hooks` is a flag of the
[`atmos terraform`](/cli/commands/terraform/usage) command and all its
subcommands (`plan`, `apply`, `deploy`, …).

## Hooks with `--all`

`atmos terraform plan --all` / `atmos terraform apply --all` walks every
terraform component in the selected stack(s) in dependency order and runs
the command for each. Hooks fire just like a single-component invocation,
with these specifics:

- **Hooks fire per component**, in dependency order. `destroy --all`
  reverses the order so dependents tear down before their dependencies.
- **Tool auto-install runs per component.** Each component's
  `dependencies.tools` is resolved on its own, so different components in
  the same `--all` run can pin different versions of the same tool —
  the toolchain PATH is rebuilt per component.
- **`--skip-hooks` propagates across the whole run.** The flag is set on
  the shared invocation state and applies uniformly to every component
  `--all` visits.
- **`on_failure: fail` aborts the remaining traversal.** A failing hook
  on component K stops `--all` before it visits any downstream node.
  Components already processed stay processed — there is no rollback.

## Failure Modes

Every hook can declare an `on_failure` mode:

- **`warn` (default)**
  Log a warning if the hook fails, continue. Built-in scanner kinds default to 
  `warn`
   so a security or cost issue doesn't block your plan unless you explicitly want it to.
- **`fail`**
  Propagate the subprocess error — the terraform command aborts. Use when a hook finding should block the operation (e.g., critical security violations).
- **`ignore`**
  Silently swallow failures. Useful for opportunistic notifications you don't want to ever break a run.

:::note `on_failure` vs `when`
These answer different questions. `on_failure` controls what happens when **the
hook itself** fails. `when` (`success` | `failure` | `always`) controls whether
the hook runs based on **the terraform operation's** outcome — see
[Reacting to success or failure](#reacting-to-success-or-failure).
:::

## Format Symmetry

When a hook's kind produces a markdown summary (every built-in kind
does), Atmos renders **the same bytes** in:

- Your terminal (via `ui.MarkdownMessage`)
- The GitHub Actions [job step summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary)
- The Atmos Pro run page (when Pro is connected)

No separate "terminal-friendly" vs "Pro-friendly" variants — write
markdown once, get it everywhere.

## CI Reporting

When running in CI (GitHub Actions), Atmos can surface scanner-hook findings
in the pipeline itself instead of leaving them buried in the `terraform plan`
log. Three independent outputs are available, all under `ci:` and all gated by
the `ci.enabled` master switch:

```yaml
ci:
  enabled: true          # required for any CI reporting to fire
  summary: true          # markdown report in the job step summary  (default: on)
  annotations: true      # inline ::error/::warning on the PR diff   (default: on)
  results: true          # upload SARIF to GitHub Code Scanning      (default: off)
```

- **`ci.summary`**
  Appends each hook's markdown summary to the GitHub Actions job step summary. Default on.
- **`ci.annotations`**
  Renders each finding as an inline GitHub annotation (
  `::error`
  /
  `::warning`
  ) anchored at its file and line on the pull request diff. This is the 
  **non-Code-Scanning**
   path — it needs no GitHub Advanced Security. Default on.
- **`ci.results`**
  Uploads the raw SARIF to GitHub 
  **Code Scanning**
   (the Security tab) as tracked alerts. Code Scanning is 
  **free on public repositories**
  ; on 
  **private/internal repositories it requires [GitHub Advanced Security](https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security)**
   — a paid add-on that 
  **GitHub**
   licenses per active committer. Default off (opt-in). The analysis category is derived automatically from the scan target (
  `atmos/&lt;component&gt;`
   for shared component source, 
  `atmos/&lt;stack&gt;/&lt;component&gt;`
   for a per-stack workdir) so per-component uploads don't overwrite each other.

All three are **best-effort**: a reporting failure logs at debug and never fails
the hook or the terraform command. Outside CI they no-op. Findings derive from the
tool's SARIF, so the three built-in SARIF scanners — `checkov`, `trivy`, `kics` —
all participate; `infracost` (cost, not findings) does not.

`ci.annotations` and `ci.results` can both place line-level feedback on a pull
request. Enable both when you intentionally want the lightweight Actions
annotation and the tracked Code Scanning alert; otherwise enable only one of
them to avoid duplicate comments on the same lines.

### Required GitHub Actions permissions

```yaml
permissions:
  contents: read
  security-events: write   # ONLY required for ci.results (SARIF upload)
```

`ci.summary` and `ci.annotations` need **no special permissions and no paid
add-on** — the step summary is a file write and annotations are log output, so
they work on any repository. `ci.results` uploads to Code Scanning, which
requires the `security-events: write` permission and — on **private/internal
repositories** — **GitHub Advanced Security, which GitHub licenses as a paid
add-on per active committer** (Code Scanning is free on public repositories).
Note: pull requests **from forks** do not receive
`security-events: write` on the default token, so `ci.results` is skipped there.

### Custom hooks

A custom `kind: command` hook gets the same reporting as the built-in scanners by
declaring `format: sarif` — any SARIF-emitting tool (tfsec, semgrep, gitleaks,
snyk, …) then flows to the summary, annotations, and Code Scanning with no extra
code:

```yaml
hooks:
  tfsec:
    events: [after.terraform.plan]
    kind: command
    command: tfsec
    args: ["--format", "sarif", "--out", "$ATMOS_OUTPUT_FILE", "$ATMOS_COMPONENT_PATH"]
    format: sarif              # parse output as SARIF
    # results: results.sarif   # optional: read SARIF from this file in $ATMOS_OUTPUT_DIR
```

The report is labeled by the SARIF's own tool name (e.g. "tfsec").

## Backwards Compatibility

The pre-rename `command:` YAML key is still accepted as an alias for
`kind:`. Existing stack manifests like:

```yaml
hooks:
  vpc-id:
    events: [after.terraform.apply]
    command: store      # legacy form — still works
    name: vpc/id
    outputs:
      id: .vpc_id
```

…parse identically post-upgrade. No migration required.

Hyphenated lifecycle events such as `after-terraform-apply` are also still
accepted. New documentation and examples use the canonical dotted form
(`after.terraform.apply`).

## Store Function Reference

For complete details on the `store` kind (configuring stores, supported
backends, output mapping, etc.), see [Sharing State with
Stores](/stacks/sharing-state/stores).

- **`hooks.<name>`**
  Map key is the hook's name (unique per component).
- **`hooks.<name>.events`**
  List of 
  [lifecycle events](#supported-lifecycle-events)
   that trigger this hook.
- **`hooks.<name>.kind`**
  Which engine runs the hook: 
  `store`
  , 
  `command`
  , 
  `git`
  , 
  `step`
  , 
  `steps`
  , 
  `infracost`
  , 
  `checkov`
  , 
  `trivy`
  , or 
  `kics`
  . Aliased from the legacy 
  `command:`
   field. The legacy 
  `ci.*`
   values parse as deprecated no-ops and must not be used for new configuration.
- **`hooks.<name>.type`**
  For 
  `kind: step`
  : the 
  [step type](#kind-step-run-a-step-type)
   to run (e.g. 
  `container`
  , 
  `http`
  , 
  `toast`
  , 
  `log`
  ).
- **`hooks.<name>.with`**
  For 
  `kind: step`
  : the step's own parameters. For 
  `kind: steps`
  : the ordered list of steps. Rendered with the hook template context and YAML functions, then validated by the step engine at run time.
- **`hooks.<name>.retry`**
  For 
  `kind: step`
  : retry policy wrapping the step execution. For 
  `kind: steps`
  : retry policy wrapping the whole ordered list. Same schema as a workflow step's 
  `retry`
  .
- **`hooks.<name>.command`**
  For 
  `kind: command`
  : the binary to execute (resolved via the toolchain or PATH). Named kinds default this.
- **`hooks.<name>.args`**
  For 
  `kind: command`
  : arguments passed to the binary. Supports Go template syntax (
  `{{ .atmos_component }}`
  ) for stack metadata and 
  `$ATMOS_*`
   env-var substitution at exec time.
- **`hooks.<name>.env`**
  Additional env vars exported to the subprocess. Supports the same templating as 
  `args`
  .
- **`hooks.<name>.format`**
  Optional rendering hint. v1 supports 
  `markdown`
   — tells Atmos to render the tool's output file as markdown in the terminal.
- **`hooks.<name>.on_failure`**
  `warn`
   (default), 
  `fail`
  , or 
  `ignore`
  . Applies to the hook's own outcome, not the terraform operation's.
- **`hooks.<name>.when`**
  Declarative condition controlling whether the hook runs. Omitted means 
  `success`
  ; if the condition does not mention 
  `success`
  , 
  `failure`
  , or 
  `always`
  , Atmos applies 
  `success`
   implicitly. See 
  [Reacting to success or failure](#reacting-to-success-or-failure)
  .
- **`hooks.<name>.name` / `hooks.<name>.outputs`**
  Store-kind specific. See 
  [store function reference](/stacks/sharing-state/stores)
  .
- **`hooks.<name>.repository`**
  For 
  `kind: git`
  : the name of a managed repository under the top-level 
  [`git.repositories`](/cli/configuration/git)
   config. Omit to target the current repository.
- **`hooks.<name>.commit.message`**
  For 
  `kind: git`
  : the commit message. Supports the standard hook template context. Empty selects the engine default (
  `Update artifacts for <component> in <stack>`
  ).
- **`hooks.<name>.commit.paths`**
  For 
  `kind: git`
  : repo-relative paths staged for the commit. Paths must resolve inside the repository worktree.
- **`hooks.<name>.push`**
  For 
  `kind: git`
  : push the created commit to the remote when 
  `true`
   (only when a commit was actually created).

## Examples

Five working examples live in the [Examples gallery](/examples):

- [`hooks-infracost`](/examples/hooks-infracost) — cost analysis with `kind: infracost`
- [`hooks-trivy`](/examples/hooks-trivy) — security scanning with `kind: trivy`
- [`hooks-checkov`](/examples/hooks-checkov) — policy scanning with `kind: checkov`
- [`hooks-kics`](/examples/hooks-kics) — IaC scanning with `kind: kics`
- [`hooks-custom-command`](/examples/hooks-custom-command) — custom Python script via `kind: command`

Each uses dummy AWS provider config so `tofu plan` succeeds offline.

## Related

- [`atmos terraform`](/cli/commands/terraform/usage) — `--skip-hooks` and other terraform flags
- [Global Flags](/cli/global-flags) — flags available on every command
- [Component Dependencies](/stacks/dependencies/components) — `dependencies.tools` syntax used for auto-install
- [Toolchain Configuration](/cli/configuration/toolchain) — registry overrides
- [External Stores](/stacks/sharing-state/stores) — reading data written by `kind: store`
- [Git Configuration](/cli/configuration/git) — managed repositories used by `kind: git`
- [`atmos git`](/cli/commands/git/usage) — imperative Git operations on the same shared service
- [Terraform State](/stacks/sharing-state/terraform-state) — alternative state-sharing pattern via `!terraform.state`
