๐ v1.2.0 Release Candidate
This is the first release candidate for Terragrunt v1.2.
This release completes the following experiments:
block-iterationocibounded-discoverycatalog-formatmutable-generateoptional-dependency-outputsoptional-hooksazure-backendversion-attributeprofilingdependency-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 -- planinputs = {
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:
filetemplatefilefilesetfileexists- the
file*hash functions, such asfilesha256
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:
assumeequalassumelistlengthassumelistlengthmaxassumelistlengthminassumemaplengthassumemaplengthmaxassumemaplengthminassumenotnullassumesetlengthassumesetlengthmaxassumesetlengthminassumestringprefixbase64gunzipcidrcontainsephemeralasnullissensitivetemplatestringurldecode
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 []string
Discovery 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=httplets 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=sopslets it decrypt SOPS-encrypted files.Unless it is granted,
sops_decrypt_filefails rather than handing an agent the cleartext of your secrets. -
--allow=envpasses 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 pointsHOMEat an empty directory, so cloud SDKs, the SOPS decrypter, andgitcommands 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-backendblock-iterationbounded-discoverycatalog-formatdependency-fetch-output-from-statemutable-generateocioptional-dependency-outputsoptional-hooksprofilingversion-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
logincommand by @yhakbar in #6804 - feat: Adding portal Catalog API client by @yhakbar in #6805
- feat: Adding portal repositories to
terragrunt catalogby @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
mcpcommand 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-offlinesupport forcatalogby @yhakbar in #6904 - fix: Fixing integration of
expansionwithautoincludeby @yhakbar in #6936 - fix: Patching bug with discovery not respecting
enabledattribute 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
TestStackExpansionGitFilterSelectsRemovedInstanceby @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_roleper unit directory and erroring on malformedexcludeblocks 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.cominstall instructions by @yhakbar in #6984 - docs: Adding docs for the
logincommand 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-iterationexperiment by @yhakbar in #6925 - chore: Completing oci experiment by @denis256 in #6926
- chore: Completing
bounded-discoveryexperiment by @yhakbar in #6928 - chore: Completing the
catalog-formatexperiment by @yhakbar in #6931 - chore: Completing the
mutable-generateexperiment by @yhakbar in #6933 - chore: Completing the
optional-dependency-outputsexperiment 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-attributeexperiment by @yhakbar in #6949 - chore: Fixing
TestAwsDependencyOutputOptimizationtests 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-hooksexperiment 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
TestAwstoTestAWSby @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_SUMMARYin 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
gotestsumbetter 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
multierroruse by @yhakbar in #7016