Skip to content

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

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

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.
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 "Additional output". So the comment always follows the order the hooks ran. The "Additional output" 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, labelled 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:

steps:
  pre_init:
    - terraform fmt -check -recursive || echo ":warning: run terraform fmt" >> "$TFPR_OUT"
  post_plan:
    - bash "$TFPR_ENGINE_DIR/scripts/conftest-gate.sh"