
Terraform and OpenTofu handle infrastructure provisioning, but almost every real-world pipeline has steps that happen around a run: lint checks before a plan, security scans before an apply, CMDB updates after a deploy, Slack notifications when something fails. The question is where those steps live and how they stay consistent across workspaces.
The typical answer is a CI/CD wrapper, such as a GitHub Actions workflow or GitLab pipeline that calls terraform plan and terraform apply in sequence with your other steps. That works, but it means every team maintains their own pipeline, your platform team loses visibility into what's running, and when you want to enforce a standard (say, "every production apply must pass a cost check"), you have to update every repo.
Scalr's hooks system is the alternative. Hooks are scripts that run inside the run container at specific points in the Terraform lifecycle: before or after plan, and before or after apply. The Hooks Registry goes further, you define a hook once, store it in VCS, and apply it to an environment. Every workspace in that environment inherits it automatically.
A hook is any executable script or command. It runs inside the same container as terraform plan or terraform apply, which means it has access to the same environment variables, the Terraform working directory, and the Terraform binary itself.
Hooks can run at five points in the lifecycle:
terraform init downloads providers and modulesterraform planterraform plan completesterraform applyterraform apply completes (regardless of success or failure)If a hook exits with a non-zero code, the run fails at that point. This makes hooks useful as gates: a pre-plan hook that runs a linter can block a plan from proceeding if the code doesn't pass. A pre-apply cost check can block an apply if the projected cost increase exceeds a threshold.
Hooks can be configured at two levels.
Workspace-level hooks are defined directly in workspace settings. They're good for one-off customizations specific to a single workspace like importing a specific resource before plan, or running a workspace-specific notification. Anyone with workspace access can configure them.
The Hooks Registry is for hooks that need to run across many workspaces consistently. A hook in the registry is stored in your VCS provider (GitHub, GitLab, Bitbucket, etc.), registered at the account level, and then assigned to one or more environments. Once assigned to an environment, it runs in every workspace in that environment, no per-workspace configuration is needed.
Registry hooks run first, before any workspace-level hooks in the same phase. A workspace can have both a registry pre-plan hook and a workspace-specific pre-plan hook will both execute, registry hook first.
This inheritance model is why the registry is the right home for platform-wide standards. Your security scan or your linter, define them once, assign them to your production environment, and they run everywhere in that environment without any workspace owner needing to configure anything.
Hooks in the registry are always pulled from VCS at run time, which means they go through your normal code review process and have a full audit trail.
Step 1: Store your script in VCS
Add your hook script to a repository your Scalr account can access. It can be in a dedicated hooks repository or alongside your Terraform code. Make sure it's executable.
Example: a TFLint pre-plan hook at hooks/pre-plan-lint.sh:
#!/usr/bin/env bash
set -e
# Install TFLint to /tmp
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh \
| TFLINT_INSTALL_PATH=/tmp bash
# Run TFLint against the workspace directory
/tmp/tflint --chdir=$SCALR_WORKSPACE_DIRStep 2: Register the hook in Scalr
Go to Account → Registries → Hooks and create a new hook. You'll specify:
bash, python3, etc.)Scalr pulls a fresh copy of the script at the start of each run, so any updates to the script in VCS take effect on the next run automatically.
Step 3: Assign it to an environment
Go to the environment where you want the hook to run, navigate to Hooks, and enable the hook. Select which phases it should run in (pre-plan, post-plan, pre-apply, post-apply). The hook will now execute for every workspace in that environment.
Full setup reference: Hooks Registry documentation.
Hooks run inside the Scalr run environment and have access to built-in variables. These are useful for building logic into hooks, for example, only sending a notification when the operation is an apply, or passing the run ID to an external system.
Some useful variables:
| Variable | Value |
|---|---|
SCALR_TERRAFORM_OPERATION |
plan or apply |
SCALR_RUN_ID |
The current run ID |
SCALR_WORKSPACE_ID |
The workspace ID |
SCALR_ENVIRONMENT_ID |
The environment ID |
SCALR_HOSTNAME |
Your Scalr account hostname |
SCALR_TOKEN |
An ephemeral token for the Scalr API |
SCALR_HOOK_DIR |
Path to the directory where hook scripts are checked out |
$SCALR_HOOK_DIR is particularly useful when your hook script depends on other files in the same repository. Reference them as $SCALR_HOOK_DIR/helper.py and Scalr will resolve the path correctly within the run environment.
The pre-init phase runs before terraform init downloads providers and modules, making it the right place to install anything the init process itself needs, like a custom credential helper, a private CA certificate, or a tool your providers require at startup.
#!/usr/bin/env bash
set -e
# Install a custom CA cert so terraform init can reach a private provider registry
cp $SCALR_HOOK_DIR/internal-ca.crt /usr/local/share/ca-certificates/
update-ca-certificates --quietOr fetch dynamic credentials from Vault before init touches the provider configuration:
#!/usr/bin/env bash
set -e
# Fetch short-lived credentials from Vault and export them for use by providers
VAULT_TOKEN=$(cat /run/secrets/vault_token)
CREDS=$(vault read -format=json aws/creds/deploy-role)
export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.data.access_key')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.data.secret_key')Rather than hardcoding values that change, such as allowed CIDR ranges, approved AMI IDs, environment-specific config, fetch them from an internal API before the plan runs. The plan then uses current values without needing a variable update.
#!/usr/bin/env bash
set -e
# Fetch the current approved AMI list from internal config service
APPROVED_AMIS=$(curl -s -H "Authorization: Bearer $CONFIG_TOKEN" \
https://config.internal/api/approved-amis | jq -r '.amis | join(",")')
# Write to a tfvars file that the workspace picks up automatically
echo "approved_ami_ids = [\"${APPROVED_AMIS//,/\",\"}\"]" > /opt/data/dynamic.auto.tfvarsImport an existing resource into state before the plan runs. This avoids the manual state manipulation workflow of downloading state, running terraform import locally, and pushing it back.
terraform import aws_instance.web i-0abc123def456789Runs directly inside the run container so no local Terraform state management needed.
For teams running IaC security scanners beyond Scalr's native Checkov integrations like tfsec, Trivy, or Semgrep a pre-plan hook is the enforcement point. It runs in the remote environment regardless of what the developer does locally.
#!/usr/bin/env bash
set -e
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /tmp
# Scan IaC config, fail on HIGH or CRITICAL findings
/tmp/trivy config . --exit-code 1 --severity HIGH,CRITICAL --quietSome compliance frameworks require a tamper-evident record of every plan that preceded an apply. A post-plan hook can export the plan as JSON and push it to an S3 bucket or GCS object with a timestamp, before any apply decision is made.
#!/usr/bin/env bash
set -e
# Export plan to JSON
terraform show -json /opt/data/terraform.tfplan.bin > /tmp/plan-${SCALR_RUN_ID}.json
# Archive to compliance bucket
aws s3 cp /tmp/plan-${SCALR_RUN_ID}.json \
s3://compliance-archives/terraform-plans/${SCALR_ENVIRONMENT_ID}/${SCALR_RUN_ID}.jsonAfter a successful apply, call an external API to register the new resources. SCALR_TOKEN and SCALR_RUN_ID are available, so you can pull workspace outputs from the Scalr API and pass them to the external system.
#!/usr/bin/env bash
set -e
# Get workspace outputs via Terraform
OUTPUTS=$(terraform output -json)
# Post to internal CMDB
curl -s -X POST https://cmdb.internal/api/assets \
-H "Authorization: Bearer $CMDB_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"run_id\": \"$SCALR_RUN_ID\", \"outputs\": $OUTPUTS}"For teams with ITSM requirements, auto-create a change record after every production apply so there's a ticketed audit trail without requiring engineers to file tickets manually.
#!/usr/bin/env bash
# Only open tickets on applies, not plans
if [ "$SCALR_TERRAFORM_OPERATION" != "apply" ]; then
exit 0
fi
curl -s -X POST "https://$SNOW_INSTANCE.service-now.com/api/now/table/change_request" \
-H "Authorization: Basic $SNOW_CREDENTIALS" \
-H "Content-Type: application/json" \
-d "{
\"short_description\": \"Terraform apply: workspace $SCALR_WORKSPACE_ID\",
\"description\": \"Run ID: $SCALR_RUN_ID. Environment: $SCALR_ENVIRONMENT_ID.\",
\"category\": \"infrastructure\"
}"If your hook script depends on other files in the same repository (helper libraries, config files, Python modules), use $SCALR_HOOK_DIR to reference them. Scalr checks out the full repository into this directory at run time, so all files are available.
#!/usr/bin/env bash
set -e
pip3 install requests click --quiet
python3 $SCALR_HOOK_DIR/validate_tags.py --run-id $SCALR_RUN_IDLike everything else in Scalr, the Hooks Registry can be managed through the Scalr Terraform provider. This lets you version-control your hook definitions and apply them consistently across environments using Terraform itself.
resource "scalr_hook" "pre_plan_lint" {
name = "pre-plan-tflint"
interpreter = "/bin/bash"
scriptfile_path = "hooks/pre-plan-lint.sh"
vcs_provider_id = scalr_vcs_provider.github.id
vcs_repo {
identifier = "my-org/scalr-hooks"
branch = "main"
}
}
resource "scalr_environment_hook" "production_lint" {
hook_id = scalr_hook.pre_plan_lint.id
environment_id = scalr_environment.production.id
events = ["pre-plan"]
}This is the recommended approach for platform teams: define hooks in a central repository, manage their assignment to environments with Terraform, and review changes through pull requests.
HCP Terraform uses Run Tasks for similar purposes, which is calling external services at defined points in a run. Run Tasks integrate with third-party services via webhooks and are configured at the workspace or organization level.
The key difference is execution context. Run Tasks are external webhook calls, which means your script or service runs outside the run container, receives a payload from HCP Terraform, and responds with a pass/fail. Scalr hooks run inside the run container, which means they have direct access to the Terraform working directory, the plan file, Terraform outputs, and the run environment. You don't need to expose an external endpoint or build a webhook receiver, any script that runs on Linux runs as a hook.
HCP Terraform agents also support hooks, but those are stored on the agent machine itself (~/.tfc-agent/hooks), not in VCS, and apply per-agent rather than per-environment. Managing them consistently across a fleet of agents requires your own deployment tooling.
chmod +x).The Hooks Registry is available on all Scalr plans. To get started:
Full documentation: Hooks Registry · Custom Hooks (workspace-level) · Scalr provider: scalr_hook
