๐๏ธ Performance Improvements
Faster read-file tracking with the mark-many-as-read experiment
With the mark-many-as-read experiment enabled, Terragrunt records every module file it marks as read during parsing. The bookkeeping for that record scaled quadratically: each new path was checked against every path recorded so far, which got expensive for units with large local module sources, and monorepos paid that cost again for every unit and every command.
Recording a path now takes constant time no matter how many paths came before it, and re-marking already-recorded files is cheaper still. The reading lists reported by find and list are unchanged.
๐ Bug Fixes
assume_role: preserve commas inside list expressions
Terragrunt previously failed to correctly parse assume_role attributes containing list values such as transitive_tag_keys or policy_arns. Commas inside nested list expressions were incorrectly treated as top-level separators, causing generated configurations to fail with parsing errors.
assume_role = {
role_arn = "arn:aws:iam::123456789012:role/test-role"
transitive_tag_keys = ["Project", "Projects"]
}This resulted in errors similar to:
Missing item separator; Expected a comma to mark the beginning of the next item.
Terragrunt now preserves commas inside nested list and object expressions when parsing assume_role blocks, allowing configurations containing array attributes to be processed correctly.
Thanks to @Rahul-Kumar-prog for contributing this fix!
Completed experiments now evaluate as permanently enabled
Features gated behind a completed experiment were treated as disabled instead of permanently enabled, so functionality that graduated out of experiment status could silently stop working.
The one affected code path was hcl validate --inputs with a git filter expression such as --filter '[HEAD~1...HEAD]': after the filter-flag experiment completed, the command stopped preparing git worktrees for the filter. Git filter expressions now work with hcl validate --inputs again, matching find, list, and the other commands that accept filters.
Exposed-include resolution errors now name the include block, file, and failing field
When resolving an include block with expose = true, Terragrunt surfaced low-level parsing or conversion errors with no indication of which include block, file, or field was at fault. This was especially hard to debug for errors that carry no source location, such as:
unsuitable value: a bool is required
The error is now annotated with the include block name, the included (parent) file path, and a single dotted locator for the failing field โ the top-level config field (dependency, inputs, locals, or feature) plus the attribute path within it when go-cty can determine one:
exposed include "root" (/path/to/root.hcl): dependency.outputs["enabled"]: unsuitable value: a bool is required
When go-cty cannot resolve a precise attribute path, the locator degrades to just the field name:
exposed include "root" (/path/to/root.hcl): dependency: unsuitable value: a bool is required
Errors that originate in HCL parsing already carry a source range (file:line:column) and are preserved unchanged. This narrows the search from the entire configuration tree to a specific file and field.
Intersecting a graph traversal with another filter no longer drops the traversed components
A graph traversal combined with an intersected filter dropped the components reached in discovery.
e.g., ...a-dependent | type=unit (the dependents of a-dependent, intersected with type of units) returned only a-dependent itself instead of its dependents, and git-change traversals such as ...[HEAD~1...HEAD] | type=unit lost the dependents of the changed units.
A component that matched both a graph expression target and a positive filesystem or git filter was classified as discovered before the graph traversal ran, so the traversal never expanded from it. Terragrunt now checks graph expression targets first, so intersecting a traversal with another filter keeps the dependencies and dependents it reaches.
generate blocks now honor hcl_fmt
Terragrunt now accepts hcl_fmt on generate blocks and preserves the setting when configurations are parsed, written, and parsed again. This lets generated .tf, .hcl, and .tofu files opt out of automatic HCL formatting by setting hcl_fmt = false, matching the existing generate = { ... } attribute-map behavior.
Telemetry resource now honors OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES
Terragrunt previously hardcoded the service.name resource attribute to terragrunt for every emitted trace and metric, ignoring the standard OpenTelemetry environment variables. Multiple Terragrunt invocations could not be distinguished in an OpenTelemetry backend without an intermediate collector to rewrite the attribute.
The resource is now composed via resource.New with WithFromEnv() placed after Terragrunt's defaults, so OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES are honored on every span and metric. Per the OpenTelemetry specification, OTEL_SERVICE_NAME takes precedence over a service.name entry in OTEL_RESOURCE_ATTRIBUTES. The default service.name remains terragrunt when neither variable is set.
s3:: sources: support virtual-hosted-style URLs
s3:: source URLs using the virtual-hosted-style S3 endpoint format were rejected:
terraform {
source = "s3::https://my-bucket.s3.us-west-2.amazonaws.com/terraform/modules/myapp.zip"
}This resulted in errors like:
ERROR downloading source url s3::https://my-bucket.s3.us-west-2.amazonaws.com/...
* URL is not a valid S3 URL
Terragrunt now accepts every AWS S3 endpoint form, including virtual-hosted-style URLs (<bucket>.s3.<region>.amazonaws.com) and modern path-style URLs (s3.<region>.amazonaws.com).
Windows console mode is restored when Terragrunt exits
On Windows, running a Terragrunt command from Nushell could leave the shell unable to read input afterward, with keystrokes such as the arrow keys appearing as raw escape sequences instead of being interpreted.
While it runs, Terragrunt reconfigures the console it shares with the parent shell so that terminal escape sequences are processed, but it did not put the original mode back when it exited. PowerShell reapplies its own console settings on every prompt and recovers on its own, so the problem surfaces only in shells that keep the inherited mode, such as Nushell. Terragrunt now records the console mode at startup and restores it on exit, returning the shell to the state it was in beforehand.
Reported in #6245.
๐ Documentation Updates
Clean Markdown is available for every docs page at <url>.md
Every docs page is now served as clean Markdown at the same URL with .md appended. For example, /getting-started/install is also available at /getting-started/install.md.
curl https://docs.terragrunt.com/getting-started/install.mdThe .md version contains the page content without the site navigation or other surrounding HTML, which makes it well suited as context for LLMs and AI tooling: it is smaller and carries only the documentation itself. Coverage includes every page, including the CLI command reference and the changelog.
This complements the existing llms.txt and llms-full.txt files by providing a per-page Markdown source.
๐งช Experiments Added
optional-hooks โ Add experimental --no-hooks flag support for terragrunt run
The terragrunt run command now supports an experimental --no-hooks flag for disabling hook execution during command runs.
The feature is gated behind the optional-hooks experiment and skips execution of before_hook, after_hook, and error_hook blocks when enabled.
TG_EXPERIMENT=optional-hooks terragrunt run --no-hooks planThis feature is currently experimental because disabling hooks changes Terragrunt execution semantics and may evolve in future releases.
Using --no-hooks without enabling the optional-hooks experiment will return an error.
hook-context-env experiment exposes additional TG_CTX_* env vars to hooks
Enable the new hook-context-env experiment to surface three additional environment variables to every before_hook, after_hook, and error_hook:
TG_CTX_HOOK_TYPEโbefore_hook,after_hook, orerror_hook, identifying which lifecycle phase invoked the hook.TG_CTX_SOURCEโ the resolved terraform source URL (CLI--sourceoverride, else evaluatedterraform.sourcewith source-map applied, else.).TG_CTX_TERRAGRUNT_DIRโ the directory of the current Terragrunt config.
terragrunt run --all --experiment hook-context-env -- applyThese variables make it easier to share a single hook script across lifecycle phases and to access the unit's source and config directory without threading them through hook arguments.
๐งช Experiments Updated
cas: fallbacks now emit telemetry
When the cas experiment is enabled and a CAS operation cannot complete, Terragrunt falls back to a slower path (the standard download client, or a temporary clone when the shared git store is unavailable) and keeps going. Until now the only record of a fallback was a warning in the logs, which made it impractical to measure how often CAS degrades across a fleet.
Each fallback now also emits a cas_fallback telemetry event whose reason attribute identifies the cause: init_error, getter_error, git_store_unavailable, probe_failure, or stack_generation_error. Operators collecting OpenTelemetry traces or metrics from Terragrunt can count and alert on these events to judge CAS health before relying on it by default.
CAS flags for the catalog command
The catalog command now accepts the --no-cas and --cas-clone-depth flags, which were already available on run, stack generate, and stack run. When --no-cas is set, catalog repositories are cloned with plain Git even if the cas experiment is enabled. --cas-clone-depth controls the git clone --depth value the CAS uses when cloning catalog repositories.
terragrunt catalog --experiment cas --cas-clone-depth=-1cas โ update_source_with_cas requires a literal source string
When a catalog unit, stack, or terraform block set update_source_with_cas = true with a source that was not a literal string, rewriting silently produced a wrong source. Interpolation such as "../units/${local.name}" had the interpolated portion dropped, leaving a bare prefix; a reference such as local.foo resolved to the directory containing the block itself. In both cases stack generation packaged the wrong directory without any error.
Stack generation now fails with an error explaining that update_source_with_cas requires a literal source string. Non-literal expressions, including interpolation, function calls, and references like local.foo, are rejected.
cas โ Malformed cas:: references fail with a clear error
A cas:: source with a malformed hash, such as cas::sha1:a, used to fail with an opaque internal error while looking the hash up in the store.
CAS references are now validated up front: the hash must be lowercase hexadecimal with exactly 40 characters for sha1 or 64 for sha256. References that don't match are rejected with an error identifying the bad reference.
cas โ Repositories with submodules now clone correctly
Cloning a repository that contains git submodules through the Content Addressable Store failed while ingesting the repository:
git_cat_file: fatal: Not a valid object name <hash>
A submodule appears in the repository tree as a pointer to a commit in another repository, so the object behind it cannot be read from the repository being cloned.
The CAS now fetches each submodule from the URL registered in .gitmodules at its pinned commit and materializes its contents in place, including nested submodules. Relative submodule URLs (such as ../sibling.git) are resolved against the parent repository URL, matching git's behavior. Submodule contents are stored and deduplicated like any other content, so repeated clones reuse the cache.
catalog-redesign โ Failures now exit nonzero and name the sources that failed
The redesigned catalog exited with code 0 even when it failed: a session that ended on an unreachable repository, a failed scaffold, or a failed copy reported success in its exit code. Repositories that failed to load during discovery were dropped too: the warning logged for each one was drawn over by the full-screen interface, so a run where every source failed showed the same "No catalog sources were discovered" screen as a run that genuinely found nothing.
The catalog now exits nonzero when the session ends on a failure: a discovery failure that leaves nothing to browse, a failed scaffold, or a failed copy. Quitting a working session still exits 0. When some sources fail to load while others succeed, the catalog stays usable and a clean quit still exits 0; the component list shows how many sources failed, and the failed repositories are printed with their causes after the catalog closes. When every source fails, the error screen lists each failed repository instead of claiming nothing was found, and dismissing it exits nonzero.
Running terragrunt catalog without an interactive terminal, such as in CI, used to fail with a raw error from the underlying TUI library:
bubbletea: error opening TTY: bubbletea: could not open TTY: open /dev/tty: no such device or address
It now fails immediately with an error stating that the catalog command requires an interactive terminal.
catalog-redesign โ Scaffolding a component no longer fails with a path-traversal error
Scaffolding a component from the catalog (pressing s) could fail on macOS while downloading the source:
subdirectory component contain path traversal out of the repository
The catalog caches each repository under the system temporary directory, which macOS reports through a symlink (/var/folders/... pointing at /private/var/folders/...). The source location Terragrunt handed to the downloader was built against the unresolved path, so it pointed outside the cached repository and was rejected.
Terragrunt now resolves the temporary directory before discovering components, so the source stays inside the repository and scaffolding proceeds.
stack-dependencies: HCL tooling now handles autoinclude
Two tooling gaps around the experimental autoinclude block are closed:
-
hcl validatenow validatesautoincludeblocks. With thestack-dependenciesexperiment enabled, validating aterragrunt.stack.hclthat declaresautoincluderuns the same strict checks asterragrunt stack generate. A malformed block (for example, alocalsblock insideautoinclude) is now reported at validation time instead of passinghcl validateand only failing later during generation. Without the experiment, validation behavior is unchanged. -
read_terragrunt_config()can read stack-level autoinclude files. Reading a generatedterragrunt.autoinclude.stack.hclpreviously failed because the file was decoded as a unit configuration, which rejects itsunitandstackblocks. With the experiment enabled, the file is now decoded as the stack-file fragment it is, returning itsunitandstackblocks the same way reading aterragrunt.stack.hcldoes. Unit-levelterragrunt.autoinclude.hclfiles already read correctly and continue to do so.
stack-dependencies: autoinclude merges like a regular include
A generated unit autoinclude (terragrunt.autoinclude.hcl) now merges into the unit's config using the same default merge as a regular include, which is a shallow merge, applied uniformly across generation, full parse, and discovery. Top-level keys from the unit and the autoinclude combine, and on a conflict the autoinclude wins and replaces the unit's value rather than deep-merging nested maps; locals stay local in scope.
A generated stack autoinclude (terragrunt.autoinclude.stack.hcl) injects unit and stack blocks into the generated terragrunt.stack.hcl. An injected block whose name matches an existing unit or stack now overrides that block wholesale, consistent with unit autoinclude override semantics, and an injected block with a new name is added. This applies uniformly across generation, full parse, and discovery, so a name match no longer produces a duplicate-name error. A stack autoinclude may not declare a top-level dependency block (stacks have no dependencies; declare the dependency inside the target unit's own autoinclude).
A dependency block injected through an autoinclude is now available before a unit's remote_state is evaluated, so referencing dependency.<name>.outputs.<key> there no longer fails. remote_state now behaves the same as generate blocks.
stack-dependencies: autoinclude blocks can reference values.*
An autoinclude block may now reference the stack's values.*. Previously a values.* reference was rejected at stack generate time, except in a dependency config_path. It now resolves to a literal like local.*, unit.<name>.path, and stack.<name>.path, wherever it appears: inputs, generate, remote_state, mock_outputs, and config_path.
Function calls in an autoinclude now resolve at generate time too, in the terragrunt.stack.hcl context, instead of being kept verbatim and evaluated in the generated unit. Only a dependency.* reference (a dependency's outputs) stays verbatim and resolves inside the unit; in a mixed expression the stack-level parts resolve and only the dependency.* reference is kept.
Because functions now evaluate against the stack file rather than the unit, directory and include functions report the stack file's location: get_terragrunt_dir returns the stack file's directory, and path_relative_to_include returns ".". If you relied on these resolving in the unit, move them to the unit's own configuration, or derive a per-unit value such as a remote_state backend key from unit.<name>.path.
A locals block inside an autoinclude remains rejected; declare stack-level locals in terragrunt.stack.hcl instead.
stack-dependencies: stack dependencies resolve values.* in the target stack's locals
Expanding a dependency that points at a generated stack directory no longer fails when that stack's terragrunt.stack.hcl reads values.* in its locals block. Previously, terragrunt stack generate succeeded but terragrunt run --all then failed with There is no variable named "values" while expanding the dependency into its units.
Dependency expansion now reads the generated terragrunt.values.hcl next to each terragrunt.stack.hcl it visits, including nested stacks, so each nesting level resolves values.* from its own values file, the same way a full stack parse does.
stack-dependencies: component path references in values no longer break next to autoinclude blocks
A unit or stack block's values can reference unit.<name>.path and stack.<name>.path even when another block in the same terragrunt.stack.hcl declares an autoinclude. Previously, the presence of any autoinclude block made stack generate reject those references with Unknown variable; There is no variable named "unit", while the same file without an autoinclude generated fine.
Pull Requests
โจ Features
- feat: add hook-context-env experiment by @arnaud-dezandee in #6189
- feat: Adding CAS fallback telemetry by @yhakbar in #6298
๐ Bug Fixes
- fix: Fixing filter tests on Windows by @yhakbar in #6247
- fix: Fix Windows nushell bug by @yhakbar in #6250
- fix(codegen): fix assume_role parsing failure when transitive_tag_keys or policy_arns arrays are present by @Rahul-Kumar-prog in #5975
- fix: Fixing scaffold path traversal check by @yhakbar in #6255
- fix(telemetry): honor
OTEL_SERVICE_NAMEandOTEL_RESOURCE_ATTRIBUTESenvironment variables by @Tensho in #6256 - fix: Fixing graph traversal bug with intersected filter by @yhakbar in #6270
- fix: resolve locals in autoinclude mock_outputs by @denis256 in #6274
- fix: report the offending field in Terragrunt config errors by @denis256 in #6284
- fix: Fixing values without autoinclude by @yhakbar in #6290
- fix: Fixing values references in locals of
terragrunt.stack.hclfiles by @yhakbar in #6291 - fix: Fixing experiment promotion by @yhakbar in #6293
- fix: Adding support for submodules in CAS by @yhakbar in #6294
- fix: Validating CAS sources by @yhakbar in #6296
- fix: Support
autoincludeinhcl validateand fixread_terragrunt_config()for configurations usingautoincludeby @yhakbar in #6297 - fix: Fixing legacy virtual hosted style S3 URLs by @yhakbar in #6311
- fix: Addressing lint findings by @yhakbar in #6312
๐ Documentation
- docs: Documenting CAS Getters by @yhakbar in #6251
- docs: Support
completedSinceby @yhakbar in #6252 - docs: Since/Before cleanup by @yhakbar in #6277
- docs: Since/Before cleanup workflow by @yhakbar in #6276
- docs: Support
.mdchangelog files by @yhakbar in #6286 - docs: Adding
autoincludedocumentation by @yhakbar in #6299
๐งน Chores
- chore: weekly tests reporting by @denis256 in #6191
- chore: Add thanks for #5975 by @yhakbar in #6253
- chore: autoinclude merge fixes by @denis256 in #6248
- chore: cleaned unused code by @denis256 in #6260
- chore: Add CodeRabbit release check by @yhakbar in #6278
- chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 by @dependabot[bot] in #6265
- chore(deps): bump docker/setup-docker-action from 5.1.0 to 5.2.0 by @dependabot[bot] in #6266
- chore(deps): bump aws-actions/configure-aws-credentials by @dependabot[bot] in #6267
- chore: Make
autoincludemore flexible, supportingvalues.*by @yhakbar in #6283 - chore: Threading venv through CLI by @yhakbar in #6089
- chore: Isolate test git servers by @yhakbar in #6233
- chore: Add
--no-casflag to thecatalogcommand by @yhakbar in #6292 - chore: Clean up from #6290 by @yhakbar in #6295
- chore: Adding DAG view tests by @yhakbar in #6300
- chore: Speeding up files read by @yhakbar in #6301
๐ Other Changes
- Make guides collapsed to start by @karlcarstensen in #6262
- Fix for light/dark mode by @karlcarstensen in #6264
- Collapse reference section by @karlcarstensen in #6263
- serve clean Markdown at .md for every page by @karlcarstensen in #6281
- Feature/optional hooks experiment by @Rahul-Kumar-prog in #6227
- Update to llms.txt. Added curated llms.txt and configured plugin to also serve full and small by @karlcarstensen in #6280
- Changelog by @karlcarstensen in #6285
- Fix #5054: support hcl_fmt in generate blocks by @DadaVinqi in #6287
- fix Fixing catalog exit codes by @yhakbar in #6302