Skip to main content

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​

HookEventsConfiguration
Component lifecycleSupported Terraform, Kubernetes, Helm, and Helmfile operations, such as before.terraform.plan or after.helm.applyhooks in stack manifests; define shared hooks globally or scope them to a component
Scaffold generationbefore.scaffold.generate and after.scaffold.generate when generating a project or componentspec.hooks in scaffold.yaml
Local GitGit events such as pre-commit, commit-msg, and pre-pushgit.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.

FieldPurpose
eventsEvents that trigger the hook; use explicit dotted names
kind: stepRun one step; type selects the step type and with supplies its settings
kind: stepsRun the ordered step list in with
whenSelect the parent operation's outcome or another condition
on_failureHandle a failed hook: fail, warn, or ignore
retryRetry 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:

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 for conditions and working-directory rules.

Git hooks​

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

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 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:

ContextScript inputs
Component lifecyclectx.component contains resolved configuration; ctx.hook identifies the hook and event; ctx.operation describes the parent operation
ScaffoldTemplate values such as .Answers and .TargetPath, passed through the step's env
GitGit'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:

examples/starlark-hooks/stacks/dev.yaml

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:

examples/starlark-hooks/stacks/unowned.yaml

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.

Allow a valid plan and stop an invalid one
 
00:00.0 / 00:00.0