Gruntwork Release Notes
171 release notes curated from 2 sources by the Releasebot Team. Last updated: Sep 24, 2026
Gruntwork Products
- Sep 24, 2026
- Date parsed from source:Sep 24, 2026
- First seen by Releasebot:Sep 24, 2026
v1.2.0-rc1
Terragrunt ships v1.2.0 release candidate with major new capabilities, including OCI module sources, non-interactive catalog output, runtime profiling, Azure state backend improvements, direct dependency reads from state, expansion blocks, and an MCP server for AI agents.
v1.2.0 Release Candidate
This is the first release candidate for Terragrunt v1.2.
This release completes the following experiments:
- block-iteration
- oci
- bounded-discovery
- catalog-format
- mutable-generate
- optional-dependency-outputs
- optional-hooks
- azure-backend
- version-attribute
- profiling
- dependency-fetch-output-from-state
Future release candidates for v1.2.0 will include bug fixes related to these experiments or other urgent bug fixes as necessary, and documentation improvements.
Please try out this release candidate in lower environments and share your feedback in the Associated GitHub discussion.
Breaking Changes
base64gzip() uses the Go 1.27 encoder
Go 1.27 changed the compressed output of its gzip encoder. Terragrunt v1.1.5 kept base64gzip() on the older output and warned once per run that this was legacy behavior. base64gzip() now returns what the Go 1.27 encoder produces.
A resource that compares the encoded value can plan a replacement on the first run after upgrading. An aws_instance with user_data_base64 set from base64gzip() and user_data_replace_on_change set to true is one such resource.
Where the encoded value has to stay stable, call the experimental base64gzip_compat(), which returns the v1.1.3 output permanently. It is behind the base64gzip-compat experiment and may be renamed or removed:
terragrunt run --all --experiment base64gzip-compat -- plan inputs = { user_data_base64 = base64gzip_compat(file("${get_terragrunt_dir()}/user-data.sh")) }This completes the legacy-base64gzip strict control.
Note
Within the 1.0 guarantees
The 1.0 guarantees make promises about how Terragrunt remains backwards compatible. This change does not break those promises. It is listed under breaking changes so you are aware of it, in case it affects your workflows.
base64gzip() still takes a string and returns valid gzipped base64 content that decompresses to the same value, but the encoded value itself changes. base64gzip_compat() keeps the old one.
S3 state buckets no longer get the RootAccess bucket policy statement
When bootstrapping an S3 state bucket, Terragrunt attached a bucket policy statement with the Sid RootAccess that granted s3:* on the bucket and its objects to arn:aws:iam::<account-id>:root, an ARN that grants access to the AWS account as a whole rather than only to its root user. That statement has been removed. It widened reach to state files that routinely hold secrets, and the account already owns the bucket.
The skip_bucket_root_access config no longer has anything to skip, and is now deprecated. Terragrunt still accepts it, and warns about it when bootstrapping a backend whose config sets it. Enable the skip-bucket-root-access strict control to turn that warning into an error.
To grant the AWS account root user access to the state bucket, set the new enable_bucket_root_access config:
# root.hcl remote_state { # ... other args omitted for brevity ... config = { # ... other config omitted for brevity ... enable_bucket_root_access = true } }Buckets that already have the statement keep it. The state backend docs cover how to remove it yourself.
Note
Within the 1.0 guarantees
The 1.0 guarantees make promises about how Terragrunt remains backwards compatible. This change does not break those promises. It is listed under breaking changes so you are aware of it, in case it affects your workflows.
The bucket policy Terragrunt writes is not part of the CLI, HCL, or output schemas the guarantees pin, and skip_bucket_root_access remains valid configuration. Removing the statement is a bug fix, and enable_bucket_root_access restores it. See Bugs in the guarantees for how a bug fix in 1.x can change your workflows.
New Features
Non-interactive terragrunt catalog output with --format
The catalog TUI needs a terminal. The --format flag (env: TG_FORMAT) writes what the catalog discovers to standard output, so a script or an agent can read the catalog without one.
--format=jsonl writes one JSON object per catalog entry, following a published JSON schema:
terragrunt catalog --format=jsonl | jq -c '{kind, title, component_source}'--format=md writes a Markdown document with a section per entry:
terragrunt catalog --format=md > catalog.mdWithout --format, terragrunt catalog opens the TUI only when standard input and standard output are both terminals. Anywhere else it writes jsonl, so piping the command needs no flag:
terragrunt catalog | jq -c '{kind, title, component_source}'Terragrunt writes each entry as it discovers it. See Non-interactive catalog for the structure of each format and how streaming behaves.
Previously gated behind the catalog-format experiment, non-interactive catalog output no longer requires --experiment catalog-format.
Collect runtime profiles
Terragrunt writes CPU, heap, and goroutine profiles on request, so you can see where a slow run spends its time. Pass --profile-cpu, --profile-mem or --profile-goroutine with a path, or --profile-dir to collect all three into one directory under conventional names. Each flag has a matching TG_PROFILE_* environment variable.
terragrunt --profile-dir /tmp/profiles run --all -- planRead the result with go tool pprof. The profiles cover Terragrunt itself, not the OpenTofu/Terraform processes it runs.
Previously gated behind the profiling experiment, the profile flags no longer require --experiment profiling.
Bound discovery with --discovery-boundary and (dir) filters
Graph filters search up to the Git repository root for dependents and follow dependencies wherever they point, so in a monorepo they can parse sibling environments a command never needed.
A (dir) operand in a graph filter stops traversal at that directory:
cd environments/staging terragrunt find --filter '(.)...vpc'From the same directory, the --discovery-boundary flag (env: TG_DISCOVERY_BOUNDARY) applies one boundary to every --filter expression on the command:
terragrunt run --all --filter '...vpc' --discovery-boundary . -- planPreviously gated behind the bounded-discovery experiment, bounded discovery no longer requires --experiment bounded-discovery.
Terragrunt docs MCP server
Terragrunt now publishes the read-only Terragrunt docs MCP server, which answers Terragrunt questions from the official docs, the CLI reference, a curated design-pattern library, and real example config. The server is public and unauthenticated. Results are pinned to a Terragrunt version, and docs pages can be read at any release tag from v0.80 onward.
For Claude Code:
claude mcp add -s user --transport http terragrunt-docs https://mcp.docs.terragrunt.com/mcpCursor and other MCP clients that read an mcp.json point at https://mcp.docs.terragrunt.com/mcp instead.
The server is in public beta. It has no availability guarantee and may change significantly.
See the install docs for the full setup.
Download modules from OCI registries
An oci:// source downloads a module from an OCI Distribution registry, such as Amazon ECR, GitHub Container Registry, Azure Container Registry, Google Artifact Registry, or a self-hosted one. It works in a terraform block:
# terragrunt.hcl terraform { source = "oci://ghcr.io/acme/tofu-modules/vpc?tag=1.0.0" }And in the unit and stack blocks of a terragrunt.stack.hcl:
# terragrunt.stack.hcl unit "vpc" { source = "oci://ghcr.io/acme/terragrunt-units/vpc?tag=1.0.0" path = "vpc" }Pin the artifact with tag or digest. Setting neither selects the latest tag, and a //subdir selector reaches a directory inside the module, unit, or stack. Credentials come from OpenTofu's CLI config, from ambient Docker config, and from credential helpers such as ecr-login, so one source string resolves the same way under both tofu and Terragrunt.
Previously gated behind the oci experiment, these sources no longer require --experiment oci. See OCI registries for the publishing contract and the full authentication order.
Track the files that OpenTofu file functions read
Terragrunt records the files that the built-in file functions read, so reading-based filters select the units that read them without a mark_as_read call:
- file
- templatefile
- fileset
- fileexists
- the file* hash functions, such as filesha256
A file read from inside a template counts too.
mark_as_read remains the way to record a file that only OpenTofu/Terraform reads, such as one passed to a module as an input, or one a run_cmd script reads.
Generated files stored in the CAS
The Content Addressable Store (CAS) now stores the files that generate blocks produce. Each unit's working directory gets a hard link to the stored copy, so every unit that includes this block shares one provider.tf on disk:
# root.hcl generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = "provider \"aws\" {}" }Generated files are read-only by default, so an existing hook or script that edits a generated file in place fails with a permission error. Set mutable = true on that generate block to give each unit a writable file of its own:
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" mutable = true contents = "provider \"aws\" {}" }Passing --no-cas turns off the CAS for a run, and Terragrunt writes generated files as plain files:
terragrunt run --all --no-cas -- planSee Generate blocks and Immutable by default for details.
Previously gated behind the mutable-generate experiment, CAS storage for generated files no longer requires --experiment mutable-generate.
Iterate unit, stack, and dependency blocks with expansion
An expansion block declares a count or a for_each, and Terragrunt reads the block it sits in once per element. This unit block generates two units, at .terragrunt-stack/aurora/web and .terragrunt-stack/aurora/api:
# terragrunt.stack.hcl unit "aurora" { expansion { for_each = toset(["web", "api"]) } source = "../units/app" path = "aurora/${each.key}" values = { role = each.key } }A stack block expands the same way, generating one stack per element.
An expanded dependency block produces one dependency per element, and inputs reads each one by its key:
# terragrunt.hcl dependency "aurora" { expansion { for_each = toset(["web", "api"]) } config_path = "../aurora-${each.key}" } inputs = { web_id = dependency.aurora["web"].outputs.id }unit and stack blocks also accept an enabled attribute. Setting it to false skips the component during stack generation:
# terragrunt.stack.hcl unit "canary" { enabled = false source = "../units/app" path = "canary" }Adding an expansion block to an existing block, or shrinking one, changes the addresses of the components it produces. Read the expansion reference before changing one that has already been applied.
Previously gated behind the block-iteration experiment, expansion blocks and the enabled attribute no longer require --experiment block-iteration.
Bootstrap, delete, and migrate Azure Storage state backends
Terragrunt provisions the resource group, storage account, and blob container backing an azurerm state, and converges blob versioning and soft delete on both new and pre-existing accounts. It also deletes state blobs and containers, and migrates state within a storage account. Dependency outputs of Azure-backed units are read straight from the state blob, the same as S3 and GCS.
If you already pass --backend-bootstrap, Terragrunt now creates Azure resources it skipped before.
Previously gated behind the azure-backend experiment, these operations no longer require --experiment azure-backend. See State Backend for configuration keys and authentication.
Set the minimum TLS version on a bootstrapped Azure storage account
The ARM API treats an unset minimum TLS version as TLS1_0, but Azure deprecated TLS1_0 and TLS1_1 in August 2025.
Terragrunt now provisions a new storage account with a minimum TLS version of TLS1_2. The minimum_tls_version option raises it to TLS1_3:
remote_state { backend = "azurerm" config = { storage_account_name = "myterragruntstate" container_name = "tfstate" key = "${path_relative_to_include()}/tofu.tfstate" resource_group_name = "tofu-rg" use_azuread_auth = true minimum_tls_version = "TLS1_3" } }TLS1_2 and TLS1_3 are the only accepted values. Terragrunt rejects the deprecated TLS1_0 and TLS1_1.
The setting applies only when Terragrunt creates the account. Terragrunt leaves an existing account's setting alone, so change it there with the Azure portal or CLI.
Call OpenTofu 1.13 built-in functions in Terragrunt configurations
Terragrunt evaluates the built-in functions in configurations using its own copy of the OpenTofu implementations, which tracks OpenTofu 1.13.
These functions are now available:
- assumeequal
- assumelistlength
- assumelistlengthmax
- assumelistlengthmin
- assumemaplength
- assumemaplengthmax
- assumemaplengthmin
- assumenotnull
- assumesetlength
- assumesetlengthmax
- assumesetlengthmin
- assumestringprefix
- base64gunzip
- cidrcontains
- ephemeralasnull
- issensitive
- templatestring
- urldecode
Read dependency outputs from state by default
Terragrunt reads dependency outputs straight from the remote state object, without initializing each dependency to run tofu output or terraform output against it. This covers the S3, GCS, and Azure Storage (azurerm) backends.
When a direct read is unsupported, such as for a backend Terragrunt has no reader for, or fails on a permissions or network error, Terragrunt falls back to tofu/terraform output -json. The outputs are the same either way, so only the speedup is lost.
Pass --no-dependency-fetch-output-from-state (env: TG_NO_DEPENDENCY_FETCH_OUTPUT_FROM_STATE) to always load dependency outputs through tofu/terraform output -json.
Previously gated behind the dependency-fetch-output-from-state experiment, direct state reads no longer require --experiment dependency-fetch-output-from-state. Passing --dependency-fetch-output-from-state still works, and the dependency-fetch-output-from-state strict control turns its deprecation warning into an error.
Version constraints for registry modules
The terraform block accepts a version attribute holding a version constraint for a tfr:// registry module. Terragrunt downloads the highest published version that satisfies the constraint, using the same syntax as the version argument on OpenTofu and Terraform module blocks:
terraform { source = "tfr://registry.opentofu.org/terraform-aws-modules/vpc/aws" version = "~> 3.3" }See the terraform block reference for the full rules.
Previously gated behind the version-attribute experiment, version constraints for registry modules no longer require --experiment version-attribute.
S3 buckets can be created in your account regional namespace
An account regional namespace is a reserved subdivision of the S3 bucket namespace that only your account can create buckets in, so no one else can take or re-create those names. Bucket names in it end with your account ID, the region, and -an.
When a bucket name matches that convention, Terragrunt creates the bucket in the account regional namespace:
# root.hcl remote_state { backend = "s3" config = { bucket = "my-tofu-state-111122223333-us-east-1-an" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" } }There is no setting to enable this. S3 accepts the -an suffix only for account regional buckets, so the name alone decides. accesslogging_bucket_name is read the same way. Buckets named any other way are created in the global namespace, and the namespace is left out of the request entirely, so S3-compatible object stores are unaffected.
A name that fits the convention but names a region other than the bucket's own fails immediately.
Skip dependency outputs with --no-dependency-outputs
The --no-dependency-outputs flag (env: TG_NO_DEPENDENCY_OUTPUTS) skips output resolution for every dependency block in a run, so Terragrunt does not call tofu output on dependencies that may not be applied yet:
terragrunt run --all --no-dependency-outputs -- validateWarning
Use this flag with commands that do not read dependency outputs, such as init and validate. While it is set, references to dependency outputs get no real value, so plan and apply can pass empty values to OpenTofu/Terraform in their place.
Previously gated behind the optional-dependency-outputs experiment, the flag no longer requires --experiment optional-dependency-outputs.
Skip hooks for a run with --no-hooks
The --no-hooks flag (env: TG_NO_HOOKS) skips every hook for a run: before_hook, after_hook, and error_hook blocks.
terragrunt run --no-hooks -- planPreviously gated behind the optional-hooks experiment, --no-hooks no longer requires --experiment optional-hooks.
Bug Fixes
S3 access log delivery is granted with a bucket policy
S3 disables ACLs on new buckets by default, and a bucket with ACLs disabled rejects the ACL grant Terragrunt wrote to make an access logging bucket accept logs. AWS recommends a bucket policy over an ACL for this grant, and recommends keeping ACLs disabled in general.
Terragrunt now creates buckets with ACLs disabled and grants access log delivery through the logging bucket's policy instead, allowing s3:PutObject for the logging.s3.amazonaws.com service principal on behalf of buckets in the same AWS account:
remote_state { backend = "s3" config = { bucket = "my-state-bucket" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" accesslogging_bucket_name = "my-logs-bucket" } }This only applies to a logging bucket Terragrunt creates. One that already exists keeps the permissions it has, whether that is the ACL grant from an earlier Terragrunt version or something you set up yourself, and Terragrunt neither reads nor writes its policy.
skip_accesslogging_bucket_policy opts out of that grant. skip_accesslogging_bucket_acl is deprecated and now has no effect: Terragrunt puts no ACL on the logging bucket, so there is nothing left for it to skip.
If you set skip_accesslogging_bucket_acl to work around an AccessControlListNotSupported failure on a bucket with ACLs disabled, drop it. The bucket policy covers that bucket, and the attribute now suppresses nothing. Set skip_accesslogging_bucket_policy only if you grant log delivery yourself. Terragrunt warns when the deprecated attribute is used, and the skip-accesslogging-bucket-acl strict control turns that warning into an error.
expansion works with autoinclude and stack dependencies
terragrunt stack generate failed with There is no variable named "each" when a unit or stack block declared both an expansion block and an autoinclude block. The error pointed at each.key in path, even when autoinclude never referenced each.
Generation now writes an autoinclude file for each element, and each.key, each.value, and count.index inside autoinclude resolve to that element:
# terragrunt.stack.hcl unit "repo" { source = "../units/repo" path = "repo" } unit "environment" { expansion { for_each = toset(["dev", "prod"]) } source = "../units/environment" path = "environment/${each.key}" autoinclude { dependency "repo" { config_path = unit.repo.path } inputs = { environment = each.key repository = dependency.repo.outputs.name } } }The prod element gets this terragrunt.autoinclude.hcl:
dependency "repo" { config_path = "../../repo" } inputs = { environment = "prod" repository = dependency.repo.outputs.name }A dependency block inside autoinclude that declared its own expansion block failed generation with the same error. A unit whose terragrunt.autoinclude.hcl contained one also failed to parse. An expanded unit could not be referenced from the stack file at all, since unit.<name>.path skipped it.
Each element of an expanded unit or stack is now referenced as unit.<name>[key].path. The generated dependency block keeps its expansion block. Generation evaluates for_each or count in the stack file, where local.* and values.* are available, writes the result as a literal, and resolves config_path for each element. The generated unit expands the dependency when it is parsed:
# terragrunt.stack.hcl locals { regions = toset(["us-east-1", "us-west-1"]) } unit "vpc" { expansion { for_each = local.regions } source = "../units/vpc" path = "vpc/${each.key}" values = { region = each.key } } unit "app" { source = "../units/app" path = "app" autoinclude { dependency "vpc" { expansion { for_each = local.regions } config_path = unit.vpc[each.key].path mock_outputs = { vpc_id = "vpc-mock-${each.key}" } } inputs = { vpc_ids = { for region, vpc in dependency.vpc : region => vpc.outputs.vpc_id } } } }# .terragrunt-stack/app/terragrunt.autoinclude.hcl dependency "vpc" { expansion { for_each = toset(["us-east-1", "us-west-1"]) } config_path = { us-east-1 = "../vpc/us-east-1" us-west-1 = "../vpc/us-west-1" }[each.key] mock_outputs = { vpc_id = "vpc-mock-${each.key}" } } inputs = { vpc_ids = { for region, vpc in dependency.vpc : region => vpc.outputs.vpc_id } }A stack file that declares the same unit or stack label both with and without an expansion now fails to parse, because unit.<name> cannot refer to both.
Discovery failed the same way on a dependency whose config_path pointed at a stack directory containing an expanded unit. It dropped the dependency instead of reporting the error, so run --all did not wait for the units in that stack. The dependency now covers every element of the expanded unit.
Malformed {} groups in glob patterns no longer crash Terragrunt
Some glob patterns with an empty or unclosed {} group crashed Terragrunt when it matched them, e.g. terragrunt find --filter '{./a{}'. Others silently failed to match, so {}a did not match a.
Terragrunt now refuses these patterns with an invalid pattern error. This covers filter queries, include_in_copy and exclude_from_copy in the terraform block, and .terragrunt-catalog-ignore files. A group with one empty option next to a non-empty one, such as main.tf{,.bak}, still works.
hcl validate --inputs reads -var and -var-file arguments verbatim
hcl validate --inputs applied shell quoting rules to each entry in extra_arguments before reading -var and -var-file from it. Those rules treat a backslash as an escape character, so on Windows a var file path such as "-var-file=${get_terragrunt_dir()}\varfiles\main.tfvars" lost its separators, and validation failed to open the file.
Terragrunt now reads each entry in arguments exactly as written, as the single argument it becomes on the OpenTofu/Terraform command line.
Units with identical configs each resolve their own iam_role
When two units had the same terragrunt.hcl content, Terragrunt could assume the first unit's IAM role for both. This hit any iam_role that depends on the unit's directory, such as:
iam_role = "arn:aws:iam::123456789012:role/${basename(get_terragrunt_dir())}"With this config in a/ and b/, b assumed role/a instead of role/b. Terragrunt now evaluates iam_role in each unit's own directory, so get_terragrunt_dir(), find_in_parent_folders(), and similar functions return that unit's paths.
terragrunt info print --all writes JSON Lines and reports each unit's own download directory
info print --all wrote each unit's info indented over several lines, one object after another, and gave every unit the root's download_dir:
$ terragrunt info print --all { "config_path": "/example/live/db/terragrunt.hcl", "download_dir": "/example/live/.terragrunt-cache", "iam_role": "", "terraform_binary": "tofu", "terraform_command": "print", "working_dir": "/example/live/.terragrunt-cache/EfNrjc2equLKYmOZbwT2qu1dO9c/ByrgT1vMBQjFneXYgAxchposVZ0" } { "config_path": "/example/live/vpc/terragrunt.hcl", "download_dir": "/example/live/.terragrunt-cache", ... }A line-oriented reader could not take one entry at a time:
$ terragrunt info print --all | head -1 | jq . jq: parse error: Unfinished JSON term at EOF at line 2, column 0The download_dir was wrong as well. run --all creates each unit's .terragrunt-cache next to that unit's configuration, so the directory reported here was not the one the unit runs against.
With --all, Terragrunt now writes one object per line, so the output is JSON Lines, and builds each unit's context the way run --all does:
$ terragrunt info print --all | jq -c '{config_path, download_dir}' {"config_path":"/example/live/db/terragrunt.hcl","download_dir":"/example/live/db/.terragrunt-cache"} {"config_path":"/example/live/vpc/terragrunt.hcl","download_dir":"/example/live/vpc/.terragrunt-cache"}Printing a single unit is unchanged: one indented object.
Malformed exclude blocks report an error
Terragrunt silently dropped an exclude block with an attribute of the wrong type, such as actions = "plan" where a list belongs, and ran the unit as if the block weren't there. The parse now fails with an error that names the file and the attribute:
exclude block in /live/unit/terragrunt.hcl: json: cannot unmarshal string into Go struct field ExcludeConfig.actions of type []stringDiscovery doesn't fetch dependency outputs, so it can't evaluate an exclude block that reads one. Terragrunt still skips that block during discovery, and now logs a warning naming the file.
Dependencies on a stack skip its disabled units
A dependency whose config_path pointed at a stack directory also depended on the units and stacks in that stack set to enabled = false. Stack generation never writes a disabled unit, so run --all failed on the missing directory:
You attempted to run terragrunt in a folder that does not contain a terragrunt.hcl file. Please add a terragrunt.hcl file and try again.find --dependencies and dag graph listed the same missing path as a dependency.
The dependency now covers only enabled units. The units of a disabled stack are left out, including a tree generated before the stack was disabled.
Stack commands respect --discovery-boundary
stack generate, stack run, and stack output scanned the whole working directory for stack files and ignored --discovery-boundary. In a monorepo with a catalog next to live infrastructure, a catalog stack referencing files that exist only in the live tree failed the command, even though the command never asked for that stack.
The boundary now applies to these commands, including an inline (dir) operand, and it holds when a Git expression such as [main...HEAD] generates stacks for both compared commits. This works from the repository root:
terragrunt stack run plan --filter '(./live/)...[main...HEAD]'Terragrunt skips the catalog units outside ./live. It still scans the whole working directory when a positive filter has no dependent-side boundary and --discovery-boundary is unset, or when the boundaries fall in separate directories.
Dependent discovery had the same gap and parsed units outside the dependent-side boundary. It now starts the search for dependents at that boundary, which can be a directory inside the working directory.
In a Git expression, a relative boundary resolves against the repository root like any other path in the expression. Changed units outside a dependent-side boundary are ignored, and a boundary that exists in neither compared commit is an error. A dependency-side boundary only limits dependency traversal.
--auth-provider-cmd and --queue-construct-as reject unquoted shell operators
Terragrunt splits --auth-provider-cmd and --queue-construct-as values into words without running a shell. An unquoted shell operator such as |, ;, &&, or > used to end the value, and Terragrunt used only the words before it, so --auth-provider-cmd 'get-creds | jq .creds' ran get-creds on its own.
A value with an unquoted shell operator is now an error. Quote the operator to pass it as part of an argument. To run a pipeline as the auth provider, put it in a script and pass the script.
Experiments Added
mcp-command โ Serve Terragrunt operations to AI agents
The new mcp-command experiment adds the mcp command, which serves Terragrunt operations to AI agents over the Model Context Protocol.
An agent can ask which units exist, how they depend on each other, whether configurations pass validation, what order the units run in, what a unit's applied outputs are, and more.
Point an MCP client at the Terragrunt binary and make sure that it enables the experiment:
// .mcp.json { "mcpServers": { "terragrunt": { "command": "terragrunt", "args": ["mcp"], "env": { "TG_EXPERIMENT": "mcp-command" } } } }By default, the server refuses to start any subprocess (e.g. tofu, terraform, git, or a run_cmd program). Wherever Terragrunt would have started one, the result substitutes a stand-in for its output, such as mock_outputs for a dependency output that tofu output -json would have fetched, and lists each substitution in a degraded field.
Pass --allow=exec to let the server run the tofu, terraform, and git that Terragrunt starts on its own during a run. A program a configuration names, through run_cmd(), a before_hook, or --auth-provider-cmd, is still refused, including a tofu, terraform, or git the configuration names for itself, so pointing the server at a repository does not hand it those programs. Allow the commands you want with --allow-cmd, a pattern matched against the program and each of its arguments (e.g. --allow-cmd='jq **'), or let the read-only tools ask: when discover, render_config, validate, or run_order meets a refused program, it sends the client an elicitation naming it, which the client usually shows the person operating the agent, and runs again with whatever they accept. plan, apply, and destroy never ask, since answering would mean running them a second time.
The remaining capabilities are denied the same way, each granted on its own:
- --allow=http lets Terragrunt make HTTP requests on its own.
This includes downloading a unit's remote terraform { source }, fetching a stack's sources, reaching a cloud API to assume a role or read a bucket, and reading remote state directly from blob stores.
- --allow=sops lets it decrypt SOPS-encrypted files.
Unless it is granted, sops_decrypt_file fails rather than handing an agent the cleartext of your secrets.
- --allow=env passes the server's environment variables to configurations and the commands the tools run.
Unless it is granted, tool calls start from an empty environment, so get_env() returns its default. The server also clears its own environment variables and points HOME at an empty directory, so cloud SDKs, the SOPS decrypter, and git commands Terragrunt runs will find no credentials in environment variables or your home directory.
Granting a capability only changes the capabilities of Terragrunt. A process started under --allow=exec can still reach out on the network on its own, so tofu init will download providers whether or not --allow=http was passed to allow Terragrunt to make network requests. The directory the server is launched in is its root. A tool call targeting a directory outside it is refused, and graph traversal is bounded there too, so a filter following dependencies or dependents cannot bring back a unit from a tree the server was never pointed at. That bounds what the server acts on, not what a configuration can read: an HCL function such as file() reads the real disk wherever it points.
You can grant multiple capabilities at once:
// .mcp.json { "mcpServers": { "terragrunt": { "command": "terragrunt", "args": ["mcp", "--allow=exec", "--allow=http"], "env": { "TG_EXPERIMENT": "mcp-command" } } } }A separate flag, --dangerously-allow-apply, adds apply and destroy tools on top of --allow=exec. Without it neither tool is registered, so a client is never told they exist, and the server won't ever run apply or destroy on behalf of a client.
With it, the tool calls return an elicitation (the protocol's way for a server to ask the client's user a question) naming the units that would be run, and the run starts only once the person operating the client accepts it. A decline ends the tool call, and a client with no way to ask anyone is refused. The approval names the units, not the changes: nothing is planned to build that list, since a plan costs a full run that the acceptance then repeats. Call the plan tool first if you want the changes in front of you before accepting.
Only grant this capability on infrastructure you are willing to lose.
Passing it without --allow=exec is refused at startup, since applying means running OpenTofu/Terraform. Launch the server in the environment directory you are willing to have changed rather than at the repository root, because that directory is as far as any tool call can reach:
// .mcp.json { "mcpServers": { "terragrunt-throwaway": { "command": "terragrunt", "args": [ "mcp", "--working-dir", "/path/to/dev", "--allow=exec", "--dangerously-allow-apply" ], "env": { "TG_EXPERIMENT": "mcp-command" } } } }Warning
Agents managing infrastructure
An agent reading your estate is still an agent acting on it. A model can be confidently wrong about what a tool does, and it can be steered by things you don't expect, including the comments in configurations, module READMEs, and command output it was pointed at. Read what an agent responds with as a proposal. Keep a person between it and anything that changes real resources, and give it credentials scoped to what you are willing to have it reach.
The restrictions this server places on itself are not a sandbox. They bound what the tools on this server do, and nothing else. An agent that can run shell commands can run terragrunt run --all apply itself, with your ambient credentials, whether or not the server was started with --allow=exec.
You remain responsible for how your agents manage infrastructure, and for what they do with the access you give them. An agent with these tools and your credentials can change or destroy real resources, and accepting that risk is your decision.
The tools that run OpenTofu/Terraform use the binary named by --tf-path, and ignore a unit's terraform_binary and engine block, since Terragrunt starts both without an --allow-cmd pattern or an approval. plan, apply, and destroy take parallelism to cap how many units run at once.
The tools the server offers, the arguments they take, and what each capability grants are documented with the mcp command.
This will not stabilize before v1.3, so treat the tool set, the flag names, and the shape of every result as subject to change until then. The experiment documentation lists the criteria that have to be met first.
Experiments Updated
Eleven experiments completed
The following experiments graduated to general availability in this release, and the features they gated are now enabled by default:
- azure-backend
- block-iteration
- bounded-discovery
- catalog-format
- dependency-fetch-output-from-state
- mutable-generate
- oci
- optional-dependency-outputs
- optional-hooks
- profiling
- version-attribute
Each feature is described in the New Features section above.
The corresponding --experiment flags (and TG_EXPERIMENT values) are no longer needed. Passing one still works, but emits a warning about the completed experiment, so you can drop it at your convenience.
Thank you to everyone who ran these experiments early and filed the feedback that got them here.
tg-login โ Signing in to the Gruntwork Developer Portal
The tg-login experiment now enables terragrunt login, which signs you in to the Gruntwork Developer Portal from the CLI:
terragrunt --experiment tg-login loginOnce you are signed in, terragrunt catalog discovers the repositories your organization selected in the portal and adds them to your catalog. See Gruntwork Developer Portal for how to select those repositories.
See the experiment documentation for what still has to land before it stabilizes.
Process Updates
Dropped the hashicorp/terraform v0.15.3 dependency
Terragrunt drew its built-in functions from github.com/hashicorp/terraform, pinned to v0.15.3 by a replace directive. Those functions now come from Terragrunt's own copy of the OpenTofu implementations, and go.mod has no replace directives.
Projects that import Terragrunt as a Go module no longer resolve github.com/hashicorp/terraform, and their dependency graph drops by roughly 175 modules, most of them cloud provider SDKs that Terraform used for its backends.
Installing and running the Terragrunt binary is unchanged.
Pull Requests
Features
- feat(profiling): collect runtime profiles by default by @denis256 in #6945
- feat(dependency): read dependency outputs from state by default by @denis256 in #6932
- feat(hcl): switch base64gzip to the current encoder by @denis256 in #6947
- feat: Adding login command by @yhakbar in #6804
- feat: Adding portal Catalog API client by @yhakbar in #6805
- feat: Adding portal repositories to terragrunt catalog by @yhakbar in #6937
- feat: Allow creation of S3 buckets in the account regional namespace by @yhakbar in #6870
- feat: Replace bucket ACL with policy for access logging by @yhakbar in #6868
- feat(azurerm): adapt minimum_tls_version in azure state for v1.2.0 by @denis256 in #6968
- feat: Adding mcp command by @yhakbar in #6851
Bug Fixes
- fix: Ensure that worktrees get cleaned up even when cancelled by @yhakbar in #6900
- fix: reuse assumed role session and fix dependency init check with absolute TF_DATA_DIR by @denis256 in #6902
- fix: Adding --cas-offline support for catalog by @yhakbar in #6904
- fix: Fixing integration of expansion with autoinclude by @yhakbar in #6936
- fix: Patching bug with discovery not respecting enabled attribute by @yhakbar in #6939
- fix(docs): serve the vendor tag proxies the shared GTM container needs by @ZachGoldberg in #6974
- fix(docs): run the Google tag on the main thread and serve the container first-party by @ZachGoldberg in #6975
- fix(docs): resolve the container's extensionless tag paths past the trailing-slash redirect by @ZachGoldberg in #6976
- fix(docs): resolve GA4's collect endpoints past the trailing-slash redirect by @ZachGoldberg in #6978
- fix: More URL redaction by @yhakbar in #6961
- fix: Remove root policy in buckets by @yhakbar in #6674
- fix: Fixing worktree optimization with Git filter expressions on Windows by @yhakbar in #6970
- fix: correct typo in base64gzip changelog note by @denis256 in #6983
- fix: Addressing feedback in #6851 by @yhakbar in #6991
- fix: respect discovery boundary in stack generate and stack run by @denis256 in #6989
- fix: guard MustWalkTerraformOutput against negative index, nil by @denis256 in #6995
- fix: Fixing race in TestStackExpansionGitFilterSelectsRemovedInstance by @yhakbar in #7004
- fix: Adjusts how we ensure that the CAS only gets a Git-compatible venv by @yhakbar in #7003
- fix(strict): name base64gzip-compat experiment in legacy-base64gzip control by @denis256 in #7014
- fix: Resolving iam_role per unit directory and erroring on malformed exclude blocks by @yhakbar in #7012
- fix: Fixing signin display name by @yhakbar in #7015
- fix: respect discovery boundary when parsing dependents and in Git worktrees by @denis256 in #6998
Documentation
- docs: clean up released version gates for v1.1.5 by @github-actions[bot] in #6905
- docs: Bumping Bun to 1.4.2 by @yhakbar in #6951
- docs: More stacks docs clean-up by @yhakbar in #6960
- docs: Dropping v1 banner by @yhakbar in #6965
- docs: Adding GW Developer Portal Private Beta callouts by @yhakbar in #6980
- docs: clean up released version gates for v1.1.6 by @github-actions[bot] in #6985
- docs: Adding mcp.docs.terragrunt.com install instructions by @yhakbar in #6984
- docs: Adding docs for the login command by @yhakbar in #6996
- docs: Adding portal catalog setup docs by @yhakbar in #6997
CI
- ci(coverage): suppress TestGitStoreEnsureCommit_PinnedSHAFetchesOneCommit from timing report by @denis256 in #7017
Chores
- chore: Enforce usage of testify by @yhakbar in #6901
- chore: PR comments by @denis256 in #6911
- chore: Completing block-iteration experiment by @yhakbar in #6925
- chore: Completing oci experiment by @denis256 in #6926
- chore: Completing bounded-discovery experiment by @yhakbar in #6928
- chore: Completing the catalog-format experiment by @yhakbar in #6931
- chore: Completing the mutable-generate experiment by @yhakbar in #6933
- chore: Completing the optional-dependency-outputs experiment by @yhakbar in #6935
- chore: Completing azure-backend experiment by @denis256 in #6929
- chore: Cleaning up stacks docs by @yhakbar in #6950
- chore: Completing the version-attribute experiment by @yhakbar in #6949
- chore: Fixing TestAwsDependencyOutputOptimization tests by @yhakbar in #6953
- chore: Vendoring OpenTofu packages by @yhakbar in #6948
- chore: Use RustFS where AWS usage isn't strictly necessary by @yhakbar in #6954
- chore: Fixing these test names by @yhakbar in #6955
- chore: Parallelizing more cloud tests by @yhakbar in #6957
- chore: Completing the optional-hooks experiment by @yhakbar in #6938
- chore: Consolidating URL redaction by @yhakbar in #6903
- chore: More stack parser parity tests by @yhakbar in #6941
- chore: Adding Windows unit tests by @yhakbar in #6907
- chore: Fix-ups from recent merge flood by @yhakbar in #6963
- chore: fail unstaged login stub requests by @denis256 in #6964
- chore: Fixing Windows tests by @yhakbar in #6966
- chore: Adding better login failure messages by @yhakbar in #6962
- chore: Renaming TestAws to TestAWS by @yhakbar in #6979
- chore(deps): bump cloud, azure, aws, golang dependencies by @denis256 in #6973
- chore: Fixing race in CAS tree test race by @yhakbar in #6982
- chore: Cache in CI better by @yhakbar in #6972
- chore: Adding full CLI fuzz by @yhakbar in #6987
- chore: Bumping JS deps by @yhakbar in #6992
- chore: Getting rid of gopls workflow by @yhakbar in #6994
- chore: Streamlining CI jobs by @yhakbar in #6840
- chore: Setting GITHUB_STEP_SUMMARY in bats tests to avoid actually writing to summaries by @yhakbar in #7000
- chore: Bumping Go tools by @yhakbar in #6986
- chore: Setting up per-run env and version to increase test parallelization by @yhakbar in #6827
- chore: Adding e2e login and catalog test by @yhakbar in #6999
- chore: Using gotestsum better by @yhakbar in #7001
- chore: Fuzz fixes by @yhakbar in #7002
- chore: lint fix by @yhakbar in #7011
- chore: Drop ContextWithEnv by @yhakbar in #7010
- chore: Using a Dev Drive in Windows CI by @yhakbar in #7013
- chore: Adding explanations for serial tests and parallelizing some tests by @yhakbar in #6824
- chore: Getting rid of multierror use by @yhakbar in #7016
- Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/teststructure/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
v2 lockstep release v2.0.0
Original source All of your release notes in one feed
Join Releasebot and get updates from Gruntwork and hundreds of other software products.
- Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/terragrunt/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
v2 lockstep release v2.0.0
Original source - Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/terraform/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
v2 lockstep release v2.0.0
Original source - Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/ssh/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
v2 lockstep release v2.0.0
Original source Similar to Gruntwork with recent updates:
- Google release notes2213 release notes ยท Latest Oct 2, 2026
- Semrush release notes32 release notes ยท Latest Jun 17, 2026
- Asana release notes17 release notes ยท Latest Feb 1, 2026
- Anthropic release notes853 release notes ยท Latest Oct 4, 2026
- Anydesk release notes96 release notes ยท Latest Sep 29, 2026
- Atlassian release notes243 release notes ยท Latest Sep 16, 2026
- Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/packer/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
v2 lockstep release v2.0.0
Original source - Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/opa/v2.0.0
Terratest releases v2 lockstep v2.0.0.
v2 lockstep release v2.0.0
Original source - Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/k8s/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
v2 lockstep release v2.0.0
Original source - Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/httphelper/v2.0.0
Terratest ships v2 lockstep release v2.0.0.
- Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/helm/v2.0.0
Terratest releases v2 lockstep v2.0.0.
v2 lockstep release v2.0.0
Original source - Sep 23, 2026
- Date parsed from source:Sep 23, 2026
- First seen by Releasebot:Sep 23, 2026
modules/gcp/v2.0.0
Terratest ships v2.0.0 lockstep release.
v2 lockstep release v2.0.0
Original source - Sep 21, 2026
- Date parsed from source:Sep 21, 2026
- First seen by Releasebot:Sep 21, 2026
v1.1.6
Terragrunt fixes a Windows bug so Git filters can again find nested units, restoring find, list and browse when only nested Terragrunt configs change.
Bug Fixes
Git filters find nested units on Windows again
On Windows, find, list and browse returned nothing for a Git-based filter such as --filter '[main...HEAD]' when the diff changed only unit configurations in nested directories, like nested\path\terragrunt.hcl.
The worktree optimization in v1.1.5 checks out only the directories of changed units for these commands. On Windows, it spelled those directories with \ separators and looked them up in Git's file listing, which uses /. None matched, so it checked out nothing. Terragrunt now uses / separators for those directories on every platform.
Original source - Sep 14, 2026
- Date parsed from source:Sep 14, 2026
- First seen by Releasebot:Sep 14, 2026
v1.1.5
Terragrunt ships performance boosts, sturdier caching, and bug fixes across dependency handling, generated files, and provider setup, while adding safeguards for duplicate dependency paths and new experiments for offline CAS, block iteration, and base64gzip compatibility.
โจ New Features
duplicate-dependency-labels also catches a shared config_path
Two dependency blocks with different labels can point at the same config_path. Both parse, so the same unit is declared twice, and the two blocks drift apart as soon as one gains a mock_outputs or skip_outputs the other lacks:
dependency "vpc" { config_path = "../vpc" } dependency "network" { config_path = "../vpc" }Terragrunt now warns when it finds this, alongside the existing warning for two blocks sharing a label. With the duplicate-dependency-labels strict control enabled, the warning becomes an error naming both addresses and the path they share:
/path/to/terragrunt.hcl: dependencies vpc and network both point at ../vpc; declare that dependency once and reference it under one name๐๏ธ Performance Improvements
Fewer remote probes for sources shared across units
run --all asked the remote what a source resolved to once per unit, so a hundred units sharing one module made a hundred requests. Each of them then read the same commit out of the store for itself.
Units that resolve the same source at the same time now share one probe, and units that need the same Git commit share the work of reading it into the CAS.
Measured over 100 units pointing at one Git module, counting the Git commands a run spawns:
100 units, one shared module
Before
After
First run: git ls-remote
100
1
First run: reading the commit into the store
202
4
First run: Git commands in total
304
7
Later run, source on a branch
100
1
Later run, source on a version tag
100
0
The last row needs the offline-cas experiment described below; the rest apply to every run. Against a local Git server the first run went from roughly 5 seconds to 0.3, and a later run from 1 second to 0.1. A real remote makes each avoided ls-remote worth more, since it costs a network round trip rather than a local process.
The new offline-cas experiment goes a step further and has the CAS record each probe answer in the store, so a later run can skip the request. How long it trusts an answer depends on the source:
A source pinned to a specific revision keeps its answer for 24 hours: a semantic version tag, an S3 object version, an OCI manifest digest, an exact registry module version, or a full Mercurial changeset node.
A source that can change upstream, such as a Git branch or an OCI tag, gets a fresh probe on every run, so a push or an upload shows up immediately.
The experiment also unlocks three flags that change how the recorded answers are used:
- --cas-offline never contacts a remote. Sources come from the local store and the recorded answers, and anything missing is an error rather than a fetch.
- --cas-refresh ignores the recorded answers for one run and asks every remote again.
- --cas-probe-ttl trusts a changeable source's answer for a duration you choose, such as 10m.
See Recorded probes and the offline-cas experiment.
Faster first-time source downloads
The first time Terragrunt stores a repository in the Content Addressable Store (CAS), it copies the content of every file out of the clone. It used to launch a separate git process for each one, and on repositories with many files those launches dominated the time.
Terragrunt now reads a repository's content through a single long-lived git process, and stores several files at a time.
In benchmarks on an Apple M3 Max:
files
before
after
change
200
2.16s
0.38s
-83%
1,000
10.41s
0.74s
-93%
3,000
34.25s
1.69s
-95%
The saving grows with the number of files.
This applies when the CAS does not already hold the content, such as the first use of a new module version or a run against an empty store. Downloads that the CAS can already serve skipped this work before and are unchanged.
CAS store improvements
The CAS no longer writes a lock file beside each object it stores. A store had one lock file for every file and every directory listing it cached, so ~/.cache/terragrunt/cas held roughly twice as many entries as the cached content needed. Lock files already written stay where they are; deleting the store while no Terragrunt process is running against it reclaims them, and the store rebuilds without them.
Terragrunt preserves a couple of files from a repository's .git directory when it materializes a Git source, and which files those are depends on the command. Those files used to be folded into the stored entry for the commit, so the first command to fetch a commit decided what every later command received from it: a commit first cached by stack generate, which asks for none of those files, left a later run against the same commit without them. Each file is now recorded against the commit on its own, and a command receives exactly the files it asked for whether the commit was already cached or not.
A source pinned to a full commit SHA now asks the remote for that commit alone, one commit deep, instead of fetching every branch and tag with full history. Remotes that will not serve a commit by name, such as an older or locked-down server, still get the full fetch, so pinning keeps working everywhere. Where the remote does serve it, the first fetch of a large repository transfers the pinned commit and nothing else.
The numbers below come from micro-benchmarks run against a git server on the same machine. The fixture is a 500-commit history whose pinned commit sits 100 commits behind the tip.
Measurement
Before
After
Change
Git objects kept after fetching the pinned commit
543
43
92% fewer
Time to fetch the pinned commit
575ms
482ms
16% faster
Against a real remote the pinned fetch saves more than the table shows, since the objects it no longer asks for would also have to cross the network.
Faster dependents filters
A filter with ... before its target, such as ...vpc, finds dependents by walking the directory tree around the target and parsing each configuration it passes to see whether it depends on the target. That walk parsed every configuration from scratch, even one Terragrunt had earlier in the same command, and it runs again from each dependent it finds. On a large repository, one query could read and parse the same unrelated unit once per dependent it selected.
Terragrunt now reuses a configuration it has already parsed, so each unit is read from disk about once per query.
In benchmarks on an Apple M3 Max, querying the dependents of a unit from its own directory, where every other unit depends on it:
units
before
after
10
15.3ms
10.0ms
50
235ms
150ms
200
3.19s
2.09s
From the repository root, where the walk only has to rule out the units that do not depend on the target, the same query over a 1,024-unit repository went from 250ms to 131ms.
Faster file work, especially on small CI runners
Terragrunt frequently does a lot of small file operations at once: copying a module into its working directory, storing a repository in the Content Addressable Store (CAS), and materializing one back out. How many it ran at once scaled with the number of vCPUs seen by the Terragrunt process or the --parallelism flag if configured.
Terragrunt now picks that number by probing the filesystem it is about to write to to guess how much throughput it can handle to improve performance.
The gain is largest where the filesystem is much faster or slower than Terragrunt would expect, just scaling off vCPUs.
Materializing a 3,000 file repository on a 2 vCPU runner:
filesystem
before
after
change
ext4
40.8ms
24.4ms
-40%
btrfs
48.7ms
34.2ms
-30%
overlayfs
71.5ms
56.4ms
-21%
On a 16 vCPU machine, storing that repository for the first time is 19% faster on ext4 and 20% faster on btrfs.
terragrunt hcl fmt now formats at most 8 files at once by default, which measured about 14% faster than one worker per CPU on a 16 core machine.
Runs with the fast-copy strict control enabled also copy module directories faster on macOS, by around 60% in benchmarks on an Apple M3 Max.
Faster worktrees for Git filters
A Git-based filter, such as --filter '[main...HEAD]', generates a worktrees to be able to run tofu in states that aren't reflected in the current worktree (e.g. when a unit is deleted, Terragrunt has to run a plan -destroy or apply -destroy in the main worktree, not the HEAD worktree in the earlier example).
As a conditional optimization, Terragrunt now reads the Git diff first and generates worktrees only when on-disk worktrees are necessary downstream.
For commands like find, list or browse worktree generation can be skipped more aggressively, and even more performance improvements were made there.
On a repository with 15,000 tracked files, terragrunt find --filter '[HEAD~1...HEAD]' went from 4.7s to 0.4s on an M3 Max machine.
Lower memory use during run --all
When you set --json-out-dir, Terragrunt saves a JSON plan for every unit it runs. It used to build each of those documents in memory in full before writing any of it to disk, so a unit with a 64 MB plan needed roughly 168 MB to save it, and every unit running in parallel needed its own. Terragrunt now writes the document as it arrives. That same plan needs about 300 KB, roughly 550x less, and saving it finishes about 18% faster.
Two other places held on to more than they needed. During run --all plan, Terragrunt kept every unit's error output until the run finished so it could check it for a single message at the end, and it now checks that as the output streams. Responses from a provider registry were read twice on the way in, and are now read once, which uses about 19% less memory per request.
JSON plans are also replaced atomically now. A run that fails part way through leaves the previous file in place instead of truncating it.
mutable = true sources are cloned instead of copied
A source marked mutable = true needs a file of its own, because a hard link would hand out the store's read-only copy. Terragrunt now asks the filesystem for a copy-on-write clone of the stored file and copies only where the filesystem has none to give. APFS, btrfs, and XFS volumes with reflink support have one.
A cloned target shares the stored content until you write to it, so it occupies disk space only for the parts you change. On those volumes, marking a source mutable in every unit costs disk space only for what each unit edits.
These micro-benchmarks time materializing an editable tree on APFS on an M3 Max, once copied as in earlier releases and once cloned.
Tree
Before (copied)
After (cloned)
Change
500 files, 7.3 MiB, most around 2 KiB
71ms
81ms
14% slower
120 files, 40 MiB, 20 of them 2 MiB each
146ms
23ms
84% faster
A clone takes about the same time for a file of any size, while a copy takes longer the bigger the file. A tree of small files takes about 10ms longer to materialize, and a tree with large files materializes about six times faster.
Terragrunt no longer records what units read unless something needs it
Every parse used to record the files it read. Part of that record is the content of each local module a unit sources, so Terragrunt walked those module directories once per unit, on every command, whether or not anything would look at the result.
Only four things consult the record: reading-based filter expressions, the --queue-include-units-reading flag, find --reading, and the file tree in terragrunt browse. Terragrunt now keeps it for those and skips the module walk everywhere else.
Benchmarks on an Apple M3 Max, across 1,000 units that all source the same local module:
files in the module
find --dependencies
render --all
50
148 ms โ 135 ms
352 ms โ 259 ms
150
180 ms โ 135 ms
430 ms โ 263 ms
400
268 ms โ 137 ms
631 ms โ 270 ms
render --all performs the same full parse of each unit that run --all performs before it invokes OpenTofu, so a run over units with large local modules saves comparable time before the first plan starts.
The saving grows with the size of the local modules a repository sources, and the new times hold steady as those modules grow. Commands that do ask about reads behave as they did before.
Finding the repository root no longer launches git
get_repo_root(), get_path_from_repo_root(), get_path_to_repo_root(), the runner, and discovery all need the root of the enclosing repository. Terragrunt used to ask Git for it by running git rev-parse --show-toplevel, and starting that process cost far more than producing the answer did.
Terragrunt now finds the root itself, by looking for a .git entry in the working directory and each directory above it. Linked worktrees and submodules resolve the way they did before.
In benchmarks on an Apple M3 Max, resolving one root, where depth is how many directories separate the starting point from the root:
depth
before
after
1
5.29ms
14ยตs
5
5.20ms
24ยตs
10
5.25ms
38ยตs
Because Terragrunt no longer asks Git, some of Git's own settings for locating a repository stop applying. GIT_CEILING_DIRECTORIES still stops the search where it did. GIT_DIR, GIT_WORK_TREE and core.worktree are ignored, and the safe.directory ownership check is not applied, so get_repo_root() now answers in a repository owned by another user where Git refuses. A path inside a bare repository still reports that there is no repository. This is assumed to be more expected from the perspective of a Terragrunt user, and usage of git rev-parse --show-toplevel from a run_cmd is still available otherwise. If this impacts your workflows, please open a bug report, and maintainers are happy to work with you on this.
๐ Bug Fixes
More generated files are written atomically
Terragrunt used to generate most files by opening the destination and writing into it, so the file spent time on disk half-written, and a run that failed partway through left a truncated one behind.
These now go to a temporary file that replaces the destination once it is complete:
- Files from generate blocks
- The config from render --write
- Run reports from --report-file
- The debug file from --debug
- .terraform.lock.hcl
- The CLI config Terragrunt generates for OpenTofu/Terraform when the Provider Cache Server is enabled
backend commands no longer fail on unapplied dependencies
backend bootstrap, backend migrate and backend delete used to read the whole configuration of every unit they touched, which meant fetching the outputs of every dependency block. Declaring a dependency on a unit you had not applied yet was enough to stop them with the "detected no outputs" error, even when nothing in remote_state read that dependency.
These commands now read only the remote_state block and the terraform block's source. They never fetch dependency outputs. A remote_state that does read a dependency output still resolves it, and still reports missing outputs when the dependency has not been applied.
base64gzip() returns the v1.1.3 bytes again
Terragrunt v1.1.4 was built with Go 1.27, which changed the compressed bytes produced by base64gzip(). The bytes decompress to the same content, but a resource that compares the encoded value, such as an EC2 instance with user_data_base64 and user_data_replace_on_change = true, planned a replacement after the upgrade.
base64gzip() now returns the bytes it returned in v1.1.3 and earlier, so upgrading plans no change. Terragrunt warns once per run that this is legacy behavior. If you already applied the v1.1.4 output, every plan shows the encoded value changing back until you apply it or enable the strict control below, and a resource that depends on stability of base64gzip bytes is replaced by that apply.
Terragrunt 1.2 will switch base64gzip() to the new encoder by default. The new base64gzip_compat() function, behind the base64gzip-compat experiment, returns the v1.1.3 bytes permanently (assuming the experiment eventually stabilizes), so call it where the encoded value must stay stable across upgrades. This function may be removed in a future release.
To keep the current Go encoder's output now and silence the warning, enable the new legacy-base64gzip strict control:
terragrunt run plan --strict-control legacy-base64gzipDeleted files in the CAS are fetched again instead of failing the run
When something removes a file from the Content Addressable Store (CAS) that a cached source still needs, Terragrunt now downloads that source again and restores what is missing, then carries on.
Terragrunt used to treat a cached source as complete once it had been downloaded, so a file deleted from the store afterwards ended the run with a read failure naming a path inside the store. Recovering meant clearing the store by hand.
A source that no longer supplies the missing content still fails, and now says which object the store is missing. The same is true of a cas:: reference in a stack file, which names stored content directly and has no source behind it to download again, and of a run under --cas-offline, which forbids the download that would restore the store.
catalog sanitizes the content it draws from a repository
terragrunt catalog browses repositories you point it at, and draws their titles, descriptions, tags and READMEs to the terminal as it finds them. The catalog command did not appropriately sanitize content from repositories to ensure that the content rendered correctly in terminals.
catalog now sanitizes everything it draws, the way terragrunt browse already sanitized the files it previews. Control characters become the Unicode replacement character, so that content draws as visible placeholders. --format jsonl and --format md keep the text as the repository wrote it.
Fixed a crash in terragrunt catalog when a repository cannot be reached
terragrunt catalog now reports the underlying git error when it cannot reach a repository listed in the catalog block. Previously, this could cause a crash part-way through loading. This affected any repository Terragrunt could not clone, e.g. an SSH URL with no usable key, a private repository without credentials, or a remote that timed out.
find --dependencies lists dependencies in a stable order
When a unit had more than one dependency, terragrunt find --dependencies --json could report them in a different order on each run, with no change to the configuration.
The order is now fixed. list, dag graph, and browse sorted before rendering already, so their output is unchanged.
Support backend assume_role during direct dependency state reads
With dependency-fetch-output-from-state enabled, direct S3 state reads now correctly chain the backend's assume_role onto the dependency's execution role. Previously, cross-account dependency state reads failed with 403 AccessDenied when the remote_state block configured a separate assume_role for state access.
Dependency state read failures fall back
With the dependency-fetch-output-from-state experiment enabled, network, permissions, and parsing failures from a direct dependency state read could end a run that worked through native output retrieval.
Outside render and render-json, Terragrunt now retries failed direct reads with tofu output or terraform output. If native output retrieval succeeds, the run continues and only the direct-read speedup is lost. Missing state and the two render commands retain their existing mock-output behavior.
This fallback also covers OpenTofu client-side state encryption. Terragrunt recognizes the encrypted envelope and retries output retrieval through the configured binary instead of treating the dependency as having no outputs. If that binary can decrypt the state and native output retrieval succeeds, only the speedup is lost. render and render-json still require --no-dependency-fetch-output-from-state when they must resolve real outputs from encrypted state.
Current flag names take precedence over deprecated ones
A setting given under both its current name and a deprecated one took the deprecated value whatever the source of each, so TERRAGRUNT_LOG_LEVEL=debug in the environment overrode TG_LOG_LEVEL=info set beside it.
A command-line argument now beats an environment variable under either name, and at the same level the current name beats the deprecated one. A --terragrunt-* argument still overrides a TG_* variable from the environment, so a script mixing the two keeps working.
exec accepts --source, --source-map, and --no-auto-init
terragrunt exec rejected --source, --source-map, and --no-auto-init as invalid flags, one message per flag: flag
--source-mapis not a valid flag forexec. It reads configuration and downloads source the same way run does, so there was no way to point exec at a local copy of a module, or to stop it from running init. All three flags are now registered on exec.terragrunt exec --source-map git::ssh://[email protected]/acme/modules.git=/local/modules -- tfmigrate planexec therefore also reads TG_SOURCE, TG_SOURCE_MAP, and TG_NO_AUTO_INIT, along with the deprecated TERRAGRUNT_SOURCE, TERRAGRUNT_SOURCE_MAP, and TERRAGRUNT_AUTO_INIT, which it previously ignored. If you export any of those for run, exec starts honoring them too.
--no-auto-init reaches the unit exec targets only under --in-download-dir, since exec otherwise never runs init for it. It also reaches units named in dependency blocks, with or without that flag, because Terragrunt initializes a dependency when resolving its outputs requires it.
Direct GCS state reads work with Workload Identity Federation
With the dependency-fetch-output-from-state experiment enabled, a GCS backend authenticated through Workload Identity Federation still ran tofu output or terraform output for every dependency, so the experiment made no difference.
It affected any credentials file of type external_account, which is what google-github-actions/auth writes and points GOOGLE_APPLICATION_CREDENTIALS at. Terragrunt read only service_account and authorized_user files directly.
Terragrunt now reads external_account credentials files directly, including the service-account impersonation that google-github-actions/auth configures when you give it a service account. A direct read requires the file's credential_source to be one of:
- url
- file with an absolute path
Any other credential_source keeps the previous behavior, and the dependency still runs tofu output or terraform output. Reading those directly would use Terragrunt's own process rather than the unit's environment to resolve the identity:
- executable would run the command with Terragrunt's environment.
- AWS (an environment_id such as aws1) would use Terragrunt's AWS credentials.
- file with a relative path would resolve against Terragrunt's working directory.
The impersonate_service_account backend setting is a separate feature and is not affected. Backends that set it still run tofu output or terraform output.
Files from generate blocks are created as 0600
A generate block writes files for Terragrunt and the processes it spawns, all of which run as the user who ran Terragrunt. Creating them as 0644 granted read access that nothing uses.
They are now created as 0600. Under the mutable-generate experiment, a block without mutable = true gets a read-only link to a copy shared between working directories, and Terragrunt stores new content as 0400 rather than 0444. With mutable = true, the block keeps a writable 0600 file of its own.
Content the CAS is already holding keeps the permissions it was stored with, since changing them would change every file linked to that copy. Those files stay 0444 until the cache is cleared. Run with --log-level debug to see which ones.
EC2 instance role credentials work again from inside a container
Since v1.1.4, Terragrunt running in a container on an EC2 instance could fail to use the instance's IAM role when the instance metadata service has a hop limit of 1. Runs failed with:
error assuming role: operation error STS: AssumeRole, get identity: get credentials: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, canceled, context deadline exceededIn that setup the IMDSv2 token request never gets an answer. Terragrunt v1.1.4 waited on it until the whole credential lookup timed out, so the IMDSv1 fallback that v1.1.3 and earlier relied on never ran.
Terragrunt now gives up on the IMDSv2 token request quickly and falls back to IMDSv1, as it did before v1.1.4. No configuration change is needed.
IAM role credentials are reused for --json-out-dir plan export
After v1.1.4, run --all plan with an IAM role and --json-out-dir could make a second sts:AssumeRole request. That request used the role session itself and failed with AccessDenied unless the role trusted itself.
Terragrunt now caches the assumed session in-process until five minutes before it expires (default session length is one hour when --iam-assume-role-duration is unset), keyed by role configuration and source identity, so the JSON export reuses the first assumption. Setting --iam-assume-role-duration was already a working workaround and remains supported.
If a session cannot be refreshed but has not yet expired, Terragrunt logs a warning and continues with the cached credentials rather than failing the run.
Provider Cache Server reads only the running implementation's CLI config files
In v1.1.4, the Provider Cache Server started reading OpenTofu's CLI config file locations (~/.tofurc and $XDG_CONFIG_HOME/opentofu/tofurc) regardless of which binary Terragrunt was running. A machine with a stray ~/.tofurc (for example, one declaring a network_mirror) could break terragrunt init for Terraform users with errors like:
ERROR Failed to get provider versions from "network_mirror '...'": invalid character '<' looking for beginning of valueTerragrunt now detects whether the configured binary is OpenTofu or Terraform before starting the cache server and reads only that implementation's CLI config files:
- OpenTofu reads the first of these that exists: ~/.tofurc, ~/.terraformrc, $XDG_CONFIG_HOME/opentofu/tofurc (on Windows: %APPDATA%\tofu.rc, then %APPDATA%\terraform.rc).
- Terraform reads only ~/.terraformrc (%APPDATA%\terraform.rc on Windows).
For both implementations, Terragrunt also merges the *.tfrc and *.tfrc.json fragments from the CLI config directory: ~/.terraform.d (%APPDATA%\terraform.d on Windows), or $XDG_CONFIG_HOME/opentofu for OpenTofu when ~/.terraform.d does not exist.
Setting TF_CLI_CONFIG_FILE continues to override the config file location for both implementations.
The same selection applies to the credentials read for module registry downloads and version-constraint resolution, kept separately per implementation within a single run.
The implementation is detected from the binary Terragrunt is configured to run at startup (--tf-path, TG_TF_PATH, or the first of tofu/terraform found on PATH); a terraform_binary setting inside a unit's configuration does not change which files the cache server reads. When a run's implementation differs from the one the cache server was configured for, and the two implementations would read different CLI config files on that machine, that run skips the provider cache and uses its own CLI configuration, and Terragrunt prints a warning when such a run initializes providers (init or providers lock). Both implementations resolve to the same files when none of the implementation-specific files above exist, or when TF_CLI_CONFIG_FILE names the file. Every run then uses the cache whichever binary it runs. If detection fails, Terragrunt falls back to OpenTofu's file locations.
Run report includes cause for units that fail before OpenTofu/Terraform
Units that failed during config evaluation or dependency output resolution were reported as a run error with an empty cause. The report now records the underlying error text in Cause.
Thanks to @Tensho for contributing this fix!
Fixed S3-compatible source downloads with environment credentials
Downloading unit sources from S3-compatible services (s3::https://minio.example.com/...) now works when credentials are supplied via environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or IAM roles rather than embedded in the URL query string. Previously, the custom endpoint was only pinned when credentials were present in the URL, causing the AWS SDK to redirect requests to amazonaws.com and fail with InvalidAccessKeyId.
Stack dependencies include outputs from nested stacks
A dependency pointing at a directory that holds a terragrunt.stack.hcl now reads the outputs of units generated by that stack's nested stack blocks. The run queue already waited for those units, but their outputs were missing from the dependency.
Each nested stack adds a level named after it, the same address terragrunt stack output gives those units:
dependency "network" { config_path = "../network" } inputs = { vpc_id = dependency.network.outputs.vpc.vpc_id subnet_id = dependency.network.outputs.subnets.subnet.subnet_id }mock_outputs entries for a nested stack's units nest under the stack's name the same way.
A unit and a nested stack with the same name in one stack file share an address, so a dependency on that stack now errors once both have outputs to read. terragrunt stack output already rejects the same configuration. Rename one of the two blocks to give each its own address.
๐งช Experiments Added
base64gzip-compat experiment adds a base64gzip_compat HCL function
Enable the new base64gzip-compat experiment to use the base64gzip_compat(str) HCL function.
base64gzip_compat returns the value base64gzip returned in Terragrunt v1.1.3 and earlier, and keeps returning it after Terragrunt 1.2 switches base64gzip() to the current Go encoder. Use it where the encoded value must stay stable across upgrades:
inputs = { user_data_base64 = base64gzip_compat(file("${get_terragrunt_dir()}/user-data.sh")) }Calling base64gzip_compat without enabling the base64gzip-compat experiment returns an error. The name may still change to match OpenTofu.
offline-cas gates the CAS probe cache
The offline-cas experiment has been added as the gate for the probe cache the CAS keeps, in which it records what each source resolved to so a later run can skip asking the remote.
Enabling the experiment turns the cache on and unlocks three flags that change how its answers are used: --cas-offline, --cas-refresh, and --cas-probe-ttl. Setting one of them without the experiment returns an error naming the flag.
terragrunt run --experiment offline-cas --all --cas-offline -- planWithout the experiment nothing is recorded or served, and every run probes every source, as before.
See the experiment documentation for what each flag does and what has to land before it stabilizes.
tg-login reserved for signing in to the Gruntwork Developer Portal
The tg-login experiment has been added as the gate for terragrunt login, a command for signing in to the Gruntwork Developer Portal. Once it lands, signing in lets terragrunt catalog read the repositories your organization selected in the portal rather than a catalog block you maintain yourself.
In this release the flag is reserved only. Enabling it has no effect, and no command reads it.
See the experiment documentation for what is planned and what has to land before it stabilizes.
๐งช Experiments Updated
azure-backend can assign the blob data role during bootstrap
Creating an Azure storage account grants no access to the blobs inside it, so an identity using use_azuread_auth could bootstrap the backend and then fail to read state as unauthorized until someone granted the data-plane role by hand.
With the azure-backend experiment enabled, assign_blob_data_role = true now has bootstrap grant Storage Blob Data Contributor on the storage account:
remote_state { backend = "azurerm" config = { storage_account_name = "myterragruntstate" container_name = "tfstate" key = "${path_relative_to_include()}/terraform.tfstate" resource_group_name = "terraform-rg" use_azuread_auth = true assign_blob_data_role = true } }The role goes to the identity Terragrunt authenticated as, resolved from the access token it already holds rather than from a directory lookup, so it works for identities that cannot read Microsoft Entra. Set principal_id to grant the role to a different user, group, or service principal.
Existing assignments are detected and left alone, so reruns need only read permission on role assignments.
The setting is opt-in: creating a role assignment requires Microsoft.Authorization/roleAssignments/write, which Contributor does not include. Leaving it unset preserves the previous behavior of assigning nothing.
expansion blocks now iterate dependency, unit, and stack blocks
With the block-iteration experiment enabled, a dependency, unit, or stack block can have an expansion block declaring a count or a for_each. Terragrunt reads the block once per element, producing one dependency, unit, or stack for each:
# terragrunt.stack.hcl unit "aurora" { expansion { for_each = toset(["web", "api"]) } source = "../units/app" path = "aurora/${each.key}" values = { role = each.key } }You address each element by its key. An expanded dependency is read as dependency.aurora["web"].outputs.id, and terragrunt stack output 'aurora["web"].role' reaches one element of an expanded unit.
Adding an expansion to a block that did not have one therefore changes its address, and shrinking a for_each or lowering a count removes addresses. Terragrunt has no moved equivalent, so nothing records the rename for you: references and stack output scripts need updating by hand, and state left behind at an address that no longer exists has to be destroyed deliberately.
The experiment also enables an enabled attribute on unit and stack blocks. Setting it to false drops the component from stack generation and from terragrunt stack output, and leaves every other address alone. dependency blocks accept enabled without the experiment.
See the expansion block reference for the rules, the addressing scheme, and how to clean up state left behind when an expansion shrinks.
symlinks experiment: include_in_copy copies the contents of symlinked directories again
In v1.1.4, files behind a symlinked directory named in include_in_copy were not copied into the OpenTofu/Terraform working directory, so they were missing from .terragrunt-cache. exclude_from_copy patterns reaching through a symlinked directory also excluded nothing.
With the symlinks experiment enabled (--experiment symlinks or TG_EXPERIMENT=symlinks), patterns rooted at a symlinked directory expand through the link again, for both include_in_copy and exclude_from_copy, as in v1.1.3 and earlier. Without the experiment, the v1.1.4 behavior is unchanged.
A link that points back at a directory already being copied, or at a parent of one, such as a link to the unit directory itself, is skipped. Terragrunt logs a warning naming the link when that happens.
render previews an expanded dependency block written in JSON
With the block-iteration experiment enabled, a configuration written in JSON now renders the same way an HCL one does. It has no HCL to quote, so Terragrunt writes the block as the HCL that means the same thing and previews the elements underneath it:
$ cat terragrunt.hcl.json {"dependency": {"shard": { "expansion": {"count": 2}, "config_path": "../shard-${count.index}" }}} $ terragrunt render --experiment block-iteration dependency "shard" { expansion { count = 2 } config_path = "../shard-${count.index}" } # Expands to: # # dependency "shard" { # config_path = "../shard-0" # } # # dependency "shard" { # config_path = "../shard-1" # }Previously the elements rendered as ordinary blocks, which repeated one label. Terragrunt warns about that and rejects it under the duplicate-dependency-labels strict control, so the rendered file did not read back.
--format json no longer drops the elements either. Its dependency map is keyed by label, which every element shares, so it kept whichever element came last. JSON has no comment to preview the elements in, so it now emits the block as it was written, references and all:
$ terragrunt render --format json --experiment block-iteration { "dependency": { "shard": { "expansion": { "count": 2 }, "config_path": "../shard-${count.index}", "skip_outputs": true } } }Whichever syntax you write and whichever format you ask for, rendering the output again returns it unchanged.
Expanded units keep their own outputs when a whole stack is a dependency
With the block-iteration experiment enabled, a dependency pointing at a directory that holds a terragrunt.stack.hcl collected the outputs of an expanded unit under the block's bare label. Every element wrote to that one label, so only the last one survived, and reading it returned another element's outputs.
Each element is now reachable under its own key, matching the address terragrunt stack output already gives it:
dependency "networking" { config_path = "../live" } inputs = { web_id = dependency.networking.outputs.aurora["web"].id api_id = dependency.networking.outputs.aurora["api"].id }A unit that declares no expansion is still read as dependency.networking.outputs.vpc.id.
Pull Requests
โจ Features
- feat: Adding tg-login experiment by @yhakbar in #6759
- feat(azure): assign blob data role on bootstrap in Azure by @denis256 in #6764
- feat: Add ability to open browser for approval by @yhakbar in #6793
- feat: Adding CLI poll for login approval by @yhakbar in #6795
- feat: Adding token storage by @yhakbar in #6801
๐ Bug Fixes
- fix(cliconfig): isolate cloned helpers and hosts by @denis256 in #6782
- fix: Check duplicate dependency config path by @yhakbar in #6834
- fix: Writing more generated files atomically by @yhakbar in #6776
- fix(config): harden dependency state output fetching by @denis256 in #6794
- fix(provider-cache): Fix provider cache configuration selection by @denis256 in #6789
- fix: Sanitizing some untrusted input by @yhakbar in #6802
- fix: restore AWS IMDS credential fallback for container environments by @denis256 in #6837
- fix(gcs): read dependency outputs from state with external_account creds by @denis256 in #6815
- fix: Expand include_in_copy/exclude_from_copy globs through symlinked directories by @denis256 in #6817
- fix: Fixing stack dependency unit output cty access by @yhakbar in #6839
- fix: adding --source-map and --no-auto-init to terragrunt exec by @denis256 in #6792
- fix(config): restore v1.1.3 base64gzip output by default by @denis256 in #6835
- fix(getter): pin S3-compatible endpoint independently of URL credentials by @denis256 in #6857
- fix: Fixing catalog nil logger panic by @yhakbar in #6871
- fix: Fixing render --json expansion logic by @yhakbar in #6763
- fix: populate run report cause for units that fail before tofu/terraform runs by @Tensho in #6819
- fix: Use partial parse when possible for backend commands by @yhakbar in #6790
- fix(config): use backend assume_role for direct dependency state reads by @denis256 in #6883
- fix: Preventing Windows flag usage panic in tests by @yhakbar in #6877
- fix: Adding store repair for corrupted CAS stores by @yhakbar in #6872
- fix: Fixing accessing nested stack outputs in stack dependencies by @yhakbar in #6892
- fix: Fixing mutable-generate tmp file leak by @yhakbar in #6895
- fix(amazonsts): stop json-out-dir IAM self-assume by @denis256 in #6899
๐๏ธ Performance
- perf: Pre-compile color table with a generate script by @yhakbar in #6780
- perf: Use git cat-file --batch instead of multiple git cat-file calls per blob by @yhakbar in #6838
- perf: Tuning FS concurrency by @yhakbar in #6854
- perf: Inline git rev-parse --show-toplevel as a Go func by @yhakbar in #6867
- perf: Streaming buffered output by @yhakbar in #6761
- perf: Using singleflight cached CAS probe by @yhakbar in #6841
- perf: Using a sync.Pool for hot buffers by @yhakbar in #6769
- perf: Reuse already-parsed configs when discovering dependents by @yhakbar in #6849
- perf: Improving worktree performance by @yhakbar in #6864
- perf: Avoid tracking reading when unnecessary by @yhakbar in #6862
- perf: Improve CAS store hygiene by @yhakbar in #6843
๐ Documentation
- docs: Bumping Bun to v1.4 by @yhakbar in #6762
- docs: Updating docs for supported flags on dag graph by @yhakbar in #6788
- docs: Updating block-iteration documentation by @yhakbar in #6844
- docs(changelog): note report Cause for prerun failures by @denis256 in #6878
- docs: Adding sign-commits to the use of peter-evans/create-pull-request for docs clean-up PRs by @yhakbar in #6891
- docs: Closing gaps in recent feature docs by @yhakbar in #6893
- docs: Addressing feedback from #6892 by @yhakbar in #6896
- docs: Fixing docs build by @yhakbar in #6897
๐งน Chores
- chore: Adding exec sandboxed logic to seatbelt by @yhakbar in #6760
- chore: Collapse runner packages by @yhakbar in #6765
- chore: Running go fix ./... on all tags by @yhakbar in #6766
- chore: dependencies update by @denis256 in #6719
- chore: Validating device auth duration better by @yhakbar in #6779
- chore: Moving integration tests to in-memory CLI tests by @yhakbar in #6771
- chore: Moving discovery tests in-memory by @yhakbar in #6774
- chore: Typed stack validation errors and consistent generated-path comparison by @yhakbar in #6777
- chore: Adding injectable cap to speed up TestUnitPathsFromStackDir_DepthCapReturnsError test by @yhakbar in #6781
- chore: Modernizing with new Go 1.27 constructs by @yhakbar in #6772
- chore: Fixing lints on main by @yhakbar in #6785
- chore: go mod fixes by @denis256 in #6786
- chore: Deterministic dependency order for find --dependencies --json by @yhakbar in #6800
- chore(deps): bump google.golang.org/grpc from 1.83.0 to 1.83.1 by @dependabot[bot] in #6808
- chore(deps): bump the js-dependencies group across 1 directory with 11 updates by @dependabot[bot] in #6806
- chore(deps): bump go dependencies by @denis256 in #6820
- chore: Adding test coverage for filters with block iteration by @yhakbar in #6830
- chore: Improving expansion diagnostics by @yhakbar in #6833
- chore: Cover object and tuple for_each in the expansion engine tests by @yhakbar in #6829
- chore: Enabling nolintlint by @yhakbar in #6803
- chore: Adding explicit capacity hints by @yhakbar in #6825
- chore: Adding full block-iteration lifecycle tests by @yhakbar in #6846
- chore: Addressing review feedback from #6854 by @yhakbar in #6865
- chore: Fixing cloud credentials test env semantics by @yhakbar in #6826
- chore: Fixing sandbox tests by @yhakbar in #6882
- chore(deps): bump astro from 7.2.7 to 7.2.8 in /docs by @dependabot[bot] in #6855
- chore(deps): bump the js-dependencies group across 1 directory with 8 updates by @dependabot[bot] in #6885
- chore: Clean-up partial worktrees on failure by @yhakbar in #6884
- chore: Bumping go-runewidth to v0.0.30 by @yhakbar in #6879
- chore: Bumping go-getter to v2.2.4 by @yhakbar in #6881
- chore: Generating .terraform directory on-demand instead of leaving it committed in the repo by @yhakbar in #6887
- chore: Globally replacing xsync.Map with a map and mutex by @yhakbar in #6880
- chore: General clean-up from recent PRs by @yhakbar in #6886
- chore: Revert to materializing worktrees with git checkout instead of git archive by @yhakbar in #6890
- Aug 27, 2026
- Date parsed from source:Aug 27, 2026
- First seen by Releasebot:Aug 28, 2026
v1.1.4
Terragrunt ships interactive scaffold prompts, stricter dependency label checks, faster startup, and a wide set of fixes across CAS, provider caching, rendering, HCL validation, and security hardening. It also adds experiment updates for direct state reads and dependency expansion previews.
โจ New Features
duplicate-dependency-labels strict control
Declaring two dependency blocks with the same label in one terragrunt.hcl configuration file parsed without error, and then quietly resolved every reference to that label to whichever block came last. The blocks before it were silently overridden:
dependency "vpc" { config_path = "../vpc-us-east-1" } dependency "vpc" { config_path = "../vpc-us-west-2" } inputs = { # Reads ../vpc-us-west-2. vpc_id = dependency.vpc.outputs.vpc_id }Terragrunt now warns when it finds this. With the new duplicate-dependency-labels strict control enabled, the warning becomes an error naming the address the blocks share:
terragrunt run plan --strict-control duplicate-dependency-labels /path/to/terragrunt.hcl: dependency vpc is declared more than once; every dependency needs an address of its ownGive each block a label of its own. A configuration that was relying on the shadowing to pick the last block should keep only that block.
scaffold asks for values interactively
Scaffolding from the command line wrote # TODO placeholders for every input and left you to fill them in by hand, while scaffolding the same component from the Catalog TUI opened a form and collected them. terragrunt scaffold now opens that same form:
terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysqlFor a module or a template it lists the source's variables; for a unit or a stack it lists the values.* references its configuration makes, which are written to terragrunt.values.hcl. Dismissing the form with esc writes nothing.
The form is skipped, and the placeholders written as before, when you pass --non-interactive, when stdin is not a terminal, or when the source asks for nothing. A scaffold in a CI job, or one run by another program, therefore behaves exactly as it did.
See Scaffold for the full behavior, and the form's keybindings for driving it.
๐๏ธ Performance Improvements
Faster startup when --tf-path is not set
When you don't set --tf-path, Terragrunt picks the binary it wraps by looking for tofu on your PATH and falling back to terraform when it isn't there. Terragrunt used to make that choice by running tofu -version, which meant launching a process at the start of every command, including commands like find and list that never run the binary. That process launch is gone, and a terragrunt --version benchmark runs roughly 1.7x faster as a result.
This changes what happens when tofu is on your PATH but can't run: Terragrunt now selects it and reports the failure rather than silently falling back to terraform. Set --tf-path or TG_TF_PATH to pick the binary yourself.
๐ Bug Fixes
Autoinclude dependency overrides no longer evaluate replaced paths
Terragrunt used to evaluate a dependency's original config_path before applying a sibling autoinclude override. This could prevent a unit from being parsed when the original path referenced a value that the unit no longer supplied, even though the autoinclude replaced that path. Terragrunt now leaves replaced dependency blocks undecoded, then applies the autoinclude override. Dependency blocks without an autoinclude override are still validated.
Blocks that use expansion are still decoded, because a bare autoinclude label does not name their instances. If the autoinclude also declares the same label without expansion, Terragrunt reports a dependency label collision.
Fixed Git sources with a depth query parameter
A terraform.source (or stack source) URL carrying the go-getter depth query parameter, such as ...vpc.git?depth=1&ref=v5.21.0, failed to download since v1.1.0, when the CAS became the default path for Git sources. Terragrunt lifted ref out of the URL but left depth in place, so git received ...vpc.git?depth=1 and rejected it as an invalid repository name. A URL with depth and no ref hit the same failure.
Terragrunt now strips depth, with or without a ref, before invoking git, so these sources download again. The clone depth itself always comes from --cas-clone-depth, which defaults to 1; a depth on a source URL is never applied for CAS clones.
CAS handles local sources that have already been initialized
With CAS enabled, reading a local source that had already been initialized failed and fell back to the slower standard copy. Generating a stack from such a unit logged CAS processing failed ... source escapes repository root.
Provider caching was the cause. Both the Provider Cache Server and the Automatic Provider Cache Dir leave the plugins under .terraform pointing into a shared cache outside the source. CAS read those links as the source reaching outside itself and refused to copy the link for safety.
CAS now leaves .terraform and .terragrunt-cache out of local sources, keeping .terraform.lock.hcl and everything else. OpenTofu, Terraform, and Terragrunt rebuild both directories on demand, so units and stacks no longer receive a stale copy of either. Running tofu init in a source directory no longer changes that source's CAS key.
Fixed the signal sent to a running command during shutdown
On Windows, when a failure rather than Ctrl+C cancelled a run, Terragrunt crashed with a nil pointer panic instead of stopping the command it had started. It now terminates the command, which is the closest thing Windows offers to an interrupt.
On every platform, when a command exited on its own during the grace period after Ctrl+C, Terragrunt could still send it the signal and then log a forwarding error against a process that was already gone.
terraform_binary respected when reading dependency outputs
Reading a dependency block's outputs ignored the terraform_binary of the unit being read and fell back to the auto-detected binary, which is OpenTofu whenever tofu is on your PATH. With terraform_binary = "terraform", a unit ran through Terraform while the dependency it consumed was read through OpenTofu. A run --all over units that each worked on their own then failed with a backend initialization error, followed by a misleading There is no variable named "dependency".
Dependency outputs are now read through the binary the dependency itself configures, so a unit's terraform_binary applies wherever its state is read. --tf-path and TG_TF_PATH still take precedence over the config value.
Numbers with extreme exponents fail fast instead of stalling
A number literal such as 9E9999999 in inputs, locals, or a dependency block's mock_outputs used to cost over a minute of CPU on a single unit. Written out in decimal that number is ten million digits long, and terragrunt render --format=json produced every digit before failing with a ten megabyte error message.
Terragrunt now rejects numbers larger than 1e4096, and non-zero numbers smaller than 1e-4096, before it tries to write them out, and names the attribute holding the value:
count: number is outside the supported range of 1e-4096 to 1e4096Numbers inside that range are unaffected.
Registry credentials are no longer copied into the generated CLI config
When the Provider Cache Server is enabled, Terragrunt writes a CLI config for OpenTofu/Terraform into each unit's working directory, based on your own CLI config. That generated file used to include a copy of every credentials block from your config, including the ones for registries Terragrunt routes through the cache server.
Those copies were never read. For a routed registry, Terragrunt sets the matching TF_TOKEN_<hostname> environment variable, which takes precedence over a credentials block, and the cache server presents your real credentials when it contacts the registry on your behalf. The generated file now leaves the block out for those registries, so your token stays in the CLI config you put it in instead of being duplicated somewhere it had no effect.
Credentials for hosts the cache server does not route are unchanged, since OpenTofu/Terraform contacts those directly and still reads them from the generated config.
Upgrading does not rewrite the files an earlier version already generated. Each is named .terraformrc and sits in a unit's working directory, which is under .terragrunt-cache for remote sources. Delete those files, or clear the cache, to get the copied credentials off disk.
Generated files are readable only by the user who ran Terragrunt
Terragrunt created several files and directories that other users on the same machine could read:
The CLI config Terragrunt writes for OpenTofu/Terraform when the Provider Cache Server is enabled, and the directory holding it.
The JSON plan files written to --json-out-dir, and that directory.
The directories holding the plan files written to --out-dir.
The config written by render --write, which holds the resolved values of inputs, locals, and dependency outputs.Terragrunt now creates those files as 0600 and those directories as 0700.
hcl fmt --stdin honors --check and --diff
terragrunt hcl fmt --stdin ignored --check and --diff. It printed the reformatted HCL and exited 0 whether or not the input needed formatting.
--check now exits with status code 1 when the input needs formatting, and --diff prints a unified diff labeled old/stdin and new/stdin. Neither flag prints the formatted content, so getting that content back means running --stdin without them.
hcl validate no longer crashes on errors that carry no source location
terragrunt hcl validate crashed while formatting its output when one of the errors it found had no position in the configuration. Terragrunt now prints that error's summary and detail, without a location line.
Fixed the deprecated environment variables for hcl validate
TG_HCLVALIDATE_STRICT_VALIDATE, the deprecated name for --strict, also turned on --show-config-path. --strict only takes effect alongside --inputs, and --show-config-path cannot be combined with --inputs. With that variable set, terragrunt hcl validate --inputs failed with specifying both -show-config-path and -inputs is invalid.
TG_HCLVALIDATE_SHOW_CONFIG_PATH, the deprecated name for --show-config-path, was not recognized at all.
TG_HCLVALIDATE_STRICT_VALIDATE now sets only --strict, and TG_HCLVALIDATE_SHOW_CONFIG_PATH sets --show-config-path. TG_STRICT_VALIDATE, TERRAGRUNT_STRICT_VALIDATE, and TERRAGRUNT_HCLVALIDATE_SHOW_CONFIG_PATH are unchanged.
Fixed panic on invalid if_disabled value with include block
A generate block with an invalid if_disabled value combined with an include block caused a nil pointer panic instead of a descriptive error. Terragrunt now returns an error naming the generate block and the invalid value, consistent with if_exists validation.
OCI sources reject Docker-style :tag suffixes instead of fetching latest
An oci:// source that pinned a version with a Docker-style suffix, like oci://ghcr.io/acme/modules/vpc:1.0.0, silently ignored the suffix and resolved the latest tag, so a run could fetch a different module version than the one pinned. Terragrunt now validates the registry and repository the same way OpenTofu does and rejects such sources with an error that shows the source rewritten in the supported ?tag=/?digest= form, for example oci://ghcr.io/acme/modules/vpc?tag=1.0.0. Repository names that violate the OCI reference grammar are also rejected before any registry is contacted.
Prompts accept a piped answer that has no trailing newline
Piping an answer to a confirmation prompt, as in printf yes | terragrunt run --all destroy, failed with an EOF error because Terragrunt discarded a final answer that ended without a newline. Terragrunt now reads that final answer, and only a prompt that gets no input at all reports EOF.
Provider cache supports signed provider download URLs
When a provider mirror returned a signed download URL, the Provider Cache Server used the entire URL, including its query string, as the archive filename. Long authentication parameters could exceed filesystem filename limits and fail with file name too long.
Terragrunt now derives the archive filename only from the URL path while preserving the query string when downloading it. Signed provider URLs, including archives in nested object paths and relative mirror URLs, now download and cache correctly.
find and list reject a --queue-construct-as value that holds no command
A value made only of shell punctuation, such as terragrunt find --queue-construct-as=';', ended the run with a crash report. A value that quotes an empty command, such as --queue-construct-as='""', was accepted even though it names no command.
find and list now exit with an error that repeats the value you passed and shows what --queue-construct-as expects instead.
render --write picks a default filename without a format flag
terragrunt render --write failed with is a directory unless it was paired with --format or --json. Only those flags set the default filename, so a bare --write had no output path and Terragrunt tried to write to the unit directory itself.
The default now follows the format in use. terragrunt render --write writes terragrunt.rendered.hcl next to the unit configuration, and --json or --format=json writes terragrunt.rendered.json. An explicit --out still takes precedence.
sops_decrypt_file now uses the credentials your auth provider supplies
When a run obtained credentials from --auth-provider-cmd, sops_decrypt_file ignored them for any variable already set in the environment Terragrunt started with. The rest of the run honored the auth provider, and correctly overrode any ambient environment variables. OpenTofu/Terraform received those credentials, and so did the AWS calls Terragrunt makes on a unit's behalf, such as get_aws_account_id.
Decryption now runs as the identity Terragrunt resolved for the unit, the same one the rest of the run uses, regardless of ambient environment variables.
info strict list <name> now honors --all
Passing a control name to info strict list shows that control's subcontrols. Unlike the top-level listing, it ignored the --all flag and always included completed subcontrols.
Terragrunt now applies the same rule when you name a control.
String inputs reach modules with ${...} intact
Passing a string input that contains ${...} to a variable declared with a type other than string used to fail with Variables not allowed, because OpenTofu/Terraform parse those values as HCL expressions and read ${...} as an interpolation. Reading a JSON or YAML file into an input hit this whenever the file happened to contain that sequence:
inputs = { config = file("./config.json") }Terragrunt now escapes interpolation sequences in string inputs when the module declares the variable with a type that makes the value parse as HCL, so ${...} arrives as literal text instead of failing the run. Variables declared as string, and variables declared with no type at all, are read verbatim by OpenTofu/Terraform, and their values are still passed through untouched.
๐งช Experiments Updated
Read dependency outputs directly from Azure state
The dependency-fetch-output-from-state experiment can now read dependency outputs directly from Azure Storage (azurerm) state, in addition to S3. This avoids initializing the dependency and running tofu output or terraform output.
Azure direct reads require the azure-backend experiment as well. Unsupported configurations requiring native-only authentication, endpoint, timeout, or customer-provided-key behavior continue to use the native output path.
When a dependency has no state yet, Terragrunt uses that dependency block's mock_outputs, as it already does for S3. When Azure direct reads resolve a storage account key through Azure Resource Manager, which is the case unless access_key, sas_token, or use_azuread_auth is set, a resource_group_name, storage_account_name, or subscription_id naming a resource that does not exist fails with an error naming those keys rather than substituting mock outputs.
Read dependency outputs directly from GCS state
The dependency-fetch-output-from-state experiment can now read dependency outputs directly from GCS state, in addition to S3. This avoids initializing the dependency and running tofu output or terraform output.
Unsupported GCS configurations continue to use the native output path. When a dependency has no state yet, Terragrunt uses that dependency block's mock_outputs, as it already does for S3.
Thanks to @joshmyers for the original GCS implementation.
render previews what an expanded dependency block expanded to
With the block-iteration experiment enabled, a dependency block that carries an expansion block now renders as it was written, followed by the elements it expanded into, commented out and with their bodies resolved:
$ terragrunt render --experiment block-iteration dependency "aurora" { expansion { for_each = toset(["web", "api"]) } config_path = "../aurora-${each.key}" } # Expands to: # # dependency "aurora" { # config_path = "../aurora-api" # } # # dependency "aurora" { # config_path = "../aurora-web" # }The elements are comments because they aren't valid Terragrunt HCL configurations (you are not allowed to use the same dependency label twice in Terragrunt configurations), the previews are there to help you understand how expansion will resolve.
โ๏ธ Process Updates
Go bumped to v1.27
The version of Golang used to compile the Terragrunt binary has been updated from v1.26.6 to v1.27.0.
If you build Terragrunt from source, or import it as a Go module, you now need a Go 1.27 toolchain.
OpenTelemetry SDK updated to v1.45.0
Terragrunt's OpenTelemetry tracing and metrics dependencies have been updated from v1.44.0 to v1.45.0. The logging packages and exporters have also been updated to their compatible releases, and Terragrunt now uses the v1.43.0 semantic conventions.
Telemetry behavior is unchanged.
Pull Requests
โจ Features
- feat: Adding interactive scaffold form by @yhakbar in #6615
- feat(azure): End to end Azure CICD by @denis256 in #6574
- feat: Adding bare enabled to unit and stack blocks by @yhakbar in #6714
- feat: Keying stack output addresses by iteration key by @yhakbar in #6715
- feat: Adding render preview for expansion by @yhakbar in #6737
- feat: remote state reading for GCP and Azure by @denis256 in #6710
๐ Bug Fixes
- fix: Strip credentials before local CLI config write by @yhakbar in #6678
- fix: Tightening file permissions for generated files by @yhakbar in #6675
- fix: Reject oci:// sources with docker-style name suffixes instead of resolving latest by @denis256 in #6696
- fix: Escape interpolation in string inputs when type is verified by @yhakbar in #6685
- fix: Fixing local copies when symlinks exist from provider caching by @yhakbar in #6684
- fix(provider-cache): fixed handling arguments in urls by @denis256 in #6680
- fix(cas): strip go-getter depth query parameter before invoking git by @HalisCz in #6513
- fix(cas): use venvtest helper in depth query param tests by @denis256 in #6712
- fix(test): fix for failing test TestNewSignalsForwarderMultipleUnix by @denis256 in #6713
- fix: prevent nil pointer panic on invalid if_disabled by @denis256 in #6718
- fix: Fixing --queue-construct-as resulting in empty tokenization by @yhakbar in #6720
- fix: Fixing render --write when no --format is supplied by @yhakbar in #6724
- fix: Preventing extremely small or large float exponents from crashing Terragrunt by @yhakbar in #6727
- fix: Handling situation when diagnostics contain nil range by @yhakbar in #6729
- fix: Addressing nil Range and Snippet in SourceSnippets by @yhakbar in #6731
- fix: Propagating flag parse errors instead of swallowing them by @yhakbar in #6732
- fix: Use the appropriate absolute path to unit config file, not basename when checking version constraints by @yhakbar in #6728
- fix(security): upgrade Go to 1.27 by @denis256 in #6736
- fix(test): pass config fixture to setupTest by @denis256 in #6738
- fix: Fixing info strict list without --all by @yhakbar in #6725
- fix: Fixing deprecated TG_HCLVALIDATE_STRICT_VALIDATE env var by @yhakbar in #6730
- fix: Fixing hcl fmt with --stdin combined with --check and/or --diff by @yhakbar in #6726
- fix: autoinclude config path fixes by @denis256 in #6711
- fix: Normalize paths using ToSlash to hande old/new prefix appropriately by @yhakbar in #6744
- fix: Fixing tofu/terraform binary selection for run --all usage by @yhakbar in #6753
- fix: Updating render --write file permissions by @yhakbar in #6756
๐๏ธ Performance
- perf: Refactor default --tf-path resolution by @yhakbar in #6651
๐ Documentation
- docs: give each environment its own state backup path in the Terralith guide by @yhakbar in #6683
- docs: serve the Google tag container first-party by @ZachGoldberg in #6699
- docs: proxy CORS-less GTM tags so they load under Partytown by @ZachGoldberg in #6704
- docs: Clean-up for v1.1.4 changelog by @yhakbar in #6747
โ Tests
- test(ci): validate OCI registry authentication by @denis256 in #6662
๐ค CI
- ci: Add weekly security scans by @denis256 in #6691
- ci(coverage): added support for test suppressions in weekly test report by @denis256 in #6752
๐งน Chores
- chore: tests coverage increase by @denis256 in #6668
- chore(deps): update aws-sdk-go-v2 by @denis256 in #6655
- chore: runner pool test coverage by @denis256 in #6647
- chore: A lot more venv plumbing by @yhakbar in #6644
- chore(deps): bump @astrojs/vercel from 11.0.0 to 11.0.3 in /docs by @dependabot[bot] in #6658
- chore: docs sync by @denis256 in #6676
- chore: venv plumbing for cloud SDKs by @yhakbar in #6645
- chore: Plumbing venv into cloud getters by @yhakbar in #6650
- chore(deps): update OpenTelemetry SDK to v1.45.0 by @denis256 in #6686
- chore: Resolve env var defined flags from venv by @yhakbar in #6652
- chore: Increasing discovery boundary test coverage by @yhakbar in #6671
- chore: Wire expansion into dependency parse by @yhakbar in #6679
- chore: Virtualizing util.file.go functions by @yhakbar in #6653
- chore: Upgrading go to 1.26.6 by @yhakbar in #6697
- chore: Nesting the dependency cty map by iteration key by @yhakbar in #6689
- chore(deps): bump github.com/moby/go-archive from 0.2.0 to 0.3.0 by @dependabot[bot] in #6705
- chore: Increasing venv coverage further by @yhakbar in #6677
- chore: Adding integration coverage for expanded dependencies by @yhakbar in #6693
- chore: Use local catalog for TestCatalogWithLocalDefaultTemplate by @yhakbar in #6700
- chore: Updating TestNewSignalsForwarderMultipleUnix to actually check for the signal by @yhakbar in #6701
- chore: Use an injectable cap in TestPartialEval_DeeplyNestedExpressionReturnsTypedError by @yhakbar in #6702
- chore: Moving the TestDiscovery_GraphConcurrentConfigAccessWithRacing in-memory by @yhakbar in #6703
- chore: Wiring expansion into the unit and stack parse by @yhakbar in #6694
- chore: Completing venv abstraction by @yhakbar in #6698
- chore: addressing PR #6736 comemtns by @denis256 in #6741
- chore: Adding sandboxed unit tests in CI by @yhakbar in #6742
- chore: Isolate TestDependencyOutputSkipDependencyOutputsFlag fixtures by @yhakbar in #6740
- chore: Preventing flakes from TestNewSignalsForwarderMultipleUnix by @yhakbar in #6750
- chore: Adding FS sandboxed unit tests in CI by @yhakbar in #6754
- chore: Running go fix ./... by @yhakbar in #6758
- Aug 13, 2026
- Date parsed from source:Aug 13, 2026
- First seen by Releasebot:Aug 13, 2026
v1.1.3
Terragrunt ships a broad release with major bug fixes, new experiments, and smoother workflows. It improves dependency mocks, scaffold behavior, provider caching, filtering, and hooks, while adding browse-tui, bounded discovery, mutable generate output, and OCI source support.
๐ Bug Fixes
Fixed Unsupported attribute errors for values.* inputs that autoinclude overrides
A unit input referencing a values.* key that the unit's values file doesn't define no longer fails with Unsupported attribute when an autoinclude block supplies that input. The autoinclude value is applied as intended.
# stacks/terragrunt.stack.hcl unit "subnet" { source = "../units/subnet" path = "subnet" autoinclude { dependency "vpc" { config_path = unit.vpc.path mock_outputs = { vpc_id = "mock" } } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } } values = { cidr_block = "10.0.0.0/24" } } # units/subnet/terragrunt.hcl inputs = { vpc_id = values.vpc_id # supplied by autoinclude, not the values file cidr_block = values.cidr_block # still resolves from values file }Fixed overwrite_terragrunt and remove_terragrunt on files with no trailing newline
generate blocks using if_exists = "overwrite_terragrunt" or if_disabled = "remove_terragrunt" failed to properly handle existing files when the file at the target path had no newline after its first line, empty files included.
Terragrunt now properly handles files like this, so a file carrying the Terragrunt signature is overwritten or removed as configured, and a file without it produces the usual error naming the path Terragrunt would not touch.
Dependency mock_outputs apply when the state bucket doesn't exist yet
When reading a dependency's outputs directly from remote state (--dependency-fetch-output-from-state), Terragrunt fell back to mock_outputs only when the state object was missing, not when the S3 bucket itself didn't exist. A dependency on an environment that hadn't been bootstrapped yet would fail instead of using its mocks.
A missing bucket is now treated the same as a missing state object, so commands like plan and validate can resolve mocks before the dependency's backend has been created.
Source permissions preserved on hidden directories copied by include_in_copy
With the fast-copy strict control enabled, a hidden directory that Terragrunt copied due to include_in_copy matching something within it took the permissions of the first file generated within it, instead of the permissions it had in the source.
Those directories now keep their source permissions, matching the copy Terragrunt performs with the control disabled.
Applied the positive half of a filter that begins with a negation
When a --filter query began with a negation, Terragrunt treated the whole query as an exclusion. The expressions chained after the negation stopped restricting the selection and only narrowed what got subtracted, so components matching none of them came back in the results. Those expressions are now applied.
$ terragrunt list bar baz foo $ terragrunt list --filter '!name=foo | name=bar' bar baz foo $ terragrunt list --filter '!name=foo | name=bar' barThis follows the left-to-right refinement that | has everywhere else: each expression narrows what the one before it selected. A query is only treated as an exclusion when every one of its expressions is negated, such as '!name=foo' or '!name=foo | !name=bar'.
See Combining Expressions for how negation, intersection and union interact.
Fixed a race condition that left cached provider archives in the working directory
With the provider cache server enabled via --provider-cache, a race let the server start responding to requests before it had finished preparing the directories it caches into. A provider requested in that window had its archive and lock file written relative to the working directory instead of into the cache, leaving zip files behind in your project.
That race condition has been fixed. Providers now always download into the cache directory.
Fixed a race condition between concurrent Terragrunt runs downloading providers
A race condition in the logic used to synchronize provider downloads meant that two Terragrunt runs on the same machine could interfere with each other while caching the same provider. Each run staged its downloads at the same path, so a run that finished first could delete an archive another run was still unpacking, failing that run with failed to open zip archive.
That race condition is now fixed. Two runs can cache the same provider at the same time.
Fixed space-delimited flag values in providers lock
The space-delimited form, providers lock -platform linux_amd64, now reaches OpenTofu and Terraform intact. Previously it was the attached form, -platform=linux_amd64, that worked: given the value as a separate argument, Terragrunt moved it to the end of the command, where it was read as a provider address and the run failed with Invalid provider type "linux_amd64".
-fs-mirror and -net-mirror were moved the same way, and now keep their values too.
With --provider-cache enabled, platforms are also split correctly across the per-platform providers lock runs used to warm the cache.
Fixed scaffold on units and stacks
terragrunt scaffold read every source as an OpenTofu/Terraform module. Given a unit or a stack, which are Terragrunt configurations rather than OpenTofu/Terraform modules, it exited successfully having written an invalid terragrunt.hcl file.
Units and stacks are now scaffolded the way the Catalog TUI scaffolds them: their files are copied into the working directory for you to edit in place, along with a terragrunt.values.hcl listing every values.* reference the configuration makes.
terragrunt scaffold 'github.com/gruntwork-io/terragrunt-scale-catalog//units/aws/oidc/iam-oidc-role'Copying refuses to overwrite: a file that would land on an existing path stops the command before anything is written. Modules and templates are unaffected and are still scaffolded from their variables.
See Scaffold for what gets copied and how the values file is filled in.
Answered every prompt when input is piped in
A run that asks for confirmation more than once, such as terragrunt backend delete prompting for both the lock table entry and the state object, used to read only the first answer when the answers were piped in rather than typed. The remaining answers were discarded while reading ahead, and the next prompt failed with an end-of-input error. Every prompt in a run now reads from the same input, so piping yes for each one works.
Stack dependencies honor mock_outputs with --dependency-fetch-output-from-state
A dependency block that reads outputs from a stack (its config_path points at a terragrunt.stack.hcl directory) used to fail when a unit in that stack had no state yet, even when the dependency declared mock_outputs. This blocked commands like plan and validate against a stack that hadn't been applied.
Such a dependency now falls back to mock_outputs for the units that have no state yet. In a partially applied stack, applied units resolve to their real outputs while the rest use their mocks.
Mocks for a stack dependency are keyed by unit name, so mock_outputs has to be a map or object. Declaring it as any other type now reports that directly, instead of leaving the units it can't cover out of the stack outputs.
Fixed --config= being ignored by the tflint hook
The built-in tflint hook reads the configuration file out of the arguments you give it, then uses that path for tflint init and for the lint run. It only recognized the space-separated --config spelling, so a hook written as:
before_hook "tflint" { commands = ["plan"] execute = ["tflint", "--config=custom.tflint.hcl"] }was treated as though no configuration file had been named at all. Terragrunt searched the unit directory and its parents for a .tflint.hcl file instead, and either failed with a config-not-found error or ran tflint init against whatever unrelated configuration the search turned up. Terragrunt now recognizes --config , --config=, -c , and -c=.
The hook also builds --var arguments from the unit's inputs and from TF_VAR_ entries in extra_arguments blocks. Those arguments came out in a different order on every run, which made the logged command line, and anything comparing it between runs, needlessly unstable. They are now ordered by variable name.
๐งช Experiments Added
block-iteration experiment reserves the expansion block
The block-iteration experiment has been added as the gate for iterating a dependency, unit, or stack block over a count or for_each, declared through a nested expansion block, along with an enabled attribute on unit and stack blocks.
In this release the flag is reserved only, and enabling it has no behavioral effect. Writing an expansion block without the experiment now reports an error naming the flag, rather than leaving the block to be silently discarded:
the unit "app" block in /path/to/terragrunt.stack.hcl uses an expansion block, which requires the 'block-iteration' experiment; enable it with --experiment block-iterationTrack progress and share feedback in #4504.
bounded-discovery โ Added a directory boundary for graph traversal
Filter expressions that traverse the dependency graph reach beyond the working directory: dependents (--filter '...{unit}') by walking up to the Git repository root, dependencies (--filter '{unit}...') by following declared paths. Either way, Terragrunt reads and parses every configuration it touches. In monorepos with isolated environments, that traversal can fail or do wasted work reading sibling environments.
Enable the new bounded-discovery experiment to set a boundary for that traversal. The --discovery-boundary flag (env: TG_DISCOVERY_BOUNDARY) replaces the Git repository root as the enclosure for a whole run:
cd environments/staging terragrunt run --all plan --experiment bounded-discovery --filter '...{vpc}' --discovery-boundary .The experiment also unlocks an inline (dir) boundary operand, which bounds a single expression and overrides the flag. It occupies the same slot as a traversal depth, so it bounds discovery by location the way a number bounds it by graph hops:
cd environments/staging terragrunt run --all plan --experiment bounded-discovery --filter '(.)...{vpc}'Any configuration that resolves outside the boundary, whether a dependent or a dependency, is not read, parsed, or returned: find does not list it and run --all does not run it. Configurations inside the boundary are discovered as usual.
The boundary must be an existing directory, and relative paths are resolved against the working directory. Dependent traversal searches upward from the working directory, so filters that use it also need the boundary to be the working directory or one of its parents. Dependency traversal follows declared paths from the units a filter matched, so dependency-only filters accept any directory, including one below the working directory:
# From the repository root, follow app's dependencies but keep them within prod terragrunt find --experiment bounded-discovery --filter '{./prod/app}...' --discovery-boundary ./prodReserving ( and ) for the boundary operand changes how --filter reads those characters everywhere, not only when the experiment is enabled. An expression such as --filter '1...(foo | bar)' previously matched a unit literally named (foo or bar); it is now rejected as a malformed boundary. Wrap a name or path containing parentheses in braces (e.g. --filter '{./weird(name)}') to keep it literal.
browse-tui โ Added an interactive browser for your estate
The new browse-tui experiment adds the terragrunt browse command. With the experiment enabled, terragrunt browse opens a three-column Terminal User Interface (TUI) browser of your infrastructure estate: the parent directory on the left, the current directory in the middle, and a detail pane on the right showing metadata for the highlighted unit, stack, or directory. The browser opens immediately and fills in metadata as discovery completes in the background.
Enable it with --experiment browse-tui or TG_EXPERIMENT=browse-tui. See the experiment documentation for the keybindings, search, and the criteria for stabilization.
mutable-generate โ Deduplicated generate block output
The mutable-generate experiment has been added. With it enabled, the contents a generate block produces are stored in the Content Addressable Store (CAS), and the file written at path is a read-only link to that stored copy rather than a file of its own.
Since the stored copy is addressed by the hash of its contents, anything generating identical contents links to the same copy. A generate block inherited by several hundred units therefore costs one copy in .terragrunt-cache rather than several hundred.
The link is read-only because that copy is shared. Where a generated file does need to be edited in place, a new mutable attribute on the generate block gives it a writable file of its own:
generate "provider" { path = "provider.tf" if_exists = "overwrite" mutable = true contents = "..." }Setting mutable without the experiment enabled is an error, since earlier Terragrunt versions reject the attribute. The CAS is required, so --no-cas writes generated files directly and mutable has no effect.
For details, see the experiment documentation.
optional-dependency-outputs โ Added --no-dependency-outputs flag to skip dependency output resolution
Added a --no-dependency-outputs flag that skips all dependency output resolution globally, mirroring the existing skip_outputs = true attribute on individual dependency blocks.
The feature is gated behind the optional-dependency-outputs experiment:
TG_EXPERIMENT=optional-dependency-outputs terragrunt run --no-dependency-outputs -- initUsing --no-dependency-outputs without enabling the optional-dependency-outputs experiment will return an error.
Thanks to @pjrm for contributing this feature!
๐งช Experiments Updated
catalog-format โ Added reading the catalog as JSON Lines
The catalog command draws a terminal user interface, and refuses to start where there is no terminal to draw it on. With the catalog-format experiment enabled, --format=jsonl writes the same discovery to standard output instead, as one JSON object per line:
terragrunt catalog --experiment=catalog-format --format=jsonl | jq -c '{kind, title, component_source}'Entries are written as they are discovered rather than collected first, so output is readable while the remaining repositories are still loading, and a reader that stops early ends the command quietly:
terragrunt catalog --experiment=catalog-format --format=jsonl | head -5Note
Closing the pipeIn this example, the head program exits after reading in five lines, and Terragrunt detects the SIGPIPE signal from the OS, and shuts down cleanly.
Entries appear in discovery order, which interleaves the repositories being loaded and differs between runs. Every entry carries the complete body of the component's README in the doc field. Combine usage of Terragrunt with other tools like jq to drop it.
terragrunt catalog --experiment=catalog-format --format=jsonl | jq -c 'del(.doc)'Entries follow a published JSON schema. For the fields and their meanings, see Non-interactive catalog.
--format=tui is the default, and leaves the terminal user interface exactly as it was.
catalog-format โ Added reading the catalog as Markdown
The catalog-format experiment gains a second non-interactive format. Where --format=jsonl writes a record per catalog entry for a program to parse, --format=md writes one Markdown document for a person or an agent to read:
terragrunt catalog --experiment=catalog-format --format=md > catalog.mdEach entry becomes a section holding the metadata the catalog user interface shows for it, the source the component is scaffolded from, and the component's README. Sections are written as entries are discovered, so the document is readable while the remaining repositories are still loading.
READMEs are reproduced inside fenced blocks, so the headings one carries are not read as sections of the catalog document. The document closes with a table naming every component it holds and a count of what was discovered, which is how a reader tells a complete document from one that was cut short by a consumer that stopped reading.
For the fields each section carries, see Non-interactive catalog.
oci โ Added OCI sources for stack units and stacks
terragrunt.stack.hcl now accepts oci:// sources in unit and stack blocks, so a stack can pull its components straight from an OCI registry. Without the oci experiment enabled, such a source fails with a clear error instead of an unsupported-scheme failure.
oci โ Added OpenTofu CLI-config credentials for OCI module sources
oci:// module downloads now read OpenTofu's CLI-config credentials, so one configuration serves both OpenTofu and Terragrunt.
Terragrunt honors the oci_credentials "[/]" blocks (username and password, OAuth tokens, or a docker_credentials_helper, which like tofu may only be set on a whole registry) and the oci_default_credentials fallback helper. A TF_CLI_CONFIG_FILE or TERRAFORM_CONFIG value selects the config file outright; otherwise Terragrunt reads the first of ~/.tofurc and ~/.terraformrc that exists, and merges the *.tfrc and *.tfrc.json files in OpenTofu's config directory.
Terragrunt picks the most specific matching source across CLI config and ambient Docker config; an explicit CLI-config entry wins when both match equally. Set discover_ambient_credentials = false in the oci_default_credentials block to use CLI config only.
โ๏ธ Process Updates
Go bumped to v1.26.5
The version of Golang used to compile the Terragrunt binary has been updated from v1.26.0 to v1.26.5.
Thanks to @apoiget for contributing this upgrade!
Pull Requests
โจ Features
- feat: Adding graph boundary via () syntax by @yhakbar in #6365
- feat: Adding --discovery-boundary flag by @yhakbar in #6355
- feat(getter): OpenTofu CLI-config credentials for oci:// sources by @denis256 in #6531
- feat: Adding browse by @yhakbar in #6219
- feat: Adding mutable attribute to the generate block by @yhakbar in #6563
- feat: Adding md format for catalog by @yhakbar in #6608
- feat: Add --skip-dependency-outputs flag to skip dependency output resolution by @pjrm in #6422
๐ Bug Fixes
- fix(providercache): log -lockfile=readonly skip at debug level by @bryanhorstmann in #6577
- fix: Fixing handling of EOF in generate blocks by @yhakbar in #6592
- fix: Fixing the --config= form of flags used in the tflint hook by @yhakbar in #6591
- fix: Fixing fast-copy ancestor directory permissions by @yhakbar in #6593
- fix: Addressing providers lock -platform usage with space delimited values by @yhakbar in #6597
- fix: Fixing bug with negation | positive expression in the same query. by @yhakbar in #6598
- fix: Fixing provider cache server archive dir race by @yhakbar in #6620
- fix: Fixing scaffold on units and stacks by @yhakbar in #6607
- fix: Addressing feedback from #6565 and #6605 by @yhakbar in #6628
- fix: autoinclude values override for inputs by @denis256 in #6626
- fix: Fixing md format catalog escaping by @yhakbar in #6638
- fix: Plumbing through evalCtx for discovery boundary by @yhakbar in #6632
- fix: Fixing stack dependency mock outputs by @yhakbar in #6530
- fix: Fixing issue where dependency mock outputs aren't used when bootstrapping hasn't run yet. by @yhakbar in #6534
๐๏ธ Performance
- perf: Reducing allocations in tree parse by @yhakbar in #6648
๐ Documentation
- docs: Add call out for terragrunt scale in quick start by @yhakbar in #6583
- docs: document oci module sources, authentication, and caching by @denis256 in #6636
- docs: Cleaning up changelog for v1.1.3 by @yhakbar in #6669
- docs: Cleaning up experiment docs by @yhakbar in #6627
- docs: address review feedback on the oci and autoinclude docs by @denis256 in #6643
โ Tests
- test(getter): integration tests against a local OCI distribution registry by @denis256 in #6614
- test: prove oci module portability between tofu and terragrunt by @denis256 in #6629
- test(git): add unit coverage for internal/git command wrappers and parsers by @denis256 in #6661
๐งน Chores
- chore: Pin exact provider versions for terralith to terragrunt guide by @yhakbar in #6578
- chore: Using vfs handle for ParseFromFile by @yhakbar in #6561
- chore: Walk in discovery with vfs by @yhakbar in #6564
- chore: Registring catalog-format experiment by @yhakbar in #6582
- chore(deps): update AWS, Azure, GCP SDKs by @denis256 in #6590
- chore: Continuing clean-up of go test ./... on a fresh clone of the repo by @yhakbar in #6553
- chore: Fixing usage of deprecated aws sdk by @yhakbar in #6600
- chore: Cleaning up profile tests per feedback in #6553 by @yhakbar in #6599
- chore: Clean-up by @yhakbar in #6584
- chore: Addressing feedback from #6365 and #6355 by @yhakbar in #6603
- chore: Register the block-iteration experiment by @yhakbar in #6562
- chore: address review feedback from #6531 by @denis256 in #6609
- chore: Addressing flake in TestCatalogJSONLFormatCleansUpOnEarlyExit by @yhakbar in #6613
- chore: updated TestDiscovery_GraphConcurrentConfigAccessWithRacing to use VFS by @denis256 in #6622
- chore: Adding expansion detection and internal expansion logic by @yhakbar in #6565
- chore: Adding expansion blocks to the configs that accept expansion by @yhakbar in #6605
- chore: Threading venv through getters and hcl fmt by @yhakbar in #6621
- chore: Addressing feedback from #6621 by @yhakbar in #6633
- chore: Fixing experiment tag in sidebar by @yhakbar in #6635
- chore: Updating mem exec so that it fails closed by @yhakbar in #6634
- chore: Gate real hg usage test behind the exec build flag by @yhakbar in #6637
- chore: Replacing aws provider with null provider in init-cache fixture by @yhakbar in #6639
- chore: Cleaning up NewParsingContext constructor by passing in venv as a param by @yhakbar in #6630
- chore: Addressing lint finding by @yhakbar in #6649
- chore: Refactoring markdown deps into internal/md by @yhakbar in #6640
- chore: Adding unit tests for internal packages by @denis256 in #6660
- chore: Bumping Go to 1.26.5 (#6664) by @apoiget in #6666
- chore: Dropping stale tree parse test case by @yhakbar in #6672
Curated by the Releasebot team
Releasebot is an aggregator of official release notes from hundreds of software vendors and thousands of sources.
Our editorial process involves the manual review and audit of release notes procured with the help of automated systems.