# Lifecycle Hooks

A hook runs an action when an event occurs. Use hooks to validate inputs before
an operation, check its result, or run follow-up tasks automatically.

Hooks can run commands, scripts, or sequences of steps. Starlark scripts use the
same built-in functions as scripts in workflows and custom commands.

## Where hooks run

| Hook | Events | Configuration |
| --- | --- | --- |
| Component lifecycle | Supported Terraform, Kubernetes, Helm, and Helmfile operations, such as `before.terraform.plan` or `after.helm.apply` | `hooks` in stack manifests; define shared hooks globally or scope them to a component |
| Scaffold generation | `before.scaffold.generate` and `after.scaffold.generate` when generating a project or component | `spec.hooks` in `scaffold.yaml` |
| Local Git | Git events such as `pre-commit`, `commit-msg`, and `pre-push` | `git.hooks` in `atmos.yaml`, installed with `atmos git hooks install` |

Component and scaffold hooks use Atmos lifecycle events. Local Git hooks are
triggered by Git and have their own configuration syntax.

[Native CI](/ci) also uses aggregate events, such as
`after.terraform.plan.aggregate`, to report results for a group of components.
Those events use CI provider bindings rather than per-component stack hooks.

## Lifecycle hook syntax

A lifecycle hook has a name, an event list, and a `kind`. To run one script, use
`kind: step`, set `type: script`, and put the script settings under `with`:

```yaml
hooks:
  report:
    events: [after.terraform.apply]
    kind: step
    type: script
    on_failure: fail
    with:
      interpreter: starlark
      script: |
        ui.success("Apply finished")
```

This is a stack-manifest fragment. Put `hooks` at the top level to share it
across components, or under a component to limit its scope. Hooks inherit and
merge with the rest of the stack configuration.

| Field | Purpose |
| --- | --- |
| `events` | Events that trigger the hook; use explicit dotted names |
| `kind: step` | Run one step; `type` selects the step type and `with` supplies its settings |
| `kind: steps` | Run the ordered step list in `with` |
| `when` | Select the parent operation's outcome or another condition |
| `on_failure` | Handle a failed hook: `fail`, `warn`, or `ignore` |
| `retry` | Retry the hook; for `kind: steps`, rerun the entire list |

To run several steps:

```yaml
hooks:
  verify:
    events: [after.terraform.apply]
    kind: steps
    on_failure: fail
    with:
      - type: script
        interpreter: starlark
        script: ui.info("Checking deployment")
      - type: shell
        command: ./smoke-test
```

Steps run in order and stop at the first failure. Retry only work that is safe
to repeat. See the [hooks reference](/stacks/hooks) for other kinds, supported
events, inheritance, and execution settings.

### Scaffold hooks

Put the same hook definition under `spec.hooks` in `scaffold.yaml`. Scaffold
hooks support `kind: step` and `kind: steps`:

```yaml title="scaffold.yaml (fragment)"
spec:
  hooks:
    report:
      events: [after.scaffold.generate]
      kind: step
      type: script
      with:
        interpreter: starlark
        env:
          TARGET: '{{ .TargetPath }}'
        script: ui.success("Generated " + env["TARGET"])
```

Script steps run in the generated target directory by default. Pass scaffold
answers through `env` using `{{ .Answers.<field> }}`. See
[generation hooks](/cli/commands/scaffold/usage#generation-hooks) for conditions
and working-directory rules.

### Git hooks

Git hook definitions use a `steps` list directly, without `kind`, `events`, or
`with`:

```yaml title="atmos.yaml (fragment)"
git:
  hooks:
    pre-commit:
      steps:
        - type: script
          interpreter: starlark
          script: |
            if not fs.exists("README.md"):
                fail("Add a README.md before committing")
```

Install the hooks with `atmos git hooks install`. Test one manually with
`atmos git hooks run pre-commit`. Steps run from the repository root and stop
at the first failure. This check reads the working tree, not just staged files.
See [Git hooks](/cli/configuration/git#inline-script-steps) for arguments, stdin,
and the supported step settings.

## Choose events and failure behavior

Use before-events for checks that must pass before an operation starts.
After-hooks run on success by default; set `when: failure` for failure handling
or `when: always` for either outcome.

`when` tests the parent operation's outcome. `on_failure` controls what happens
if the hook itself fails. Set `on_failure: fail` for a required check: a failed
before-hook stops the operation, while a failed after-hook reports failure after
the operation has already run.

## Work with lifecycle context

Inputs depend on the kind of hook:

| Context | Script inputs |
| --- | --- |
| Component lifecycle | `ctx.component` contains resolved configuration; `ctx.hook` identifies the hook and event; `ctx.operation` describes the parent operation |
| Scaffold | Template values such as `.Answers` and `.TargetPath`, passed through the step's `env` |
| Git | Git's positional arguments in `ctx.args`; see the [Git input contract](/cli/configuration/git) for stdin |

Scaffold and Git hooks do not supply `ctx.component`, `ctx.hook`, or
`ctx.operation`. Avoid reading component fields in a script shared by all hook
types without first checking whether `ctx.component` is set. Lifecycle contexts
without an individual component binding also have `ctx.component == None`.

Before a component operation, status, exit code, and error are `None`.
After-hooks expose the supplied outcome. Ordinary lifecycle hooks do not capture
the parent command's stdout or stderr, and component context does not include
Terraform outputs. See the [context reference](/steps/type/script#lifecycle-hook-context).

## Keep data commands clean

A hook script's `print()` writes to stdout, like `print()` anywhere else. When a hook is attached to a command that
produces data, such as `terraform output -json`, anything the hook prints lands in the data a pipe or `jq` reads. Use
`ui.info`, `ui.success`, and `ui.warning` for human messages in such hooks. They write to stderr and leave stdout clean:

```python
# Safe for: atmos terraform output vpc -s dev --format=json | jq .
ui.success("Owner check passed")
```

## Nested Atmos calls and recursion

Scripts in hooks can call Atmos itself with the [`atmos.*` functions](/functions/automation/atmos.run). The `--profile`
and `--identity` selections you gave the parent command reach those nested calls, just as they do in standalone scripts
and `type: atmos` steps, so a hook that runs `atmos.terraform(...)` uses the same profile and identity.

A hook that runs the Atmos command it is attached to would start itself again. For component lifecycle hooks, Atmos counts nesting in the
`ATMOS_HOOK_DEPTH` environment variable: each hook runs with a value one higher than the command that triggered it, and
depth 8 is allowed. Attempting to start a ninth nested hook fails with `hook recursion limit exceeded`, naming the hook
and the event. Scope the hook to
the event that needs it, or check `ATMOS_HOOK_DEPTH` to guard the command it runs.

## Interrupting a hook

Ctrl-C cancels the running script, runs its [`defer`](/functions/automation/defer) calls with a 30 second grace period,
and exits with status 130 without an error box. A second Ctrl-C exits immediately.

## Example: require a component owner

The [owner-check project](/examples/starlark-hooks) stops Terraform plans for
components without an owner. It requires Atmos and Terraform on your `PATH`.
Its Terraform module has no providers or resources, so no cloud credentials
are needed.

### Add a check before planning

This stack defines an `api` component and a hook requiring it to have an owner:

The hook selects `before.terraform.plan` and uses `kind: step` with `type: script`
to run one script step. Its `with` block configures that step, including
`interpreter: starlark` to run Starlark.

`on_failure: fail` makes a failed check stop the operation. The script reads
`ctx.component.vars`, calls `fail()` if the owner is absent, and otherwise prints
a success message with `ui.success()`.

The component is named `api` in the stack; its implementation directory is
`service`. `ctx.component.name` retains the component name used by the caller,
so messages identify `api`.

### Run the passing case

Run from the example's directory:

```shell
atmos terraform plan api -s dev
```

The hook prints:

```text
Owner check passed: api belongs to platform (dev).
```

Terraform then reports no changes.

### Stop an invalid operation

The `unowned` stack imports the same configuration and clears the owner:

Run the same operation against that stack:

```shell
atmos terraform plan api -s unowned
```

The command fails with `Set an owner before planning api`. Because the check runs
before the plan and has a failing policy, the Terraform plan does not start.
The inherited hook protects both stacks using the same logic.
