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 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:
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:
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 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:
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 for conditions
and working-directory rules.
Git hooks
Git hook definitions use a steps list directly, without kind, events, or
with:
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 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 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.
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:
# 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. 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 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 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:
atmos terraform plan api -s dev
The hook prints:
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:
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.