Hook and step model¶
Hooks let you run shell commands at fixed points around init, plan, and apply. The three gates are implemented as hooks, and you can add your own (format, validate, a source scan, a notification). Hook variables use the engine's word project; in Terragrunt mode a project is exactly a unit.
The five seams¶
| Seam | Runs |
|---|---|
pre_init |
before terraform init |
post_init |
after init, before plan |
post_plan |
after plan, with the plan available |
pre_apply |
after plan, before apply |
post_apply |
after a successful apply |
Two stages¶
Since engine v6.0.0 the plan run and the apply run each have their own hook
list, nested under plan: and apply: in the steps: block:
steps:
plan: # also the drift sweep
post_plan:
- bash "$TFPR_ENGINE_DIR/scripts/conftest-gate.sh"
- CHECKOV_SOFT_FAIL=1 bash "$TFPR_ENGINE_DIR/scripts/checkov-gate.sh"
- bash "$TFPR_ENGINE_DIR/scripts/infracost-gate.sh"
apply: # default: no hooks
pre_apply: []
post_apply: []
Firing order:
- plan and drift:
pre_init, init,post_init, plan,post_plan - apply:
pre_init, init,post_init, plan,post_plan,pre_apply, apply,post_apply
Apply re-plans before it applies, and the reviewed-plan guard refuses an apply
whose plan differs from the one reviewed, so the apply stage runs no hooks
unless its own list names them: the plan run already gated the same plan. The
apply report then carries a one-line note, "Plan-stage gates not re-run: the
plan matches the one reviewed and gated at <commit>". /apply --force
skips the guard and still does not fall back to the plan-stage hooks; force
means the operator has read the fresh plan. A repository that wants a gate at
apply time lists it under apply:.
The flat form, hook slots at the top level with no plan: or apply: key,
keeps working and means the plan stage only. A flat pre_apply or
post_apply is refused by discovery with the fix named, because an apply-time
hook that never runs would read as a gate that passed; a misspelt slot is
refused for the same reason.
Init, plan, the reviewed-plan guard, the unit lock and the ordered apply are
not steps and cannot be replaced from projects.yml. Hooks are the
customization surface; the guards are the product.
Each command runs in the unit directory under bash -eo pipefail. A non-zero
command sets the unit's status to error, which blocks the merge gate and stops the
rest of the hooks. End a command with || true to make it advisory.
Environment exposed to hooks¶
| Variable | Meaning |
|---|---|
TFPR_PLANFILE |
The binary plan file. Available from post_plan on. |
TFPR_PLANJSON |
The plan rendered as JSON (terraform show -json). Available from post_plan on. |
TFPR_OUT |
Append markdown here to surface it in the PR comment or drift issue. |
TFPR_ENGINE_DIR |
The engine checkout root. Call bundled gates as bash "$TFPR_ENGINE_DIR/scripts/<gate>.sh". Defaults to the workspace in vendored mode. |
TFPR_BIN |
Path to the compiled tfpr binary, for hooks that invoke engine subcommands. |
PROJECT_NAME, PROJECT_DIR, PROJECT_LABEL |
The unit's name, directory, and full-path label. |
TFPR_OUT is staged around the plan: output written in pre_init or post_init
renders above the plan as "Pre-plan checks", and output written from post_plan on
renders below the plan as "Validation checks". So the comment always follows the
order the hooks ran. The "Validation checks" panel is status-aware: it collapses
when clean and auto-expands when your hook output contains a :x: or :warning:
marker, or when the run fails. Emit those markers to have a finding surface
automatically.
The gate hooks¶
All three ship in scripts/ and are meant to run as post_plan hooks.
conftest-gate.sh¶
Tests TFPR_PLANJSON against a policy directory and appends a policy summary to
TFPR_OUT. Gating: exits non-zero on any deny.
| Variable | Default | Purpose |
|---|---|---|
CONFTEST_POLICY_DIR |
<repo>/policy |
Directory of .rego policies. |
CONFTEST_NAMESPACE |
main |
Rego namespace to test. |
CONFTEST_FAIL_ON_WARN |
unset | Set to 1 to also fail on warn. |
Passes with a note if no policies exist yet, so you can enable the hook before you write any.
checkov-gate.sh¶
Scans TFPR_PLANJSON (plan scanning, which also sidesteps Terragrunt) and appends
the failed checks to TFPR_OUT. Gating: exits with checkov's status unless
soft-fail is set.
| Variable | Default | Purpose |
|---|---|---|
CHECKOV_SOFT_FAIL |
unset | Set to 1 to report without ever gating. |
CHECKOV_HARD_FAIL_ON |
unset | Check IDs that must gate. |
CHECKOV_REPO_ROOT |
$PWD |
Enables inline checkov:skip comment support. |
CHECKOV_ARGS |
unset | Extra CLI arguments. |
infracost-gate.sh¶
Runs infracost breakdown against TFPR_PLANJSON and appends a monthly-cost table
to TFPR_OUT, labeled with the unit path. Report-only: always exits zero.
| Variable | Default | Purpose |
|---|---|---|
INFRACOST_API_KEY |
unset | Hosted pricing API key. Without it the hook notes it and skips. |
INFRACOST_BREAKDOWN_ARGS |
unset | Extra arguments for infracost breakdown. |
Writing your own hook¶
A hook is any shell command. For example, run terraform fmt -check before init and
surface the result in the comment: