Repository: gruntwork-io/terragrunt

Issue:
Make -version cache key files configurable
## Current behavior

The `-version` output is currently memoized using a hardcoded list of files defined in `version_check.go`:

```go
var versionFiles = []string{".terraform-version", ".tool-versions", "mise.toml", ".mise.toml"}
```

## Problem
Users cannot modify or extend the list of files used as cache keys, which limits flexibility in custom setups or when using alternative version management tools.

**Proposed solution**
Allow users to configure which files are used as memoization keys for the `-version` execution. This could be achieved via a CLI flag, environment variable, or configuration file.

## Benefits

* More flexibility for teams using custom workflows or tools
* Easier integration with alternative version management setups
* Avoids hardcoding assumptions in the codebase


## Additional context

This would improve maintainability and user control over version caching logic.

Things you might want to address include:

- [ ] Changes required.
- [ ] Implications of the feature.
- [ ] Alternatives considered.
- [ ] Level of effort.

## PoC (Proof of Concept)

Link to a Proof of Concept if you have one:

- [ ] [PoC]()

Including a PoC can help others understand the feature better and implement it faster.

## RFC Not Needed

- [ ] I have evaluated the complexity of this enhancement, and I believe it does not require an RFC.



Hints from issue comments:

Solution patch:
diff --git a/.github/scripts/setup/cas.sh b/.github/scripts/setup/cas.sh
new file mode 100755
index 00000000..8637176c
--- /dev/null
+++ b/.github/scripts/setup/cas.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -euo pipefail
+
+: "${ENV_FILE:?ENV_FILE is not set}"
+
+touch "$ENV_FILE"
+
+printf "export TG_EXPERIMENT='%s'\n" "cas" >> "$ENV_FILE"
diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml
index e9c8f0d4..6f63bb32 100644
--- a/.github/workflows/integration-test.yml
+++ b/.github/workflows/integration-test.yml
@@ -137,6 +137,11 @@ jobs:
             target: ./...
             tags: parse
             run: '^TestParse'
+          - name: CAS
+            os: ubuntu
+            target: ./...
+            setup_scripts:
+              - .github/scripts/setup/cas.sh
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
diff --git a/cli/commands/hcl/validate/validate.go b/cli/commands/hcl/validate/validate.go
index 4b446428..2aba7c83 100644
--- a/cli/commands/hcl/validate/validate.go
+++ b/cli/commands/hcl/validate/validate.go
@@ -101,6 +101,14 @@ func RunValidate(ctx context.Context, l log.Logger, opts *options.TerragruntOpti
 		if err := writeDiagnostics(l, opts, diags); err != nil {
 			return err
 		}
+
+		// If there were diagnostics and stackErr is currently nil,
+		// create a new error to signal overall validation failure.
+		//
+		// This also ensures a non-zero exit code is returned by Terragrunt.
+		if stackErr == nil {
+			stackErr = errors.Errorf("%d HCL validation error(s) found", len(diags))
+		}
 	}
 
 	return stackErr
@@ -276,8 +284,8 @@ func getTerraformInputNamesFromEnvVar(opts *options.TerragruntOptions, terragrun
 	)
 
 	for envName := range envVars {
-		if strings.HasPrefix(envName, tfVarPrefix) {
-			inputName := strings.TrimPrefix(envName, tfVarPrefix)
+		if after, ok := strings.CutPrefix(envName, tfVarPrefix); ok {
+			inputName := after
 			out = append(out, inputName)
 		}
 	}
@@ -450,8 +458,8 @@ func GetVarFlagsFromArgList(argList []string) ([]string, []string, error) {
 			vars = append(vars, splitArg[1])
 		}
 
-		if strings.HasPrefix(shlexedArg, "-var-file=") {
-			varFiles = append(varFiles, strings.TrimPrefix(shlexedArg, "-var-file="))
+		if after, ok := strings.CutPrefix(shlexedArg, "-var-file="); ok {
+			varFiles = append(varFiles, after)
 		}
 	}
 
diff --git a/cli/commands/run/download_source.go b/cli/commands/run/download_source.go
index 9b757925..8473d052 100644
--- a/cli/commands/run/download_source.go
+++ b/cli/commands/run/download_source.go
@@ -8,8 +8,10 @@ import (
 	"strings"
 
 	"github.com/hashicorp/go-getter"
+	getterv2 "github.com/hashicorp/go-getter/v2"
 
 	"github.com/gruntwork-io/terragrunt/config"
+	"github.com/gruntwork-io/terragrunt/internal/cas"
 	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/gruntwork-io/terragrunt/internal/experiment"
 	"github.com/gruntwork-io/terragrunt/internal/report"
@@ -108,27 +110,29 @@ func DownloadTerraformSourceIfNecessary(
 		if err := os.RemoveAll(terraformSource.DownloadDir); err != nil {
 			return errors.New(err)
 		}
-	}
-
-	alreadyLatest, err := AlreadyHaveLatestCode(l, terraformSource, opts)
-	if err != nil {
-		return err
-	}
-
-	if alreadyLatest {
-		if err := ValidateWorkingDir(terraformSource); err != nil {
+	} else {
+		alreadyLatest, err := AlreadyHaveLatestCode(l, terraformSource, opts)
+		if err != nil {
 			return err
 		}
 
-		l.Debugf("%s files in %s are up to date. Will not download again.", opts.TerraformImplementation, terraformSource.WorkingDir)
+		if alreadyLatest {
+			if err := ValidateWorkingDir(terraformSource); err != nil {
+				return err
+			}
 
-		return nil
+			l.Debugf("%s files in %s are up to date. Will not download again.", opts.TerraformImplementation, terraformSource.WorkingDir)
+
+			return nil
+		}
 	}
 
 	var previousVersion = ""
 	// read previous source version
 	// https://github.com/gruntwork-io/terragrunt/issues/1921
 	if util.FileExists(terraformSource.VersionFile) {
+		var err error
+
 		previousVersion, err = readVersionFile(terraformSource)
 		if err != nil {
 			return err
@@ -283,6 +287,41 @@ func downloadSource(ctx context.Context, l log.Logger, src *tf.Source, opts *opt
 		canonicalSourceURL,
 		src.DownloadDir)
 
+	allowCAS := opts.Experiments.Evaluate(experiment.CAS)
+	if allowCAS {
+		l.Debugf("CAS experiment enabled: attempting to use Content Addressable Storage for source: %s", canonicalSourceURL)
+
+		c, err := cas.New(cas.Options{})
+		if err != nil {
+			l.Warnf("Failed to initialize CAS: %v. Falling back to standard getter.", err)
+		} else {
+			cloneOpts := cas.CloneOptions{
+				Dir:              src.DownloadDir,
+				IncludedGitFiles: []string{"HEAD", "config"},
+			}
+
+			casGetter := cas.NewCASGetter(l, c, &cloneOpts)
+
+			// Use go-getter v2 Client to properly process the Request
+			client := getterv2.Client{
+				Getters: []getterv2.Getter{casGetter},
+			}
+
+			req := &getterv2.Request{
+				Src: src.CanonicalSourceURL.String(),
+				Dst: src.DownloadDir,
+			}
+
+			if _, casErr := client.Get(ctx, req); casErr == nil {
+				l.Debugf("Successfully downloaded source using CAS: %s", canonicalSourceURL)
+				return nil
+			} else {
+				l.Warnf("CAS download failed: %v. Falling back to standard getter.", casErr)
+			}
+		}
+	}
+
+	// Fallback to standard go-getter
 	return opts.RunWithErrorHandling(ctx, l, r, func() error {
 		return getter.GetAny(src.DownloadDir, src.CanonicalSourceURL.String(), UpdateGetters(opts, cfg))
 	})
diff --git a/cli/commands/run/flags.go b/cli/commands/run/flags.go
index 793aec9a..fa72c0ba 100644
--- a/cli/commands/run/flags.go
+++ b/cli/commands/run/flags.go
@@ -30,6 +30,7 @@ const (
 	DependencyFetchOutputFromStateFlagName = "dependency-fetch-output-from-state"
 	UsePartialParseConfigCacheFlagName     = "use-partial-parse-config-cache"
 	SummaryPerUnitFlagName                 = "summary-per-unit"
+	VersionManagerFileNameFlagName         = "version-manager-file-name"
 
 	BackendBootstrapFlagName        = "backend-bootstrap"
 	BackendRequireBootstrapFlagName = "backend-require-bootstrap"
@@ -342,6 +343,13 @@ func NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix
 		},
 			flags.WithDeprecatedNames(terragruntPrefix.FlagNames("use-partial-parse-config-cache"), terragruntPrefixControl)),
 
+		flags.NewFlag(&cli.SliceFlag[string]{
+			Name:        VersionManagerFileNameFlagName,
+			EnvVars:     tgPrefix.EnvVars(VersionManagerFileNameFlagName),
+			Destination: &opts.VersionManagerFileName,
+			Usage:       "File names used during the computation of the cache key for the version manager files.",
+		}),
+
 		flags.NewFlag(&cli.BoolFlag{
 			Name:        DependencyFetchOutputFromStateFlagName,
 			EnvVars:     tgPrefix.EnvVars(DependencyFetchOutputFromStateFlagName),
diff --git a/cli/commands/run/test-fixtures/dir-with-files-to-test-cache-key/.terraform-version b/cli/commands/run/test-fixtures/dir-with-files-to-test-cache-key/.terraform-version
new file mode 100644
index 00000000..e69de29b
diff --git a/cli/commands/run/test-fixtures/dir-with-files-to-test-cache-key/.tool-versions b/cli/commands/run/test-fixtures/dir-with-files-to-test-cache-key/.tool-versions
new file mode 100644
index 00000000..e69de29b
diff --git a/cli/commands/run/version_check.go b/cli/commands/run/version_check.go
index 6566915e..5d5ecf7a 100644
--- a/cli/commands/run/version_check.go
+++ b/cli/commands/run/version_check.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"path/filepath"
 	"regexp"
 	"strings"
 
@@ -32,8 +31,6 @@ var TerraformVersionRegex = regexp.MustCompile(`^(\S+)\s(v?\d+\.\d+\.\d+)`)
 
 const versionParts = 3
 
-var versionFiles = []string{".terraform-version", ".tool-versions", "mise.toml", ".mise.toml"}
-
 // CheckVersionConstraints checks the version constraints of both terragrunt and terraform. Note that as a side effect this will set the
 // following settings on terragruntOptions:
 // - TerraformPath
@@ -96,7 +93,8 @@ func CheckVersionConstraints(ctx context.Context, l log.Logger, terragruntOption
 // The caller also gets a copy of the logger with the config path set.
 func PopulateTerraformVersion(ctx context.Context, l log.Logger, terragruntOptions *options.TerragruntOptions) (log.Logger, error) {
 	versionCache := GetRunVersionCache(ctx)
-	cacheKey := computeVersionFilesCacheKey(terragruntOptions.WorkingDir)
+	cacheKey := computeVersionFilesCacheKey(terragruntOptions.WorkingDir, terragruntOptions.VersionManagerFileName)
+	l.Debugf("using cache key for version files: %s", cacheKey)
 
 	if cachedOutput, found := versionCache.Get(ctx, cacheKey); found {
 		terraformVersion, err := ParseTerraformVersion(cachedOutput)
@@ -244,15 +242,22 @@ func parseTerraformImplementationType(versionCommandOutput string) (options.Terr
 	}
 }
 
-// Helper to compute a cache key from the checksums of .terraform-version and .tool-versions
-func computeVersionFilesCacheKey(workingDir string) string {
+// Helper to compute a cache key from the checksums of provided files
+func computeVersionFilesCacheKey(workingDir string, versionFiles []string) string {
 	var hashes []string
 
 	for _, file := range versionFiles {
-		path := filepath.Join(workingDir, file)
+		path, err := util.SanitizePath(workingDir, file)
+		if err != nil {
+			continue
+		}
+
 		if util.FileExists(path) {
 			hash, err := util.FileSHA256(path)
 			if err == nil {
+				// We use `file` as part of the cache key because the `path` becomes an absolute path after sanitization.
+				// Without implementing a full "mock filesystem", this would be difficult to test currently.
+				// Note: This approach may potentially create duplicate cache files in some edge cases.
 				hashes = append(hashes, file+":"+hex.EncodeToString(hash))
 			}
 		}
diff --git a/cli/commands/scaffold/scaffold.go b/cli/commands/scaffold/scaffold.go
index 85b62014..b8c32f52 100644
--- a/cli/commands/scaffold/scaffold.go
+++ b/cli/commands/scaffold/scaffold.go
@@ -440,6 +440,11 @@ func rewriteTemplateURL(ctx context.Context, l log.Logger, opts *options.Terragr
 			return nil, errors.New(err)
 		}
 
+		if rootSourceURL.Scheme == "" || rootSourceURL.Scheme == "file" {
+			l.Debugf("Skipping git tag lookup for local template path: %s", rootSourceURL)
+			return updatedTemplateURL, nil
+		}
+
 		tag, err := shell.GitLastReleaseTag(ctx, l, opts, rootSourceURL)
 		if err != nil || tag == "" {
 			l.Warnf("Failed to find last release tag for URL %s, so will not add a ref param to the URL", rootSourceURL)
diff --git a/config/config_helpers.go b/config/config_helpers.go
index 5b148233..1270b5ef 100644
--- a/config/config_helpers.go
+++ b/config/config_helpers.go
@@ -271,7 +271,12 @@ func getPathToRepoRoot(ctx *ParsingContext, l log.Logger) (string, error) {
 
 // GetTerragruntDir returns the directory where the Terragrunt configuration file lives.
 func GetTerragruntDir(ctx *ParsingContext, l log.Logger) (string, error) {
-	terragruntConfigFileAbsPath, err := filepath.Abs(ctx.TerragruntOptions.TerragruntConfigPath)
+	path := ctx.TerragruntOptions.TerragruntConfigPath
+	if val, ok := ctx.Context.Value(stackParserContext{}).(stackParserContext); ok {
+		path = val.stackConfigFile
+	}
+
+	terragruntConfigFileAbsPath, err := filepath.Abs(path)
 	if err != nil {
 		return "", errors.New(err)
 	}
diff --git a/config/dependency.go b/config/dependency.go
index 7f03b883..c841efa3 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -414,8 +414,6 @@ func dependencyBlocksToCtyValue(ctx *ParsingContext, l log.Logger, dependencyCon
 	dependencyErrGroup, _ := errgroup.WithContext(ctx)
 
 	for _, dependencyConfig := range dependencyConfigs {
-		dependencyConfig := dependencyConfig // https://golang.org/doc/faq#closures_and_goroutines
-
 		dependencyErrGroup.Go(func() error {
 			// Loose struct to hold the attributes of the dependency. This includes:
 			// - outputs: The module outputs of the target config
diff --git a/config/stack.go b/config/stack.go
index 43387c82..9d8962ff 100644
--- a/config/stack.go
+++ b/config/stack.go
@@ -72,6 +72,10 @@ type Stack struct {
 	Path         string     `hcl:"path,attr"`
 }
 
+type stackParserContext struct {
+	stackConfigFile string
+}
+
 // GenerateStacks generates the stack files.
 func GenerateStacks(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {
 	processedFiles := make(map[string]bool)
@@ -687,6 +691,7 @@ func (u *Unit) ReadOutputs(ctx context.Context, l log.Logger, opts *options.Terr
 func ReadStackConfigFile(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, filePath string, values *cty.Value) (*StackConfig, error) {
 	l.Debugf("Reading Terragrunt stack config file at %s", filePath)
 
+	ctx = context.WithValue(ctx, stackParserContext{}, stackParserContext{stackConfigFile: filePath})
 	parser := NewParsingContext(ctx, l, opts)
 
 	file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(filePath)
diff --git a/docs-starlight/src/components/Flag.astro b/docs-starlight/src/components/Flag.astro
index 17e229f7..63c28f39 100644
--- a/docs-starlight/src/components/Flag.astro
+++ b/docs-starlight/src/components/Flag.astro
@@ -11,7 +11,7 @@ const { slug } = Astro.props;
 
 const flagEntry = await getEntry('flags', slug) as CollectionEntry<'flags'>;
 const { Content } = await render(flagEntry);
-const { aliases, description, type, env, name } = flagEntry.data;
+const { aliases, description, type, env, name, defaultVal } = flagEntry.data;
 ---
 
 <section id={name}>
@@ -21,6 +21,11 @@ const { aliases, description, type, env, name } = flagEntry.data;
     <div>
       <span>Type: </span><Badge text={type} variant="note" style={{ fontWeight: 'bold' }}/>
     </div>
+    {defaultVal && (
+      <div>
+        <span>Default: </span><Badge text={defaultVal} variant="tip" style={{ fontWeight: 'bold' }}/>
+      </div>
+    )}
     {aliases && aliases.length > 0 && (
       <>
         <p>Aliases:</p>
diff --git a/docs-starlight/src/content.config.ts b/docs-starlight/src/content.config.ts
index 50f42c2a..3ae96e22 100644
--- a/docs-starlight/src/content.config.ts
+++ b/docs-starlight/src/content.config.ts
@@ -52,6 +52,7 @@ const flags = defineCollection({
 	schema: z.object({
 		name: z.string(),
 		description: z.string(),
+		defaultVal: z.string().optional(),
 		type: z.string(),
 		env: z.array(z.string()).optional(),
 		aliases: z.array(z.string()).optional(),
diff --git a/docs-starlight/src/content/docs/02-features/15-cas.md b/docs-starlight/src/content/docs/02-features/15-cas.md
index 3d2f3b48..fcb45a99 100644
--- a/docs-starlight/src/content/docs/02-features/15-cas.md
+++ b/docs-starlight/src/content/docs/02-features/15-cas.md
@@ -8,13 +8,15 @@ sidebar:
 
 Terragrunt supports a Content Addressable Store (CAS) to deduplicate content across multiple Terragrunt configurations. This feature is still experimental and not recommended for general production usage.
 
-At the moment, the only supported use case for the CAS is to speed up catalog cloning. In the future, the CAS can be used to store more content.
+The CAS is used to speed up both catalog cloning and OpenTofu/Terraform source cloning by avoiding redundant downloads of Git repositories.
 
 To use the CAS, you will need to enable the [cas](/docs/reference/experiments/#cas) experiment.
 
 ## Usage
 
-When you enable the `cas` experiment, Terragrunt will automatically use the CAS when cloning any compatible source (right now, only Git repositories).
+When you enable the `cas` experiment, Terragrunt will automatically use the CAS when cloning any compatible source (Git repositories).
+
+### Catalog Usage
 
 ```hcl
 # root.hcl
@@ -26,7 +28,17 @@ catalog {
 }
 ```
 
-When Terragrunt clones a repository while using the CAS. If the repository is not found in the CAS, Terragrunt will clone the repository from the original URL and store it in the CAS for future use.
+### OpenTofu/Terraform Source Usage
+
+```hcl
+# terragrunt.hcl
+
+terraform {
+  source = "git@github.com:acme/infrastructure-modules.git//vpc?ref=v1.0.0"
+}
+```
+
+When Terragrunt clones a repository while using the CAS, if the repository is not found in the CAS, Terragrunt will clone the repository from the original URL and store it in the CAS for future use.
 
 When generating a repository from the CAS, Terragrunt will hard link entries from the CAS to the new repository. This allows Terragrunt to deduplicate content across multiple repositories.
 
diff --git a/docs-starlight/src/content/docs/04-reference/04-experiments.md b/docs-starlight/src/content/docs/04-reference/04-experiments.md
index f3565aef..1a0e2884 100644
--- a/docs-starlight/src/content/docs/04-reference/04-experiments.md
+++ b/docs-starlight/src/content/docs/04-reference/04-experiments.md
@@ -99,9 +99,9 @@ Support for Terragrunt Content Addressable Storage (CAS).
 
 #### `cas` - What it does
 
-Allow Terragrunt to store and retrieve state files from a Content Addressable Storage (CAS) system.
+Allow Terragrunt to store and retrieve Git repositories from a Content Addressable Storage (CAS) system.
 
-At the moment, the CAS is only used to speed up catalog cloning, but in the future, it can be used to store more content.
+The CAS is used to speed up both catalog cloning and OpenTofu/Terraform source cloning by avoiding redundant downloads of Git repositories.
 
 #### `cas` - How to provide feedback
 
@@ -116,7 +116,7 @@ Feedback is crucial for ensuring the feature meets real-world use cases. Please
 To transition the `cas` feature to a stable release, the following must be addressed:
 
 - [x] Add support for storing and retrieving catalog repositories from the CAS.
-- [ ] Add support for storing and retrieving OpenTofu/Terraform modules from the CAS.
+- [x] Add support for storing and retrieving OpenTofu/Terraform modules from the CAS.
 - [ ] Add support for storing and retrieving Unit/Stack configurations from the CAS.
 
 ### `report`
diff --git a/docs-starlight/src/data/commands/run.mdx b/docs-starlight/src/data/commands/run.mdx
index 532e28ff..9b2b8341 100644
--- a/docs-starlight/src/data/commands/run.mdx
+++ b/docs-starlight/src/data/commands/run.mdx
@@ -74,6 +74,7 @@ flags:
   - tf-path
   - units-that-include
   - use-partial-parse-config-cache
+  - version-manager-file-name
 ---
 
 import { Aside } from '@astrojs/starlight/components';
diff --git a/docs-starlight/src/data/flags/queue-excludes-file.mdx b/docs-starlight/src/data/flags/queue-excludes-file.mdx
index 744116e8..f9907d51 100644
--- a/docs-starlight/src/data/flags/queue-excludes-file.mdx
+++ b/docs-starlight/src/data/flags/queue-excludes-file.mdx
@@ -1,11 +1,17 @@
 ---
 name: queue-excludes-file
-description: Path to a file containing Unix-style globs of directories to exclude from the queue of Units to run.
+description: Path to a file with a list of directories that need to be excluded when running `run --all` commands.
 type: string
+defaultVal: .terragrunt-excludes
 env:
   - TG_QUEUE_EXCLUDES_FILE
 ---
 
-Specifies a file containing patterns of directories to exclude when running commands with [`--all`](/docs/reference/cli/commands/run#all).
+Units in these directories will be excluded during execution of the commands. If a relative path is specified, it should be relative from
+[--working-dir](/docs/reference/cli/global-flags#working-directory). This will only exclude the module, not its dependencies.
 
-To learn more about how to use this flag, see the [Stacks](/docs/features/stacks/#the-run---all-command) feature documentation.
+This flag has been designed to integrate nicely with the `hcl validate` command, which can return a list of invalid files delimited by newlines when passed the `--show-config-path` flag. To integrate the two, you can run something like the following using bash process substitution:
+
+```bash
+terragrunt run --all plan --queue-excludes-file <(terragrunt hcl validate --show-config-path || true)
+```
diff --git a/docs-starlight/src/data/flags/version-manager-file-name.mdx b/docs-starlight/src/data/flags/version-manager-file-name.mdx
new file mode 100644
index 00000000..296bc35f
--- /dev/null
+++ b/docs-starlight/src/data/flags/version-manager-file-name.mdx
@@ -0,0 +1,16 @@
+---
+name: version-manager-file-name
+description: File names used during the computation of the cache key for the version manager files.
+type: string
+env:
+  - TG_VERSION_MANAGER_FILE_NAME
+---
+
+By default, terragrunt is specifying:
+
+```shell
+.terraform-version
+.tool-versions
+.mise.toml
+mise.toml
+```
\ No newline at end of file
diff --git a/docs/_docs/02_features/15-cas.md b/docs/_docs/02_features/15-cas.md
index 7aaf0d89..f4640aea 100644
--- a/docs/_docs/02_features/15-cas.md
+++ b/docs/_docs/02_features/15-cas.md
@@ -13,13 +13,15 @@ slug: cas
 
 Terragrunt supports a Content Addressable Store (CAS) to deduplicate content across multiple Terragrunt configurations. This feature is still experimental and not recommended for general production usage.
 
-At the moment, the only supported use case for the CAS is to speed up catalog cloning. In the future, the CAS can be used to store more content.
+The CAS is used to speed up both catalog cloning and OpenTofu/Terraform source cloning by avoiding redundant downloads of Git repositories.
 
 To use the CAS, you will need to enable the [cas](/docs/reference/experiments/#cas) experiment.
 
 ## Usage
 
-When you enable the `cas` experiment, Terragrunt will automatically use the CAS when cloning any compatible source (right now, only Git repositories).
+When you enable the `cas` experiment, Terragrunt will automatically use the CAS when cloning any compatible source (Git repositories).
+
+### Catalog Usage
 
 ```hcl
 # root.hcl
@@ -31,7 +33,17 @@ catalog {
 }
 ```
 
-When Terragrunt clones a repository while using the CAS. If the repository is not found in the CAS, Terragrunt will clone the repository from the original URL and store it in the CAS for future use.
+### OpenTofu/Terraform Source Usage
+
+```hcl
+# terragrunt.hcl
+
+terraform {
+  source = "git@github.com:acme/infrastructure-modules.git//vpc?ref=v1.0.0"
+}
+```
+
+When Terragrunt clones a repository while using the CAS, if the repository is not found in the CAS, Terragrunt will clone the repository from the original URL and store it in the CAS for future use.
 
 When generating a repository from the CAS, Terragrunt will hard link entries from the CAS to the new repository. This allows Terragrunt to deduplicate content across multiple repositories.
 
diff --git a/docs/_docs/04_reference/02-cli-options.md b/docs/_docs/04_reference/02-cli-options.md
index 05dad011..eee2a75d 100644
--- a/docs/_docs/04_reference/02-cli-options.md
+++ b/docs/_docs/04_reference/02-cli-options.md
@@ -1270,6 +1270,7 @@ terragrunt dag graph  | dot -Tpng > graph.png
   - [strict-control](#strict-control)
   - [strict-mode](#strict-mode)
   - [in-download-dir](#in-download-dir)
+  - [version-manager-file-name](#version-manager-file-name)
 
 ### all
 
@@ -1783,14 +1784,14 @@ When passed in, the `*-all` commands continue processing components even if a de
 **Environment Variable Alias**: `TERRAGRUNT_EXCLUDES_FILE` (deprecated: [See migration guide](/docs/migrate/cli-redesign/))<br/>
 **Requires an argument**: `--queue-excludes-file /path/to/file`<br/>
 
-Path to a file with a list of directories that need to be excluded when running *-all commands, by default `.terragrunt-excludes`. Modules under these directories will be
+Path to a file with a list of directories that need to be excluded when running `run --all` commands, by default `.terragrunt-excludes`. Units in these directories will be
 excluded during execution of the commands. If a relative path is specified, it should be relative from
 [--working-dir](#working-dir). This will only exclude the module, not its dependencies.
 
-This flag has been designed to integrate nicely with the `hclvalidate` command, which can return a list of invalid files delimited by newlines when passed the `--show-config-path` flag. To integrate the two, you can run something like the following using bash process substitution:
+This flag has been designed to integrate nicely with the `hcl validate` command, which can return a list of invalid files delimited by newlines when passed the `--show-config-path` flag. To integrate the two, you can run something like the following using bash process substitution:
 
 ```bash
-terragrunt run --all plan --queue-excludes-file <(terragrunt hclvalidate --show-config-path)
+terragrunt run --all plan --queue-excludes-file <(terragrunt hcl validate --show-config-path || true)
 ```
 
 ### queue-exclude-dir
@@ -2496,6 +2497,21 @@ For more information, see the [Strict Mode](/docs/reference/strict-mode) documen
 
 Execute the provided command in the download directory.
 
+### version-manager-file-name
+
+**CLI Arg**: `--version-manager-file-name`<br/>
+**Environment Variable**: `TG_VERSION_MANAGER_FILE_NAME`<br/>
+
+File names used during the computation of the cache key for the version manager files.<br/>
+By default terragrunt is specifying:
+
+```shell
+.terraform-version
+.tool-versions
+.mise.toml
+mise.toml
+```
+
 ## Deprecated
 
 ### Deprecated Commands
diff --git a/docs/_docs/04_reference/06-experiments.md b/docs/_docs/04_reference/06-experiments.md
index 1cfbe245..6763f8e3 100644
--- a/docs/_docs/04_reference/06-experiments.md
+++ b/docs/_docs/04_reference/06-experiments.md
@@ -99,9 +99,9 @@ Support for Terragrunt Content Addressable Storage (CAS).
 
 #### `cas` - What it does
 
-Allow Terragrunt to store and retrieve state files from a Content Addressable Storage (CAS) system.
+Allow Terragrunt to store and retrieve Git repositories from a Content Addressable Storage (CAS) system.
 
-At the moment, the CAS is only used to speed up catalog cloning, but in the future, it can be used to store more content.
+The CAS is used to speed up both catalog cloning and OpenTofu/Terraform source cloning by avoiding redundant downloads of Git repositories.
 
 #### `cas` - How to provide feedback
 
@@ -116,7 +116,7 @@ Feedback is crucial for ensuring the feature meets real-world use cases. Please
 To transition the `cas` feature to a stable release, the following must be addressed:
 
 - [x] Add support for storing and retrieving catalog repositories from the CAS.
-- [ ] Add support for storing and retrieving OpenTofu/Terraform modules from the CAS.
+- [x] Add support for storing and retrieving OpenTofu/Terraform modules from the CAS.
 - [ ] Add support for storing and retrieving Unit/Stack configurations from the CAS.
 
 ### `report`
diff --git a/go.mod b/go.mod
index e2a3021b..cb1d3ce4 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ require (
 	github.com/gruntwork-io/boilerplate v0.6.3
 	github.com/gruntwork-io/go-commons v0.17.2
 	github.com/gruntwork-io/terragrunt-engine-go v0.0.15
-	github.com/gruntwork-io/terratest v0.49.0
+	github.com/gruntwork-io/terratest v0.50.0
 	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/hashicorp/go-getter v1.7.8
 	github.com/hashicorp/go-getter/v2 v2.2.3
@@ -41,7 +41,7 @@ require (
 	github.com/hashicorp/terraform-config-inspect v0.0.0-20210318070130-9a80970d6b34
 	github.com/hashicorp/terraform-svchost v0.1.1
 	github.com/huandu/go-clone v1.7.2
-	github.com/labstack/echo/v4 v4.13.3
+	github.com/labstack/echo/v4 v4.13.4
 	github.com/mattn/go-isatty v0.0.20
 	github.com/mattn/go-zglob v0.0.6
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
diff --git a/go.sum b/go.sum
index c5c6bfae..2f2e202f 100644
--- a/go.sum
+++ b/go.sum
@@ -1244,8 +1244,8 @@ github.com/gruntwork-io/go-commons v0.17.2 h1:14dsCJ7M5Vv2X3BIPKeG9Kdy6vTMGhM8L4
 github.com/gruntwork-io/go-commons v0.17.2/go.mod h1:zs7Q2AbUKuTarBPy19CIxJVUX/rBamfW8IwuWKniWkE=
 github.com/gruntwork-io/terragrunt-engine-go v0.0.15 h1:s9vFSIvHDSgvoZblGOtymYgFbbDJsqL50PCykbhxSeA=
 github.com/gruntwork-io/terragrunt-engine-go v0.0.15/go.mod h1:xwRmPVdxLPNxj5eNmA1iNWdIA/YuaFACQgdf6/AB5Xs=
-github.com/gruntwork-io/terratest v0.49.0 h1:GurfpHEOEr8vntB77QcxDh+P7aiQRUgPFdgb6q9PuWI=
-github.com/gruntwork-io/terratest v0.49.0/go.mod h1:/+dfGio9NqUpvvukuPo29B8zy6U5FYJn9PdmvwztK4A=
+github.com/gruntwork-io/terratest v0.50.0 h1:AbBJ7IRCpLZ9H4HBrjeoWESITv8nLjN6/f1riMNcAsw=
+github.com/gruntwork-io/terratest v0.50.0/go.mod h1:see0lbKvAqz6rvzvN2wyfuFQQG4PWcAb2yHulF6B2q4=
 github.com/hashicorp/aws-sdk-go-base v0.6.0/go.mod h1:2fRjWDv3jJBeN6mVWFHV6hFTNeFBx2gpDLQaZNxUVAY=
 github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -1418,8 +1418,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
-github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
+github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
 github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
diff --git a/internal/cas/cas.go b/internal/cas/cas.go
index 4fd1399f..085c0d0b 100644
--- a/internal/cas/cas.go
+++ b/internal/cas/cas.go
@@ -14,11 +14,11 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
-	"sync"
-	"time"
 
+	"github.com/gofrs/flock"
 	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/gruntwork-io/terragrunt/telemetry"
 )
 
 // Options configures the behavior of CAS
@@ -45,10 +45,9 @@ type CloneOptions struct {
 
 // CAS clones a git repository using content-addressable storage.
 type CAS struct {
-	cloneStart time.Time
-	store      *Store
-	git        *GitRunner
-	opts       Options
+	store *Store
+	git   *GitRunner
+	opts  Options
 }
 
 // New creates a new CAS instance with the given options
@@ -82,53 +81,70 @@ func New(opts Options) (*CAS, error) {
 //
 // TODO: Make options optional
 func (c *CAS) Clone(ctx context.Context, l log.Logger, opts *CloneOptions, url string) error {
-	c.cloneStart = time.Now()
+	// Ensure the store path exists
+	if err := os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {
+		return fmt.Errorf("failed to create store path: %w", err)
+	}
 
-	targetDir := c.prepareTargetDirectory(opts.Dir, url)
+	// Acquire global clone lock to ensure only one clone at a time
+	globalLock := flock.New(filepath.Join(c.store.Path(), "clone.lock"))
 
-	// Create a temporary directory for git operations
-	tempDir, cleanup, err := c.git.CreateTempDir()
-	if err != nil {
-		return err
+	if err := globalLock.Lock(); err != nil {
+		return fmt.Errorf("failed to acquire global clone lock: %w", err)
 	}
 
 	defer func() {
-		if cleanupErr := cleanup(); cleanupErr != nil {
-			l.Warnf("cleanup error: %v", cleanupErr)
+		if unlockErr := globalLock.Unlock(); unlockErr != nil {
+			l.Warnf("failed to release global clone lock: %v", unlockErr)
 		}
 	}()
 
-	// Set the working directory for git operations
-	c.git.SetWorkDir(tempDir)
-
-	hash, err := c.resolveReference(ctx, url, opts.Branch)
-	if err != nil {
-		return err
-	}
-
-	if c.store.NeedsWrite(hash, c.cloneStart) {
-		if err := c.cloneAndStoreContent(ctx, l, opts, url, hash); err != nil {
+	return telemetry.TelemeterFromContext(ctx).Collect(ctx, "cas_clone", map[string]any{
+		"url":    url,
+		"branch": opts.Branch,
+	}, func(childCtx context.Context) error {
+		hash, err := c.resolveReference(childCtx, url, opts.Branch)
+		if err != nil {
 			return err
 		}
-	}
 
-	content := NewContent(c.store)
+		targetDir := c.prepareTargetDirectory(opts.Dir, url)
 
-	treeData, err := content.Read(hash)
-	if err != nil {
-		return err
-	}
+		if c.store.NeedsWrite(hash) {
+			// Create a temporary directory for git operations
+			tempDir, cleanup, createTempDirErr := c.git.CreateTempDir()
+			if createTempDirErr != nil {
+				return createTempDirErr
+			}
 
-	tree, err := ParseTree(string(treeData), targetDir)
-	if err != nil {
-		return err
-	}
+			defer func() {
+				if cleanupErr := cleanup(); cleanupErr != nil {
+					l.Warnf("cleanup error: %v", cleanupErr)
+				}
+			}()
 
-	if err := tree.LinkTree(ctx, c.store, targetDir); err != nil {
-		return err
-	}
+			// Set the working directory for git operations
+			c.git.SetWorkDir(tempDir)
 
-	return nil
+			if cloneAndStoreErr := c.cloneAndStoreContent(childCtx, l, opts, url, hash); cloneAndStoreErr != nil {
+				return cloneAndStoreErr
+			}
+		}
+
+		content := NewContent(c.store)
+
+		treeData, err := content.Read(hash)
+		if err != nil {
+			return err
+		}
+
+		tree, err := ParseTree(string(treeData), targetDir)
+		if err != nil {
+			return err
+		}
+
+		return tree.LinkTree(childCtx, c.store, targetDir)
+	})
 }
 
 func (c *CAS) prepareTargetDirectory(dir, url string) string {
@@ -167,27 +183,12 @@ func (c *CAS) cloneAndStoreContent(ctx context.Context, l log.Logger, opts *Clon
 
 func (c *CAS) storeRootTree(ctx context.Context, l log.Logger, hash string, opts *CloneOptions) error {
 	// Get all blobs recursively in a single git ls-tree -r call at the root
-	allBlobsTree, err := c.git.LsTreeRecursive(ctx, hash, ".")
+	tree, err := c.git.LsTreeRecursive(ctx, hash, ".")
 	if err != nil {
 		return err
 	}
 
-	// Collect all blobs from recursive listing
-	var allBlobs []TreeEntry
-
-	for _, entry := range allBlobsTree.Entries() {
-		if entry.Type == "blob" {
-			allBlobs = append(allBlobs, entry)
-		}
-	}
-
-	// Store all blobs concurrently (single batch from recursive call)
-	if err := c.storeBlobs(ctx, allBlobs); err != nil {
-		return err
-	}
-
-	// Now store the tree structure (which won't need to handle blobs again)
-	if err := c.storeTree(ctx, l, hash, ""); err != nil {
+	if err = c.storeTreeRecursive(ctx, l, hash, tree); err != nil {
 		return err
 	}
 
@@ -214,149 +215,57 @@ func (c *CAS) storeRootTree(ctx context.Context, l log.Logger, hash string, opts
 
 		workDirPath := filepath.Join(c.git.WorkDir, file)
 
-		hash, err := hashFile(workDirPath)
+		includedHash, err := hashFile(workDirPath)
 		if err != nil {
 			return err
 		}
 
-		content := NewContent(c.store)
+		includedContent := NewContent(c.store)
 
-		if err := content.EnsureCopy(l, hash, workDirPath); err != nil {
+		if err := includedContent.EnsureCopy(l, includedHash, workDirPath); err != nil {
 			return err
 		}
 
 		path := filepath.Join(".git", file)
 
-		data = append(data, fmt.Appendf(nil, "%06o blob %s\t%s\n", stat.Mode().Perm(), hash, path)...)
+		data = append(data, fmt.Appendf(nil, "%06o blob %s\t%s\n", stat.Mode().Perm(), includedHash, path)...)
 	}
 
 	// Overwrite the root tree with the new data
 	return content.Store(l, hash, data)
 }
 
-func (c *CAS) storeTree(ctx context.Context, l log.Logger, hash, prefix string) error {
-	if !c.store.NeedsWrite(hash, c.cloneStart) {
+// storeTreeRecursive stores a tree fetched from git ls-tree -r
+func (c *CAS) storeTreeRecursive(ctx context.Context, l log.Logger, hash string, tree *Tree) error {
+	if !c.store.NeedsWrite(hash) {
 		return nil
 	}
 
-	// Get tree structure (no recursive blobs needed - they're already stored)
-	tree, err := c.git.LsTree(ctx, hash, ".")
-	if err != nil {
-		return err
-	}
-
-	// Only collect immediate tree entries (blobs are already handled at root)
-	var immediateTrees []TreeEntry
-
-	for _, entry := range tree.Entries() {
-		if prefix != "" {
-			entry.Path = filepath.Join(prefix, entry.Path)
-		}
-
-		if entry.Type == "tree" {
-			immediateTrees = append(immediateTrees, entry)
-		}
-	}
-
-	// Store tree objects recursively
-	if err := c.storeTrees(ctx, l, immediateTrees, prefix); err != nil {
+	if err := c.storeBlobs(ctx, tree.Entries()); err != nil {
 		return err
 	}
 
-	// Store the current tree object
+	// Store the tree object itself
 	content := NewContent(c.store)
-	if err := content.Ensure(l, hash, tree.Data()); err != nil {
+	if err := content.EnsureWithWait(l, hash, tree.Data()); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-// storeTrees concurrently stores trees in the CAS
-func (c *CAS) storeTrees(ctx context.Context, l log.Logger, entries []TreeEntry, prefix string) error {
-	ch := make(chan error, len(entries))
-
-	var wg sync.WaitGroup
-
-	for _, entry := range entries {
-		if !c.store.NeedsWrite(entry.Hash, c.cloneStart) {
-			continue
-		}
-
-		wg.Add(1)
-
-		go func(hash string) {
-			defer wg.Done()
-
-			if err := c.storeTree(ctx, l, hash, prefix); err != nil {
-				ch <- err
-				return
-			}
-
-			ch <- nil
-		}(entry.Hash)
-	}
-
-	wg.Wait()
-	close(ch)
-
-	var errs []error
-
-	for err := range ch {
-		if err != nil {
-			errs = append(errs, err)
-		}
-	}
-
-	if len(errs) > 0 {
-		return errors.Join(errs...)
-	}
-
-	return nil
-}
-
-// storeBlobs concurrently stores blobs in the CAS
+// storeBlobs stores blobs in the CAS
 func (c *CAS) storeBlobs(ctx context.Context, entries []TreeEntry) error {
-	ch := make(chan error, len(entries))
-
-	var wg sync.WaitGroup
-
 	for _, entry := range entries {
-		if !c.store.NeedsWrite(entry.Hash, c.cloneStart) {
+		if !c.store.NeedsWrite(entry.Hash) {
 			continue
 		}
 
-		wg.Add(1)
-
-		go func(hash string) {
-			defer wg.Done()
-
-			if err := c.ensureBlob(ctx, hash); err != nil {
-				ch <- err
-
-				return
-			}
-
-			ch <- nil
-		}(entry.Hash)
-	}
-
-	wg.Wait()
-
-	close(ch)
-
-	errs := []error{}
-
-	for err := range ch {
-		if err != nil {
-			errs = append(errs, err)
+		if err := c.ensureBlob(ctx, entry.Hash); err != nil {
+			return err
 		}
 	}
 
-	if len(errs) > 0 {
-		return errors.Join(errs...)
-	}
-
 	return nil
 }
 
@@ -364,22 +273,24 @@ func (c *CAS) storeBlobs(ctx context.Context, entries []TreeEntry) error {
 // It doesn't use the standard content.Store method because
 // we want to take advantage of the ability to write to the
 // entry using `git cat-file`.
-func (c *CAS) ensureBlob(ctx context.Context, hash string) (err error) {
-	c.store.mapLock.Lock()
-
-	if _, ok := c.store.locks[hash]; !ok {
-		c.store.locks[hash] = &sync.Mutex{}
+func (c *CAS) ensureBlob(ctx context.Context, hash string) error {
+	needsWrite, lock, err := c.store.EnsureWithWait(hash)
+	if err != nil {
+		return err
 	}
 
-	c.store.locks[hash].Lock()
-	defer c.store.locks[hash].Unlock()
-
-	c.store.mapLock.Unlock()
-
-	if !c.store.NeedsWrite(hash, c.cloneStart) {
+	// If content already exists or was written by another process, we're done
+	if !needsWrite {
 		return nil
 	}
 
+	// We have the lock and need to write the content
+	defer func() {
+		if unlockErr := lock.Unlock(); unlockErr != nil {
+			err = errors.Join(err, unlockErr)
+		}
+	}()
+
 	content := NewContent(c.store)
 
 	tmpHandle, err := content.GetTmpHandle(hash)
diff --git a/internal/cas/content.go b/internal/cas/content.go
index a3eedf8f..08132d4a 100644
--- a/internal/cas/content.go
+++ b/internal/cas/content.go
@@ -2,13 +2,14 @@ package cas
 
 import (
 	"bufio"
+	"context"
 	"io"
 	"os"
 	"path/filepath"
 	"runtime"
-	"sync"
 
 	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/gruntwork-io/terragrunt/telemetry"
 )
 
 // Content manages git object storage and linking
@@ -23,6 +24,8 @@ const (
 	StoredFilePerms = os.FileMode(0444)
 	// RegularFilePerms represents standard file permissions (rw-r--r--)
 	RegularFilePerms = os.FileMode(0644)
+	// WindowsOS is the name of the Windows operating system
+	WindowsOS = "windows"
 )
 
 // NewContent creates a new Content instance
@@ -33,100 +36,128 @@ func NewContent(store *Store) *Content {
 }
 
 // Link creates a hard link from the store to the target path
-func (c *Content) Link(hash, targetPath string) error {
-	if err := c.ensureTargetDirectory(targetPath); err != nil {
-		return err
-	}
-
-	sourcePath := c.getPath(hash)
+func (c *Content) Link(ctx context.Context, hash, targetPath string) error {
+	return telemetry.TelemeterFromContext(ctx).Collect(ctx, "cas_link", map[string]any{
+		"hash": hash,
+		"path": targetPath,
+	}, func(childCtx context.Context) error {
+		sourcePath := c.getPath(hash)
+
+		// Try to create hard link directly (most efficient path)
+		if err := os.Link(sourcePath, targetPath); err != nil {
+			// Check if it's because target already exists
+			if os.IsExist(err) {
+				// File already exists, which is fine
+				return nil
+			}
 
-	// Check if target exists
-	if _, err := os.Stat(targetPath); err == nil {
-		// File exists, skip creating link
-		return nil
-	} else if !os.IsNotExist(err) {
-		// Some other error occurred
-		return &WrappedError{
-			Op:   "stat_target",
-			Path: targetPath,
-			Err:  err,
-		}
-	}
+			// If hard link fails for other reasons, try to copy the file
+			data, readErr := os.ReadFile(sourcePath)
+			if readErr != nil {
+				return &WrappedError{
+					Op:   "read_source",
+					Path: sourcePath,
+					Err:  ErrReadFile,
+				}
+			}
 
-	// Create hard link
-	if err := os.Link(sourcePath, targetPath); err != nil {
-		// If hard link fails, try to copy the file
-		data, readErr := os.ReadFile(sourcePath)
+			// Write to temporary file first
+			tempPath := targetPath + ".tmp"
+			if err := os.WriteFile(tempPath, data, RegularFilePerms); err != nil {
+				return &WrappedError{
+					Op:   "write_target",
+					Path: tempPath,
+					Err:  err,
+				}
+			}
 
-		if readErr != nil {
-			return &WrappedError{
-				Op:   "read_source",
-				Path: sourcePath,
-				Err:  ErrReadFile,
+			// Atomic rename to final path
+			if err := os.Rename(tempPath, targetPath); err != nil {
+				return &WrappedError{
+					Op:   "rename_target",
+					Path: tempPath,
+					Err:  err,
+				}
 			}
 		}
 
-		// Write to temporary file first
-		tempPath := targetPath + ".tmp"
+		return nil
+	})
+}
 
-		if err := os.WriteFile(tempPath, data, RegularFilePerms); err != nil {
-			return &WrappedError{
-				Op:   "write_target",
-				Path: tempPath,
-				Err:  err,
-			}
-		}
+// Store stores a single content item. This is typically used for trees,
+// As blobs are written directly from git cat-file stdout.
+func (c *Content) Store(l log.Logger, hash string, data []byte) error {
+	lock, err := c.store.AcquireLock(hash)
+	if err != nil {
+		return wrapError("acquire_lock", hash, err)
+	}
 
-		// Rename to a final path
-		if err := os.Rename(tempPath, targetPath); err != nil {
-			return &WrappedError{
-				Op:   "rename_target",
-				Path: tempPath,
-				Err:  err,
-			}
+	defer func() {
+		if unlockErr := lock.Unlock(); unlockErr != nil {
+			l.Warnf("failed to unlock filesystem lock for hash %s: %v", hash, unlockErr)
 		}
+	}()
+
+	if err = os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {
+		return wrapError("create_store_dir", c.store.Path(), ErrCreateDir)
 	}
 
-	return nil
+	// Ensure partition directory exists
+	partitionDir := c.getPartition(hash)
+	if err = os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+		return wrapError("create_partition_dir", partitionDir, ErrCreateDir)
+	}
+
+	return c.writeContentToFile(l, hash, data)
 }
 
-func (c *Content) ensureTargetDirectory(targetPath string) error {
-	targetDir := filepath.Dir(targetPath)
-	if err := os.MkdirAll(targetDir, DefaultDirPerms); err != nil {
-		return &WrappedError{
-			Op:   "create_target_dir",
-			Path: targetDir,
-			Err:  ErrCreateDir,
-		}
+// Ensure ensures that a content item exists in the store
+func (c *Content) Ensure(l log.Logger, hash string, data []byte) error {
+	path := c.getPath(hash)
+	if c.store.hasContent(path) {
+		return nil
 	}
 
-	return nil
+	return c.Store(l, hash, data)
 }
 
-// Store stores a single content item. This is typically used for trees,
-// As blobs are written directly from git cat-file stdout.
-func (c *Content) Store(l log.Logger, hash string, data []byte) error {
-	c.store.mapLock.Lock()
-
-	if _, ok := c.store.locks[hash]; !ok {
-		c.store.locks[hash] = &sync.Mutex{}
+// EnsureWithWait ensures that a content item exists in the store, with optimization
+// to wait for concurrent writes instead of doing redundant work
+func (c *Content) EnsureWithWait(l log.Logger, hash string, data []byte) error {
+	needsWrite, lock, err := c.store.EnsureWithWait(hash)
+	if err != nil {
+		return wrapError("ensure_with_wait", hash, err)
 	}
 
-	c.store.locks[hash].Lock()
-	defer c.store.locks[hash].Unlock()
+	// If content already exists or was written by another process, we're done
+	if !needsWrite {
+		return nil
+	}
 
-	c.store.mapLock.Unlock()
+	// We have the lock and need to write the content
+	defer func() {
+		if unlockErr := lock.Unlock(); unlockErr != nil {
+			l.Warnf("failed to unlock filesystem lock for hash %s: %v", hash, unlockErr)
+		}
+	}()
 
-	if err := os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {
+	if err = os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {
 		return wrapError("create_store_dir", c.store.Path(), ErrCreateDir)
 	}
 
 	// Ensure partition directory exists
 	partitionDir := c.getPartition(hash)
-	if err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+	if err = os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
 		return wrapError("create_partition_dir", partitionDir, ErrCreateDir)
 	}
 
+	return c.writeContentToFile(l, hash, data)
+}
+
+// writeContentToFile writes data to a temporary file,
+// sets appropriate permissions, and performs an atomic rename.
+func (c *Content) writeContentToFile(l log.Logger, hash string, data []byte) error {
 	path := c.getPath(hash)
 	tempPath := path + ".tmp"
 
@@ -140,8 +171,8 @@ func (c *Content) Store(l log.Logger, hash string, data []byte) error {
 	if _, err := buf.Write(data); err != nil {
 		f.Close()
 
-		if err := os.Remove(tempPath); err != nil {
-			l.Warnf("failed to remove temp file %s: %v", tempPath, err)
+		if removeErr := os.Remove(tempPath); removeErr != nil {
+			l.Warnf("failed to remove temp file %s: %v", tempPath, removeErr)
 		}
 
 		return wrapError("write_to_store", tempPath, err)
@@ -150,16 +181,16 @@ func (c *Content) Store(l log.Logger, hash string, data []byte) error {
 	if err := buf.Flush(); err != nil {
 		f.Close()
 
-		if err := os.Remove(tempPath); err != nil {
-			l.Warnf("failed to remove temp file %s: %v", tempPath, err)
+		if removeErr := os.Remove(tempPath); removeErr != nil {
+			l.Warnf("failed to remove temp file %s: %v", tempPath, removeErr)
 		}
 
 		return wrapError("flush_buffer", tempPath, err)
 	}
 
 	if err := f.Close(); err != nil {
-		if err := os.Remove(tempPath); err != nil {
-			l.Warnf("failed to remove temp file %s: %v", tempPath, err)
+		if removeErr := os.Remove(tempPath); removeErr != nil {
+			l.Warnf("failed to remove temp file %s: %v", tempPath, removeErr)
 		}
 
 		return wrapError("close_file", tempPath, err)
@@ -167,15 +198,15 @@ func (c *Content) Store(l log.Logger, hash string, data []byte) error {
 
 	// Set read-only permissions on the temporary file
 	if err := os.Chmod(tempPath, StoredFilePerms); err != nil {
-		if err := os.Remove(tempPath); err != nil {
-			l.Warnf("failed to remove temp file %s: %v", tempPath, err)
+		if removeErr := os.Remove(tempPath); removeErr != nil {
+			l.Warnf("failed to remove temp file %s: %v", tempPath, removeErr)
 		}
 
 		return wrapError("chmod_temp_file", tempPath, err)
 	}
 
 	// For Windows, handle readonly attributes specifically
-	if runtime.GOOS == "windows" {
+	if runtime.GOOS == WindowsOS {
 		// Check if a destination file exists and is read-only
 		if _, err := os.Stat(path); err == nil {
 			// File exists, make it writable before rename operation
@@ -187,15 +218,15 @@ func (c *Content) Store(l log.Logger, hash string, data []byte) error {
 
 	// Atomic rename
 	if err := os.Rename(tempPath, path); err != nil {
-		if err := os.Remove(tempPath); err != nil {
-			l.Warnf("failed to remove temp file %s: %v", tempPath, err)
+		if removeErr := os.Remove(tempPath); removeErr != nil {
+			l.Warnf("failed to remove temp file %s: %v", tempPath, removeErr)
 		}
 
 		return wrapError("finalize_store", path, err)
 	}
 
 	// For Windows, we need to set the permissions again after rename
-	if runtime.GOOS == "windows" {
+	if runtime.GOOS == WindowsOS {
 		// Ensure the file has read-only permissions after rename
 		if err := os.Chmod(path, StoredFilePerms); err != nil {
 			return wrapError("chmod_final_file", path, err)
@@ -205,16 +236,6 @@ func (c *Content) Store(l log.Logger, hash string, data []byte) error {
 	return nil
 }
 
-// Ensure ensures that a content item exists in the store
-func (c *Content) Ensure(l log.Logger, hash string, data []byte) error {
-	path := c.getPath(hash)
-	if c.store.hasContent(path) {
-		return nil
-	}
-
-	return c.Store(l, hash, data)
-}
-
 // EnsureCopy ensures that a content item exists in the store by copying from a file
 func (c *Content) EnsureCopy(l log.Logger, hash, src string) error {
 	path := c.getPath(hash)
@@ -222,20 +243,20 @@ func (c *Content) EnsureCopy(l log.Logger, hash, src string) error {
 		return nil
 	}
 
-	c.store.mapLock.Lock()
-
-	if _, ok := c.store.locks[hash]; !ok {
-		c.store.locks[hash] = &sync.Mutex{}
+	lock, err := c.store.AcquireLock(hash)
+	if err != nil {
+		return wrapError("acquire_lock", hash, err)
 	}
 
-	c.store.locks[hash].Lock()
-	defer c.store.locks[hash].Unlock()
-
-	c.store.mapLock.Unlock()
+	defer func() {
+		if unlockErr := lock.Unlock(); unlockErr != nil {
+			l.Warnf("failed to unlock filesystem lock for hash %s: %v", hash, unlockErr)
+		}
+	}()
 
 	// Ensure partition directory exists
 	partitionDir := c.getPartition(hash)
-	if err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+	if err = os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
 		return wrapError("create_partition_dir", partitionDir, ErrCreateDir)
 	}
 
diff --git a/internal/cas/getter.go b/internal/cas/getter.go
index ad694019..b4024e87 100644
--- a/internal/cas/getter.go
+++ b/internal/cas/getter.go
@@ -2,7 +2,9 @@ package cas
 
 import (
 	"context"
+	"fmt"
 	"net/url"
+	"os"
 	"strings"
 
 	"github.com/gruntwork-io/terragrunt/internal/errors"
@@ -28,6 +30,7 @@ func NewCASGetter(l log.Logger, cas *CAS, opts *CloneOptions) *CASGetter {
 			new(getter.GitDetector),
 			new(getter.BitBucketDetector),
 			new(getter.GitLabDetector),
+			new(getter.FileDetector),
 		},
 		CAS:    cas,
 		Logger: l,
@@ -36,6 +39,11 @@ func NewCASGetter(l log.Logger, cas *CAS, opts *CloneOptions) *CASGetter {
 }
 
 func (g *CASGetter) Get(ctx context.Context, req *getter.Request) error {
+	if req.Copy {
+		// Handle local directory by persisting to CAS and linking
+		return g.CAS.StoreLocalDirectory(ctx, g.Logger, req.Src, req.Dst)
+	}
+
 	ref := ""
 
 	url := req.URL()
@@ -90,10 +98,31 @@ func (g *CASGetter) Detect(req *getter.Request) (bool, error) {
 		}
 
 		if ok {
+			// Check if this is a FileDetector using type assertion
+			if _, isFileDetector := detector.(*getter.FileDetector); isFileDetector {
+				info, statErr := os.Stat(src)
+				if statErr != nil {
+					return false, fmt.Errorf("%w: %s", ErrDirectoryNotFound, src)
+				}
+
+				if !info.IsDir() {
+					return false, fmt.Errorf("%w: %s", ErrNotADirectory, src)
+				}
+
+				// We use this as a simple way to indicate that we're working with a local directory.
+				req.Copy = true
+			}
+
 			req.Src = src
+
 			return ok, nil
 		}
 	}
 
 	return false, nil
 }
+
+var (
+	ErrDirectoryNotFound = errors.New("directory not found")
+	ErrNotADirectory     = errors.New("not a directory")
+)
diff --git a/internal/cas/local.go b/internal/cas/local.go
new file mode 100644
index 00000000..b4b008e1
--- /dev/null
+++ b/internal/cas/local.go
@@ -0,0 +1,127 @@
+package cas
+
+import (
+	"context"
+	"crypto/sha1"
+	"encoding/hex"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+)
+
+// StoreLocalDirectory persists all content from a local source directory into the CAS
+// and then links the persisted files to the target directory
+func (c *CAS) StoreLocalDirectory(ctx context.Context, l log.Logger, sourceDir, targetDir string) error {
+	// Generate a synthetic hash for the local directory based on its contents
+	hash, treeData, err := c.hashDirectory(sourceDir)
+	if err != nil {
+		return fmt.Errorf("failed to hash local directory %s: %w", sourceDir, err)
+	}
+
+	// Store all files from the directory into the CAS
+	if err = c.storeLocalContent(l, sourceDir, hash, treeData); err != nil {
+		return fmt.Errorf("failed to store local content: %w", err)
+	}
+
+	// Parse the tree data and link to target directory
+	tree, err := ParseTree(string(treeData), targetDir)
+	if err != nil {
+		return fmt.Errorf("failed to parse local tree: %w", err)
+	}
+
+	return tree.LinkTree(ctx, c.store, targetDir)
+}
+
+// hashDirectory creates a synthetic hash and tree structure for a local directory
+func (c *CAS) hashDirectory(sourceDir string) (string, []byte, error) {
+	var treeData []byte
+
+	var allHashes []string
+
+	err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Implicitly handled by tracking the file hashes.
+		if info.IsDir() {
+			return nil
+		}
+
+		relPath, err := filepath.Rel(sourceDir, path)
+		if err != nil {
+			return err
+		}
+
+		// Convert to forward slashes for consistency (git-style paths)
+		relPath = strings.ReplaceAll(relPath, string(filepath.Separator), "/")
+
+		fileHash, err := hashFile(path)
+		if err != nil {
+			return fmt.Errorf("failed to hash file %s: %w", path, err)
+		}
+
+		// Artificially create a tree entry for the file.
+		mode := fmt.Sprintf("%06o", info.Mode().Perm())
+		treeLine := fmt.Sprintf("%s blob %s\t%s\n", mode, fileHash, relPath)
+		treeData = append(treeData, []byte(treeLine)...)
+
+		// Collect all hashes for directory hash calculation
+		allHashes = append(allHashes, fileHash)
+
+		return nil
+	})
+
+	if err != nil {
+		return "", nil, err
+	}
+
+	// Create a synthetic hash for the entire directory based on all file hashes
+	// This ensures the same directory contents always get the same hash
+	dirHash := hashString(strings.Join(allHashes, ""))
+
+	return dirHash, treeData, nil
+}
+
+// storeLocalContent stores all files from a local directory into the CAS
+func (c *CAS) storeLocalContent(l log.Logger, sourceDir, dirHash string, treeData []byte) error {
+	// First store the tree object itself
+	content := NewContent(c.store)
+	if err := content.Ensure(l, dirHash, treeData); err != nil {
+		return fmt.Errorf("failed to store tree data: %w", err)
+	}
+
+	// Walk the directory and store all files
+	return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Skip directories and the root directory itself
+		if info.IsDir() {
+			return nil
+		}
+
+		// Hash the file to get its content hash
+		fileHash, err := hashFile(path)
+		if err != nil {
+			return fmt.Errorf("failed to hash file %s: %w", path, err)
+		}
+
+		if err := content.EnsureCopy(l, fileHash, path); err != nil {
+			return fmt.Errorf("failed to store file %s: %w", path, err)
+		}
+
+		return nil
+	})
+}
+
+func hashString(s string) string {
+	h := sha1.New()
+	h.Write([]byte(s))
+
+	return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/internal/cas/store.go b/internal/cas/store.go
index 571d4521..ce85578a 100644
--- a/internal/cas/store.go
+++ b/internal/cas/store.go
@@ -3,22 +3,19 @@ package cas
 import (
 	"os"
 	"path/filepath"
-	"sync"
-	"time"
+
+	"github.com/gofrs/flock"
 )
 
-// Store manages the store directory and locks to prevent concurrent writes
+// Store manages the store directory and filesystem locks to prevent concurrent writes
 type Store struct {
-	locks   map[string]*sync.Mutex
-	path    string
-	mapLock sync.Mutex
+	path string
 }
 
 // NewStore creates a new Store instance.
 func NewStore(path string) *Store {
 	return &Store{
-		path:  path,
-		locks: make(map[string]*sync.Mutex),
+		path: path,
 	}
 }
 
@@ -28,11 +25,11 @@ func (s *Store) Path() string {
 }
 
 // NeedsWrite checks if a given hash needs to be stored
-func (s *Store) NeedsWrite(hash string, cloneStart time.Time) bool {
+func (s *Store) NeedsWrite(hash string) bool {
 	partitionDir := filepath.Join(s.path, hash[:2])
 	path := filepath.Join(partitionDir, hash)
 
-	return !s.hasContent(path) && !s.writeInProgress(path, cloneStart)
+	return !s.hasContent(path)
 }
 
 // HasContent checks if a given hash exists in the store
@@ -42,19 +39,104 @@ func (s *Store) hasContent(path string) bool {
 	return err == nil
 }
 
-// writeInProgress checks if a write is in progress for a given hash
-func (s *Store) writeInProgress(path string, cloneStart time.Time) bool {
-	path += ".tmp"
+// AcquireLock acquires a filesystem lock for the given hash
+// Returns the flock instance that should be unlocked when done
+func (s *Store) AcquireLock(hash string) (*flock.Flock, error) {
+	partitionDir := filepath.Join(s.path, hash[:2])
+	lockPath := filepath.Join(partitionDir, hash+".lock")
+
+	// Ensure the partition directory exists
+	if err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+		return nil, err
+	}
+
+	lock := flock.New(lockPath)
+	if err := lock.Lock(); err != nil {
+		return nil, err
+	}
+
+	return lock, nil
+}
+
+// TryAcquireLock attempts to acquire a filesystem lock for the given hash without blocking
+// Returns the flock instance and true if successful, nil and false if the lock is already held
+func (s *Store) TryAcquireLock(hash string) (*flock.Flock, bool, error) {
+	partitionDir := filepath.Join(s.path, hash[:2])
+	lockPath := filepath.Join(partitionDir, hash+".lock")
+
+	// Ensure the partition directory exists
+	if err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+		return nil, false, err
+	}
+
+	lock := flock.New(lockPath)
+
+	acquired, err := lock.TryLock()
+	if err != nil {
+		return nil, false, err
+	}
 
-	stat, err := os.Stat(path)
-	if err != nil && os.IsNotExist(err) {
-		return false
+	if !acquired {
+		return nil, false, nil
 	}
 
-	// To avoid deadlocks, assume that a write is in progress
-	// only if the file exists and was modified
-	// after the start of cloning.
-	modifiedTime := stat.ModTime()
+	return lock, true, nil
+}
+
+// EnsureWithWait tries to acquire a lock for the given hash, and if another process
+// is writing the same content, waits for it to complete instead of doing redundant work.
+// This is an optimization for read operations that avoids duplicate writes.
+//
+// Returns:
+// - needsWrite: true if content doesn't exist and caller should write it
+// - lock: the acquired lock (nil if needsWrite is false)
+// - error: any error that occurred
+func (s *Store) EnsureWithWait(hash string) (needsWrite bool, lock *flock.Flock, err error) {
+	// Fast path: check if content already exists
+	partitionDir := filepath.Join(s.path, hash[:2])
+	path := filepath.Join(partitionDir, hash)
+
+	if s.hasContent(path) {
+		return false, nil, nil
+	}
+
+	// Try to acquire lock without blocking
+	flockLock, acquired, err := s.TryAcquireLock(hash)
+	if err != nil {
+		return false, nil, err
+	}
+
+	if acquired {
+		// We got the lock immediately, check if we still need to write
+		// (another process might have completed while we were trying)
+		if !s.NeedsWrite(hash) {
+			// Content appeared while we were acquiring lock, no write needed
+			if err = flockLock.Unlock(); err != nil {
+				return false, nil, err
+			}
+
+			return false, nil, nil
+		}
+		// We have the lock and content doesn't exist, caller should write
+		return true, flockLock, nil
+	}
+
+	// Lock is held by another process, wait for it to complete
+	waitLock, err := s.AcquireLock(hash)
+	if err != nil {
+		return false, nil, err
+	}
+
+	// Now we have the lock, check if the other process wrote the content
+	if !s.NeedsWrite(hash) {
+		// Content was written by the other process, no write needed
+		if err := waitLock.Unlock(); err != nil {
+			return false, nil, err
+		}
+
+		return false, nil, nil
+	}
 
-	return modifiedTime.After(cloneStart)
+	// Content still doesn't exist, caller should write it
+	return true, waitLock, nil
 }
diff --git a/internal/cas/tree.go b/internal/cas/tree.go
index 62b5d95f..597c15ca 100644
--- a/internal/cas/tree.go
+++ b/internal/cas/tree.go
@@ -5,7 +5,10 @@ import (
 	"context"
 	"os"
 	"path/filepath"
+	"runtime"
 	"strings"
+
+	"golang.org/x/sync/errgroup"
 )
 
 const (
@@ -99,40 +102,92 @@ func ParseTree(output, path string) (*Tree, error) {
 func (t *Tree) LinkTree(ctx context.Context, store *Store, targetDir string) error {
 	content := NewContent(store)
 
-	for _, entry := range t.entries {
-		// Check for cancellation
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		default:
-		}
+	dirsToCreate := make(map[string]struct{}, len(t.entries))
+
+	type workItem struct {
+		itemType string
+		entry    TreeEntry
+		path     string
+		dirPath  string
+	}
 
+	workItems := make([]workItem, 0, len(t.entries))
+
+	for _, entry := range t.entries {
 		entryPath := filepath.Join(targetDir, entry.Path)
-		if err := os.MkdirAll(filepath.Dir(entryPath), DefaultDirPerms); err != nil {
-			return wrapError("mkdir_all", entryPath, err)
-		}
+		dirPath := filepath.Dir(entryPath)
+
+		dirsToCreate[dirPath] = struct{}{}
+
+		// If the parent directory is in dirsToCreate,
+		// we can remove it, since it will be created
+		// when creating the subtree anyways.
+		parentDirPath := filepath.Dir(dirPath)
+		delete(dirsToCreate, parentDirPath)
 
+		// Create work items based on entry type
 		switch entry.Type {
 		case "blob":
-			if err := content.Link(entry.Hash, entryPath); err != nil {
-				return wrapError("link_blob", entryPath, err)
-			}
+			workItems = append(workItems, workItem{
+				itemType: "link",
+				entry:    entry,
+				path:     entryPath,
+				dirPath:  dirPath,
+			})
 		case "tree":
-			treeData, err := content.Read(entry.Hash)
-			if err != nil {
-				return wrapError("read_tree", entry.Hash, err)
-			}
+			workItems = append(workItems, workItem{
+				itemType: "subtree",
+				entry:    entry,
+				path:     entryPath,
+				dirPath:  dirPath,
+			})
+		}
+	}
 
-			subTree, err := ParseTree(string(treeData), entryPath)
-			if err != nil {
-				return wrapError("parse_tree", entry.Hash, err)
-			}
+	for dirPath := range dirsToCreate {
+		if err := os.MkdirAll(dirPath, DefaultDirPerms); err != nil {
+			return wrapError("mkdir_all", dirPath, err)
+		}
+	}
 
-			if err := subTree.LinkTree(ctx, store, entryPath); err != nil {
-				return wrapError("link_subtree", entryPath, err)
+	// Use errgroup for concurrent processing
+	g, ctx := errgroup.WithContext(ctx)
+
+	// Set concurrency limit
+	scalingFactor := 2
+	maxWorkers := max(1, runtime.NumCPU()/scalingFactor)
+	g.SetLimit(maxWorkers)
+
+	// Process work items concurrently
+	for _, work := range workItems {
+		g.Go(func() error {
+			switch work.itemType {
+			case "link":
+				err := content.Link(ctx, work.entry.Hash, work.path)
+				if err != nil {
+					return wrapError("link_blob", work.path, err)
+				}
+			case "subtree":
+				treeData, err := content.Read(work.entry.Hash)
+				if err != nil {
+					return wrapError("read_tree", work.entry.Hash, err)
+				}
+
+				subTree, err := ParseTree(string(treeData), work.path)
+				if err != nil {
+					return wrapError("parse_tree", work.entry.Hash, err)
+				}
+
+				err = subTree.LinkTree(ctx, store, work.path)
+				if err != nil {
+					return wrapError("link_subtree", work.path, err)
+				}
 			}
-		}
+
+			return nil
+		})
 	}
 
-	return nil
+	// Wait for all goroutines to complete and return first error if any
+	return g.Wait()
 }
diff --git a/options/options.go b/options/options.go
index 109b5e8e..1c43f01c 100644
--- a/options/options.go
+++ b/options/options.go
@@ -75,6 +75,13 @@ var (
 		"state",
 	}
 
+	defaultVersionManagerFileName = []string{
+		".terraform-version",
+		".tool-versions",
+		"mise.toml",
+		".mise.toml",
+	}
+
 	// Pattern used to clean error message when looking for retry and ignore patterns.
 	errorCleanPattern = regexp.MustCompile(`[^a-zA-Z0-9./'"(): ]+`)
 )
@@ -202,6 +209,8 @@ type TerragruntOptions struct {
 	ModulesThatInclude []string
 	// When used with `run --all`, restrict the units in the stack to only those that read at least one of the files in this list.
 	UnitsReading []string
+	// When set, it will be used to compute the cache key for `-version` checks.
+	VersionManagerFileName []string
 	// Experiments is a map of experiments, and their status.
 	Experiments experiment.Experiments `clone:"shadowcopy"`
 	// Maximum number of times to retry errors matching RetryableErrors
@@ -418,6 +427,7 @@ func NewTerragruntOptionsWithWriters(stdout, stderr io.Writer) *TerragruntOption
 		Telemetry:                  new(telemetry.Options),
 		NoStackValidate:            false,
 		NoStackGenerate:            false,
+		VersionManagerFileName:     defaultVersionManagerFileName,
 	}
 }
 
diff --git a/tf/cache/server.go b/tf/cache/server.go
index 699a5e27..4c89d677 100644
--- a/tf/cache/server.go
+++ b/tf/cache/server.go
@@ -92,8 +92,6 @@ func (server *Server) Run(ctx context.Context, ln net.Listener) error {
 	errGroup, ctx := errgroup.WithContext(ctx)
 
 	for _, service := range server.services {
-		service := service
-
 		errGroup.Go(func() error {
 			return service.Run(ctx)
 		})
diff --git a/tflint/tflint.go b/tflint/tflint.go
index 4f20610b..d1cc06ee 100644
--- a/tflint/tflint.go
+++ b/tflint/tflint.go
@@ -182,8 +182,8 @@ func tfArgumentsToTflintVar(l log.Logger, hook config.Hook,
 		if arg.EnvVars != nil {
 			// extract env_vars
 			for name, value := range *arg.EnvVars {
-				if strings.HasPrefix(name, tfVarPrefix) {
-					varName := strings.TrimPrefix(name, tfVarPrefix)
+				if after, ok := strings.CutPrefix(name, tfVarPrefix); ok {
+					varName := after
 
 					varValue, err := util.AsTerraformEnvVarJSONValue(value)
 					if err != nil {
@@ -199,14 +199,14 @@ func tfArgumentsToTflintVar(l log.Logger, hook config.Hook,
 		if arg.Arguments != nil {
 			// extract variables and var files from arguments
 			for _, value := range *arg.Arguments {
-				if strings.HasPrefix(value, argVarPrefix) {
-					varName := strings.TrimPrefix(value, argVarPrefix)
+				if after, ok := strings.CutPrefix(value, argVarPrefix); ok {
+					varName := after
 					newVar := fmt.Sprintf("--var='%s'", varName)
 					variables = append(variables, newVar)
 				}
 
-				if strings.HasPrefix(value, argVarFilePrefix) {
-					varName := strings.TrimPrefix(value, argVarFilePrefix)
+				if after, ok := strings.CutPrefix(value, argVarFilePrefix); ok {
+					varName := after
 					newVar := "--var-file=" + varName
 					variables = append(variables, newVar)
 				}
diff --git a/util/file.go b/util/file.go
index ffdc0c12..2e90dfab 100644
--- a/util/file.go
+++ b/util/file.go
@@ -6,6 +6,7 @@ import (
 	"crypto/sha256"
 	"encoding/gob"
 	"io"
+	"net/url"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -461,6 +462,14 @@ func WriteFileWithSamePermissions(source string, destination string, contents []
 		return errors.New(err)
 	}
 
+	// If destination exists, remove it first to avoid permission issues
+	// This is especially important when CAS creates read-only files
+	if FileExists(destination) {
+		if err := os.Remove(destination); err != nil {
+			return errors.New(err)
+		}
+	}
+
 	return os.WriteFile(destination, contents, fileInfo.Mode())
 }
 
@@ -972,3 +981,36 @@ func evalRealPathAndInfo(currentPath string) (string, os.FileInfo, error) {
 
 	return realPath, realInfo, nil
 }
+
+// SanitizePath resolves a file path within a base directory, returning the sanitized path or an error if it attempts
+// to access anything outside the base directory.
+func SanitizePath(baseDir string, file string) (string, error) {
+	if baseDir == "" || file == "" {
+		return "", errors.New("baseDir and file must be provided")
+	}
+
+	file, err := url.QueryUnescape(file)
+	if err != nil {
+		return "", err
+	}
+
+	baseDir, err = url.QueryUnescape(baseDir)
+	if err != nil {
+		return "", err
+	}
+
+	root, err := os.OpenRoot(baseDir)
+	if err != nil {
+		return "", err
+	}
+	defer root.Close() //nolint:errcheck
+
+	fileInfo, err := root.Stat(file)
+	if err != nil {
+		return "", err
+	}
+
+	fullPath := baseDir + string(os.PathSeparator) + fileInfo.Name()
+
+	return fullPath, nil
+}



Test patch:
diff --git a/cli/commands/catalog/tui/model_test.go b/cli/commands/catalog/tui/model_test.go
index c9eb42f0..2bc68c1b 100644
--- a/cli/commands/catalog/tui/model_test.go
+++ b/cli/commands/catalog/tui/model_test.go
@@ -41,7 +41,7 @@ func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) cat
 		// Initialize git repository
 		gitDir := filepath.Join(dummyRepoDir, ".git")
 		os.MkdirAll(gitDir, 0755)
-		os.WriteFile(filepath.Join(gitDir, "config"), []byte(fmt.Sprintf(`[core]
+		os.WriteFile(filepath.Join(gitDir, "config"), fmt.Appendf(nil, `[core]
 	repositoryformatversion = 0
 	filemode = true
 	bare = false
@@ -52,7 +52,7 @@ func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) cat
 [branch "main"]
 	remote = origin
 	merge = refs/heads/main
-`, repoURL)), 0644)
+`, repoURL), 0644)
 		os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644)
 
 		// Create refs directory structure
diff --git a/cli/commands/hcl/format/format_test.go b/cli/commands/hcl/format/format_test.go
index f3157b48..50d0ddbf 100644
--- a/cli/commands/hcl/format/format_test.go
+++ b/cli/commands/hcl/format/format_test.go
@@ -50,7 +50,6 @@ func TestHCLFmt(t *testing.T) {
 		}
 		for _, dir := range dirs {
 			// Capture range variable into for block so it doesn't change while looping
-			dir := dir
 
 			t.Run(dir, func(t *testing.T) {
 				t.Parallel()
@@ -114,7 +113,6 @@ func TestHCLFmtErrors(t *testing.T) {
 	}
 	for _, dir := range dirs {
 		// Capture range variable into for block so it doesn't change while looping
-		dir := dir
 
 		t.Run(dir, func(t *testing.T) {
 			t.Parallel()
@@ -164,7 +162,6 @@ func TestHCLFmtCheck(t *testing.T) {
 
 	for _, dir := range dirs {
 		// Capture range variable into for block so it doesn't change while looping
-		dir := dir
 
 		t.Run(dir, func(t *testing.T) {
 			t.Parallel()
@@ -210,7 +207,6 @@ func TestHCLFmtCheckErrors(t *testing.T) {
 
 	for _, dir := range dirs {
 		// Capture range variable into for block so it doesn't change while looping
-		dir := dir
 
 		t.Run(dir, func(t *testing.T) {
 			t.Parallel()
@@ -268,7 +264,6 @@ func TestHCLFmtFile(t *testing.T) {
 	// test that none of the other files were formatted
 	for _, dir := range dirs {
 		// Capture range variable into for block so it doesn't change while looping
-		dir := dir
 
 		t.Run(dir, func(t *testing.T) {
 			t.Parallel()
diff --git a/cli/commands/run/cli_test.go b/cli/commands/run/cli_test.go
index 45c2eca3..58790acf 100644
--- a/cli/commands/run/cli_test.go
+++ b/cli/commands/run/cli_test.go
@@ -37,7 +37,6 @@ func TestAction(t *testing.T) {
 	}
 
 	for _, tc := range tt {
-		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
diff --git a/cli/commands/run/download_source_test.go b/cli/commands/run/download_source_test.go
index 90645a8c..c4db7e3e 100644
--- a/cli/commands/run/download_source_test.go
+++ b/cli/commands/run/download_source_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/gruntwork-io/terragrunt/config"
+	"github.com/gruntwork-io/terragrunt/internal/experiment"
 	"github.com/gruntwork-io/terragrunt/options"
 	"github.com/gruntwork-io/terragrunt/util"
 	"github.com/hashicorp/go-getter"
@@ -533,3 +534,237 @@ func TestUpdateGettersExcludeFromCopy(t *testing.T) {
 		})
 	}
 }
+
+// TestDownloadSourceWithCASExperimentDisabled tests that CAS is not used when the experiment is disabled
+func TestDownloadSourceWithCASExperimentDisabled(t *testing.T) {
+	t.Parallel()
+
+	tmpDir := t.TempDir()
+
+	localSourcePath := absPath(t, "../../../test/fixtures/download-source/hello-world")
+	src := &tf.Source{
+		CanonicalSourceURL: parseURL(t, "file://"+localSourcePath),
+		DownloadDir:        tmpDir,
+		WorkingDir:         tmpDir,
+		VersionFile:        filepath.Join(tmpDir, "version-file.txt"),
+	}
+
+	opts, err := options.NewTerragruntOptionsForTest("./should-not-be-used")
+	require.NoError(t, err)
+
+	// Ensure CAS experiment is not enabled
+	opts.Experiments = experiment.NewExperiments()
+
+	cfg := &config.TerragruntConfig{
+		Terraform: &config.TerraformConfig{
+			ExtraArgs: []config.TerraformExtraArguments{},
+			Source:    nil,
+		},
+	}
+
+	l := logger.CreateLogger()
+	l.SetOptions(log.WithOutput(io.Discard))
+
+	// Mock the download source function call
+	r := report.NewReport()
+
+	err = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, opts, cfg, r)
+
+	require.NoError(t, err)
+
+	// Verify the file was downloaded
+	expectedFilePath := filepath.Join(tmpDir, "main.tf")
+	assert.FileExists(t, expectedFilePath)
+}
+
+// TestDownloadSourceWithCASExperimentEnabled tests that CAS is attempted when the experiment is enabled
+func TestDownloadSourceWithCASExperimentEnabled(t *testing.T) {
+	t.Parallel()
+
+	tmpDir := t.TempDir()
+
+	localSourcePath := absPath(t, "../../../test/fixtures/download-source/hello-world")
+	src := &tf.Source{
+		CanonicalSourceURL: parseURL(t, "file://"+localSourcePath),
+		DownloadDir:        tmpDir,
+		WorkingDir:         tmpDir,
+		VersionFile:        filepath.Join(tmpDir, "version-file.txt"),
+	}
+
+	// Create options with CAS experiment enabled
+	opts, err := options.NewTerragruntOptionsForTest("./should-not-be-used")
+	require.NoError(t, err)
+
+	// Enable CAS experiment
+	opts.Experiments = experiment.NewExperiments()
+	err = opts.Experiments.EnableExperiment(experiment.CAS)
+	require.NoError(t, err)
+
+	cfg := &config.TerragruntConfig{
+		Terraform: &config.TerraformConfig{
+			ExtraArgs: []config.TerraformExtraArguments{},
+			Source:    nil,
+		},
+	}
+
+	l := logger.CreateLogger()
+	l.SetOptions(log.WithOutput(io.Discard))
+
+	r := report.NewReport()
+
+	err = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, opts, cfg, r)
+	require.NoError(t, err)
+
+	expectedFilePath := filepath.Join(tmpDir, "main.tf")
+	assert.FileExists(t, expectedFilePath)
+}
+
+// TestDownloadSourceWithCASGitSource tests CAS functionality with a Git source
+func TestDownloadSourceWithCASGitSource(t *testing.T) {
+	t.Parallel()
+
+	tmpDir := t.TempDir()
+
+	src := &tf.Source{
+		CanonicalSourceURL: parseURL(t, "github.com/gruntwork-io/terragrunt//test/fixtures/download/hello-world"),
+		DownloadDir:        tmpDir,
+		WorkingDir:         tmpDir,
+		VersionFile:        filepath.Join(tmpDir, "version-file.txt"),
+	}
+
+	opts, err := options.NewTerragruntOptionsForTest("./should-not-be-used")
+	require.NoError(t, err)
+
+	// Enable CAS experiment
+	opts.Experiments = experiment.NewExperiments()
+	err = opts.Experiments.EnableExperiment(experiment.CAS)
+	require.NoError(t, err)
+
+	cfg := &config.TerragruntConfig{
+		Terraform: &config.TerraformConfig{
+			ExtraArgs: []config.TerraformExtraArguments{},
+			Source:    nil,
+		},
+	}
+
+	l := logger.CreateLogger()
+	l.SetOptions(log.WithOutput(io.Discard))
+
+	r := report.NewReport()
+
+	err = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, opts, cfg, r)
+	require.NoError(t, err)
+
+	// Verify the file was downloaded
+	expectedFilePath := filepath.Join(tmpDir, "main.tf")
+	assert.FileExists(t, expectedFilePath)
+}
+
+// TestDownloadSourceCASInitializationFailure tests the fallback behavior when CAS initialization fails
+func TestDownloadSourceCASInitializationFailure(t *testing.T) {
+	t.Parallel()
+
+	tmpDir := t.TempDir()
+
+	localSourcePath := absPath(t, "../../../test/fixtures/download-source/hello-world")
+	src := &tf.Source{
+		CanonicalSourceURL: parseURL(t, "file://"+localSourcePath),
+		DownloadDir:        tmpDir,
+		WorkingDir:         tmpDir,
+		VersionFile:        filepath.Join(tmpDir, "version-file.txt"),
+	}
+
+	opts, err := options.NewTerragruntOptionsForTest("./should-not-be-used")
+	require.NoError(t, err)
+
+	// Enable CAS experiment
+	opts.Experiments = experiment.NewExperiments()
+	err = opts.Experiments.EnableExperiment(experiment.CAS)
+	require.NoError(t, err)
+
+	cfg := &config.TerragruntConfig{
+		Terraform: &config.TerraformConfig{
+			ExtraArgs: []config.TerraformExtraArguments{},
+			Source:    nil,
+		},
+	}
+
+	l := logger.CreateLogger()
+	l.SetOptions(log.WithOutput(io.Discard))
+
+	r := report.NewReport()
+
+	err = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, opts, cfg, r)
+	require.NoError(t, err)
+
+	expectedFilePath := filepath.Join(tmpDir, "main.tf")
+	assert.FileExists(t, expectedFilePath)
+}
+
+// TestDownloadSourceWithCASMultipleSources tests that CAS works with multiple different sources
+func TestDownloadSourceWithCASMultipleSources(t *testing.T) {
+	t.Parallel()
+
+	opts, err := options.NewTerragruntOptionsForTest("./should-not-be-used")
+	require.NoError(t, err)
+	opts.Env = env.Parse(os.Environ())
+
+	// Enable CAS experiment
+	opts.Experiments = experiment.NewExperiments()
+	err = opts.Experiments.EnableExperiment(experiment.CAS)
+	require.NoError(t, err)
+
+	cfg := &config.TerragruntConfig{
+		Terraform: &config.TerraformConfig{
+			ExtraArgs: []config.TerraformExtraArguments{},
+			Source:    nil,
+		},
+	}
+
+	l := logger.CreateLogger()
+	l.SetOptions(log.WithOutput(io.Discard))
+
+	r := report.NewReport()
+
+	testCases := []struct {
+		name      string
+		sourceURL string
+		expectCAS bool
+	}{
+		{
+			name:      "Local file source",
+			sourceURL: "file://" + absPath(t, "../../../test/fixtures/download-source/hello-world"),
+			expectCAS: false, // CAS doesn't handle file:// URLs
+		},
+		{
+			name:      "HTTP source",
+			sourceURL: "https://example.com/repo.tar.gz",
+			expectCAS: false, // CAS doesn't handle HTTP sources
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			tmpDir := t.TempDir()
+
+			src := &tf.Source{
+				CanonicalSourceURL: parseURL(t, tc.sourceURL),
+				DownloadDir:        tmpDir,
+				WorkingDir:         tmpDir,
+				VersionFile:        filepath.Join(tmpDir, "version-file.txt"),
+			}
+
+			err = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, opts, cfg, r)
+
+			if tc.name == "Local file source" {
+				require.NoError(t, err)
+				expectedFilePath := filepath.Join(tmpDir, "main.tf")
+				assert.FileExists(t, expectedFilePath)
+			} else {
+				t.Logf("Source %s result: %v", tc.sourceURL, err)
+			}
+		})
+	}
+}
diff --git a/cli/commands/run/version_check_internal_test.go b/cli/commands/run/version_check_internal_test.go
new file mode 100644
index 00000000..244326b3
--- /dev/null
+++ b/cli/commands/run/version_check_internal_test.go
@@ -0,0 +1,58 @@
+package run
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_computeVersionFilesCacheKey(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name         string
+		workingDir   string
+		want         string
+		versionFiles []string
+	}{
+		{
+			name:         "version files slice is empty",
+			workingDir:   "",
+			versionFiles: nil,
+			want:         "r01AJjVD7VSXCQk1ORuh_no_NRY", // "no-version-files"
+		},
+		{
+			name:       "workdir contains version files",
+			workingDir: "test-fixtures/dir-with-files-to-test-cache-key",
+			versionFiles: []string{
+				".terraform-version",
+				".tool-versions",
+			},
+			want: "XBE-VO9pOnQjPQDmLQCvSCdckSQ",
+		},
+		{
+			name:       "workdir contains version files and we try to escape the working dir",
+			workingDir: "test-fixtures/dir-with-files-to-test-cache-key",
+			versionFiles: []string{
+				".terraform-version",
+				".tool-versions",
+				"../../../dev/random",
+			},
+			want: "XBE-VO9pOnQjPQDmLQCvSCdckSQ",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			assert.Equalf(
+				t,
+				tt.want,
+				computeVersionFilesCacheKey(tt.workingDir, tt.versionFiles),
+				"computeVersionFilesCacheKey(%v, %v)",
+				tt.workingDir,
+				tt.versionFiles,
+			)
+		})
+	}
+}
diff --git a/config/config_helpers_test.go b/config/config_helpers_test.go
index 5e56135f..6a4c9f2b 100644
--- a/config/config_helpers_test.go
+++ b/config/config_helpers_test.go
@@ -202,7 +202,6 @@ func TestRunCommand(t *testing.T) {
 		},
 	}
 	for _, tc := range testCases {
-		tc := tc
 
 		t.Run(tc.terragruntOptions.TerragruntConfigPath, func(t *testing.T) {
 			t.Parallel()
@@ -315,7 +314,6 @@ func TestFindInParentFolders(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
@@ -385,7 +383,6 @@ func TestResolveTerragruntInterpolation(t *testing.T) {
 	for _, tc := range testCases {
 		// The following is necessary to make sure tc's values don't
 		// get updated due to concurrency within the scope of t.Run(..) below
-		tc := tc
 
 		t.Run(fmt.Sprintf("%s--%s", tc.str, tc.terragruntOptions.TerragruntConfigPath), func(t *testing.T) {
 			t.Parallel()
@@ -485,7 +482,6 @@ func TestResolveEnvInterpolationConfigString(t *testing.T) {
 	for _, tc := range testCases {
 		// The following is necessary to make sure tc's values don't
 		// get updated due to concurrency within the scope of t.Run(..) below
-		tc := tc
 		t.Run(tc.str, func(t *testing.T) {
 			t.Parallel()
 
@@ -535,7 +531,6 @@ func TestResolveCommandsInterpolationConfigString(t *testing.T) {
 	for _, tc := range testCases {
 		// The following is necessary to make sure tc's values don't
 		// get updated due to concurrency within the scope of t.Run(..) below
-		tc := tc
 
 		t.Run(tc.str, func(t *testing.T) {
 			t.Parallel()
@@ -796,7 +791,6 @@ func TestTerraformBuiltInFunctions(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
diff --git a/internal/cas/benchmark_test.go b/internal/cas/benchmark_test.go
index e5f41172..90a05bdf 100644
--- a/internal/cas/benchmark_test.go
+++ b/internal/cas/benchmark_test.go
@@ -149,12 +149,12 @@ func BenchmarkGitOperations(b *testing.B) {
 
 	ctx := b.Context()
 
-	if err := git.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", false, 1, "main"); err != nil {
+	if err = git.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", false, 1, "main"); err != nil {
 		b.Fatal(err)
 	}
 
 	b.Run("ls-remote", func(b *testing.B) {
-		git, err := cas.NewGitRunner()
+		git, err = cas.NewGitRunner()
 		if err != nil {
 			b.Fatal(err)
 		}
diff --git a/internal/cas/content_test.go b/internal/cas/content_test.go
index 1169e655..e444e2dd 100644
--- a/internal/cas/content_test.go
+++ b/internal/cas/content_test.go
@@ -106,8 +106,9 @@ func TestContent_Link(t *testing.T) {
 
 		// Then create a link to it
 		targetDir := t.TempDir()
-		targetPath := filepath.Join(targetDir, "subdir", "test.txt")
-		err = content.Link(testHash, targetPath)
+		targetPath := filepath.Join(targetDir, "test.txt")
+
+		err = content.Link(t.Context(), testHash, targetPath)
 		require.NoError(t, err)
 
 		// Verify link was created and contains correct content
@@ -143,7 +144,7 @@ func TestContent_Link(t *testing.T) {
 		require.NoError(t, err)
 
 		// Try to create link
-		err = content.Link(testHash, targetPath)
+		err = content.Link(t.Context(), testHash, targetPath)
 		require.NoError(t, err)
 
 		// Verify original content remains
@@ -152,3 +153,98 @@ func TestContent_Link(t *testing.T) {
 		assert.Equal(t, []byte("existing content"), existingData)
 	})
 }
+
+func TestContent_EnsureWithWait(t *testing.T) {
+	t.Parallel()
+
+	l := logger.CreateLogger()
+
+	t.Run("content already exists", func(t *testing.T) {
+		t.Parallel()
+
+		store := cas.NewStore(t.TempDir())
+		content := cas.NewContent(store)
+		testHash := testHashValue
+		testData := []byte("test content")
+
+		// Store content first
+		err := content.Store(l, testHash, testData)
+		require.NoError(t, err)
+
+		// EnsureWithWait should not need to write again
+		err = content.EnsureWithWait(l, testHash, []byte("different content"))
+		require.NoError(t, err)
+
+		// Verify original content remains
+		partitionDir := filepath.Join(store.Path(), testHash[:2])
+		storedPath := filepath.Join(partitionDir, testHash)
+		storedData, err := os.ReadFile(storedPath)
+		require.NoError(t, err)
+		assert.Equal(t, testData, storedData)
+	})
+
+	t.Run("content doesn't exist", func(t *testing.T) {
+		t.Parallel()
+
+		store := cas.NewStore(t.TempDir())
+		content := cas.NewContent(store)
+		testHash := "newcontent123456"
+		testData := []byte("new test content")
+
+		// EnsureWithWait should store the content
+		err := content.EnsureWithWait(l, testHash, testData)
+		require.NoError(t, err)
+
+		// Verify content was stored
+		partitionDir := filepath.Join(store.Path(), testHash[:2])
+		storedPath := filepath.Join(partitionDir, testHash)
+		storedData, err := os.ReadFile(storedPath)
+		require.NoError(t, err)
+		assert.Equal(t, testData, storedData)
+	})
+
+	t.Run("concurrent writes - optimization", func(t *testing.T) {
+		t.Parallel()
+
+		store := cas.NewStore(t.TempDir())
+		content := cas.NewContent(store)
+		testHash := "concurrent123456"
+
+		// Channel to coordinate the test
+		process1Started := make(chan struct{})
+		process1Done := make(chan struct{})
+		process2Done := make(chan struct{})
+
+		// Process 1: acquires lock first
+		go func() {
+			defer close(process1Done)
+
+			err := content.EnsureWithWait(l, testHash, []byte("process 1 data"))
+			assert.NoError(t, err)
+
+			close(process1Started)
+		}()
+
+		// Process 2: should wait for process 1 and not duplicate work
+		go func() {
+			defer close(process2Done)
+
+			// Wait for process 1 to start
+			<-process1Started
+
+			err := content.EnsureWithWait(l, testHash, []byte("process 2 data"))
+			assert.NoError(t, err)
+		}()
+
+		// Wait for both to complete
+		<-process1Done
+		<-process2Done
+
+		// Verify only one content exists (from process 1)
+		partitionDir := filepath.Join(store.Path(), testHash[:2])
+		storedPath := filepath.Join(partitionDir, testHash)
+		storedData, err := os.ReadFile(storedPath)
+		require.NoError(t, err)
+		assert.Equal(t, []byte("process 1 data"), storedData)
+	})
+}
diff --git a/internal/cas/getter_test.go b/internal/cas/getter_test.go
index 1baae2f3..acd57cb6 100644
--- a/internal/cas/getter_test.go
+++ b/internal/cas/getter_test.go
@@ -2,6 +2,7 @@ package cas_test
 
 import (
 	"net/url"
+	"os"
 	"path/filepath"
 	"testing"
 
@@ -38,21 +39,32 @@ func TestCASGetterDetect(t *testing.T) {
 
 	g := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})
 
+	tmp := t.TempDir()
+
+	os.MkdirAll(filepath.Join(tmp, "fake-module"), 0755)
+	os.WriteFile(filepath.Join(tmp, "fake-module", "main.tf"), []byte(""), 0644)
+
 	tests := []struct {
-		name     string
-		src      string
-		pwd      string
-		expected bool
+		expectedErr error
+		name        string
+		src         string
+		pwd         string
 	}{
 		{
-			name:     "GitHub repository",
-			src:      "github.com/gruntwork-io/terragrunt",
-			expected: true,
+			name: "GitHub repository",
+			src:  "github.com/gruntwork-io/terragrunt",
+			pwd:  tmp,
 		},
 		{
-			name:     "Invalid URL",
-			src:      "not-a-valid-url",
-			expected: false,
+			name:        "Invalid URL",
+			src:         "not-a-valid-url",
+			pwd:         tmp,
+			expectedErr: cas.ErrDirectoryNotFound,
+		},
+		{
+			name: "Local directory",
+			src:  "./fake-module",
+			pwd:  tmp,
 		},
 	}
 
@@ -65,8 +77,12 @@ func TestCASGetterDetect(t *testing.T) {
 				Pwd: tt.pwd,
 			}
 			ok, err := g.Detect(req)
-			require.NoError(t, err)
-			assert.Equal(t, tt.expected, ok)
+			if tt.expectedErr != nil {
+				require.ErrorIs(t, err, tt.expectedErr)
+			} else {
+				require.NoError(t, err)
+				assert.True(t, ok)
+			}
 		})
 	}
 }
@@ -125,3 +141,57 @@ func TestCASGetterGet(t *testing.T) {
 		})
 	}
 }
+
+func TestCASGetterLocalDir(t *testing.T) {
+	t.Parallel()
+
+	tmp := t.TempDir()
+	storePath := filepath.Join(tmp, "store")
+
+	c, err := cas.New(cas.Options{
+		StorePath: storePath,
+	})
+	require.NoError(t, err)
+
+	opts := &cas.CloneOptions{
+		Branch: "main",
+	}
+
+	l := logger.CreateLogger()
+
+	g := cas.NewCASGetter(l, c, opts)
+
+	fakeModule := filepath.Join(tmp, "fake-module")
+	os.MkdirAll(fakeModule, 0755)
+
+	fakeModuleSubdir := filepath.Join(fakeModule, "subdir")
+	os.MkdirAll(fakeModuleSubdir, 0755)
+
+	os.WriteFile(filepath.Join(fakeModule, "main.tf"), []byte(""), 0644)
+	os.WriteFile(filepath.Join(fakeModuleSubdir, "subfile.tf"), []byte(""), 0644)
+
+	fakeDest := filepath.Join(tmp, "fake-dest")
+
+	req := &getter.Request{
+		Src: fakeModule,
+		Dst: fakeDest,
+		Pwd: tmp,
+	}
+
+	ok, err := g.Detect(req)
+	require.NoError(t, err)
+	assert.True(t, ok)
+
+	assert.True(t, req.Copy)
+
+	err = g.Get(t.Context(), req)
+	require.NoError(t, err)
+
+	stat, err := os.Stat(filepath.Join(fakeDest, "main.tf"))
+	require.NoError(t, err)
+	assert.Equal(t, os.FileMode(0644), stat.Mode())
+
+	stat, err = os.Stat(filepath.Join(fakeDest, "subdir", "subfile.tf"))
+	require.NoError(t, err)
+	assert.Equal(t, os.FileMode(0644), stat.Mode())
+}
diff --git a/internal/cas/integration_test.go b/internal/cas/integration_test.go
index 84e386b0..81d86f78 100644
--- a/internal/cas/integration_test.go
+++ b/internal/cas/integration_test.go
@@ -4,7 +4,6 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
-	"time"
 
 	"github.com/gruntwork-io/terragrunt/internal/cas"
 	"github.com/gruntwork-io/terragrunt/test/helpers/logger"
@@ -132,7 +131,7 @@ func TestIntegration_TreeStorage(t *testing.T) {
 		// Verify the tree object is stored
 		store := cas.NewStore(storePath)
 		require.NoError(t, err)
-		assert.False(t, store.NeedsWrite(commitHash, time.Now()), "Tree object should be stored")
+		assert.False(t, store.NeedsWrite(commitHash), "Tree object should be stored")
 
 		// Verify we can read the tree content
 		content := cas.NewContent(store)
diff --git a/internal/cas/store_test.go b/internal/cas/store_test.go
index 72a310ac..367632c3 100644
--- a/internal/cas/store_test.go
+++ b/internal/cas/store_test.go
@@ -60,7 +60,215 @@ func TestStore_NeedsWrite(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			assert.Equal(t, tt.want, store.NeedsWrite(tt.hash, time.Now()))
+			assert.Equal(t, tt.want, store.NeedsWrite(tt.hash))
 		})
 	}
 }
+
+func TestStore_AcquireLock(t *testing.T) {
+	t.Parallel()
+	tempDir := t.TempDir()
+	store := cas.NewStore(tempDir)
+	testHash := "abcdef1234567890abcdef1234567890abcdef12"
+
+	// Test successful lock acquisition
+	lock, err := store.AcquireLock(testHash)
+	require.NoError(t, err)
+	assert.NotNil(t, lock)
+
+	// Verify lock file exists
+	lockPath := filepath.Join(tempDir, testHash[:2], testHash+".lock")
+	assert.FileExists(t, lockPath)
+
+	// Clean up
+	err = lock.Unlock()
+	require.NoError(t, err)
+}
+
+func TestStore_TryAcquireLock(t *testing.T) {
+	t.Parallel()
+	tempDir := t.TempDir()
+	store := cas.NewStore(tempDir)
+	testHash := "abcdef1234567890abcdef1234567890abcdef12"
+
+	// Test successful lock acquisition
+	lock1, acquired, err := store.TryAcquireLock(testHash)
+	require.NoError(t, err)
+	assert.True(t, acquired)
+	assert.NotNil(t, lock1)
+
+	// Test lock contention - should fail to acquire
+	lock2, acquired, err := store.TryAcquireLock(testHash)
+	require.NoError(t, err)
+	assert.False(t, acquired)
+	assert.Nil(t, lock2)
+
+	// Clean up first lock
+	err = lock1.Unlock()
+	require.NoError(t, err)
+
+	// Now should be able to acquire again
+	lock3, acquired, err := store.TryAcquireLock(testHash)
+	require.NoError(t, err)
+	assert.True(t, acquired)
+	assert.NotNil(t, lock3)
+
+	// Clean up
+	err = lock3.Unlock()
+	assert.NoError(t, err)
+}
+
+func TestStore_LockConcurrency(t *testing.T) {
+	t.Parallel()
+	tempDir := t.TempDir()
+	store := cas.NewStore(tempDir)
+	testHash := "abcdef1234567890abcdef1234567890abcdef12"
+
+	// Test that multiple goroutines can't acquire the same lock
+	done := make(chan bool, 2)
+	acquired := make(chan bool, 2)
+
+	// First goroutine acquires lock and holds it briefly
+	go func() {
+		lock, err := store.AcquireLock(testHash)
+		assert.NoError(t, err)
+		acquired <- true
+		time.Sleep(100 * time.Millisecond) // Hold lock briefly
+		err = lock.Unlock()
+		assert.NoError(t, err)
+		done <- true
+	}()
+
+	// Second goroutine tries to acquire the same lock
+	go func() {
+		<-acquired // Wait for first goroutine to acquire lock
+
+		// Should block until first lock is released
+		start := time.Now()
+		lock, err := store.AcquireLock(testHash)
+		elapsed := time.Since(start)
+
+		assert.NoError(t, err)
+		assert.Greater(t, elapsed, 50*time.Millisecond, "Second lock should have been blocked")
+
+		err = lock.Unlock()
+		assert.NoError(t, err)
+		done <- true
+	}()
+
+	// Wait for both goroutines to complete
+	<-done
+	<-done
+}
+
+func TestStore_EnsureWithWait(t *testing.T) {
+	t.Parallel()
+	tempDir := t.TempDir()
+	store := cas.NewStore(tempDir)
+	testHash := "abcdef1234567890abcdef1234567890abcdef12"
+
+	t.Run("content already exists", func(t *testing.T) {
+		t.Parallel()
+
+		// Create the content manually
+		partitionDir := filepath.Join(tempDir, testHash[:2])
+		err := os.MkdirAll(partitionDir, 0755)
+		require.NoError(t, err)
+
+		contentPath := filepath.Join(partitionDir, testHash)
+		err = os.WriteFile(contentPath, []byte("existing content"), 0644)
+		require.NoError(t, err)
+
+		// EnsureWithWait should return false (no write needed)
+		needsWrite, lock, err := store.EnsureWithWait(testHash)
+		require.NoError(t, err)
+		assert.False(t, needsWrite)
+		assert.Nil(t, lock)
+	})
+
+	t.Run("content doesn't exist, no contention", func(t *testing.T) {
+		t.Parallel()
+
+		testHashNew := "fedcba0987654321fedcba0987654321fedcba09"
+
+		// EnsureWithWait should return true (write needed) and provide lock
+		needsWrite, lock, err := store.EnsureWithWait(testHashNew)
+		require.NoError(t, err)
+		assert.True(t, needsWrite)
+		assert.NotNil(t, lock)
+
+		// Clean up
+		err = lock.Unlock()
+		assert.NoError(t, err)
+	})
+
+	t.Run("concurrent writes - wait optimization", func(t *testing.T) {
+		t.Parallel()
+
+		store := cas.NewStore(t.TempDir())
+		testHashConcurrent := "1234567890abcdef1234567890abcdef12345678"
+
+		// Channel to coordinate the test
+		process1Started := make(chan struct{})
+		process1CanContinue := make(chan struct{})
+		process2Done := make(chan bool)
+
+		// Simulate process 1: acquires lock and holds it
+		go func() {
+			needsWrite, lock, err := store.EnsureWithWait(testHashConcurrent)
+			assert.NoError(t, err)
+			assert.True(t, needsWrite)
+			assert.NotNil(t, lock)
+
+			// Signal that process 1 has the lock
+			close(process1Started)
+
+			// Wait for signal to continue
+			<-process1CanContinue
+
+			// Write the content
+			partitionDir := filepath.Join(store.Path(), testHashConcurrent[:2])
+			err = os.MkdirAll(partitionDir, 0755)
+			assert.NoError(t, err)
+
+			contentPath := filepath.Join(partitionDir, testHashConcurrent)
+			err = os.WriteFile(contentPath, []byte("written by process 1"), 0644)
+			assert.NoError(t, err)
+
+			// Release the lock
+			err = lock.Unlock()
+			assert.NoError(t, err)
+		}()
+
+		// Wait for process 1 to acquire the lock
+		<-process1Started
+
+		// Simulate process 2: should wait and then find content exists
+		go func() {
+			defer close(process2Done)
+
+			needsWrite, lock, err := store.EnsureWithWait(testHashConcurrent)
+			assert.NoError(t, err)
+
+			// Process 2 should not need to write (process 1 wrote it)
+			assert.False(t, needsWrite)
+			assert.Nil(t, lock)
+
+			process2Done <- true
+		}()
+
+		// Let process 1 complete its work
+		close(process1CanContinue)
+
+		// Wait for process 2 to complete
+		result := <-process2Done
+		assert.True(t, result)
+
+		// Verify content exists
+		partitionDir := filepath.Join(store.Path(), testHashConcurrent[:2])
+		contentPath := filepath.Join(partitionDir, testHashConcurrent)
+		content, err := os.ReadFile(contentPath)
+		require.NoError(t, err)
+		assert.Equal(t, []byte("written by process 1"), content)
+	})
+}
diff --git a/internal/report/report_test.go b/internal/report/report_test.go
index ca3c24c3..110e28f6 100644
--- a/internal/report/report_test.go
+++ b/internal/report/report_test.go
@@ -674,7 +674,7 @@ func TestWriteJSON(t *testing.T) {
 			require.NoError(t, err)
 
 			// Parse both expected and actual JSON to compare them
-			var expectedJSON, actualJSON []map[string]interface{}
+			var expectedJSON, actualJSON []map[string]any
 			err = json.Unmarshal([]byte(tt.expected), &expectedJSON)
 			require.NoError(t, err)
 			err = json.Unmarshal(actualBytes, &actualJSON)
@@ -795,7 +795,7 @@ func TestWriteSchema(t *testing.T) {
 	assert.JSONEq(t, ExpectedSchema, buf.String())
 
 	// Parse the schema
-	var schema map[string]interface{}
+	var schema map[string]any
 	err = json.Unmarshal(buf.Bytes(), &schema)
 	require.NoError(t, err)
 
@@ -805,15 +805,15 @@ func TestWriteSchema(t *testing.T) {
 	assert.Equal(t, "Terragrunt Run Report Schema", schema["title"])
 
 	// Verify the items schema
-	items, ok := schema["items"].(map[string]interface{})
+	items, ok := schema["items"].(map[string]any)
 	require.True(t, ok)
 
 	// Verify the properties
-	properties, ok := items["properties"].(map[string]interface{})
+	properties, ok := items["properties"].(map[string]any)
 	require.True(t, ok)
 
 	// Verify required fields
-	required, ok := items["required"].([]interface{})
+	required, ok := items["required"].([]any)
 	require.True(t, ok)
 	assert.Contains(t, required, "Name")
 	assert.Contains(t, required, "Started")
@@ -821,17 +821,17 @@ func TestWriteSchema(t *testing.T) {
 	assert.Contains(t, required, "Result")
 
 	// Verify field types
-	assert.Equal(t, "string", properties["Name"].(map[string]interface{})["type"])
-	assert.Equal(t, "string", properties["Result"].(map[string]interface{})["type"])
-	assert.Equal(t, "string", properties["Started"].(map[string]interface{})["type"])
-	assert.Equal(t, "string", properties["Ended"].(map[string]interface{})["type"])
+	assert.Equal(t, "string", properties["Name"].(map[string]any)["type"])
+	assert.Equal(t, "string", properties["Result"].(map[string]any)["type"])
+	assert.Equal(t, "string", properties["Started"].(map[string]any)["type"])
+	assert.Equal(t, "string", properties["Ended"].(map[string]any)["type"])
 
 	// Verify optional fields
-	reason, ok := properties["Reason"].(map[string]interface{})
+	reason, ok := properties["Reason"].(map[string]any)
 	require.True(t, ok)
 	assert.Equal(t, "string", reason["type"])
 
-	cause, ok := properties["Cause"].(map[string]interface{})
+	cause, ok := properties["Cause"].(map[string]any)
 	require.True(t, ok)
 	assert.Equal(t, "string", cause["type"])
 }
diff --git a/internal/worker/worker_test.go b/internal/worker/worker_test.go
index 23d02c63..96a0cc7c 100644
--- a/internal/worker/worker_test.go
+++ b/internal/worker/worker_test.go
@@ -68,7 +68,6 @@ func TestSomeTasksReturnErrors(t *testing.T) {
 
 	// Submit tasks, half of which return an error
 	for i := range 10 {
-		i := i
 		wp.Submit(func() error {
 			if i%2 == 0 {
 				return errors.New("mock error")
diff --git a/shell/error_explainer_test.go b/shell/error_explainer_test.go
index 6025eb5f..db42d87c 100644
--- a/shell/error_explainer_test.go
+++ b/shell/error_explainer_test.go
@@ -40,7 +40,6 @@ func TestExplainError(t *testing.T) {
 	}
 
 	for _, tt := range testCases {
-		tt := tt
 
 		t.Run(tt.errorOutput, func(t *testing.T) {
 			t.Parallel()
diff --git a/telemetry/meter_test.go b/telemetry/meter_test.go
index 1e283c3d..31bd3136 100644
--- a/telemetry/meter_test.go
+++ b/telemetry/meter_test.go
@@ -56,7 +56,6 @@ func TestNewMetricsExporter(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
@@ -122,7 +121,6 @@ func TestCleanMetricName(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			result := telemetry.CleanMetricName(tc.input)
diff --git a/telemetry/tracer_test.go b/telemetry/tracer_test.go
index 34793156..cf065505 100644
--- a/telemetry/tracer_test.go
+++ b/telemetry/tracer_test.go
@@ -68,7 +68,6 @@ func TestNewTraceExporter(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
diff --git a/test/benchmarks/integration_bench_test.go b/test/benchmarks/integration_bench_test.go
index 6856a1e0..17658ace 100644
--- a/test/benchmarks/integration_bench_test.go
+++ b/test/benchmarks/integration_bench_test.go
@@ -31,6 +31,9 @@ terraform {
 	// Create 1 units
 	helpers.GenerateNUnits(b, tmpDir, 1, includeRootConfig, emptyMainTf)
 
+	// Do an initial init to avoid noise from the first iteration being slower
+	helpers.Init(b, tmpDir)
+
 	b.Run("1 units", func(b *testing.B) {
 		for b.Loop() {
 			helpers.Init(b, tmpDir)
@@ -58,6 +61,9 @@ terraform {
 
 	helpers.GenerateNUnits(b, tmpDir, 2, includeRootConfig, emptyMainTf)
 
+	// Do an initial init to avoid noise from the first iteration being slower
+	helpers.Init(b, tmpDir)
+
 	b.Run("2 units", func(b *testing.B) {
 		for b.Loop() {
 			helpers.Init(b, tmpDir)
@@ -85,6 +91,9 @@ terraform {
 
 	helpers.GenerateNUnits(b, tmpDir, 1000, includeRootConfig, emptyMainTf)
 
+	// Do an initial init to avoid noise from the first iteration being slower
+	helpers.Init(b, tmpDir)
+
 	b.Run("1000 units", func(b *testing.B) {
 		for b.Loop() {
 			helpers.Init(b, tmpDir)
@@ -114,6 +123,8 @@ terraform {
 	// Create 1 units
 	helpers.GenerateNUnits(b, tmpDir, 1, includeRootConfig, emptyMainTf)
 
+	helpers.Init(b, tmpDir)
+
 	b.Run("1 units", func(b *testing.B) {
 		for b.Loop() {
 
@@ -142,6 +153,8 @@ func BenchmarkTwoEmptyTerragruntPlans(b *testing.B) {
 
 	helpers.GenerateNUnits(b, tmpDir, 2, includeRootConfig, emptyMainTf)
 
+	helpers.Init(b, tmpDir)
+
 	b.Run("2 units", func(b *testing.B) {
 		for b.Loop() {
 			helpers.Plan(b, tmpDir)
@@ -168,6 +181,8 @@ func BenchmarkManyEmptyTerragruntPlans(b *testing.B) {
 
 	helpers.GenerateNUnits(b, tmpDir, 1000, includeRootConfig, emptyMainTf)
 
+	helpers.Init(b, tmpDir)
+
 	b.Run("1000 units", func(b *testing.B) {
 		for b.Loop() {
 			helpers.Plan(b, tmpDir)
diff --git a/test/benchmarks/integration_cas_bench_test.go b/test/benchmarks/integration_cas_bench_test.go
new file mode 100644
index 00000000..0e4ca67d
--- /dev/null
+++ b/test/benchmarks/integration_cas_bench_test.go
@@ -0,0 +1,171 @@
+package test_test
+
+import (
+	"os"
+	"path/filepath"
+	"strconv"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/test/benchmarks/helpers"
+	"github.com/stretchr/testify/require"
+)
+
+// BenchmarkCASInit benchmarks Terragrunt init with remote source with and without CAS enabled
+func BenchmarkCASInit(b *testing.B) {
+	setup := func(tmpDir string) {
+		// Copy the remote fixture content to our test directory
+		remoteFixtureSource := filepath.Join("..", "fixtures", "download", "remote")
+		remoteTerragruntConfigPath := filepath.Join(tmpDir, "terragrunt.hcl")
+
+		// Read the original terragrunt.hcl from the remote fixture
+		originalConfig, err := os.ReadFile(filepath.Join(remoteFixtureSource, "terragrunt.hcl"))
+		require.NoError(b, err)
+
+		// Write the config to our test directory
+		require.NoError(b, os.WriteFile(remoteTerragruntConfigPath, originalConfig, helpers.DefaultFilePermissions))
+
+		// Run initial init to avoid noise from the first iteration being slower
+		helpers.RunTerragruntCommand(
+			b,
+			"terragrunt",
+			"init",
+			"--experiment", "cas",
+			"--non-interactive",
+			"--provider-cache",
+			"--working-dir",
+			tmpDir,
+		)
+	}
+
+	b.Run("remote init without CAS", func(b *testing.B) {
+		tmpDir := b.TempDir()
+
+		setup(tmpDir)
+
+		b.ResetTimer()
+
+		for b.Loop() {
+			helpers.RunTerragruntCommand(
+				b,
+				"terragrunt",
+				"init",
+				"--non-interactive",
+				"--provider-cache",
+				"--source-update",
+				"--working-dir", tmpDir)
+		}
+
+		b.StopTimer()
+	})
+
+	b.Run("remote init with CAS", func(b *testing.B) {
+		tmpDir := b.TempDir()
+
+		setup(tmpDir)
+
+		b.ResetTimer()
+
+		for b.Loop() {
+			helpers.RunTerragruntCommand(
+				b,
+				"terragrunt",
+				"init",
+				"--experiment", "cas",
+				"--non-interactive",
+				"--provider-cache",
+				"--source-update",
+				"--working-dir",
+				tmpDir)
+		}
+
+		b.StopTimer()
+	})
+}
+
+// BenchmarkCASWithManyUnits benchmarks Terragrunt init with many remote units with and without CAS enabled
+func BenchmarkCASWithManyUnits(b *testing.B) {
+	setup := func(tmpDir string, count int) {
+		remoteFixtureSource := filepath.Join("..", "fixtures", "download", "remote")
+		originalConfig, err := os.ReadFile(filepath.Join(remoteFixtureSource, "terragrunt.hcl"))
+		require.NoError(b, err)
+
+		// Generate units with the remote configuration
+		for i := range count {
+			unitDir := filepath.Join(tmpDir, "unit-"+strconv.Itoa(i))
+			require.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions))
+
+			unitTerragruntConfigPath := filepath.Join(unitDir, "terragrunt.hcl")
+			require.NoError(b, os.WriteFile(unitTerragruntConfigPath, originalConfig, helpers.DefaultFilePermissions))
+		}
+
+		// Run initial init to avoid noise from the first iteration being slower
+		helpers.RunTerragruntCommand(
+			b,
+			"terragrunt",
+			"run",
+			"--all",
+			"init",
+			"--non-interactive",
+			"--provider-cache",
+			"--source-update",
+			"--working-dir",
+			tmpDir,
+		)
+	}
+
+	counts := []int{
+		1,
+		2,
+		4,
+		8,
+		16,
+		32,
+		64,
+		128,
+	}
+
+	for _, count := range counts {
+		for _, cas := range []bool{false, true} {
+			name := strconv.Itoa(count) + " remote units " + (func() string {
+				if cas {
+					return "with CAS"
+				}
+				return "without CAS"
+			})()
+
+			b.Run(name, func(b *testing.B) {
+				tmpDir := b.TempDir()
+
+				setup(tmpDir, count)
+
+				args := []string{
+					"terragrunt",
+					"run",
+					"--all",
+					"init",
+					"--non-interactive",
+					"--provider-cache",
+					"--source-update",
+					"--working-dir",
+					tmpDir,
+				}
+
+				if cas {
+					args = append(args, "--experiment", "cas")
+				}
+
+				b.ResetTimer()
+
+				for b.Loop() {
+					helpers.RunTerragruntCommand(
+						b,
+						args...,
+					)
+
+				}
+
+				b.StopTimer()
+			})
+		}
+	}
+}
diff --git a/test/fixtures/catalog/local-template/.boilerplate/boilerplate.yml b/test/fixtures/catalog/local-template/.boilerplate/boilerplate.yml
new file mode 100644
index 00000000..a9f82d5b
--- /dev/null
+++ b/test/fixtures/catalog/local-template/.boilerplate/boilerplate.yml
@@ -0,0 +1,8 @@
+variables:
+  - name: EnableRootInclude
+    description: Should include root module
+    type: bool
+    default: true
+  - name: RootFileName
+    description: Name of the root Terragrunt configuration file
+    type: string
diff --git a/test/fixtures/catalog/local-template/.boilerplate/custom-template.txt b/test/fixtures/catalog/local-template/.boilerplate/custom-template.txt
new file mode 100644
index 00000000..94ccf764
--- /dev/null
+++ b/test/fixtures/catalog/local-template/.boilerplate/custom-template.txt
@@ -0,0 +1 @@
+This file proves that the local template was used for scaffolding.
diff --git a/test/fixtures/catalog/local-template/.boilerplate/terragrunt.hcl b/test/fixtures/catalog/local-template/.boilerplate/terragrunt.hcl
new file mode 100644
index 00000000..f33f6d43
--- /dev/null
+++ b/test/fixtures/catalog/local-template/.boilerplate/terragrunt.hcl
@@ -0,0 +1,24 @@
+# Custom local template
+terraform {
+  source = "{{ .sourceUrl }}"
+}
+
+include "root" {
+  path = find_in_parent_folders("root.hcl")
+}
+
+inputs = {
+  # This is a custom template from a local directory
+  template_type = "local"
+
+  # Required variables would be listed here if any exist
+  {{- if .requiredVariables }}
+  {{- range .requiredVariables }}
+  # {{ if .Description }}{{ .Description }}{{ else }}Variable: {{ .Name }}{{ end }}
+  # Type: {{ .Type }}
+  {{ .Name }} = {{ .DefaultValuePlaceholder }}  # TODO: fill in value
+  {{- end }}
+  {{- else }}
+  # No required variables found for this module
+  {{- end }}
+}
diff --git a/test/fixtures/catalog/local-template/app/.gitkeep b/test/fixtures/catalog/local-template/app/.gitkeep
new file mode 100644
index 00000000..b5766c51
--- /dev/null
+++ b/test/fixtures/catalog/local-template/app/.gitkeep
@@ -0,0 +1 @@
+# This file ensures the app directory exists
diff --git a/test/fixtures/catalog/local-template/root.hcl b/test/fixtures/catalog/local-template/root.hcl
new file mode 100644
index 00000000..27b900bf
--- /dev/null
+++ b/test/fixtures/catalog/local-template/root.hcl
@@ -0,0 +1,4 @@
+catalog {
+  urls             = ["."]
+  default_template = "${get_parent_terragrunt_dir()}/.boilerplate"
+}
diff --git a/test/fixtures/stacks/terragrunt-dir/live/root.hcl b/test/fixtures/stacks/terragrunt-dir/live/root.hcl
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixtures/stacks/terragrunt-dir/live/tennant_1/terragrunt.stack.hcl b/test/fixtures/stacks/terragrunt-dir/live/tennant_1/terragrunt.stack.hcl
new file mode 100644
index 00000000..9dc89759
--- /dev/null
+++ b/test/fixtures/stacks/terragrunt-dir/live/tennant_1/terragrunt.stack.hcl
@@ -0,0 +1,7 @@
+unit "unit_a" {
+  source = "../../unit_a"
+  path   = "unit_a"
+  values = {
+    terragrunt_dir = get_terragrunt_dir()
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/stacks/terragrunt-dir/unit_a/main.tf b/test/fixtures/stacks/terragrunt-dir/unit_a/main.tf
new file mode 100644
index 00000000..d312e7d6
--- /dev/null
+++ b/test/fixtures/stacks/terragrunt-dir/unit_a/main.tf
@@ -0,0 +1,5 @@
+variable "terragrunt_dir" {}
+
+output "terragrunt_dir" {
+  value = var.terragrunt_dir
+}
\ No newline at end of file
diff --git a/test/fixtures/stacks/terragrunt-dir/unit_a/terragrunt.hcl b/test/fixtures/stacks/terragrunt-dir/unit_a/terragrunt.hcl
new file mode 100644
index 00000000..e1ec65f7
--- /dev/null
+++ b/test/fixtures/stacks/terragrunt-dir/unit_a/terragrunt.hcl
@@ -0,0 +1,12 @@
+
+include "root" {
+  path = find_in_parent_folders("root.hcl")
+}
+
+terraform {
+  source = "."
+}
+
+inputs = {
+  terragrunt_dir = values.terragrunt_dir
+}
diff --git a/test/fixtures/version-files-cache-key/.terraform-version b/test/fixtures/version-files-cache-key/.terraform-version
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixtures/version-files-cache-key/.tool-versions b/test/fixtures/version-files-cache-key/.tool-versions
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixtures/version-files-cache-key/main.tf b/test/fixtures/version-files-cache-key/main.tf
new file mode 100644
index 00000000..06ce6d6b
--- /dev/null
+++ b/test/fixtures/version-files-cache-key/main.tf
@@ -0,0 +1,3 @@
+output "output" {
+  value = "message"
+}
diff --git a/test/fixtures/version-files-cache-key/terragrunt.hcl b/test/fixtures/version-files-cache-key/terragrunt.hcl
new file mode 100644
index 00000000..e69de29b
diff --git a/test/helpers/package.go b/test/helpers/package.go
index 5d9ccacf..91f4f134 100644
--- a/test/helpers/package.go
+++ b/test/helpers/package.go
@@ -867,7 +867,11 @@ func RunTerragruntValidateInputs(t *testing.T, moduleDir string, extraArgs []str
 		moduleDir = maybeNested
 	}
 
-	cmd := fmt.Sprintf("terragrunt hcl validate --inputs %s --log-level trace --non-interactive --working-dir %s", strings.Join(extraArgs, " "), moduleDir)
+	cmd := fmt.Sprintf(
+		"terragrunt hcl validate --inputs %s --log-level trace --non-interactive --working-dir %s",
+		strings.Join(extraArgs, " "),
+		moduleDir,
+	)
 	t.Logf("Command: %s", cmd)
 	_, _, err := RunTerragruntCommandWithOutput(t, cmd)
 
diff --git a/test/integration_catalog_test.go b/test/integration_catalog_test.go
index 752653db..ed7ba092 100644
--- a/test/integration_catalog_test.go
+++ b/test/integration_catalog_test.go
@@ -11,10 +11,15 @@ import (
 	"github.com/gruntwork-io/terragrunt/options"
 	"github.com/gruntwork-io/terragrunt/test/helpers"
 	"github.com/gruntwork-io/terragrunt/test/helpers/logger"
+	"github.com/gruntwork-io/terragrunt/util"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
+const (
+	testFixtureCatalogLocalTemplate = "fixtures/catalog/local-template"
+)
+
 func TestCatalogGitRepoUpdate(t *testing.T) {
 	t.Parallel()
 
@@ -124,6 +129,31 @@ func TestScaffoldGitModuleHttps(t *testing.T) {
 	helpers.RunTerragrunt(t, "terragrunt init --non-interactive --working-dir "+opts.WorkingDir)
 }
 
+func TestCatalogWithLocalDefaultTemplate(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureCatalogLocalTemplate, ".boilerplate")
+	helpers.CleanupTerraformFolder(t, tmpEnvPath)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureCatalogLocalTemplate)
+
+	targetPath := filepath.Join(rootPath, "app")
+	moduleURL := "github.com/gruntwork-io/terragrunt//test/fixtures/inputs"
+
+	_, stderr, err := helpers.RunTerragruntCommandWithOutput(
+		t,
+		"terragrunt scaffold --non-interactive --working-dir "+targetPath+" "+moduleURL,
+	)
+
+	require.NoError(t, err)
+	assert.Contains(t, stderr, "Scaffolding completed")
+	assert.FileExists(t, filepath.Join(targetPath, "terragrunt.hcl"))
+	assert.FileExists(t, filepath.Join(targetPath, "custom-template.txt"))
+
+	content, err := util.ReadFileAsString(filepath.Join(targetPath, "terragrunt.hcl"))
+	require.NoError(t, err)
+	assert.Contains(t, content, "# Custom local template")
+}
+
 func readConfig(t *testing.T, opts *options.TerragruntOptions) *config.TerragruntConfig {
 	t.Helper()
 
diff --git a/test/integration_debug_test.go b/test/integration_debug_test.go
index 70342c94..27f15ec8 100644
--- a/test/integration_debug_test.go
+++ b/test/integration_debug_test.go
@@ -101,7 +101,6 @@ func TestTerragruntValidateInputs(t *testing.T) {
 	for _, module := range moduleDirs {
 		// capture range var within range scope so it doesn't change as the tests are spun to the background in the
 		// t.Parallel call.
-		module := module
 
 		name := filepath.Base(module)
 		t.Run(name, func(t *testing.T) {
diff --git a/test/integration_deprecated_test.go b/test/integration_deprecated_test.go
index f8225ff0..bfc7abac 100644
--- a/test/integration_deprecated_test.go
+++ b/test/integration_deprecated_test.go
@@ -30,7 +30,7 @@ func TestDeprecatedHclvalidateCommand_HclvalidateInvalidConfigPath(t *testing.T)
 	}
 
 	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt hclvalidate --terragrunt-working-dir %s --terragrunt-hclvalidate-json --terragrunt-hclvalidate-show-config-path", rootPath))
-	require.NoError(t, err)
+	require.Error(t, err)
 
 	var actualPaths []string
 
diff --git a/test/integration_download_test.go b/test/integration_download_test.go
index 4a0fb490..ba3b0e5b 100644
--- a/test/integration_download_test.go
+++ b/test/integration_download_test.go
@@ -595,3 +595,50 @@ func TestPreventDestroyDependencies(t *testing.T) {
 		}
 	}
 }
+
+func TestDownloadWithCASEnabled(t *testing.T) {
+	t.Parallel()
+
+	fixturePath := "fixtures/download/remote"
+
+	tmpEnvPath := helpers.CopyEnvironment(t, fixturePath)
+	testPath := util.JoinPath(tmpEnvPath, fixturePath)
+	helpers.CleanupTerraformFolder(t, testPath)
+
+	// Run with CAS experiment enabled
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+
+	cmd := "terragrunt apply --auto-approve --non-interactive --experiment cas --log-level debug --working-dir " + testPath
+	err := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)
+	require.NoError(t, err)
+
+	assert.Contains(t, stderr.String(), "Downloading Terraform configurations")
+}
+
+func TestCASStorageDirectory(t *testing.T) {
+	t.Parallel()
+
+	homeDir, err := os.UserHomeDir()
+	require.NoError(t, err)
+
+	expectedCASDir := filepath.Join(homeDir, ".cache", "terragrunt", "cas")
+
+	tmpEnvPath := helpers.CopyEnvironment(t, "fixtures/download")
+	testPath := util.JoinPath(tmpEnvPath, "fixtures/download/local")
+
+	helpers.CleanupTerraformFolder(t, testPath)
+
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+
+	cmd := "terragrunt plan --experiment cas --working-dir " + testPath
+	_ = helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)
+
+	_, err = os.Stat(expectedCASDir)
+	require.NoError(t, err)
+
+	storeDir := filepath.Join(expectedCASDir, "store")
+	_, err = os.Stat(storeDir)
+	require.NoError(t, err)
+}
diff --git a/test/integration_parse_test.go b/test/integration_parse_test.go
index b64a7210..03ed2214 100644
--- a/test/integration_parse_test.go
+++ b/test/integration_parse_test.go
@@ -23,11 +23,12 @@ import (
 )
 
 var knownBadFiles = []string{
-	"fixtures/hclvalidate/second/a/terragrunt.hcl",
+	"fixtures/catalog/local-template/.boilerplate/terragrunt.hcl",
+	"fixtures/disabled/unit-disabled/terragrunt.hcl",
 	"fixtures/hclfmt-errors/dangling-attribute/terragrunt.hcl",
 	"fixtures/hclfmt-errors/invalid-character/terragrunt.hcl",
 	"fixtures/hclfmt-errors/invalid-key/terragrunt.hcl",
-	"fixtures/disabled/unit-disabled/terragrunt.hcl",
+	"fixtures/hclvalidate/second/a/terragrunt.hcl",
 }
 
 func TestParseAllFixtureFiles(t *testing.T) {
diff --git a/test/integration_report_test.go b/test/integration_report_test.go
index 49943e59..020c9661 100644
--- a/test/integration_report_test.go
+++ b/test/integration_report_test.go
@@ -134,7 +134,7 @@ func TestTerragruntReportExperimentSaveToFile(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc // capture range variable
+		// capture range variable
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
@@ -323,7 +323,7 @@ func TestTerragruntReportExperimentSaveToFileWithFormat(t *testing.T) {
 				assert.True(t, strings.HasPrefix(string(content), "Name,Started,Ended,Result,Reason,Cause"))
 			case "json":
 				// For JSON, verify it's valid JSON and has the expected structure
-				var jsonContent []map[string]interface{}
+				var jsonContent []map[string]any
 				err := json.Unmarshal(content, &jsonContent)
 
 				require.NoError(t, err)
@@ -348,7 +348,7 @@ func TestTerragruntReportExperimentSaveToFileWithFormat(t *testing.T) {
 				require.NoError(t, err)
 
 				// Verify it's valid JSON
-				var schema map[string]interface{}
+				var schema map[string]any
 				err = json.Unmarshal(schemaContent, &schema)
 				require.NoError(t, err)
 
@@ -358,11 +358,11 @@ func TestTerragruntReportExperimentSaveToFileWithFormat(t *testing.T) {
 				assert.Equal(t, "Terragrunt Run Report Schema", schema["title"])
 
 				// Verify items schema
-				items, ok := schema["items"].(map[string]interface{})
+				items, ok := schema["items"].(map[string]any)
 				require.True(t, ok)
 
 				// Verify required fields
-				required, ok := items["required"].([]interface{})
+				required, ok := items["required"].([]any)
 				require.True(t, ok)
 				assert.Contains(t, required, "Name")
 				assert.Contains(t, required, "Started")
@@ -370,14 +370,14 @@ func TestTerragruntReportExperimentSaveToFileWithFormat(t *testing.T) {
 				assert.Contains(t, required, "Result")
 
 				// Verify properties
-				properties, ok := items["properties"].(map[string]interface{})
+				properties, ok := items["properties"].(map[string]any)
 				require.True(t, ok)
 
 				// Verify field types
-				assert.Equal(t, "string", properties["Name"].(map[string]interface{})["type"])
-				assert.Equal(t, "string", properties["Result"].(map[string]interface{})["type"])
-				assert.Equal(t, "string", properties["Started"].(map[string]interface{})["type"])
-				assert.Equal(t, "string", properties["Ended"].(map[string]interface{})["type"])
+				assert.Equal(t, "string", properties["Name"].(map[string]any)["type"])
+				assert.Equal(t, "string", properties["Result"].(map[string]any)["type"])
+				assert.Equal(t, "string", properties["Started"].(map[string]any)["type"])
+				assert.Equal(t, "string", properties["Ended"].(map[string]any)["type"])
 			}
 		})
 	}
@@ -508,7 +508,7 @@ func TestReportWithExternalDependenciesExcluded(t *testing.T) {
 	require.NoError(t, err)
 
 	// Verify that the report file contains the expected content
-	var report []map[string]interface{}
+	var report []map[string]any
 	err = json.Unmarshal(reportContent, &report)
 	require.NoError(t, err)
 
diff --git a/test/integration_run_test.go b/test/integration_run_test.go
index 20097c8d..41dfa1fb 100644
--- a/test/integration_run_test.go
+++ b/test/integration_run_test.go
@@ -3,6 +3,7 @@ package test_test
 import (
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/gruntwork-io/terragrunt/test/helpers"
@@ -124,3 +125,61 @@ func TestRunNoStacksGenerate(t *testing.T) {
 		})
 	}
 }
+
+func TestRunVersionFilesCacheKey(t *testing.T) {
+	t.Parallel()
+
+	testdata := []struct {
+		name         string
+		expect       string
+		versionFiles []string
+	}{
+		{
+			name:         "use default",
+			expect:       "r01AJjVD7VSXCQk1ORuh_no_NRY",
+			versionFiles: nil,
+		},
+		{
+			name:   "custom files provided",
+			expect: "XBE-VO9pOnQjPQDmLQCvSCdckSQ",
+			versionFiles: []string{
+				".terraform-version",
+				".tool-versions",
+			},
+		},
+	}
+
+	helpers.CleanupTerraformFolder(t, testFixtureVersionFilesCacheKey)
+
+	for _, tt := range testdata {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			tmpEnvPath := helpers.CopyEnvironment(t, testFixtureVersionFilesCacheKey, tt.versionFiles...)
+			path := util.JoinPath(tmpEnvPath, testFixtureVersionFilesCacheKey)
+			flags := []string{
+				"-non-interactive",
+				"--log-level debug",
+				"--working-dir",
+				path,
+			}
+
+			for _, file := range tt.versionFiles {
+				flags = append(
+					flags,
+					"--version-manager-file-name",
+					file,
+				)
+			}
+
+			cmd := "terragrunt run apply " + strings.Join(flags, " ") + " -- -auto-approve"
+
+			stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)
+			require.NoError(t, err)
+			assert.NotEmpty(t, stdout)
+			assert.NotEmpty(t, stderr)
+			assert.Contains(t, stderr, "using cache key for version files: "+tt.expect)
+		})
+	}
+
+}
diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go
index bcfe2a48..ef05c782 100644
--- a/test/integration_stacks_test.go
+++ b/test/integration_stacks_test.go
@@ -47,6 +47,7 @@ const (
 	testFixtureStackSelfInclude                = "fixtures/stacks/self-include"
 	testFixtureStackNestedOutputs              = "fixtures/stacks/nested-outputs"
 	testFixtureStackNoValidation               = "fixtures/stacks/no-validation"
+	testFixtureStackTerragruntDir              = "fixtures/stacks/terragrunt-dir"
 )
 
 func TestStacksGenerateBasic(t *testing.T) {
@@ -1296,3 +1297,20 @@ func validateStackDir(t *testing.T, path string) {
 
 	assert.True(t, hasSubdirectories, "The .terragrunt-stack directory should contain at least one subdirectory")
 }
+
+func TestStackTerragruntDir(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureStackTerragruntDir)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackTerragruntDir)
+	gitPath := util.JoinPath(tmpEnvPath, testFixtureStackTerragruntDir)
+	helpers.CreateGitRepo(t, gitPath)
+	rootPath := util.JoinPath(gitPath, "live")
+
+	_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack generate --no-stack-validate --working-dir "+rootPath)
+	require.NoError(t, err)
+
+	out, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply --all --non-interactive --working-dir "+rootPath)
+	require.NoError(t, err)
+	assert.Contains(t, out, `terragrunt_dir = "./tennant_1"`)
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index ddc029ce..1efaae7d 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -114,6 +114,7 @@ const (
 	testFixtureTfPath                         = "fixtures/tf-path"
 	testFixtureTraceParent                    = "fixtures/trace-parent"
 	testFixtureVersionInvocation              = "fixtures/version-invocation"
+	testFixtureVersionFilesCacheKey           = "fixtures/version-files-cache-key"
 
 	terraformFolder = ".terraform"
 
@@ -702,7 +703,7 @@ func TestHclvalidateDiagnostic(t *testing.T) {
 	}
 
 	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt hcl validate --working-dir %s --json", rootPath))
-	require.NoError(t, err)
+	require.Error(t, err)
 
 	var actualDiags diagnostic.Diagnostics
 
@@ -712,6 +713,24 @@ func TestHclvalidateDiagnostic(t *testing.T) {
 	assert.ElementsMatch(t, expectedDiags, actualDiags)
 }
 
+func TestHclvalidateReturnsNonZeroExitCodeOnError(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureHclvalidate)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureHclvalidate)
+
+	// We expect an error because the fixture has HCL validation issues.
+	// The content of stdout and stderr isn't the primary focus here,
+	// rather the fact that an error (non-zero exit code) is returned.
+	_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt hcl validate --working-dir "+rootPath)
+	require.Error(t, err, "terragrunt hcl validate should return a non-zero exit code on HCL errors")
+
+	// As an additional check, we can verify that the error message indicates HCL validation errors.
+	// This makes the test more robust.
+	assert.Contains(t, err.Error(), "HCL validation error(s) found")
+}
+
 func TestHclvalidateInvalidConfigPath(t *testing.T) {
 	t.Parallel()
 
@@ -725,7 +744,7 @@ func TestHclvalidateInvalidConfigPath(t *testing.T) {
 	}
 
 	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt hcl validate --working-dir %s --json --show-config-path", rootPath))
-	require.NoError(t, err)
+	require.Error(t, err)
 
 	var actualPaths []string
 
diff --git a/test/integration_tofu_state_encryption_test.go b/test/integration_tofu_state_encryption_test.go
index 20ec407f..bf4095aa 100644
--- a/test/integration_tofu_state_encryption_test.go
+++ b/test/integration_tofu_state_encryption_test.go
@@ -87,13 +87,13 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 	jsonBytes, err := os.ReadFile(jsonOut)
 	require.NoError(t, err)
 
-	var rendered map[string]interface{}
+	var rendered map[string]any
 	require.NoError(t, json.Unmarshal(jsonBytes, &rendered))
 
 	// Make sure all terraform block is visible
 	terraformBlock, hasTerraform := rendered["terraform"]
 	if assert.True(t, hasTerraform) {
-		source, hasSource := terraformBlock.(map[string]interface{})["source"]
+		source, hasSource := terraformBlock.(map[string]any)["source"]
 		assert.True(t, hasSource)
 		assert.Equal(t, "./module", source)
 	}
@@ -103,23 +103,23 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 	if assert.True(t, hasRemoteState) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
+			map[string]any{
 				"backend": "local",
-				"generate": map[string]interface{}{
+				"generate": map[string]any{
 					"path":      "backend.tf",
 					"if_exists": "overwrite_terragrunt",
 				},
-				"config": map[string]interface{}{
+				"config": map[string]any{
 					"path": "foo.tfstate",
 				},
-				"encryption": map[string]interface{}{
+				"encryption": map[string]any{
 					"key_provider": "pbkdf2",
 					"passphrase":   "correct-horse-battery-staple",
 				},
 				"disable_init":                    false,
 				"disable_dependency_optimization": false,
 			},
-			remoteState.(map[string]interface{}),
+			remoteState.(map[string]any),
 		)
 	}
 
@@ -128,8 +128,8 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 	if assert.True(t, hasDependency) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
-				"dep": map[string]interface{}{
+			map[string]any{
+				"dep": map[string]any{
 					"name":         "dep",
 					"config_path":  "../dep",
 					"outputs":      nil,
@@ -142,7 +142,7 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 					"skip":                                    nil,
 				},
 			},
-			dependencyBlocks.(map[string]interface{}),
+			dependencyBlocks.(map[string]any),
 		)
 	}
 
@@ -151,8 +151,8 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 	if assert.True(t, hasGenerate) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
-				"provider": map[string]interface{}{
+			map[string]any{
+				"provider": map[string]any{
 					"path":              "provider.tf",
 					"comment_prefix":    "# ",
 					"disable_signature": false,
@@ -166,7 +166,7 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 `,
 				},
 			},
-			generateBlocks.(map[string]interface{}),
+			generateBlocks.(map[string]any),
 		)
 	}
 
@@ -175,13 +175,13 @@ func TestTofuRenderJSONConfigWithEncryption(t *testing.T) {
 	if assert.True(t, hasInputs) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
+			map[string]any{
 				"env":        "qa",
 				"name":       "dep",
 				"type":       "main",
 				"aws_region": "us-east-1",
 			},
-			inputsBlock.(map[string]interface{}),
+			inputsBlock.(map[string]any),
 		)
 	}
 }
@@ -201,13 +201,13 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 	jsonBytes, err := os.ReadFile(jsonOut)
 	require.NoError(t, err)
 
-	var rendered map[string]interface{}
+	var rendered map[string]any
 	require.NoError(t, json.Unmarshal(jsonBytes, &rendered))
 
 	// Make sure all terraform block is visible
 	terraformBlock, hasTerraform := rendered["terraform"]
 	if assert.True(t, hasTerraform) {
-		source, hasSource := terraformBlock.(map[string]interface{})["source"]
+		source, hasSource := terraformBlock.(map[string]any)["source"]
 		assert.True(t, hasSource)
 		assert.Equal(t, "./module", source)
 	}
@@ -217,23 +217,23 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 	if assert.True(t, hasRemoteState) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
+			map[string]any{
 				"backend": "local",
-				"generate": map[string]interface{}{
+				"generate": map[string]any{
 					"path":      "backend.tf",
 					"if_exists": "overwrite_terragrunt",
 				},
-				"config": map[string]interface{}{
+				"config": map[string]any{
 					"path": "foo.tfstate",
 				},
-				"encryption": map[string]interface{}{
+				"encryption": map[string]any{
 					"key_provider": "pbkdf2",
 					"passphrase":   "correct-horse-battery-staple",
 				},
 				"disable_init":                    false,
 				"disable_dependency_optimization": false,
 			},
-			remoteState.(map[string]interface{}),
+			remoteState.(map[string]any),
 		)
 	}
 
@@ -242,8 +242,8 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 	if assert.True(t, hasDependency) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
-				"dep": map[string]interface{}{
+			map[string]any{
+				"dep": map[string]any{
 					"name":         "dep",
 					"config_path":  "../dep",
 					"outputs":      nil,
@@ -256,7 +256,7 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 					"skip":                                    nil,
 				},
 			},
-			dependencyBlocks.(map[string]interface{}),
+			dependencyBlocks.(map[string]any),
 		)
 	}
 
@@ -265,8 +265,8 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 	if assert.True(t, hasGenerate) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
-				"provider": map[string]interface{}{
+			map[string]any{
+				"provider": map[string]any{
 					"path":              "provider.tf",
 					"comment_prefix":    "# ",
 					"disable_signature": false,
@@ -280,7 +280,7 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 `,
 				},
 			},
-			generateBlocks.(map[string]interface{}),
+			generateBlocks.(map[string]any),
 		)
 	}
 
@@ -289,13 +289,13 @@ func TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {
 	if assert.True(t, hasInputs) {
 		assert.Equal(
 			t,
-			map[string]interface{}{
+			map[string]any{
 				"env":        "qa",
 				"name":       "dep",
 				"type":       "main",
 				"aws_region": "us-east-1",
 			},
-			inputsBlock.(map[string]interface{}),
+			inputsBlock.(map[string]any),
 		)
 	}
 }
@@ -313,7 +313,7 @@ func validateStateIsEncrypted(t *testing.T, fileName string, path string) {
 	byteValue, err := io.ReadAll(file)
 	require.NoError(t, err)
 
-	var result map[string]interface{}
+	var result map[string]any
 	err = json.Unmarshal(byteValue, &result)
 	require.NoError(t, err, "Error unmarshalling the state file '%s'", fileName)
 
diff --git a/util/file_test.go b/util/file_test.go
index 9bc6cf68..7ab27861 100644
--- a/util/file_test.go
+++ b/util/file_test.go
@@ -647,3 +647,80 @@ func TestWalkWithSymlinksErrors(t *testing.T) {
 		return err
 	}))
 }
+
+func Test_sanitizePath(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		baseDir string
+		file    string
+		want    string
+		wantErr bool
+	}{
+		{
+			name:    "happy path",
+			baseDir: "./testdata/fixture-sanitize-path/env/unit",
+			file:    ".terraform-version",
+			want:    "./testdata/fixture-sanitize-path/env/unit/.terraform-version",
+		},
+		{
+			name:    "base dir is empty",
+			baseDir: "",
+			file:    ".terraform-version",
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name:    "try to escape base dir",
+			baseDir: "./testdata/fixture-sanitize-path/env/unit",
+			file:    "../../../dev/random",
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name:    "file is empty",
+			baseDir: "./testdata/fixture-sanitize-path/env/unit",
+			file:    "",
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name:    "file is just a slash",
+			baseDir: "./testdata/fixture-sanitize-path/env/unit",
+			file:    "/",
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name:    "file is just a dot",
+			baseDir: "./testdata/fixture-sanitize-path/env/unit",
+			file:    ".",
+			want:    "./testdata/fixture-sanitize-path/env/unit/.",
+			wantErr: false,
+		},
+		{
+			name:    "encoded characters",
+			baseDir: "./testdata/fixture-sanitize-path/env/unit",
+			file:    "..%2F..%2Fetc%2Fpasswd",
+			want:    "",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			got, err := util.SanitizePath(tt.baseDir, tt.file)
+
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			assert.Equalf(t, tt.want, got, "sanitizePath(%v, %v)", tt.baseDir, tt.file)
+		})
+	}
+}
diff --git a/util/testdata/fixture-sanitize-path/env/unit/.terraform-version b/util/testdata/fixture-sanitize-path/env/unit/.terraform-version
new file mode 100644
index 00000000..e69de29b