Repository: gruntwork-io/terragrunt

Issue:
Iac Engines not used for dependency blocks
## Describe the bug

If a terragrunt module is configured to use Iac Engines, the engine is not used when that module's output is read via a `dependency` block

## Steps To Reproduce

- Configure 2 terragrunt modules, both of which have an `engine` block
- Make module 2 a dependency of module 1
- Run `terragrunt plan` for module 2


## Expected behavior

When terragrunt process the dependency on module 1, the `terraform output -json` command should be executed using the IaC engine.


## Versions

- Terragrunt version: v0.75.0
- Engine source: https://github.com/gruntwork-io/terragrunt-engine-go/tree/main/examples/client-server (with a small change to use non-relative paths)
- Engine version: main branch
- Environment details (Ubuntu 20.04, Windows 10, etc.): Ubuntu 24.04


## Additional context

See https://github.com/gruntwork-io/terragrunt/discussions/3962 for more details

Hints from issue comments:

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index af744cb7..51db48e5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -60,16 +60,6 @@ install_tflint: &install_tflint
     popd
     tflint --version
 
-install_tofu_engine: &install_tofu_engine
-  name: Install OpenTofu Engine
-  command: |
-    # Download the OpenTofu Engine binary
-    set -x
-    export REPO="gruntwork-io/terragrunt-engine-opentofu"
-    export ASSET_NAME="terragrunt-iac-engine-opentofu_rpc_${TOFU_ENGINE_VERSION}_linux_amd64.zip"
-    wget -O "engine.zip" "https://github.com/${REPO}/releases/download/${TOFU_ENGINE_VERSION}/${ASSET_NAME}"
-    unzip -o "engine.zip"
-
 setup_test_environment: &setup_test_environment
   name: Setup integration tests environment
   command: |
@@ -343,7 +333,7 @@ jobs:
           path: logs
 
   integration_test_tofu_only:
-    resource_class: medium
+    resource_class: "medium+"
     <<: *defaults
     <<: *env
     steps:
@@ -395,8 +385,6 @@ jobs:
       - checkout
       - run:
           <<: *install_tofu
-      - run:
-          <<: *install_tofu_engine
       - run:
           <<: *setup_test_environment
       - run:
@@ -461,6 +449,7 @@ jobs:
           path: logs
 
   integration_test_tofu_aws:
+    resource_class: "large"
     <<: *defaults
     <<: *env
     steps:
@@ -479,6 +468,7 @@ jobs:
           path: logs
 
   integration_test_terraform_aws:
+    resource_class: "medium+"
     <<: *defaults
     <<: *env
     steps:
@@ -495,6 +485,7 @@ jobs:
           path: logs
 
   integration_test_terraform_latest_aws:
+    resource_class: "medium+"
     <<: *defaults
     <<: *env
     steps:
diff --git a/cli/commands/catalog/action.go b/cli/commands/catalog/action.go
index f5efb0ed..f474c444 100644
--- a/cli/commands/catalog/action.go
+++ b/cli/commands/catalog/action.go
@@ -38,11 +38,12 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, repoURL string) e
 	var modules module.Modules
 
 	walkWithSymlinks := opts.Experiments.Evaluate(experiment.Symlinks)
+	allowCAS := opts.Experiments.Evaluate(experiment.CAS)
 
 	for _, repoURL := range repoURLs {
-		tempDir := filepath.Join(os.TempDir(), fmt.Sprintf(tempDirFormat, util.EncodeBase64Sha1(repoURL)))
+		path := filepath.Join(os.TempDir(), fmt.Sprintf(tempDirFormat, util.EncodeBase64Sha1(repoURL)))
 
-		repo, err := module.NewRepo(ctx, opts.Logger, repoURL, tempDir, walkWithSymlinks)
+		repo, err := module.NewRepo(ctx, opts.Logger, repoURL, path, walkWithSymlinks, allowCAS)
 		if err != nil {
 			return err
 		}
diff --git a/cli/commands/catalog/module/module.go b/cli/commands/catalog/module/module.go
index 624c98db..43f5397e 100644
--- a/cli/commands/catalog/module/module.go
+++ b/cli/commands/catalog/module/module.go
@@ -32,7 +32,7 @@ type Module struct {
 	url       string
 }
 
-// NewModule returns a module instance if the given `moduleDir` path contains a Terraform module, otherwise returns nil.
+// NewModule returns a module instance if the given `moduleDir` path contains an OpenTofu/Terraform module, otherwise returns nil.
 func NewModule(repo *Repo, moduleDir string) (*Module, error) {
 	module := &Module{
 		Repo:      repo,
diff --git a/cli/commands/catalog/module/repo.go b/cli/commands/catalog/module/repo.go
index 6f3d82d0..c3b5e4f9 100644
--- a/cli/commands/catalog/module/repo.go
+++ b/cli/commands/catalog/module/repo.go
@@ -3,7 +3,6 @@ package module
 import (
 	"context"
 	"fmt"
-	"net/url"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -13,10 +12,11 @@ import (
 
 	"github.com/gitsight/go-vcsurl"
 	"github.com/gruntwork-io/go-commons/files"
+	"github.com/gruntwork-io/terragrunt/internal/cas"
 	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/gruntwork-io/terragrunt/pkg/log"
 	"github.com/gruntwork-io/terragrunt/tf"
-	"github.com/hashicorp/go-getter"
+	"github.com/hashicorp/go-getter/v2"
 	"gopkg.in/ini.v1"
 )
 
@@ -27,13 +27,17 @@ const (
 	azuredevHost          = "dev.azure.com"
 	bitbucketHost         = "bitbucket.org"
 	gitlabSelfHostedRegex = `^(gitlab\.(.+))$`
+
+	cloneCompleteSentinel = ".catalog-clone-complete"
 )
 
 var (
 	gitHeadBranchNameReg    = regexp.MustCompile(`^.*?([^/]+)$`)
-	repoNameFromCloneURLReg = regexp.MustCompile(`(?i)^.*?([-a-z_.]+)[^/]*?(?:\.git)?$`)
+	repoNameFromCloneURLReg = regexp.MustCompile(`(?i)^.*?([-a-z0-9_.]+)[^/]*?(?:\.git)?$`)
 
 	modulesPaths = []string{"modules"}
+
+	includedGitFiles = []string{"HEAD", "config"}
 )
 
 type Repo struct {
@@ -46,17 +50,19 @@ type Repo struct {
 	BranchName string
 
 	walkWithSymlinks bool
+	allowCAS         bool
 }
 
-func NewRepo(ctx context.Context, logger log.Logger, cloneURL, tempDir string, walkWithSymlinks bool) (*Repo, error) {
+func NewRepo(ctx context.Context, l log.Logger, cloneURL, path string, walkWithSymlinks bool, allowCAS bool) (*Repo, error) {
 	repo := &Repo{
-		logger:           logger,
+		logger:           l,
 		cloneURL:         cloneURL,
-		path:             tempDir,
+		path:             path,
 		walkWithSymlinks: walkWithSymlinks,
+		allowCAS:         allowCAS,
 	}
 
-	if err := repo.clone(ctx); err != nil {
+	if err := repo.clone(ctx, l); err != nil {
 		return nil, err
 	}
 
@@ -163,69 +169,164 @@ func (repo *Repo) ModuleURL(moduleDir string) (string, error) {
 	return "", errors.Errorf("hosting: %q is not supported yet", remote.Host)
 }
 
-// clone clones the repository to a temporary directory if the repoPath is URL
-func (repo *Repo) clone(ctx context.Context) error {
+type CloneOptions struct {
+	SourceURL  string
+	TargetPath string
+	Context    context.Context
+	Logger     log.Logger
+}
+
+func (repo *Repo) clone(ctx context.Context, l log.Logger) error {
+	cloneURL, err := repo.resolveCloneURL()
+	if err != nil {
+		return err
+	}
+
+	// Handle local directory case
+	if files.IsDir(cloneURL) {
+		return repo.handleLocalDir(cloneURL)
+	}
+
+	// Prepare clone options
+	opts := CloneOptions{
+		SourceURL:  cloneURL,
+		TargetPath: repo.path,
+		Context:    ctx,
+		Logger:     repo.logger,
+	}
+
+	if err := repo.prepareCloneDirectory(); err != nil {
+		return err
+	}
+
+	if repo.cloneCompleted() {
+		repo.logger.Debugf("The repo dir exists and %q exists. Skipping cloning.", cloneCompleteSentinel)
+
+		return nil
+	}
+
+	return repo.performClone(ctx, l, &opts)
+}
+
+func (repo *Repo) resolveCloneURL() (string, error) {
 	if repo.cloneURL == "" {
 		currentDir, err := os.Getwd()
 		if err != nil {
-			return errors.New(err)
+			return "", errors.New(err)
 		}
 
-		repo.cloneURL = currentDir
+		return currentDir, nil
 	}
 
-	if repoPath := repo.cloneURL; files.IsDir(repoPath) {
-		if !filepath.IsAbs(repoPath) {
-			absRepoPath, err := filepath.Abs(repoPath)
-			if err != nil {
-				return errors.New(err)
-			}
+	return repo.cloneURL, nil
+}
 
-			repo.logger.Debugf("Converting relative path %q to absolute %q", repoPath, absRepoPath)
+func (repo *Repo) handleLocalDir(repoPath string) error {
+	if !filepath.IsAbs(repoPath) {
+		absRepoPath, err := filepath.Abs(repoPath)
+		if err != nil {
+			return errors.New(err)
 		}
 
-		repo.path = repoPath
+		repo.logger.Debugf("Converting relative path %q to absolute %q", repoPath, absRepoPath)
+		repo.path = absRepoPath
 
 		return nil
 	}
 
+	repo.path = repoPath
+
+	return nil
+}
+
+func (repo *Repo) prepareCloneDirectory() error {
 	if err := os.MkdirAll(repo.path, os.ModePerm); err != nil {
 		return errors.New(err)
 	}
 
+	repoName := repo.extractRepoName()
+	repo.path = filepath.Join(repo.path, repoName)
+
+	// Clean up incomplete clones
+	if repo.shouldCleanupIncompleteClone() {
+		repo.logger.Debugf("The repo dir exists but %q does not. Removing the repo dir for cloning from the remote source.", cloneCompleteSentinel)
+
+		if err := os.RemoveAll(repo.path); err != nil {
+			return errors.New(err)
+		}
+	}
+
+	return nil
+}
+
+func (repo *Repo) extractRepoName() string {
 	repoName := "temp"
 	if match := repoNameFromCloneURLReg.FindStringSubmatch(repo.cloneURL); len(match) > 0 && match[1] != "" {
 		repoName = match[1]
 	}
 
-	repo.path = filepath.Join(repo.path, repoName)
+	return repoName
+}
 
-	// Since we are cloning the repository into a temporary directory, some operating systems such as MacOS have a service for deleting files that have not been accessed for a long time.
-	// For example, in MacOS the service is responsible for deleting unused files deletes only files while leaving the directory structure is untouched, which in turn misleads `go-getter`, which thinks that the repository exists but cannot update it due to the lack of files. In such cases, we simply delete the temporary directory in order to clone the one again.
-	// See https://github.com/gruntwork-io/terragrunt/pull/2888
-	if files.FileExists(repo.path) && !files.FileExists(repo.gitHeadfile()) {
-		repo.logger.Debugf("The repo dir exists but git file %q does not. Removing the repo dir for cloning from the remote source.", repo.gitHeadfile())
+func (repo *Repo) shouldCleanupIncompleteClone() bool {
+	return files.FileExists(repo.path) && !repo.cloneCompleted()
+}
 
-		if err := os.RemoveAll(repo.path); err != nil {
-			return errors.New(err)
+func (repo *Repo) cloneCompleted() bool {
+	return files.FileExists(filepath.Join(repo.path, cloneCompleteSentinel))
+}
+
+func (repo *Repo) performClone(ctx context.Context, l log.Logger, opts *CloneOptions) error {
+	client := getter.DefaultClient
+
+	if repo.allowCAS {
+		c, err := cas.New(cas.Options{})
+		if err != nil {
+			return err
+		}
+
+		cloneOpts := cas.CloneOptions{
+			Dir:              repo.path,
+			IncludedGitFiles: includedGitFiles,
 		}
+
+		client.Getters = append([]getter.Getter{cas.NewCASGetter(&l, c, &cloneOpts)}, client.Getters...)
 	}
 
-	sourceURL, err := tf.ToSourceURL(repo.cloneURL, "")
+	sourceURL, err := tf.ToSourceURL(opts.SourceURL, "")
 	if err != nil {
 		return err
 	}
 
 	repo.cloneURL = sourceURL.String()
+	opts.Logger.Infof("Cloning repository %q to temporary directory %q", repo.cloneURL, repo.path)
 
-	repo.logger.Infof("Cloning repository %q to temporary directory %q", repo.cloneURL, repo.path)
+	// Check first if the query param ref is already set
+	q := sourceURL.Query()
 
-	// We need to explicitly specify the reference, otherwise we will get an error:
-	// "fatal: The empty string is not a valid pathspec. Use . instead if you wanted to match all paths"
-	// when updating an existing repository.
-	sourceURL.RawQuery = (url.Values{"ref": []string{"HEAD"}}).Encode()
+	ref := q.Get("ref")
+	if ref != "" {
+		q.Set("ref", "HEAD")
+	}
+
+	sourceURL.RawQuery = q.Encode()
 
-	if err := getter.Get(repo.path, strings.Trim(sourceURL.String(), "/"), getter.WithContext(ctx), getter.WithMode(getter.ClientModeDir)); err != nil {
+	_, err = client.Get(ctx, &getter.Request{
+		Src:     sourceURL.String(),
+		Dst:     repo.path,
+		GetMode: getter.ModeDir,
+	})
+	if err != nil {
+		return err
+	}
+
+	// Create the sentinel file to indicate that the clone is complete
+	f, err := os.Create(filepath.Join(repo.path, cloneCompleteSentinel))
+	if err != nil {
+		return errors.New(err)
+	}
+
+	if err := f.Close(); err != nil {
 		return errors.New(err)
 	}
 
@@ -237,7 +338,7 @@ func (repo *Repo) parseRemoteURL() error {
 	gitConfigPath := filepath.Join(repo.path, ".git", "config")
 
 	if !files.FileExists(gitConfigPath) {
-		return errors.Errorf("the specified path %q is not a git repository", repo.path)
+		return errors.Errorf("the specified path %q is not a git repository (no .git/config file found)", repo.path)
 	}
 
 	repo.logger.Debugf("Parsing git config %q", gitConfigPath)
@@ -280,11 +381,12 @@ func (repo *Repo) gitHeadfile() string {
 func (repo *Repo) parseBranchName() error {
 	data, err := files.ReadFileAsString(repo.gitHeadfile())
 	if err != nil {
-		return errors.Errorf("the specified path %q is not a git repository", repo.path)
+		return errors.Errorf("the specified path %q is not a git repository (no .git/HEAD file found)", repo.path)
 	}
 
 	if match := gitHeadBranchNameReg.FindStringSubmatch(data); len(match) > 0 {
 		repo.BranchName = strings.TrimSpace(match[1])
+
 		return nil
 	}
 
diff --git a/cli/commands/commands.go b/cli/commands/commands.go
index ff38485f..8e212c1e 100644
--- a/cli/commands/commands.go
+++ b/cli/commands/commands.go
@@ -2,6 +2,7 @@
 package commands
 
 import (
+	"github.com/gruntwork-io/terragrunt/cli/commands/find"
 	"github.com/gruntwork-io/terragrunt/cli/commands/info"
 	"github.com/gruntwork-io/terragrunt/cli/commands/stack"
 	"github.com/gruntwork-io/terragrunt/options"
@@ -33,6 +34,8 @@ const (
 	MainCommandsCategoryName = "Main commands"
 	// CatalogCommandsCategoryName represents commands for managing Terragrunt catalogs.
 	CatalogCommandsCategoryName = "Catalog commands"
+	// DiscoveryCommandsCategoryName represents commands for discovering Terragrunt configurations.
+	DiscoveryCommandsCategoryName = "Discovery commands"
 	// ConfigurationCommandsCategoryName represents commands for managing Terragrunt configurations.
 	ConfigurationCommandsCategoryName = "Configuration commands"
 	// ShortcutsCommandsCategoryName represents OpenTofu-specific shortcut commands.
@@ -65,6 +68,15 @@ func New(opts *options.TerragruntOptions) cli.Commands {
 		},
 	)
 
+	discoveryCommands := cli.Commands{
+		find.NewCommand(opts), // find
+	}.SetCategory(
+		&cli.Category{
+			Name:  DiscoveryCommandsCategoryName,
+			Order: 30, //nolint: mnd
+		},
+	)
+
 	configurationCommands := cli.Commands{
 		graphdependencies.NewCommand(opts),  // graph-dependencies
 		outputmodulegroups.NewCommand(opts), // output-module-groups
@@ -80,19 +92,20 @@ func New(opts *options.TerragruntOptions) cli.Commands {
 	}.SetCategory(
 		&cli.Category{
 			Name:  ConfigurationCommandsCategoryName,
-			Order: 30, //nolint: mnd
+			Order: 40, //nolint: mnd
 		},
 	)
 
 	shortcutsCommands := NewShortcutsCommands(opts).SetCategory(
 		&cli.Category{
 			Name:  ShortcutsCommandsCategoryName,
-			Order: 40, //nolint: mnd
+			Order: 50, //nolint: mnd
 		},
 	)
 
 	allCommands := mainCommands.
 		Merge(catalogCommands...).
+		Merge(discoveryCommands...).
 		Merge(configurationCommands...).
 		Merge(shortcutsCommands...).
 		Merge(NewDeprecatedCommands(opts)...)
diff --git a/cli/commands/find/action.go b/cli/commands/find/action.go
new file mode 100644
index 00000000..309290e0
--- /dev/null
+++ b/cli/commands/find/action.go
@@ -0,0 +1,134 @@
+package find
+
+import (
+	"context"
+	"encoding/json"
+	"os"
+	"path/filepath"
+
+	"github.com/gruntwork-io/terragrunt/internal/discovery"
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+	"github.com/mgutz/ansi"
+)
+
+// Run runs the find command.
+func Run(ctx context.Context, opts *Options) error {
+	d := discovery.NewDiscovery(opts.WorkingDir)
+
+	if opts.Hidden {
+		d = d.WithHidden()
+	}
+
+	configs, err := d.Discover()
+	if err != nil {
+		return errors.New(err)
+	}
+
+	switch opts.Format {
+	case "text":
+		return outputText(opts, configs)
+	case "json":
+		return outputJSON(opts, configs)
+	default:
+		// This should never happen, because of validation in the command.
+		// If it happens, we want to throw so we can fix the validation.
+		return errors.New("invalid format: " + opts.Format)
+	}
+}
+
+// outputJSON outputs the discovered configurations in JSON format.
+func outputJSON(opts *Options, configs discovery.DiscoveredConfigs) error {
+	jsonBytes, err := json.MarshalIndent(configs, "", "  ")
+	if err != nil {
+		return errors.New(err)
+	}
+
+	_, err = opts.Writer.Write(append(jsonBytes, []byte("\n")...))
+	if err != nil {
+		return errors.New(err)
+	}
+
+	return nil
+}
+
+// Colorizer is a colorizer for the discovered configurations.
+type Colorizer struct {
+	unitColorizer  func(string) string
+	stackColorizer func(string) string
+	pathColorizer  func(string) string
+}
+
+// NewColorizer creates a new Colorizer.
+func NewColorizer() *Colorizer {
+	return &Colorizer{
+		unitColorizer:  ansi.ColorFunc("blue+bh"),
+		stackColorizer: ansi.ColorFunc("green+bh"),
+		pathColorizer:  ansi.ColorFunc("white+d"),
+	}
+}
+
+func (c *Colorizer) Colorize(config *discovery.DiscoveredConfig) string {
+	path := config.Path
+
+	// Get the directory and base name using filepath
+	dir, base := filepath.Split(path)
+
+	if dir == "" {
+		// No directory part, color the whole path
+		switch config.Type {
+		case discovery.ConfigTypeUnit:
+			return c.unitColorizer(path)
+		case discovery.ConfigTypeStack:
+			return c.stackColorizer(path)
+		default:
+			return path
+		}
+	}
+
+	// Color the components differently
+	coloredPath := c.pathColorizer(dir)
+
+	switch config.Type {
+	case discovery.ConfigTypeUnit:
+		return coloredPath + c.unitColorizer(base)
+	case discovery.ConfigTypeStack:
+		return coloredPath + c.stackColorizer(base)
+	default:
+		return path
+	}
+}
+
+// outputText outputs the discovered configurations in text format.
+func outputText(opts *Options, configs discovery.DiscoveredConfigs) error {
+	if opts.TerragruntOptions.Logger.Formatter().DisabledColors() || isStdoutRedirected() {
+		for _, config := range configs {
+			_, err := opts.Writer.Write([]byte(config.Path + "\n"))
+			if err != nil {
+				return errors.New(err)
+			}
+		}
+
+		return nil
+	}
+
+	colorizer := NewColorizer()
+
+	for _, config := range configs {
+		_, err := opts.Writer.Write([]byte(colorizer.Colorize(config) + "\n"))
+		if err != nil {
+			return errors.New(err)
+		}
+	}
+
+	return nil
+}
+
+// isStdoutRedirected returns true if the stdout is redirected.
+func isStdoutRedirected() bool {
+	stat, err := os.Stdout.Stat()
+	if err != nil {
+		return false
+	}
+
+	return (stat.Mode() & os.ModeCharDevice) == 0
+}
diff --git a/cli/commands/find/command.go b/cli/commands/find/command.go
new file mode 100644
index 00000000..0dbb5ae9
--- /dev/null
+++ b/cli/commands/find/command.go
@@ -0,0 +1,84 @@
+// Package find provides the ability to find Terragrunt configurations in your codebase
+// via the `terragrunt find` command.
+package find
+
+import (
+	"github.com/gruntwork-io/terragrunt/cli/flags"
+	"github.com/gruntwork-io/terragrunt/internal/cli"
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+	"github.com/gruntwork-io/terragrunt/internal/experiment"
+	"github.com/gruntwork-io/terragrunt/options"
+)
+
+const (
+	CommandName  = "find"
+	CommandAlias = "fd"
+
+	FormatFlagName = "format"
+	JSONFlagName   = "json"
+	SortFlagName   = "sort"
+	HiddenFlagName = "hidden"
+)
+
+func NewFlags(opts *Options, prefix flags.Prefix) cli.Flags {
+	tgPrefix := prefix.Prepend(flags.TgPrefix)
+
+	return cli.Flags{
+		flags.NewFlag(&cli.GenericFlag[string]{
+			Name:        FormatFlagName,
+			EnvVars:     tgPrefix.EnvVars(FormatFlagName),
+			Destination: &opts.Format,
+			Usage:       "Output format for the find results. Valid values: text, json",
+			DefaultText: "text",
+		}),
+		flags.NewFlag(&cli.BoolFlag{
+			Name:        JSONFlagName,
+			EnvVars:     tgPrefix.EnvVars(JSONFlagName),
+			Destination: &opts.JSON,
+			Usage:       "Output in JSON format (equivalent to --format=json)",
+		}),
+		flags.NewFlag(&cli.GenericFlag[string]{
+			Name:        SortFlagName,
+			EnvVars:     tgPrefix.EnvVars(SortFlagName),
+			Destination: &opts.Sort,
+			Usage:       "Sort order for the find results. Valid values: alpha", // TODO: add dag
+			DefaultText: "alpha",
+		}),
+		flags.NewFlag(&cli.BoolFlag{
+			Name:        HiddenFlagName,
+			EnvVars:     tgPrefix.EnvVars(HiddenFlagName),
+			Destination: &opts.Hidden,
+			Usage:       "Include hidden directories in the find results.",
+		}),
+	}
+}
+
+func NewCommand(opts *options.TerragruntOptions) *cli.Command {
+	cmdOpts := NewOptions(opts)
+
+	return &cli.Command{
+		Name:                 CommandName,
+		Aliases:              []string{CommandAlias},
+		Usage:                "Find relevant Terragrunt configurations.",
+		ErrorOnUndefinedFlag: true,
+		Flags:                NewFlags(cmdOpts, nil),
+		Before: func(ctx *cli.Context) error {
+			if !opts.Experiments.Evaluate(experiment.CLIRedesign) {
+				return cli.NewExitError(errors.Errorf("requires that the %[1]s experiment is enabled. e.g. --experiment %[1]s", experiment.CLIRedesign), cli.ExitCodeGeneralError)
+			}
+
+			if cmdOpts.JSON {
+				cmdOpts.Format = "json"
+			}
+
+			if err := cmdOpts.Validate(); err != nil {
+				return cli.NewExitError(err, cli.ExitCodeGeneralError)
+			}
+
+			return nil
+		},
+		Action: func(ctx *cli.Context) error {
+			return Run(ctx, cmdOpts)
+		},
+	}
+}
diff --git a/cli/commands/find/options.go b/cli/commands/find/options.go
new file mode 100644
index 00000000..d7e38428
--- /dev/null
+++ b/cli/commands/find/options.go
@@ -0,0 +1,70 @@
+package find
+
+import (
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+	"github.com/gruntwork-io/terragrunt/options"
+)
+
+type Options struct {
+	*options.TerragruntOptions
+
+	// Format determines the format of the output.
+	Format string
+
+	// JSON determines whether to output in JSON format.
+	// Alias for --format=json.
+	JSON bool
+
+	// Sort determines the sort order of the output.
+	Sort string
+
+	// Hidden determines whether to detect hidden directories.
+	Hidden bool
+}
+
+func NewOptions(opts *options.TerragruntOptions) *Options {
+	return &Options{
+		TerragruntOptions: opts,
+		Format:            "text",
+		Sort:              "alpha",
+		Hidden:            false,
+	}
+}
+
+func (o *Options) Validate() error {
+	errs := []error{}
+
+	if err := o.validateFormat(); err != nil {
+		errs = append(errs, err)
+	}
+
+	if err := o.validateSort(); err != nil {
+		errs = append(errs, err)
+	}
+
+	if len(errs) > 0 {
+		return errors.New(errors.Join(errs...))
+	}
+
+	return nil
+}
+
+func (o *Options) validateFormat() error {
+	switch o.Format {
+	case "text":
+		return nil
+	case "json":
+		return nil
+	default:
+		return errors.New("invalid format: " + o.Format)
+	}
+}
+
+func (o *Options) validateSort() error {
+	switch o.Sort {
+	case "alpha":
+		return nil
+	default:
+		return errors.New("invalid sort: " + o.Sort)
+	}
+}
diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go
index f7cdad34..f5b769e1 100644
--- a/cli/commands/stack/action.go
+++ b/cli/commands/stack/action.go
@@ -17,8 +17,7 @@ import (
 )
 
 const (
-	defaultStackFile = "terragrunt.stack.hcl"
-	stackDir         = ".terragrunt-stack"
+	stackDir = ".terragrunt-stack"
 )
 
 // RunGenerate runs the stack command.
diff --git a/cli/commands/stack/generate.go b/cli/commands/stack/generate.go
index f6c8e0e6..90620e6f 100644
--- a/cli/commands/stack/generate.go
+++ b/cli/commands/stack/generate.go
@@ -9,7 +9,7 @@ import (
 )
 
 func generateStack(ctx context.Context, opts *options.TerragruntOptions) error {
-	opts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile)
+	opts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, config.DefaultStackFile)
 	opts.Logger.Infof("Generating stack from %s", opts.TerragruntStackConfigPath)
 
 	return config.GenerateStacks(ctx, opts)
diff --git a/config/config.go b/config/config.go
index 0e142921..5218ad8e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -40,9 +40,10 @@ import (
 
 const (
 	DefaultTerragruntConfigPath     = "terragrunt.hcl"
+	DefaultStackFile                = "terragrunt.stack.hcl"
+	ValuesFile                      = "terragrunt.values.hcl"
 	DefaultTerragruntJSONConfigPath = "terragrunt.hcl.json"
 	RecommendedParentConfigName     = "root.hcl"
-	StackValuesFile                 = "terragrunt.values.hcl"
 
 	FoundInFile = "found_in_file"
 
diff --git a/config/config_partial.go b/config/config_partial.go
index d6fd5878..a6d38e8b 100644
--- a/config/config_partial.go
+++ b/config/config_partial.go
@@ -35,6 +35,7 @@ const (
 	TerragruntVersionConstraints
 	RemoteStateBlock
 	FeatureFlagsBlock
+	EngineBlock
 	ExcludeBlock
 	ErrorsBlock
 )
@@ -118,6 +119,12 @@ type terragruntInputs struct {
 	Remain hcl.Body   `hcl:",remain"`
 }
 
+// terragruntEngine is a struct that can only be used to decode the engine block.
+type terragruntEngine struct {
+	Engine *EngineConfig `hcl:"engine,block"`
+	Remain hcl.Body      `hcl:",remain"`
+}
+
 // DecodeBaseBlocks takes in a parsed HCL2 file and decodes the base blocks. Base blocks are blocks that should always
 // be decoded even in partial decoding, because they provide bindings that are necessary for parsing any block in the
 // file. Currently base blocks are:
@@ -329,6 +336,7 @@ func TerragruntConfigFromPartialConfig(ctx *ParsingContext, file *hclparse.File,
 //     the config.
 //   - RemoteStateBlock: Parses the `remote_state` block in the config
 //   - FeatureFlagsBlock: Parses the `feature` block in the config
+//   - EngineBlock: Parses the `engine` block in the config
 //   - ExcludeBlock : Parses the `exclude` block in the config
 //
 // Note that the following blocks are always decoded:
@@ -444,6 +452,16 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
 				output.Dependencies = dependencies
 			}
 
+		case EngineBlock:
+			decoded := terragruntEngine{}
+
+			err := file.Decode(&decoded, evalParsingContext)
+			if err != nil {
+				return nil, err
+			}
+
+			output.Engine = decoded.Engine
+
 		case TerragruntFlags:
 			decoded := terragruntFlags{}
 
diff --git a/config/dependency.go b/config/dependency.go
index 6ec869fc..eab647c4 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -756,7 +756,8 @@ func getTerragruntOutputJSON(ctx *ParsingContext, targetConfig string) ([]byte,
 	// we need to suspend logging diagnostic errors on this attempt
 	parseOptions := append(ctx.ParserOptions, hclparse.WithDiagnosticsWriter(io.Discard, true))
 
-	remoteStateTGConfig, err := PartialParseConfigFile(ctx.WithParseOption(parseOptions).WithDecodeList(RemoteStateBlock, TerragruntFlags), targetConfig, nil)
+	remoteStateTGConfig, err := PartialParseConfigFile(ctx.WithParseOption(parseOptions).WithDecodeList(
+		RemoteStateBlock, TerragruntFlags, EngineBlock), targetConfig, nil)
 	if err != nil || !canGetRemoteState(remoteStateTGConfig.RemoteState) {
 		targetOpts, err := cloneTerragruntOptionsForDependency(ctx, targetConfig)
 		if err != nil {
@@ -776,11 +777,18 @@ func getTerragruntOutputJSON(ctx *ParsingContext, targetConfig string) ([]byte,
 		return nil, err
 	}
 
+	// Fetch engine options so they can be passed to the dependency functions
+	engineOpts, err := remoteStateTGConfig.EngineOptions()
+	if err != nil {
+		return nil, err
+	}
+
 	if isInit {
-		return getTerragruntOutputJSONFromInitFolder(ctx, workingDir, remoteStateTGConfig.GetIAMRoleOptions())
+		return getTerragruntOutputJSONFromInitFolder(ctx, workingDir, remoteStateTGConfig.GetIAMRoleOptions(), engineOpts)
 	}
 
-	return getTerragruntOutputJSONFromRemoteState(ctx, targetConfig, remoteStateTGConfig.RemoteState, remoteStateTGConfig.GetIAMRoleOptions())
+	return getTerragruntOutputJSONFromRemoteState(ctx, targetConfig, remoteStateTGConfig.RemoteState,
+		remoteStateTGConfig.GetIAMRoleOptions(), engineOpts)
 }
 
 // canGetRemoteState returns true if the remote state block is not nil and dependency optimization is not disabled
@@ -833,10 +841,11 @@ func terragruntAlreadyInit(opts *options.TerragruntOptions, configPath string, c
 
 // getTerragruntOutputJSONFromInitFolder will retrieve the outputs directly from the module's working directory without
 // running init.
-func getTerragruntOutputJSONFromInitFolder(ctx *ParsingContext, terraformWorkingDir string, iamRoleOpts options.IAMRoleOptions) ([]byte, error) {
+func getTerragruntOutputJSONFromInitFolder(ctx *ParsingContext, terraformWorkingDir string, iamRoleOpts options.IAMRoleOptions,
+	engineOpts *options.EngineOptions) ([]byte, error) {
 	targetConfigPath := ctx.TerragruntOptions.TerragruntConfigPath
 
-	targetTGOptions, err := setupTerragruntOptionsForBareTerraform(ctx, terraformWorkingDir, targetConfigPath, iamRoleOpts)
+	targetTGOptions, err := setupTerragruntOptionsForBareTerraform(ctx, terraformWorkingDir, targetConfigPath, iamRoleOpts, engineOpts)
 	if err != nil {
 		return nil, err
 	}
@@ -871,6 +880,7 @@ func getTerragruntOutputJSONFromRemoteState(
 	targetConfigPath string,
 	remoteState *remote.RemoteState,
 	iamRoleOpts options.IAMRoleOptions,
+	engineOpts *options.EngineOptions,
 ) ([]byte, error) {
 	ctx.TerragruntOptions.Logger.Debugf("Detected remote state block with generate config. Resolving dependency by pulling remote state.")
 	// Create working directory where we will run terraform in. We will create the temporary directory in the download
@@ -894,7 +904,7 @@ func getTerragruntOutputJSONFromRemoteState(
 
 	ctx.TerragruntOptions.Logger.Debugf("Setting dependency working directory to %s", tempWorkDir)
 
-	targetTGOptions, err := setupTerragruntOptionsForBareTerraform(ctx, tempWorkDir, targetConfigPath, iamRoleOpts)
+	targetTGOptions, err := setupTerragruntOptionsForBareTerraform(ctx, tempWorkDir, targetConfigPath, iamRoleOpts, engineOpts)
 	if err != nil {
 		return nil, err
 	}
@@ -1017,7 +1027,8 @@ func getTerragruntOutputJSONFromRemoteStateS3(terragruntOptions *options.Terragr
 
 // setupTerragruntOptionsForBareTerraform sets up a new TerragruntOptions struct that can be used to run terraform
 // without going through the full RunTerragrunt operation.
-func setupTerragruntOptionsForBareTerraform(ctx *ParsingContext, workingDir string, configPath string, iamRoleOpts options.IAMRoleOptions) (*options.TerragruntOptions, error) {
+func setupTerragruntOptionsForBareTerraform(ctx *ParsingContext, workingDir string, configPath string, iamRoleOpts options.IAMRoleOptions,
+	engineOpts *options.EngineOptions) (*options.TerragruntOptions, error) {
 	// Here we clone the terragrunt options again since we need to make further modifications to it to allow running
 	// terraform directly.
 	// Set the terraform working dir to the tempdir, and set stdout writer to io.Discard so that output content is
@@ -1029,6 +1040,7 @@ func setupTerragruntOptionsForBareTerraform(ctx *ParsingContext, workingDir stri
 
 	targetTGOptions.WorkingDir = workingDir
 	targetTGOptions.Writer = io.Discard
+	targetTGOptions.Engine = engineOpts
 
 	// If the target config has an IAM role directive and it was not set on the command line, set it to
 	// the one we retrieved from the config.
diff --git a/docs-starlight/src/assets/img/screenshots/find-no-color.png b/docs-starlight/src/assets/img/screenshots/find-no-color.png
new file mode 100644
index 00000000..5ec4041f
Binary files /dev/null and b/docs-starlight/src/assets/img/screenshots/find-no-color.png differ
diff --git a/docs-starlight/src/assets/img/screenshots/find.png b/docs-starlight/src/assets/img/screenshots/find.png
new file mode 100644
index 00000000..2bbfe10a
Binary files /dev/null and b/docs-starlight/src/assets/img/screenshots/find.png differ
diff --git a/docs-starlight/src/components/Command.astro b/docs-starlight/src/components/Command.astro
index 8aa3e63e..2ab018b0 100644
--- a/docs-starlight/src/components/Command.astro
+++ b/docs-starlight/src/components/Command.astro
@@ -50,8 +50,8 @@ const { Content } = await render(command);
   data?.flags ? (
   <h2 id="flags">Flags</h2>
   <div>
-    {data?.flags.map((flagName) => (
-      flagName && <Flag name={flagName} />
+    {data?.flags.map((flagSlug) => (
+      flagSlug && <Flag slug={flagSlug} />
     ))}
   </div>
   ) : null
diff --git a/docs-starlight/src/components/Flag.astro b/docs-starlight/src/components/Flag.astro
index df3afb1c..bd7195c9 100644
--- a/docs-starlight/src/components/Flag.astro
+++ b/docs-starlight/src/components/Flag.astro
@@ -4,14 +4,14 @@ import type { CollectionEntry } from 'astro:content';
 import { render, getEntry } from 'astro:content';
 
 interface Props {
-  name: string;
+  slug: string;
 }
 
-const { name } = Astro.props;
+const { slug } = Astro.props;
 
-const flagEntry = await getEntry('flags', name) as CollectionEntry<'flags'>;
+const flagEntry = await getEntry('flags', slug) as CollectionEntry<'flags'>;
 const { Content } = await render(flagEntry);
-const { description, type, env } = flagEntry.data;
+const { description, type, env, name } = flagEntry.data;
 ---
 
 <section id={name}>
diff --git a/docs-starlight/src/content/docs/02-features/14-cas.md b/docs-starlight/src/content/docs/02-features/14-cas.md
new file mode 100644
index 00000000..6b71d026
--- /dev/null
+++ b/docs-starlight/src/content/docs/02-features/14-cas.md
@@ -0,0 +1,39 @@
+---
+title: Content Addressable Store (CAS)
+description: Learn how Terragrunt supports deduplication of content using a Content Addressable Store (CAS).
+slug: docs/features/cas
+sidebar:
+  order: 14
+---
+
+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.
+
+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).
+
+```hcl
+# root.hcl
+
+catalog {
+  urls = [
+    "git@github.com:acme/modules.git"
+  ]
+}
+```
+
+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.
+
+In the event that hard linking fails due to some operating system / host incompatibility with hard links, Terragrunt will fall back to performing copies of the content from the CAS.
+
+## Storage
+
+The CAS is stored in the `~/.cache/terragrunt/cas` directory. This directory can be safely deleted at any time, as Terragrunt will automatically regenerate the CAS as needed.
+
+Avoid partial deletions of the CAS directory without care, as that might result in partially cloned repositories and unexpected behavior.
diff --git a/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0800-find.md b/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0700-find.md
similarity index 97%
rename from docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0800-find.md
rename to docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0700-find.md
index d92e31b2..ddb846bb 100644
--- a/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0800-find.md
+++ b/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0700-find.md
@@ -3,7 +3,7 @@ title: find
 description: Find relevant Terragrunt configurations.
 slug: docs/reference/cli/commands/find
 sidebar:
-  order: 800
+  order: 700
   badge:
     text: exp
     variant: tip
diff --git a/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0700-list.md b/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0800-list.md
similarity index 97%
rename from docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0700-list.md
rename to docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0800-list.md
index 92ac9da4..93c00c97 100644
--- a/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0700-list.md
+++ b/docs-starlight/src/content/docs/04-reference/02-cli/02-commands/0800-list.md
@@ -3,7 +3,7 @@ title: list
 description: List Terragrunt configurations.
 slug: docs/reference/cli/commands/list
 sidebar:
-  order: 700
+  order: 800
   badge:
     text: exp
     variant: tip
diff --git a/docs-starlight/src/content/docs/04-reference/02-cli/98-global-flags.mdx b/docs-starlight/src/content/docs/04-reference/02-cli/98-global-flags.mdx
index 9e2fa1e1..d595ef4e 100644
--- a/docs-starlight/src/content/docs/04-reference/02-cli/98-global-flags.mdx
+++ b/docs-starlight/src/content/docs/04-reference/02-cli/98-global-flags.mdx
@@ -12,56 +12,56 @@ The Terragrunt CLI supports the following global flags:
 
 ## Experiment
 
-<Flag name="experiment" />
+<Flag slug="experiment" />
 
 ## Experiment Mode
 
-<Flag name="experiment-mode" />
+<Flag slug="experiment-mode" />
 
 ## Log Custom Format
 
-<Flag name="log-custom-format" />
+<Flag slug="log-custom-format" />
 
 ## Log Disable
 
-<Flag name="log-disable" />
+<Flag slug="log-disable" />
 
 ## Log Format
 
-<Flag name="log-format" />
+<Flag slug="log-format" />
 
 ## Log Level
 
-<Flag name="log-level" />
+<Flag slug="log-level" />
 
 ## Show Absolute Paths
 
-<Flag name="log-show-abs-paths" />
+<Flag slug="log-show-abs-paths" />
 
 ## No Color
 
-<Flag name="no-color" />
+<Flag slug="no-color" />
 
 ## Non-Interactive
 
-<Flag name="non-interactive" />
+<Flag slug="non-interactive" />
 
 ## Strict Control
 
-<Flag name="strict-control" />
+<Flag slug="strict-control" />
 
 ## Strict Mode
 
-<Flag name="strict-mode" />
+<Flag slug="strict-mode" />
 
 ## Working Directory
 
-<Flag name="working-dir" />
+<Flag slug="working-dir" />
 
 ## Help
 
-<Flag name="help" />
+<Flag slug="help" />
 
 ## Version
 
-<Flag name="version" />
+<Flag slug="version" />
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 cd100222..2cc6a559 100644
--- a/docs-starlight/src/content/docs/04-reference/04-experiments.md
+++ b/docs-starlight/src/content/docs/04-reference/04-experiments.md
@@ -156,4 +156,39 @@ To transition `cli-redesign` features to a stable release, the following must be
 - [ ] Add support for the `info` command.
 - [ ] Add support for the `dag` command.
 - [ ] Add support for the `find` command.
+  - [x] Add support for `find` without flags.
+  - [x] Add support for `find` with colorful output.
+  - [x] Add support for `find` with `--format=json` flag.
+  - [x] Add support for `find` with stdout redirection detection.
+  - [x] Add support for `find` with `--hidden` flag.
+  - [x] Add support for `find` with `--sort=alpha` flag.
+  - [ ] Add support for `find` with `--sort=dag` flag.
+  - [ ] Add support for `find` with the `exclude` block used to exclude units from the search.
+  - [ ] Add integration with `symlinks` experiment to support finding units/stacks via symlinks.
 - [ ] Add support for the `list` command.
+
+### `cas`
+
+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.
+
+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.
+
+#### `cas` - How to provide feedback
+
+Share your experience with this feature in the [CAS](https://github.com/gruntwork-io/terragrunt/discussions/3939) Feedback GitHub Discussion.
+Feedback is crucial for ensuring the feature meets real-world use cases. Please include:
+
+- Any bugs or issues encountered (including logs or stack traces if possible).
+- Suggestions for additional improvements or enhancements.
+
+#### `cas` - Criteria for stabilization
+
+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.
+- [ ] Add support for storing and retrieving Unit/Stack configurations from the CAS.
diff --git a/docs-starlight/src/data/commands/find.mdx b/docs-starlight/src/data/commands/find.mdx
index 4673981e..3683a641 100644
--- a/docs-starlight/src/data/commands/find.mdx
+++ b/docs-starlight/src/data/commands/find.mdx
@@ -3,27 +3,104 @@ name: find
 path: find
 category: discovery
 sidebar:
-  order: 800
+  order: 700
 description: Find relevant Terragrunt configurations.
 usage: |
-  Find Terragrunt configurations in your codebase that match specific criteria.
+  The `find` command helps you discover Terragrunt configurations in your codebase.
+
+  It recursively searches for `terragrunt.hcl` and `terragrunt.stack.hcl` files and displays them in formatted output.
 experiment:
   control: cli-redesign
   name: CLI Redesign
 examples:
   - description: |
-      Find all units in the current directory.
+      Find all configurations (units and stacks) in the current directory.
     code: |
       terragrunt find
   - description: |
-      Find all units and emit them as a JSON string.
+      Find all configurations in a different directory.
+    code: |
+      terragrunt find --working-dir /path/to/working/dir
+  - description: |
+      Disable color output.
+    code: |
+      terragrunt find --no-color
+  - description: |
+      Find all configurations in the current directory and emit them as a JSON string.
     code: |
       terragrunt find --format 'json'
   - description: |
-      Find all units and sort them in the order they would be run in the Directed Acyclic Graph (DAG).
+      Find all configurations and output them in JSON format (alias for --format=json).
     code: |
-      terragrunt find --sort 'dag'
+      terragrunt find --json
+  # - description: |
+  #     Find all units and sort them in the order they would be run in the Directed Acyclic Graph (DAG).
+  #   code: |
+  #     terragrunt find --sort 'dag'
 flags:
   - find-format
-  - find-sort
+  - find-json
+  # - find-sort
+  - find-hidden
 ---
+
+import { Aside, Badge } from '@astrojs/starlight/components';
+
+## Color Output
+
+When used without any flags, all units and stacks discovered in the current working directory are displayed in colorful text format.
+
+![find](../../assets/img/screenshots/find.png)
+
+<Aside type="note" title="Color Coding">
+
+Discovered configurations are color coded to help you identify them at a glance:
+
+- <Badge text="Units" style={{ backgroundColor: '#1B46DD', color: '#FFFFFF' }} /> are displayed in blue
+- <Badge text="Stacks" style={{ backgroundColor: '#2E8B57', color: '#FFFFFF' }} /> are displayed in green
+
+</Aside>
+
+## Output Formats
+
+The `find` command supports two output formats:
+
+### Text Format (Default)
+
+The default text format displays each configuration on a new line, with color coding for different types.
+
+### JSON Format
+
+You can output the results in JSON format using either:
+
+```bash
+terragrunt find --format json
+```
+
+or the shorter alias:
+
+```bash
+terragrunt find --json
+```
+
+The JSON output includes additional metadata about each configuration, such as its type (unit or stack) and path.
+
+## Disabling Color Output
+
+You can disable color output by using the global `--no-color` flag:
+
+```bash
+terragrunt find --no-color
+```
+
+When stdout is redirected, color output is disabled automatically to prevent undesired interference with other tools.
+
+![find-no-color](../../assets/img/screenshots/find-no-color.png)
+
+## Working Directory
+
+You can change the working directory for `find` by using the global `--working-dir` flag:
+
+```bash
+terragrunt find --working-dir /path/to/working/dir
+```
diff --git a/docs-starlight/src/data/commands/list.mdx b/docs-starlight/src/data/commands/list.mdx
index f90a9d39..aa93f94d 100644
--- a/docs-starlight/src/data/commands/list.mdx
+++ b/docs-starlight/src/data/commands/list.mdx
@@ -3,7 +3,7 @@ name: list
 path: list
 category: discovery
 sidebar:
-  order: 700
+  order: 800
 description: List Terragrunt configurations.
 usage: |
   List all Terragrunt configurations in the current directory tree.
diff --git a/docs-starlight/src/data/flags/find-format.mdx b/docs-starlight/src/data/flags/find-format.mdx
index fc700a3e..d53ee1c1 100644
--- a/docs-starlight/src/data/flags/find-format.mdx
+++ b/docs-starlight/src/data/flags/find-format.mdx
@@ -1,19 +1,30 @@
 ---
 name: format
-description: Format the results as specified. Supported values (json). default json.
+description: |
+  Format the results as specified. Supported values (text, json). Default: text.
 type: string
 env:
   - TG_FORMAT
 ---
 
-Specifies the output format for the find command results. Currently supports:
-
-- `json` - Output results in JSON format (default)
-
 This is particularly useful when you need to process the results programmatically or integrate with other tools.
 
 Example:
 
 ```bash
-terragrunt find --format json
+$ terragrunt find --format=json | jq '.[:3]'
+[
+  {
+    "type": "stack",
+    "path": "basic"
+  },
+  {
+    "type": "unit",
+    "path": "basic/units/chick"
+  },
+  {
+    "type": "unit",
+    "path": "basic/units/chicken"
+  }
+]
 ```
diff --git a/docs-starlight/src/data/flags/find-hidden.mdx b/docs-starlight/src/data/flags/find-hidden.mdx
new file mode 100644
index 00000000..5e5cc8bb
--- /dev/null
+++ b/docs-starlight/src/data/flags/find-hidden.mdx
@@ -0,0 +1,13 @@
+---
+name: hidden
+description: Find configurations in hidden directories.
+type: bool
+env:
+  - TG_HIDDEN
+---
+
+Example:
+
+```bash
+terragrunt find --hidden
+```
diff --git a/docs-starlight/src/data/flags/find-json.mdx b/docs-starlight/src/data/flags/find-json.mdx
new file mode 100644
index 00000000..3ec5ea68
--- /dev/null
+++ b/docs-starlight/src/data/flags/find-json.mdx
@@ -0,0 +1,38 @@
+---
+name: json
+description: |
+  Output results in JSON format. This is equivalent to using `--format=json`.
+type: bool
+env:
+  - TG_JSON
+---
+
+import { Aside } from '@astrojs/starlight/components';
+
+This flag is a convenient shorthand for `--format=json`. It's particularly useful when you need to process the results programmatically or integrate with other tools.
+
+Example:
+
+```bash
+$ terragrunt find --json | jq '.[:3]'
+[
+  {
+    "type": "stack",
+    "path": "basic"
+  },
+  {
+    "type": "unit",
+    "path": "basic/units/chick"
+  },
+  {
+    "type": "unit",
+    "path": "basic/units/chicken"
+  }
+]
+```
+
+<Aside type="note" title="Equivalent Flags">
+
+The `--json` flag is equivalent to `--format=json`. You can use either one to get the same JSON output.
+
+</Aside>
diff --git a/docs/_docs/02_features/14-cas.md b/docs/_docs/02_features/14-cas.md
new file mode 100644
index 00000000..cacef50a
--- /dev/null
+++ b/docs/_docs/02_features/14-cas.md
@@ -0,0 +1,44 @@
+---
+layout: collection-browser-doc
+title: Content Addressable Store (CAS)
+category: features
+categories_url: features
+excerpt: Learn how Terragrunt supports deduplication of content using a Content Addressable Store (CAS).
+tags: ["cas"]
+order: 214
+nav_title: Documentation
+nav_title_link: /docs/
+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.
+
+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).
+
+```hcl
+# root.hcl
+
+catalog {
+  urls = [
+    "git@github.com:acme/modules.git"
+  ]
+}
+```
+
+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.
+
+In the event that hard linking fails due to some operating system / host incompatibility with hard links, Terragrunt will fall back to performing copies of the content from the CAS.
+
+## Storage
+
+The CAS is stored in the `~/.cache/terragrunt/cas` directory. This directory can be safely deleted at any time, as Terragrunt will automatically regenerate the CAS as needed.
+
+Avoid partial deletions of the CAS directory without care, as that might result in partially cloned repositories and unexpected behavior.
diff --git a/docs/_docs/04_reference/02-cli-options.md b/docs/_docs/04_reference/02-cli-options.md
index 78d63347..fcae888a 100644
--- a/docs/_docs/04_reference/02-cli-options.md
+++ b/docs/_docs/04_reference/02-cli-options.md
@@ -36,6 +36,11 @@ The commands relevant to managing an IaC catalog are:
   - [catalog](#catalog)
   - [scaffold](#scaffold)
 
+The commands relevant to discovering Terragrunt configurations are:
+
+- [Discovery commands](#discovery-commands)
+  - [find](#find)
+
 The commands used for managing Terragrunt configuration itself are:
 
 - [Configuration commands](#configuration-commands)
@@ -537,6 +542,77 @@ Generate Terragrunt files from existing OpenTofu/Terraform modules.
 
 More details in [scaffold section](https://terragrunt.gruntwork.io/docs/features/scaffold/).
 
+### Discovery commands
+
+#### find
+
+**[NOTE] The `find` command is experimental, usage requires the [`--experiment cli-redesign` flag](/docs/reference/experiments/#cli-redesign).**
+
+Find Terragrunt configurations in your codebase.
+
+The `find` command helps you discover Terragrunt configurations in your codebase. It recursively searches for `terragrunt.hcl` and `terragrunt.stack.hcl` files and displays them in formatted output.
+
+```bash
+terragrunt find [options]
+```
+
+When used without any flags, all units and stacks discovered in the current working directory are displayed in colorful text format.
+
+[![find](/assets/img/screenshots/find.png)](/assets/img/screenshots/find.png)
+
+Discovered configurations are color coded to help you identify them at a glance:
+
+- Units are displayed in blue
+- Stacks are displayed in green
+
+You can disable color output by using the `--no-color` flag.
+
+```bash
+terragrunt find --no-color
+```
+
+When stdout is redirected, color output is disabled automatically to prevent undesired interference with other tools.
+
+[![find-no-color](/assets/img/screenshots/find-no-color.png)](/assets/img/screenshots/find-no-color.png)
+
+You can change the working directory used by the command by using the `--working-dir` flag.
+
+```bash
+terragrunt find --working-dir /path/to/working/dir
+```
+
+In JSON format, `find` outputs a structured representation of the discovered configurations, including their types and path relative to the working directory.
+
+```bash
+$ terragrunt find --format=json | jq '.[:3]'
+[
+  {
+    "type": "stack",
+    "path": "basic"
+  },
+  {
+    "type": "unit",
+    "path": "basic/units/chick"
+  },
+  {
+    "type": "unit",
+    "path": "basic/units/chicken"
+  }
+]
+```
+
+Note that you can also use the `--json` flag to get the same output.
+
+```bash
+terragrunt find --json
+```
+
+By default, hidden directories (those starting with `.`) are excluded from the search. Use `--hidden` to include them.
+
+```bash
+terragrunt find --hidden
+```
+
 ### Configuration commands
 
 #### graph-dependencies
@@ -814,6 +890,8 @@ This command will exit with an error if terragrunt detects any unused inputs or
   - [Catalog commands](#catalog-commands)
     - [catalog](#catalog)
     - [scaffold](#scaffold)
+  - [Discovery commands](#discovery-commands)
+    - [find](#find)
   - [Configuration commands](#configuration-commands)
     - [graph-dependencies](#graph-dependencies)
     - [hclfmt](#hclfmt)
diff --git a/docs/_docs/04_reference/04-config-blocks-and-attributes.md b/docs/_docs/04_reference/04-config-blocks-and-attributes.md
index 326a9328..13985801 100644
--- a/docs/_docs/04_reference/04-config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/04-config-blocks-and-attributes.md
@@ -22,11 +22,15 @@ The following is a reference of all the supported blocks and attributes in the c
   - [terraform](#terraform)
     - [A note about using modules from the registry](#a-note-about-using-modules-from-the-registry)
   - [remote\_state](#remote_state)
+    - [backend](#backend)
+    - [encryption](#encryption)
   - [include](#include)
     - [Single include](#single-include)
     - [Multiple includes](#multiple-includes)
     - [Limitations on accessing exposed config](#limitations-on-accessing-exposed-config)
   - [locals](#locals)
+    - [Complex locals](#complex-locals)
+    - [Computed locals](#computed-locals)
   - [dependency](#dependency)
   - [dependencies](#dependencies)
   - [generate](#generate)
@@ -34,9 +38,15 @@ The following is a reference of all the supported blocks and attributes in the c
   - [feature](#feature)
   - [exclude](#exclude)
   - [errors](#errors)
+    - [Retry Configuration](#retry-configuration)
+    - [Ignore Configuration](#ignore-configuration)
+    - [Combined Example](#combined-example)
+    - [Errors during source fetching](#errors-during-source-fetching)
   - [unit](#unit)
+  - [stack](#stack)
 - [Attributes](#attributes)
   - [inputs](#inputs)
+    - [Variable Precedence](#variable-precedence)
   - [download\_dir](#download_dir)
   - [prevent\_destroy](#prevent_destroy)
   - [skip](#skip)
@@ -44,6 +54,7 @@ The following is a reference of all the supported blocks and attributes in the c
   - [iam\_assume\_role\_duration](#iam_assume_role_duration)
   - [iam\_assume\_role\_session\_name](#iam_assume_role_session_name)
   - [iam\_web\_identity\_token](#iam_web_identity_token)
+    - [Git Provider Configuration](#git-provider-configuration)
   - [terraform\_binary](#terraform_binary)
   - [terraform\_version\_constraint](#terraform_version_constraint)
   - [terragrunt\_version\_constraint](#terragrunt_version_constraint)
@@ -63,6 +74,7 @@ The following is a reference of all the supported blocks and attributes in the c
 - [exclude](#exclude)
 - [errors](#errors)
 - [unit](#unit)
+- [stack](#stack)
 
 ### terraform
 
@@ -1761,7 +1773,9 @@ Terragrunt will recursively generate a stack using the contents of the `.terragr
     - [Retry Configuration](#retry-configuration)
     - [Ignore Configuration](#ignore-configuration)
     - [Combined Example](#combined-example)
+    - [Errors during source fetching](#errors-during-source-fetching)
   - [unit](#unit)
+  - [stack](#stack)
 - [Attributes](#attributes)
   - [inputs](#inputs)
     - [Variable Precedence](#variable-precedence)
diff --git a/docs/_docs/04_reference/06-experiments.md b/docs/_docs/04_reference/06-experiments.md
index 4b3d0090..d32def92 100644
--- a/docs/_docs/04_reference/06-experiments.md
+++ b/docs/_docs/04_reference/06-experiments.md
@@ -118,7 +118,7 @@ To transition the `stacks` feature to a stable release, the following must be ad
 - [ ] Confirm compatibility with parallelism flags (e.g., `--parallel`), especially for stacks with dependencies.
 - [ ] Ensure that error handling and failure recovery strategies work as intended across large and nested stacks.
 
-### cli-redesign
+### `cli-redesign`
 
 Support for the new Terragrunt CLI design.
 
@@ -156,4 +156,39 @@ To transition `cli-redesign` features to a stable release, the following must be
 - [ ] Add support for the `info` command.
 - [ ] Add support for the `dag` command.
 - [ ] Add support for the `find` command.
+  - [x] Add support for `find` without flags.
+  - [x] Add support for `find` with colorful output.
+  - [x] Add support for `find` with `--format=json` flag.
+  - [x] Add support for `find` with stdout redirection detection.
+  - [x] Add support for `find` with `--hidden` flag.
+  - [x] Add support for `find` with `--sort=alpha` flag.
+  - [ ] Add support for `find` with `--sort=dag` flag.
+  - [ ] Add support for `find` with the `exclude` block used to exclude units from the search.
+  - [ ] Add integration with `symlinks` experiment to support finding units/stacks via symlinks.
 - [ ] Add support for the `list` command.
+
+### `cas`
+
+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.
+
+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.
+
+#### `cas` - How to provide feedback
+
+Share your experience with this feature in the [CAS](https://github.com/gruntwork-io/terragrunt/discussions/3939) Feedback GitHub Discussion.
+Feedback is crucial for ensuring the feature meets real-world use cases. Please include:
+
+- Any bugs or issues encountered (including logs or stack traces if possible).
+- Suggestions for additional improvements or enhancements.
+
+#### `cas` - Criteria for stabilization
+
+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.
+- [ ] Add support for storing and retrieving Unit/Stack configurations from the CAS.
diff --git a/docs/assets/img/screenshots/find-no-color.png b/docs/assets/img/screenshots/find-no-color.png
new file mode 100644
index 00000000..5ec4041f
Binary files /dev/null and b/docs/assets/img/screenshots/find-no-color.png differ
diff --git a/docs/assets/img/screenshots/find.png b/docs/assets/img/screenshots/find.png
new file mode 100644
index 00000000..2bbfe10a
Binary files /dev/null and b/docs/assets/img/screenshots/find.png differ
diff --git a/engine/engine.go b/engine/engine.go
index 56c1c411..33d1fc0e 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -16,6 +16,8 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+
 	"github.com/gruntwork-io/terragrunt/internal/cache"
 
 	"github.com/hashicorp/go-getter"
@@ -504,7 +506,11 @@ func createEngine(terragruntOptions *options.TerragruntOptions) (*proto.EngineCl
 
 	engineLogLevel := terragruntOptions.EngineLogLevel
 	if len(engineLogLevel) == 0 {
-		engineLogLevel = terragruntOptions.Logger.Level().String()
+		engineLogLevel = hclog.Warn.String()
+		// update log level if it is different from info
+		if terragruntOptions.Logger.Level() != log.InfoLevel {
+			engineLogLevel = terragruntOptions.Logger.Level().String()
+		}
 		// turn off log formatting if disabled for Terragrunt
 		if terragruntOptions.Logger.Formatter().DisabledOutput() {
 			engineLogLevel = hclog.Off.String()
diff --git a/go.mod b/go.mod
index b210ee1b..de94ba5f 100644
--- a/go.mod
+++ b/go.mod
@@ -83,6 +83,8 @@ require (
 	gopkg.in/ini.v1 v1.67.0
 )
 
+require github.com/aws/aws-sdk-go-v2/service/s3 v1.74.0
+
 require (
 	atomicgo.dev/cursor v0.2.0 // indirect
 	atomicgo.dev/keyboard v0.2.9 // indirect
@@ -144,7 +146,6 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/lambda v1.69.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.74.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sns v1.33.6 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 // indirect
diff --git a/internal/cas/cas.go b/internal/cas/cas.go
new file mode 100644
index 00000000..c5d79204
--- /dev/null
+++ b/internal/cas/cas.go
@@ -0,0 +1,458 @@
+// Package cas implements a content-addressable storage for git content.
+//
+// Blobs are copied from cloned repositories to a local store, along with trees.
+// When the same content is requested again, the content is read from the local store,
+// avoiding the need to clone the repository or read from the network.
+package cas
+
+import (
+	"context"
+	"crypto/sha1"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+)
+
+// Options configures the behavior of CAS
+type Options struct {
+	// StorePath specifies a custom path for the content store
+	// If empty, uses $HOME/.cache/terragrunt/cas/store
+	StorePath string
+}
+
+// CloneOptions configures the behavior of a specific clone operation
+type CloneOptions struct {
+	// Dir specifies the target directory for the clone
+	// If empty, uses the repository name
+	Dir string
+
+	// Branch specifies which branch to clone
+	// If empty, uses HEAD
+	Branch string
+
+	// IncludedGitFiles specifies the files to preserve from the .git directory
+	// If empty, does not preserve any files
+	IncludedGitFiles []string
+}
+
+// CAS clones a git repository using content-addressable storage.
+type CAS struct {
+	store      *Store
+	git        *GitRunner
+	opts       Options
+	cloneStart time.Time
+}
+
+// New creates a new CAS instance with the given options
+//
+// TODO: Make these options optional
+func New(opts Options) (*CAS, error) {
+	if opts.StorePath == "" {
+		home, err := os.UserHomeDir()
+		if err != nil {
+			return nil, err
+		}
+
+		opts.StorePath = filepath.Join(home, ".cache", "terragrunt", "cas", "store")
+	}
+
+	store := NewStore(opts.StorePath)
+
+	return &CAS{
+		store: store,
+		git:   NewGitRunner(),
+		opts:  opts,
+	}, nil
+}
+
+// Clone performs the clone operation
+//
+// TODO: Make options optional
+func (c *CAS) Clone(ctx context.Context, l *log.Logger, opts *CloneOptions, url string) error {
+	c.cloneStart = time.Now()
+
+	targetDir := c.prepareTargetDirectory(opts.Dir, url)
+
+	// Create a temporary directory for git operations
+	tempDir, cleanup, err := c.git.CreateTempDir()
+	if err != nil {
+		return err
+	}
+
+	defer func() {
+		if cleanupErr := cleanup(); cleanupErr != nil {
+			(*l).Warnf("cleanup error: %v", cleanupErr)
+		}
+	}()
+
+	// 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 err
+		}
+	}
+
+	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
+	}
+
+	if err := tree.LinkTree(ctx, c.store, targetDir); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *CAS) prepareTargetDirectory(dir, url string) string {
+	targetDir := dir
+	if targetDir == "" {
+		targetDir = GetRepoName(url)
+	}
+
+	return filepath.Clean(targetDir)
+}
+
+func (c *CAS) resolveReference(ctx context.Context, url, branch string) (string, error) {
+	results, err := c.git.LsRemote(ctx, url, branch)
+	if err != nil {
+		return "", err
+	}
+
+	if len(results) == 0 {
+		return "", &WrappedError{
+			Op:      "clone",
+			Context: "no matching reference",
+			Err:     ErrNoMatchingReference,
+		}
+	}
+
+	return results[0].Hash, nil
+}
+
+func (c *CAS) cloneAndStoreContent(ctx context.Context, l *log.Logger, opts *CloneOptions, url string, hash string) error {
+	if err := c.git.Clone(ctx, url, true, 1, opts.Branch); err != nil {
+		return err
+	}
+
+	return c.storeRootTree(ctx, l, hash, opts)
+}
+
+func (c *CAS) storeRootTree(ctx context.Context, l *log.Logger, hash string, opts *CloneOptions) error {
+	if err := c.storeTree(ctx, l, hash, ""); err != nil {
+		return err
+	}
+
+	if len(opts.IncludedGitFiles) == 0 {
+		return nil
+	}
+
+	content := NewContent(c.store)
+
+	data, err := content.Read(hash)
+	if err != nil {
+		return err
+	}
+
+	for _, file := range opts.IncludedGitFiles {
+		stat, err := os.Stat(filepath.Join(c.git.WorkDir, file))
+		if err != nil {
+			return err
+		}
+
+		if stat.IsDir() {
+			continue
+		}
+
+		workDirPath := filepath.Join(c.git.WorkDir, file)
+
+		hash, err := hashFile(workDirPath)
+		if err != nil {
+			return err
+		}
+
+		content := NewContent(c.store)
+
+		if err := content.EnsureCopy(l, hash, workDirPath); err != nil {
+			return err
+		}
+
+		path := filepath.Join(".git", file)
+
+		data = append(data, []byte(fmt.Sprintf("%06o blob %s\t%s\n", stat.Mode().Perm(), hash, 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) {
+		return nil
+	}
+
+	tree, err := c.git.LsTree(ctx, hash, ".")
+	if err != nil {
+		return err
+	}
+
+	// Optimistically assume half the entries are trees and half are blobs
+	var (
+		trees = make([]TreeEntry, 0, len(tree.Entries())/2) //nolint:mnd
+		blobs = make([]TreeEntry, 0, len(tree.Entries())/2) //nolint:mnd
+	)
+
+	for _, entry := range tree.Entries() {
+		if prefix != "" {
+			entry.Path = filepath.Join(prefix, entry.Path)
+		}
+
+		switch entry.Type {
+		case "blob":
+			blobs = append(blobs, entry)
+		case "tree":
+			trees = append(trees, entry)
+		}
+	}
+
+	ch := make(chan error, 2) //nolint:mnd
+
+	var wg sync.WaitGroup
+
+	wg.Add(1)
+
+	go func() {
+		defer wg.Done()
+
+		if err := c.storeBlobs(ctx, blobs); err != nil {
+			ch <- err
+
+			return
+		}
+
+		ch <- nil
+	}()
+
+	wg.Add(1)
+
+	go func() {
+		defer wg.Done()
+
+		if err := c.storeTrees(ctx, l, trees, prefix); err != nil {
+			ch <- err
+
+			return
+		}
+
+		ch <- nil
+	}()
+
+	wg.Wait()
+
+	close(ch)
+
+	errs := []error{}
+
+	for err := range ch {
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+
+	content := NewContent(c.store)
+
+	if err := content.Ensure(l, hash, tree.Data()); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// storeBlobs concurrently 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) {
+			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 len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+
+	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)
+
+	errs := []error{}
+
+	for err := range ch {
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+
+	return nil
+}
+
+// ensureBlob ensures that a blob exists in the CAS.
+// 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{}
+	}
+
+	c.store.locks[hash].Lock()
+	defer c.store.locks[hash].Unlock()
+
+	c.store.mapLock.Unlock()
+
+	if !c.store.NeedsWrite(hash, c.cloneStart) {
+		return nil
+	}
+
+	content := NewContent(c.store)
+
+	tmpHandle, err := content.GetTmpHandle(hash)
+	if err != nil {
+		return err
+	}
+
+	tmpPath := tmpHandle.Name()
+
+	// We want to make sure we remove the temporary file
+	// if we encounter an error
+	defer func() {
+		if _, osStatErr := os.Stat(tmpPath); osStatErr == nil {
+			err = errors.Join(err, os.Remove(tmpPath))
+		}
+	}()
+
+	err = c.git.CatFile(ctx, hash, tmpHandle)
+	if err != nil {
+		return err
+	}
+
+	if err = tmpHandle.Close(); err != nil {
+		return err
+	}
+
+	if err = os.Rename(tmpPath, content.getPath(hash)); err != nil {
+		return err
+	}
+
+	if err = os.Chmod(content.getPath(hash), StoredFilePerms); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func hashFile(path string) (string, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+
+	defer file.Close()
+
+	h := sha1.New()
+
+	if _, err := io.Copy(h, file); err != nil {
+		return "", err
+	}
+
+	return hex.EncodeToString(h.Sum(nil)), nil
+}
diff --git a/internal/cas/content.go b/internal/cas/content.go
new file mode 100644
index 00000000..44e00d7d
--- /dev/null
+++ b/internal/cas/content.go
@@ -0,0 +1,275 @@
+package cas
+
+import (
+	"bufio"
+	"io"
+	"os"
+	"path/filepath"
+	"sync"
+
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+)
+
+// Content manages git object storage and linking
+type Content struct {
+	store *Store
+}
+
+const (
+	// DefaultDirPerms represents standard directory permissions (rwxr-xr-x)
+	DefaultDirPerms = os.FileMode(0755)
+	// StoredFilePerms represents read-only file permissions (r--r--r--)
+	StoredFilePerms = os.FileMode(0444)
+	// RegularFilePerms represents standard file permissions (rw-r--r--)
+	RegularFilePerms = os.FileMode(0644)
+)
+
+// NewContent creates a new Content instance
+func NewContent(store *Store) *Content {
+	return &Content{
+		store: store,
+	}
+}
+
+// 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)
+
+	// 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,
+		}
+	}
+
+	// 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)
+
+		if readErr != nil {
+			return &WrappedError{
+				Op:   "read_source",
+				Path: sourcePath,
+				Err:  ErrReadFile,
+			}
+		}
+
+		// 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,
+			}
+		}
+
+		// Rename to final path
+		if err := os.Rename(tempPath, targetPath); err != nil {
+			return &WrappedError{
+				Op:   "rename_target",
+				Path: tempPath,
+				Err:  err,
+			}
+		}
+	}
+
+	return nil
+}
+
+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,
+		}
+	}
+
+	return nil
+}
+
+// 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{}
+	}
+
+	c.store.locks[hash].Lock()
+	defer c.store.locks[hash].Unlock()
+
+	c.store.mapLock.Unlock()
+
+	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 {
+		return wrapError("create_partition_dir", partitionDir, ErrCreateDir)
+	}
+
+	path := c.getPath(hash)
+	tempPath := path + ".tmp"
+
+	f, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, RegularFilePerms)
+	if err != nil {
+		return wrapError("create_temp_file", tempPath, err)
+	}
+
+	buf := bufio.NewWriter(f)
+
+	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)
+		}
+
+		return wrapError("write_to_store", tempPath, err)
+	}
+
+	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)
+		}
+
+		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)
+		}
+
+		return wrapError("close_file", tempPath, err)
+	}
+
+	// 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)
+		}
+
+		return wrapError("chmod_temp_file", tempPath, err)
+	}
+
+	// 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)
+		}
+
+		return wrapError("finalize_store", path, err)
+	}
+
+	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)
+	if c.store.hasContent(path) {
+		return nil
+	}
+
+	c.store.mapLock.Lock()
+
+	if _, ok := c.store.locks[hash]; !ok {
+		c.store.locks[hash] = &sync.Mutex{}
+	}
+
+	c.store.locks[hash].Lock()
+	defer c.store.locks[hash].Unlock()
+
+	c.store.mapLock.Unlock()
+
+	// Ensure partition directory exists
+	partitionDir := c.getPartition(hash)
+	if err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+		return wrapError("create_partition_dir", partitionDir, ErrCreateDir)
+	}
+
+	f, err := os.Create(path)
+	if err != nil {
+		return wrapError("create_file", path, err)
+	}
+
+	defer f.Close()
+
+	r, err := os.Open(src)
+	if err != nil {
+		return wrapError("open_source", src, err)
+	}
+
+	defer r.Close()
+
+	if _, err := io.Copy(f, r); err != nil {
+		return wrapError("copy_file", src, err)
+	}
+
+	return nil
+}
+
+// GetTmpHandle returns a file handle to a temporary file where content will be stored.
+func (c *Content) GetTmpHandle(hash string) (*os.File, error) {
+	partitionDir := c.getPartition(hash)
+	if err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {
+		return nil, wrapError("create_partition_dir", partitionDir, ErrCreateDir)
+	}
+
+	path := c.getPath(hash)
+	tempPath := path + ".tmp"
+
+	f, err := os.Create(tempPath)
+	if err != nil {
+		return nil, wrapError("create_temp_file", tempPath, err)
+	}
+
+	return f, err
+}
+
+// Read retrieves content from the store by hash
+func (c *Content) Read(hash string) ([]byte, error) {
+	path := c.getPath(hash)
+	return os.ReadFile(path)
+}
+
+// getPartition returns the partition path for a given hash
+func (c *Content) getPartition(hash string) string {
+	return filepath.Join(c.store.Path(), hash[:2])
+}
+
+// getPath returns the full path for a given hash
+func (c *Content) getPath(hash string) string {
+	return filepath.Join(c.getPartition(hash), hash)
+}
diff --git a/internal/cas/errors.go b/internal/cas/errors.go
new file mode 100644
index 00000000..203ef97e
--- /dev/null
+++ b/internal/cas/errors.go
@@ -0,0 +1,75 @@
+package cas
+
+import (
+	"fmt"
+
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+)
+
+// Error types that can be returned by the cas package
+type Error string
+
+func (e Error) Error() string {
+	return string(e)
+}
+
+const (
+	// ErrTempDir is returned when failing to create or close a temporary directory
+	ErrTempDir Error = "failed to create or manage temporary directory"
+	// ErrCreateDir is returned when failing to create a directory
+	ErrCreateDir Error = "failed to create directory"
+	// ErrReadFile is returned when failing to read a file
+	ErrReadFile Error = "failed to read file"
+	// ErrParseTree is returned when failing to parse git tree output
+	ErrParseTree Error = "failed to parse git tree output"
+	// ErrGitClone is returned when the git clone operation fails
+	ErrGitClone Error = "failed to complete git clone"
+	// ErrCreateTempDir is returned when failing to create a temporary directory
+	ErrCreateTempDir Error = "failed to create temporary directory"
+	// ErrCleanupTempDir is returned when failing to clean up a temporary directory
+	ErrCleanupTempDir Error = "failed to clean up temporary directory"
+)
+
+// WrappedError provides additional context for errors
+type WrappedError struct {
+	Op      string // Operation that failed
+	Path    string // File path if applicable
+	Err     error  // Original error
+	Context string // Additional context
+}
+
+func (e *WrappedError) Error() string {
+	if e.Context != "" {
+		return fmt.Sprintf("%s: %s: %v", e.Op, e.Context, e.Err)
+	}
+
+	return fmt.Sprintf("%s: %v", e.Op, e.Err)
+}
+
+func (e *WrappedError) Unwrap() error {
+	return e.Err
+}
+
+// Git operation errors
+var (
+	ErrCommandSpawn        = errors.New("failed to spawn git command")
+	ErrNoMatchingReference = errors.New("no matching reference")
+	ErrReadTree            = errors.New("failed to read tree")
+	ErrNoWorkDir           = errors.New("working directory not set")
+)
+
+func wrapError(op, path string, err error) error {
+	return &WrappedError{
+		Op:   op,
+		Path: path,
+		Err:  err,
+	}
+}
+
+func wrapErrorWithContext(op, context string, err error) error {
+	return &WrappedError{
+		Op:      op,
+		Context: context,
+		Err:     err,
+	}
+}
diff --git a/internal/cas/getter.go b/internal/cas/getter.go
new file mode 100644
index 00000000..765057fa
--- /dev/null
+++ b/internal/cas/getter.go
@@ -0,0 +1,99 @@
+package cas
+
+import (
+	"context"
+	"net/url"
+	"strings"
+
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/hashicorp/go-getter/v2"
+)
+
+// Assert that CASGetter implements the Getter interface
+var _ getter.Getter = &CASGetter{}
+
+// CASGetter is a go-getter Getter implementation.
+type CASGetter struct {
+	Detectors []getter.Detector
+	CAS       *CAS
+	Logger    *log.Logger
+	Opts      *CloneOptions
+}
+
+func NewCASGetter(l *log.Logger, cas *CAS, opts *CloneOptions) *CASGetter {
+	return &CASGetter{
+		Detectors: []getter.Detector{
+			new(getter.GitHubDetector),
+			new(getter.GitDetector),
+			new(getter.BitBucketDetector),
+			new(getter.GitLabDetector),
+		},
+		CAS:    cas,
+		Logger: l,
+		Opts:   opts,
+	}
+}
+
+func (g *CASGetter) Get(ctx context.Context, req *getter.Request) error {
+	ref := ""
+
+	url := req.URL()
+
+	q := url.Query()
+	if len(q) > 0 {
+		ref = q.Get("ref")
+		q.Del("ref")
+
+		url.RawQuery = q.Encode()
+	}
+
+	opts := g.Opts
+	opts.Branch = ref
+	opts.Dir = req.Dst
+
+	urlStr := url.String()
+	urlStr = strings.TrimPrefix(urlStr, "git::")
+
+	// We have to switch back to the original URL scheme to clone the repository
+	// go-getter sets the URL like this:
+	// git::ssh://git@github.com/gruntwork-io/terragrunt.git
+	// We need to switch to a valid Git URL to clone the repository
+	// Like this:
+	// git@github.com:gruntwork-io/terragrunt.git
+	if strings.HasPrefix(urlStr, "ssh://") {
+		urlStr = strings.TrimPrefix(urlStr, "ssh://")
+		// Replace the first slash with a colon
+		urlStr = strings.Replace(urlStr, "/", ":", 1)
+	}
+
+	return g.CAS.Clone(ctx, g.Logger, opts, urlStr)
+}
+
+func (g *CASGetter) GetFile(_ context.Context, req *getter.Request) error {
+	return errors.New("GetFile not implemented")
+}
+
+func (g *CASGetter) Mode(_ context.Context, url *url.URL) (getter.Mode, error) {
+	return getter.ModeDir, nil
+}
+
+func (g *CASGetter) Detect(req *getter.Request) (bool, error) {
+	if req.Forced == "git" {
+		return true, nil
+	}
+
+	for _, detector := range g.Detectors {
+		src, ok, err := detector.Detect(req.Src, req.Pwd)
+		if err != nil {
+			return ok, err
+		}
+
+		if ok {
+			req.Src = src
+			return ok, nil
+		}
+	}
+
+	return false, nil
+}
diff --git a/internal/cas/git.go b/internal/cas/git.go
new file mode 100644
index 00000000..8f653c2d
--- /dev/null
+++ b/internal/cas/git.go
@@ -0,0 +1,237 @@
+package cas
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+const (
+	minGitPartsLength = 2
+)
+
+// GitRunner handles git command execution
+type GitRunner struct {
+	WorkDir string
+}
+
+// NewGitRunner creates a new GitRunner instance
+func NewGitRunner() *GitRunner {
+	return &GitRunner{}
+}
+
+// WithWorkDir returns a new GitRunner with the specified working directory
+func (g *GitRunner) WithWorkDir(workDir string) *GitRunner {
+	// Create new instance instead of modifying existing one
+	return &GitRunner{
+		WorkDir: workDir,
+	}
+}
+
+// RequiresWorkDir returns an error if no working directory is set
+func (g *GitRunner) RequiresWorkDir() error {
+	if g.WorkDir == "" {
+		return &WrappedError{
+			Op:      "git",
+			Context: "no working directory set",
+			Err:     ErrNoWorkDir,
+		}
+	}
+
+	return nil
+}
+
+// LsRemoteResult represents the output of git ls-remote
+type LsRemoteResult struct {
+	Hash string
+	Ref  string
+}
+
+// LsRemote runs git ls-remote for a specific reference.
+// If ref is empty, we check HEAD instead.
+func (g *GitRunner) LsRemote(ctx context.Context, repo, ref string) ([]LsRemoteResult, error) {
+	if ref == "" {
+		ref = "HEAD"
+	}
+
+	args := []string{repo, ref}
+
+	cmd := g.prepareCommand(ctx, "ls-remote", args...)
+
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		return nil, &WrappedError{
+			Op:      "git_ls_remote",
+			Context: stderr.String(),
+			Err:     ErrCommandSpawn,
+		}
+	}
+
+	var results []LsRemoteResult
+
+	lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
+
+	for _, line := range lines {
+		if line == "" {
+			continue
+		}
+
+		parts := strings.Fields(line)
+		if len(parts) >= minGitPartsLength {
+			results = append(results, LsRemoteResult{
+				Hash: parts[0],
+				Ref:  parts[1],
+			})
+		}
+	}
+
+	if len(results) == 0 {
+		return nil, &WrappedError{
+			Op:      "git_ls_remote",
+			Context: "no matching references",
+			Err:     ErrNoMatchingReference,
+		}
+	}
+
+	return results, nil
+}
+
+// Clone performs a git clone operation
+func (g *GitRunner) Clone(ctx context.Context, repo string, bare bool, depth int, branch string) error {
+	if err := g.RequiresWorkDir(); err != nil {
+		return err
+	}
+
+	args := []string{}
+
+	if bare {
+		args = append(args, "--bare")
+	}
+
+	if depth > 0 {
+		args = append(args, "--depth", "1", "--single-branch")
+	}
+
+	if branch != "" {
+		args = append(args, "--branch", branch)
+	}
+
+	args = append(args, repo, g.WorkDir)
+
+	cmd := g.prepareCommand(ctx, "clone", args...)
+
+	var stderr bytes.Buffer
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		return &WrappedError{
+			Op:      "git_clone",
+			Context: stderr.String(),
+			Err:     ErrGitClone,
+		}
+	}
+
+	return nil
+}
+
+// CreateTempDir creates a new temporary directory for git operations
+func (g *GitRunner) CreateTempDir() (string, func() error, error) {
+	tempDir, err := os.MkdirTemp("", "terragrunt-cas-*")
+	if err != nil {
+		return "", nil, &WrappedError{
+			Op:      "create_temp_dir",
+			Context: err.Error(),
+			Err:     ErrCreateTempDir,
+		}
+	}
+
+	g.SetWorkDir(tempDir)
+
+	cleanup := func() error {
+		if err := os.RemoveAll(tempDir); err != nil {
+			return &WrappedError{
+				Op:      "cleanup_temp_dir",
+				Context: err.Error(),
+				Err:     ErrCleanupTempDir,
+			}
+		}
+
+		return nil
+	}
+
+	return tempDir, cleanup, nil
+}
+
+// GetRepoName extracts the repository name from a git URL
+func GetRepoName(repo string) string {
+	name := filepath.Base(repo)
+	return strings.TrimSuffix(name, ".git")
+}
+
+// LsTree runs git ls-tree and returns the parsed tree
+func (g *GitRunner) LsTree(ctx context.Context, reference, path string) (*Tree, error) {
+	if err := g.RequiresWorkDir(); err != nil {
+		return nil, err
+	}
+
+	cmd := g.prepareCommand(ctx, "ls-tree", reference)
+	cmd.Dir = g.WorkDir
+
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		return nil, &WrappedError{
+			Op:      "git_ls_tree",
+			Context: stderr.String(),
+			Err:     ErrReadTree,
+		}
+	}
+
+	return ParseTree(stdout.String(), path)
+}
+
+// CatFile writes the contents of a git object
+// to a given writer.
+func (g *GitRunner) CatFile(ctx context.Context, hash string, out io.Writer) error {
+	if err := g.RequiresWorkDir(); err != nil {
+		return err
+	}
+
+	var stderr bytes.Buffer
+
+	cmd := g.prepareCommand(ctx, "cat-file", "-p", hash)
+	cmd.Dir = g.WorkDir
+	cmd.Stdout = out
+	cmd.Stderr = &stderr
+
+	err := cmd.Run()
+	if err != nil {
+		context := stderr.String()
+
+		return &WrappedError{
+			Op:      "git_cat_file",
+			Context: context,
+			Err:     ErrCommandSpawn,
+		}
+	}
+
+	return nil
+}
+
+// SetWorkDir sets the working directory for git commands
+func (g *GitRunner) SetWorkDir(dir string) {
+	g.WorkDir = dir
+}
+
+func (g *GitRunner) prepareCommand(ctx context.Context, name string, args ...string) *exec.Cmd {
+	return exec.CommandContext(ctx, "git", append([]string{name}, args...)...)
+}
diff --git a/internal/cas/store.go b/internal/cas/store.go
new file mode 100644
index 00000000..544877fb
--- /dev/null
+++ b/internal/cas/store.go
@@ -0,0 +1,60 @@
+package cas
+
+import (
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+)
+
+// Store manages the store directory and locks to prevent concurrent writes
+type Store struct {
+	path    string
+	locks   map[string]*sync.Mutex
+	mapLock sync.Mutex
+}
+
+// NewStore creates a new Store instance.
+func NewStore(path string) *Store {
+	return &Store{
+		path:  path,
+		locks: make(map[string]*sync.Mutex),
+	}
+}
+
+// Path returns the current store path
+func (s *Store) Path() string {
+	return s.path
+}
+
+// NeedsWrite checks if a given hash needs to be stored
+func (s *Store) NeedsWrite(hash string, cloneStart time.Time) bool {
+	partitionDir := filepath.Join(s.path, hash[:2])
+	path := filepath.Join(partitionDir, hash)
+
+	return !s.hasContent(path) && !s.writeInProgress(path, cloneStart)
+}
+
+// HasContent checks if a given hash exists in the store
+func (s *Store) hasContent(path string) bool {
+	_, err := os.Stat(path)
+
+	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"
+
+	stat, err := os.Stat(path)
+	if err != nil && os.IsNotExist(err) {
+		return false
+	}
+
+	// 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 modifiedTime.After(cloneStart)
+}
diff --git a/internal/cas/tree.go b/internal/cas/tree.go
new file mode 100644
index 00000000..4f5ecc4a
--- /dev/null
+++ b/internal/cas/tree.go
@@ -0,0 +1,197 @@
+package cas
+
+import (
+	"bufio"
+	"context"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+)
+
+const (
+	minTreePartsLength = 4
+	maxConcurrentLinks = 4
+)
+
+// TreeEntry represents a single entry in a git tree
+type TreeEntry struct {
+	Mode string
+	Type string
+	Hash string
+	Path string
+}
+
+// Tree represents a git tree object with its entries
+type Tree struct {
+	entries []TreeEntry
+	path    string
+	data    []byte
+}
+
+// Entries returns the tree entries
+func (t *Tree) Entries() []TreeEntry {
+	return t.entries
+}
+
+// Path returns the tree path
+func (t *Tree) Path() string {
+	return t.path
+}
+
+// Data returns the tree data
+func (t *Tree) Data() []byte {
+	return t.data
+}
+
+// ParseTreeEntry parses a single line from git ls-tree output
+func ParseTreeEntry(line string) (TreeEntry, error) {
+	// Format: <mode> <type> <hash> <path>
+	parts := strings.Fields(line)
+	if len(parts) < minTreePartsLength {
+		return TreeEntry{}, &WrappedError{
+			Op:      "parse_tree_entry",
+			Context: "invalid tree entry format",
+			Err:     ErrParseTree,
+		}
+	}
+
+	return TreeEntry{
+		Mode: parts[0],
+		Type: parts[1],
+		Hash: parts[2],
+		Path: strings.Join(parts[3:], " "), // Handle paths with spaces
+	}, nil
+}
+
+// ParseTree parses the complete output of git ls-tree
+func ParseTree(output, path string) (*Tree, error) {
+	// Pre-allocate capacity based on newline count
+	capacity := strings.Count(output, "\n") + 1
+	entries := make([]TreeEntry, 0, capacity)
+
+	// Use a scanner for more efficient line reading
+	scanner := bufio.NewScanner(strings.NewReader(output))
+	for scanner.Scan() {
+		line := scanner.Text()
+		if line == "" {
+			continue
+		}
+
+		entry, err := ParseTreeEntry(line)
+		if err != nil {
+			return nil, err
+		}
+
+		entries = append(entries, entry)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, wrapErrorWithContext("scan_tree", "failed to read tree output", err)
+	}
+
+	return &Tree{
+		entries: entries,
+		path:    path,
+		data:    []byte(output),
+	}, nil
+}
+
+// LinkTree writes the tree to a target directory
+func (t *Tree) LinkTree(ctx context.Context, store *Store, targetDir string) error {
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	var (
+		errMu sync.Mutex
+		errs  []error
+	)
+
+	var wg sync.WaitGroup
+
+	semaphore := make(chan struct{}, maxConcurrentLinks)
+
+	for _, entry := range t.entries {
+		wg.Add(1)
+
+		go func(entry TreeEntry) {
+			defer wg.Done()
+
+			select {
+			case <-ctx.Done():
+				return
+			default:
+			}
+
+			select {
+			case semaphore <- struct{}{}:
+				defer func() { <-semaphore }()
+			case <-ctx.Done():
+				return
+			}
+
+			entryPath := filepath.Join(targetDir, entry.Path)
+			if err := os.MkdirAll(filepath.Dir(entryPath), DefaultDirPerms); err != nil {
+				errMu.Lock()
+				errs = append(errs, wrapError("mkdir_all", entryPath, err))
+				errMu.Unlock()
+				cancel()
+
+				return
+			}
+
+			content := NewContent(store)
+
+			switch entry.Type {
+			case "blob":
+				if err := content.Link(entry.Hash, entryPath); err != nil {
+					errMu.Lock()
+					errs = append(errs, wrapError("link_blob", entryPath, err))
+					errMu.Unlock()
+					cancel()
+
+					return
+				}
+			case "tree":
+				treeData, err := content.Read(entry.Hash)
+				if err != nil {
+					errMu.Lock()
+					errs = append(errs, wrapError("read_tree", entry.Hash, err))
+					errMu.Unlock()
+					cancel()
+
+					return
+				}
+
+				subTree, err := ParseTree(string(treeData), entryPath)
+				if err != nil {
+					errMu.Lock()
+					errs = append(errs, wrapError("parse_tree", entry.Hash, err))
+					errMu.Unlock()
+					cancel()
+
+					return
+				}
+
+				if err := subTree.LinkTree(ctx, store, entryPath); err != nil {
+					errMu.Lock()
+					errs = append(errs, wrapError("link_subtree", entryPath, err))
+					errMu.Unlock()
+					cancel()
+
+					return
+				}
+			}
+		}(entry)
+	}
+
+	wg.Wait()
+
+	if len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+
+	return nil
+}
diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go
new file mode 100644
index 00000000..91d46e61
--- /dev/null
+++ b/internal/discovery/discovery.go
@@ -0,0 +1,177 @@
+// Package discovery provides functionality for discovering Terragrunt configurations.
+package discovery
+
+import (
+	"io/fs"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/gruntwork-io/terragrunt/config"
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+)
+
+const (
+	ConfigTypeUnit  ConfigType = "unit"
+	ConfigTypeStack ConfigType = "stack"
+)
+
+// ConfigType is the type of Terragrunt configuration.
+type ConfigType string
+
+// DiscoveredConfig represents a discovered Terragrunt configuration.
+type DiscoveredConfig struct {
+	Type ConfigType `json:"type"`
+	Path string     `json:"path"`
+}
+
+// DiscoveredConfigs is a list of discovered Terragrunt configurations.
+type DiscoveredConfigs []*DiscoveredConfig
+
+// Discovery is the configuration for a Terragrunt discovery.
+type Discovery struct {
+	// WorkingDir is the directory to search for Terragrunt configurations.
+	WorkingDir string
+
+	// Hidden determines whether to detect configurations in hidden directories.
+	Hidden bool
+}
+
+// DiscoveryOption is a function that modifies a Discovery.
+type DiscoveryOption func(*Discovery)
+
+// NewDiscovery creates a new Discovery.
+func NewDiscovery(dir string, opts ...DiscoveryOption) *Discovery {
+	discovery := &Discovery{
+		WorkingDir: dir,
+		Hidden:     false,
+	}
+
+	for _, opt := range opts {
+		opt(discovery)
+	}
+
+	return discovery
+}
+
+// NewDiscoverySettings creates a new Discovery with default settings.
+func NewDiscoverySettings() *Discovery {
+	return &Discovery{
+		Hidden: false,
+	}
+}
+
+// WithHidden sets the Hidden flag to true.
+func (d *Discovery) WithHidden() *Discovery {
+	d.Hidden = true
+
+	return d
+}
+
+// String returns a string representation of a DiscoveredConfig.
+func (c *DiscoveredConfig) String() string {
+	return string(c.Type) + ": " + c.Path
+}
+
+// Discover discovers Terragrunt configurations in the WorkingDir.
+func (d *Discovery) Discover() (DiscoveredConfigs, error) {
+	var units DiscoveredConfigs
+
+	walkFn := func(path string, e fs.DirEntry, err error) error {
+		if err != nil {
+			return errors.New(err)
+		}
+
+		if e.IsDir() {
+			return nil
+		}
+
+		path, err = filepath.Rel(d.WorkingDir, path)
+		if err != nil {
+			return errors.New(err)
+		}
+
+		if !d.Hidden && isInHiddenDirectory(path) {
+			return nil
+		}
+
+		switch filepath.Base(path) {
+		case config.DefaultTerragruntConfigPath:
+			units = append(units, &DiscoveredConfig{
+				Type: ConfigTypeUnit,
+				Path: filepath.Dir(path),
+			})
+		case config.DefaultStackFile:
+			units = append(units, &DiscoveredConfig{
+				Type: ConfigTypeStack,
+				Path: filepath.Dir(path),
+			})
+		}
+
+		return nil
+	}
+
+	if err := filepath.WalkDir(d.WorkingDir, walkFn); err != nil {
+		return nil, errors.New(err)
+	}
+
+	return units, nil
+}
+
+// Sort sorts the DiscoveredConfigs by path.
+func (c DiscoveredConfigs) Sort() DiscoveredConfigs {
+	sort.Slice(c, func(i, j int) bool {
+		return c[i].Path < c[j].Path
+	})
+
+	return c
+}
+
+// Filter filters the DiscoveredConfigs by config type.
+func (c DiscoveredConfigs) Filter(configType ConfigType) DiscoveredConfigs {
+	filtered := make(DiscoveredConfigs, 0, len(c))
+
+	for _, config := range c {
+		if config.Type == configType {
+			filtered = append(filtered, config)
+		}
+	}
+
+	return filtered
+}
+
+// FilterByPath filters the DiscoveredConfigs by path.
+func (c DiscoveredConfigs) FilterByPath(path string) DiscoveredConfigs {
+	filtered := make(DiscoveredConfigs, 0, len(c))
+
+	for _, config := range c {
+		if config.Path == path {
+			filtered = append(filtered, config)
+		}
+	}
+
+	return filtered
+}
+
+// Paths returns the paths of the DiscoveredConfigs.
+func (c DiscoveredConfigs) Paths() []string {
+	paths := make([]string, 0, len(c))
+	for _, config := range c {
+		paths = append(paths, config.Path)
+	}
+
+	return paths
+}
+
+// isInHiddenDirectory returns true if the path is in a hidden directory.
+func isInHiddenDirectory(path string) bool {
+	parts := strings.Split(path, string(os.PathSeparator))
+	for _, part := range parts {
+		if strings.HasPrefix(part, ".") {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/internal/experiment/experiment.go b/internal/experiment/experiment.go
index 0a0ac56e..fddd6e13 100644
--- a/internal/experiment/experiment.go
+++ b/internal/experiment/experiment.go
@@ -20,6 +20,9 @@ const (
 	CLIRedesign = "cli-redesign"
 	// Stacks is the experiment that allows stacks to be used in Terragrunt.
 	Stacks = "stacks"
+	// CAS is the experiment that enables using the CAS package for git operations
+	// in the catalog command, which provides better performance through content-addressable storage.
+	CAS = "cas"
 )
 
 const (
@@ -45,6 +48,9 @@ func NewExperiments() Experiments {
 		{
 			Name: Stacks,
 		},
+		{
+			Name: CAS,
+		},
 	}
 }
 
diff --git a/pkg/log/logger.go b/pkg/log/logger.go
index ac9f3bff..ce4ce985 100644
--- a/pkg/log/logger.go
+++ b/pkg/log/logger.go
@@ -26,7 +26,7 @@ type Logger interface {
 	// SetFormatter sets the logger formatter.
 	SetFormatter(formatter Formatter)
 
-	// SetFormatter returns the logger formatter.
+	// Formatter returns the logger formatter.
 	Formatter() Formatter
 
 	// WithOptions clones and sets the given options for the new instance.



Test patch:
diff --git a/cli/app_test.go b/cli/app_test.go
index 3d9f167b..d31cb2f5 100644
--- a/cli/app_test.go
+++ b/cli/app_test.go
@@ -533,8 +533,6 @@ func (err argMissingValueError) Error() string {
 }
 
 func TestAutocomplete(t *testing.T) { //nolint:paralleltest
-	defer os.Unsetenv("COMP_LINE")
-
 	testCases := []struct {
 		compLine          string
 		expectedCompletes []string
@@ -558,7 +556,7 @@ func TestAutocomplete(t *testing.T) { //nolint:paralleltest
 	}
 
 	for _, testCase := range testCases {
-		os.Setenv("COMP_LINE", "terragrunt "+testCase.compLine)
+		t.Setenv("COMP_LINE", "terragrunt "+testCase.compLine)
 
 		output := &bytes.Buffer{}
 		opts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)
diff --git a/cli/commands/catalog/module/repo_test.go b/cli/commands/catalog/module/repo_test.go
index 86ce5cd8..472d5737 100644
--- a/cli/commands/catalog/module/repo_test.go
+++ b/cli/commands/catalog/module/repo_test.go
@@ -64,7 +64,7 @@ func TestFindModules(t *testing.T) {
 
 			ctx := context.Background()
 
-			repo, err := module.NewRepo(ctx, log.New(), testCase.repoPath, "", false)
+			repo, err := module.NewRepo(ctx, log.New(), testCase.repoPath, "", false, false)
 			require.NoError(t, err)
 
 			modules, err := repo.FindModules(ctx)
diff --git a/cli/commands/find/action_test.go b/cli/commands/find/action_test.go
new file mode 100644
index 00000000..4eed47c7
--- /dev/null
+++ b/cli/commands/find/action_test.go
@@ -0,0 +1,294 @@
+package find_test
+
+import (
+	"context"
+	"encoding/json"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/cli/commands/find"
+	"github.com/gruntwork-io/terragrunt/internal/discovery"
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRun(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name          string
+		setup         func(t *testing.T) string
+		expectedPaths []string
+		format        string
+		hidden        bool
+		validate      func(t *testing.T, output string, expectedPaths []string)
+	}{
+		{
+			name: "basic discovery",
+			setup: func(t *testing.T) string {
+				t.Helper()
+
+				tmpDir := t.TempDir()
+
+				// Create test directory structure
+				testDirs := []string{
+					"unit1",
+					"unit2",
+					"stack1",
+					".hidden/unit3",
+					"nested/unit4",
+				}
+
+				for _, dir := range testDirs {
+					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
+					require.NoError(t, err)
+				}
+
+				// Create test files
+				testFiles := map[string]string{
+					"unit1/terragrunt.hcl":         "",
+					"unit2/terragrunt.hcl":         "",
+					"stack1/terragrunt.stack.hcl":  "",
+					".hidden/unit3/terragrunt.hcl": "",
+					"nested/unit4/terragrunt.hcl":  "",
+				}
+
+				for path, content := range testFiles {
+					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
+					require.NoError(t, err)
+				}
+
+				return tmpDir
+			},
+			expectedPaths: []string{"unit1", "unit2", "nested/unit4", "stack1"},
+			format:        "text",
+			validate: func(t *testing.T, output string, expectedPaths []string) {
+				t.Helper()
+
+				// Split output into lines and trim whitespace
+				lines := strings.Split(strings.TrimSpace(output), "\n")
+
+				// Verify we have the expected number of lines
+				assert.Len(t, lines, len(expectedPaths))
+
+				// Verify each line is a clean path without any formatting
+				for _, line := range lines {
+					line = strings.TrimSpace(line)
+					assert.NotEmpty(t, line)
+					assert.NotContains(t, line, "\n")
+					assert.NotContains(t, line, "\t")
+				}
+
+				// Verify all expected paths are present
+				assert.ElementsMatch(t, expectedPaths, lines)
+			},
+		},
+		{
+			name: "json output format",
+			setup: func(t *testing.T) string {
+				t.Helper()
+
+				tmpDir := t.TempDir()
+
+				// Create test directory structure
+				testDirs := []string{
+					"unit1",
+					"unit2",
+					"stack1",
+				}
+
+				for _, dir := range testDirs {
+					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
+					require.NoError(t, err)
+				}
+
+				// Create test files
+				testFiles := map[string]string{
+					"unit1/terragrunt.hcl":        "",
+					"unit2/terragrunt.hcl":        "",
+					"stack1/terragrunt.stack.hcl": "",
+				}
+
+				for path, content := range testFiles {
+					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
+					require.NoError(t, err)
+				}
+
+				return tmpDir
+			},
+			expectedPaths: []string{"unit1", "unit2", "stack1"},
+			format:        "json",
+			validate: func(t *testing.T, output string, expectedPaths []string) {
+				t.Helper()
+
+				// Verify the output is valid JSON
+				var configs []discovery.DiscoveredConfig
+				err := json.Unmarshal([]byte(output), &configs)
+				require.NoError(t, err)
+
+				// Verify we have the expected number of configs
+				assert.Len(t, configs, len(expectedPaths))
+
+				// Extract paths from configs
+				var paths []string
+				for _, config := range configs {
+					paths = append(paths, config.Path)
+				}
+
+				// Verify all expected paths are present
+				assert.ElementsMatch(t, expectedPaths, paths)
+
+				// Verify each config has a valid type
+				for _, config := range configs {
+					assert.NotEmpty(t, config.Type)
+					assert.True(t, config.Type == discovery.ConfigTypeUnit || config.Type == discovery.ConfigTypeStack)
+				}
+			},
+		},
+		{
+			name: "hidden discovery",
+			setup: func(t *testing.T) string {
+				t.Helper()
+
+				tmpDir := t.TempDir()
+
+				// Create test directory structure
+				testDirs := []string{
+					"unit1",
+					"unit2",
+					"stack1",
+					".hidden/unit3",
+					"nested/unit4",
+				}
+
+				for _, dir := range testDirs {
+					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
+					require.NoError(t, err)
+				}
+
+				// Create test files
+				testFiles := map[string]string{
+					"unit1/terragrunt.hcl":         "",
+					"unit2/terragrunt.hcl":         "",
+					"stack1/terragrunt.stack.hcl":  "",
+					".hidden/unit3/terragrunt.hcl": "",
+					"nested/unit4/terragrunt.hcl":  "",
+				}
+
+				for path, content := range testFiles {
+					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
+					require.NoError(t, err)
+				}
+
+				return tmpDir
+			},
+			expectedPaths: []string{"unit1", "unit2", "nested/unit4", "stack1", ".hidden/unit3"},
+			format:        "text",
+			hidden:        true,
+			validate: func(t *testing.T, output string, expectedPaths []string) {
+				t.Helper()
+
+				// Split output into lines and trim whitespace
+				lines := strings.Split(strings.TrimSpace(output), "\n")
+
+				// Verify we have the expected number of lines
+				assert.Len(t, lines, len(expectedPaths))
+
+				// Verify all expected paths are present
+				assert.ElementsMatch(t, expectedPaths, lines)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt // capture range variable
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			// Setup test directory
+			tmpDir := tt.setup(t)
+
+			tgOpts := options.NewTerragruntOptions()
+			tgOpts.WorkingDir = tmpDir
+
+			// Create options
+			opts := find.NewOptions(tgOpts)
+			opts.Format = tt.format
+			opts.Hidden = tt.hidden
+
+			// Create a pipe to capture output
+			r, w, err := os.Pipe()
+			require.NoError(t, err)
+
+			// Set the writer in options
+			opts.Writer = w
+
+			err = find.Run(context.Background(), opts)
+			require.NoError(t, err)
+
+			// Close the write end of the pipe
+			w.Close()
+
+			// Read all output
+			output, err := io.ReadAll(r)
+			require.NoError(t, err)
+
+			// Validate the output
+			tt.validate(t, string(output), tt.expectedPaths)
+		})
+	}
+}
+
+func TestColorizer(t *testing.T) {
+	t.Parallel()
+
+	colorizer := find.NewColorizer()
+
+	tests := []struct {
+		name   string
+		config *discovery.DiscoveredConfig
+		// We can't test exact ANSI codes as they might vary by environment,
+		// so we'll test that different types result in different outputs
+		shouldBeDifferent []discovery.ConfigType
+	}{
+		{
+			name: "unit config",
+			config: &discovery.DiscoveredConfig{
+				Type: discovery.ConfigTypeUnit,
+				Path: "path/to/unit",
+			},
+			shouldBeDifferent: []discovery.ConfigType{discovery.ConfigTypeStack},
+		},
+		{
+			name: "stack config",
+			config: &discovery.DiscoveredConfig{
+				Type: discovery.ConfigTypeStack,
+				Path: "path/to/stack",
+			},
+			shouldBeDifferent: []discovery.ConfigType{discovery.ConfigTypeUnit},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			result := colorizer.Colorize(tt.config)
+			assert.NotEmpty(t, result)
+
+			// Test that different types produce different colorized outputs
+			for _, diffType := range tt.shouldBeDifferent {
+				diffConfig := &discovery.DiscoveredConfig{
+					Type: diffType,
+					Path: tt.config.Path,
+				}
+				diffResult := colorizer.Colorize(diffConfig)
+				assert.NotEqual(t, result, diffResult)
+			}
+		})
+	}
+}
diff --git a/internal/cas/benchmark_test.go b/internal/cas/benchmark_test.go
new file mode 100644
index 00000000..e84d7f06
--- /dev/null
+++ b/internal/cas/benchmark_test.go
@@ -0,0 +1,190 @@
+package cas_test
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strconv"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+)
+
+func BenchmarkClone(b *testing.B) {
+	// Use a small, public repository for consistent results
+	repo := "https://github.com/gruntwork-io/terragrunt.git"
+
+	l := log.New()
+
+	b.Run("fresh clone", func(b *testing.B) {
+		tempDir := b.TempDir()
+
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			storePath := filepath.Join(tempDir, "store", strconv.Itoa(i))
+			targetPath := filepath.Join(tempDir, "repo", strconv.Itoa(i))
+
+			c, err := cas.New(cas.Options{
+				StorePath: storePath,
+			})
+			if err != nil {
+				b.Fatal(err)
+			}
+
+			if err := c.Clone(context.TODO(), &l, &cas.CloneOptions{
+				Dir: targetPath,
+			}, repo); err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+
+	b.Run("clone with existing store", func(b *testing.B) {
+		tempDir := b.TempDir()
+		storePath := filepath.Join(tempDir, "store")
+
+		// First clone to populate store
+		c, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		if err != nil {
+			b.Fatal(err)
+		}
+		if err := c.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir: filepath.Join(tempDir, "initial"),
+		}, repo); err != nil {
+			b.Fatal(err)
+		}
+
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			targetPath := filepath.Join(tempDir, "repo", strconv.Itoa(i))
+
+			c, err := cas.New(cas.Options{
+				StorePath: storePath,
+			})
+			if err != nil {
+				b.Fatal(err)
+			}
+
+			if err := c.Clone(context.TODO(), &l, &cas.CloneOptions{
+				Dir: targetPath,
+			}, repo); err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+}
+
+func BenchmarkContent(b *testing.B) {
+	store := cas.NewStore(b.TempDir())
+
+	content := cas.NewContent(store)
+
+	// Prepare test data
+	testData := []byte("test content for benchmarking")
+
+	l := log.New()
+
+	b.Run("store", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			hash := fmt.Sprintf("benchmark%d", i)
+			if err := content.Store(&l, hash, testData); err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+
+	b.Run("parallel_store", func(b *testing.B) {
+		var mu sync.Mutex
+		seen := make(map[string]bool)
+
+		b.RunParallel(func(pb *testing.PB) {
+			i := 0
+			for pb.Next() {
+				// Generate unique hash for each goroutine iteration
+				hash := fmt.Sprintf("benchmark%d_%d_%d", b.N, i, time.Now().UnixNano())
+				mu.Lock()
+				if seen[hash] {
+					mu.Unlock()
+					continue
+				}
+				seen[hash] = true
+				mu.Unlock()
+
+				if err := content.Store(&l, hash, testData); err != nil {
+					b.Fatal(err)
+				}
+				i++
+			}
+		})
+	})
+}
+
+func BenchmarkGitOperations(b *testing.B) {
+	// Setup a git repository for testing
+	repoDir := b.TempDir()
+	git := cas.NewGitRunner().WithWorkDir(repoDir)
+
+	ctx := context.Background()
+
+	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 := cas.NewGitRunner() // No workDir needed for ls-remote
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			_, err := git.LsRemote(ctx, "https://github.com/gruntwork-io/terragrunt.git", "HEAD")
+			if err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+
+	b.Run("ls-tree", func(b *testing.B) {
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			_, err := git.LsTree(ctx, "HEAD", ".")
+			if err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+
+	b.Run("cat-file", func(b *testing.B) {
+		// First get a valid hash
+		tree, err := git.LsTree(ctx, "HEAD", ".")
+		if err != nil {
+			b.Fatal(err)
+		}
+
+		if len(tree.Entries()) == 0 {
+			b.Fatal("no entries in tree")
+		}
+
+		hash := tree.Entries()[0].Hash
+
+		tmpFile := b.TempDir() + "/cat-file"
+		tmp, err := os.Create(tmpFile)
+		if err != nil {
+			b.Fatal(err)
+		}
+
+		defer os.Remove(tmpFile)
+		defer tmp.Close()
+
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			err := git.CatFile(ctx, hash, tmp)
+			if err != nil {
+				b.Fatal(err)
+			}
+		}
+	})
+}
diff --git a/internal/cas/cas_test.go b/internal/cas/cas_test.go
new file mode 100644
index 00000000..d2c0300b
--- /dev/null
+++ b/internal/cas/cas_test.go
@@ -0,0 +1,90 @@
+package cas_test
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCAS_Clone(t *testing.T) {
+	t.Parallel()
+
+	l := log.New()
+
+	t.Run("clone new repository", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+		storePath := filepath.Join(tempDir, "store")
+		targetPath := filepath.Join(tempDir, "repo")
+
+		c, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		require.NoError(t, err)
+
+		err = c.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir: targetPath,
+		}, "https://github.com/gruntwork-io/terragrunt.git")
+		require.NoError(t, err)
+
+		// Verify repository was cloned
+		_, err = os.Stat(filepath.Join(targetPath, "README.md"))
+		require.NoError(t, err)
+
+		// Verify nested files were linked
+		_, err = os.Stat(filepath.Join(targetPath, "test", "integration_test.go"))
+		require.NoError(t, err)
+	})
+
+	t.Run("clone with specific branch", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+		storePath := filepath.Join(tempDir, "store")
+		targetPath := filepath.Join(tempDir, "repo")
+
+		c, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		require.NoError(t, err)
+
+		err = c.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir:    targetPath,
+			Branch: "main",
+		}, "https://github.com/gruntwork-io/terragrunt.git")
+		require.NoError(t, err)
+
+		// Verify repository was cloned
+		_, err = os.Stat(filepath.Join(targetPath, "README.md"))
+		require.NoError(t, err)
+	})
+
+	t.Run("clone with included git files", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+		storePath := filepath.Join(tempDir, "store")
+		targetPath := filepath.Join(tempDir, "repo")
+
+		c, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		require.NoError(t, err)
+
+		err = c.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir:              targetPath,
+			IncludedGitFiles: []string{"HEAD", "config"},
+		}, "https://github.com/gruntwork-io/terragrunt.git")
+		require.NoError(t, err)
+
+		// Verify repository was cloned
+		_, err = os.Stat(filepath.Join(targetPath, ".git", "HEAD"))
+		require.NoError(t, err)
+
+		_, err = os.Stat(filepath.Join(targetPath, ".git", "config"))
+		require.NoError(t, err)
+	})
+}
diff --git a/internal/cas/content_test.go b/internal/cas/content_test.go
new file mode 100644
index 00000000..5a6f613a
--- /dev/null
+++ b/internal/cas/content_test.go
@@ -0,0 +1,154 @@
+package cas_test
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const testHashValue = "abcdef123456"
+
+func TestContent_Store(t *testing.T) {
+	t.Parallel()
+
+	l := log.New()
+
+	t.Run("store new content", func(t *testing.T) {
+		t.Parallel()
+		store := cas.NewStore(t.TempDir())
+
+		content := cas.NewContent(store)
+		testHash := testHashValue
+		testData := []byte("test content")
+
+		err := content.Store(&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("ensure existing content", func(t *testing.T) {
+		t.Parallel()
+
+		store := cas.NewStore(t.TempDir())
+
+		content := cas.NewContent(store)
+		testHash := testHashValue
+		testData := []byte("test content")
+		differentData := []byte("different content")
+
+		// Store content twice
+		err := content.Ensure(&l, testHash, testData)
+		require.NoError(t, err)
+		err = content.Ensure(&l, testHash, differentData)
+		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("overwrite existing content", func(t *testing.T) {
+		t.Parallel()
+
+		store := cas.NewStore(t.TempDir())
+
+		content := cas.NewContent(store)
+		testHash := testHashValue
+		testData := []byte("test content")
+		differentData := []byte("different content")
+
+		// Store content twice
+		err := content.Store(&l, testHash, testData)
+		require.NoError(t, err)
+		err = content.Store(&l, testHash, differentData)
+		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, differentData, storedData)
+	})
+}
+
+func TestContent_Link(t *testing.T) {
+	t.Parallel()
+
+	l := log.New()
+
+	t.Run("create new link", func(t *testing.T) {
+		t.Parallel()
+		storeDir := t.TempDir()
+		store := cas.NewStore(storeDir)
+
+		content := cas.NewContent(store)
+		testHash := testHashValue
+		testData := []byte("test content")
+
+		// First store some content
+		err := content.Store(&l, testHash, testData)
+		require.NoError(t, err)
+
+		// Then create a link to it
+		targetDir := t.TempDir()
+		targetPath := filepath.Join(targetDir, "subdir", "test.txt")
+		err = content.Link(testHash, targetPath)
+		require.NoError(t, err)
+
+		// Verify link was created and contains correct content
+		linkedData, err := os.ReadFile(targetPath)
+		require.NoError(t, err)
+		assert.Equal(t, testData, linkedData)
+
+		// Verify it's a hard link by checking inode numbers
+		partitionDir := filepath.Join(store.Path(), testHash[:2])
+		sourceInfo, err := os.Stat(filepath.Join(partitionDir, testHash))
+		require.NoError(t, err)
+		targetInfo, err := os.Stat(targetPath)
+		require.NoError(t, err)
+		assert.Equal(t, sourceInfo.Sys(), targetInfo.Sys())
+	})
+
+	t.Run("link to existing file", func(t *testing.T) {
+		t.Parallel()
+		store := cas.NewStore(t.TempDir())
+
+		content := cas.NewContent(store)
+		testHash := testHashValue
+		testData := []byte("test content")
+
+		// Store content
+		err := content.Store(&l, testHash, testData)
+		require.NoError(t, err)
+
+		// Create target file
+		targetDir := t.TempDir()
+		targetPath := filepath.Join(targetDir, "test.txt")
+		err = os.WriteFile(targetPath, []byte("existing content"), 0644)
+		require.NoError(t, err)
+
+		// Try to create link
+		err = content.Link(testHash, targetPath)
+		require.NoError(t, err)
+
+		// Verify original content remains
+		existingData, err := os.ReadFile(targetPath)
+		require.NoError(t, err)
+		assert.Equal(t, []byte("existing content"), existingData)
+	})
+}
diff --git a/internal/cas/errors_test.go b/internal/cas/errors_test.go
new file mode 100644
index 00000000..668df17a
--- /dev/null
+++ b/internal/cas/errors_test.go
@@ -0,0 +1,81 @@
+package cas_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestErrorString(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name string
+		err  cas.Error
+		want string
+	}{
+		{
+			name: "temp dir error",
+			err:  cas.ErrTempDir,
+			want: "failed to create or manage temporary directory",
+		},
+		{
+			name: "git clone error",
+			err:  cas.ErrGitClone,
+			want: "failed to complete git clone",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			assert.Equal(t, tt.want, tt.err.Error())
+		})
+	}
+}
+
+func TestWrappedError(t *testing.T) {
+	t.Parallel()
+	baseErr := errors.New("base error")
+	tests := []struct {
+		name    string
+		wrapped *cas.WrappedError
+		want    string
+	}{
+		{
+			name: "with path",
+			wrapped: &cas.WrappedError{
+				Op:   "clone",
+				Path: "/tmp/repo",
+				Err:  baseErr,
+			},
+			want: "clone: base error",
+		},
+		{
+			name: "with context",
+			wrapped: &cas.WrappedError{
+				Op:      "clone",
+				Context: "repository not found",
+				Err:     baseErr,
+			},
+			want: "clone: repository not found: base error",
+		},
+		{
+			name: "basic",
+			wrapped: &cas.WrappedError{
+				Op:  "clone",
+				Err: baseErr,
+			},
+			want: "clone: base error",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			assert.Equal(t, tt.want, tt.wrapped.Error())
+			assert.Equal(t, baseErr, tt.wrapped.Unwrap())
+		})
+	}
+}
diff --git a/internal/cas/getter_test.go b/internal/cas/getter_test.go
new file mode 100644
index 00000000..424f0258
--- /dev/null
+++ b/internal/cas/getter_test.go
@@ -0,0 +1,132 @@
+package cas_test
+
+import (
+	"context"
+	"net/url"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/hashicorp/go-getter/v2"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCASGetterMode(t *testing.T) {
+	t.Parallel()
+
+	g := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})
+	testURL, err := url.Parse("https://github.com/gruntwork-io/terragrunt")
+	require.NoError(t, err)
+
+	mode, err := g.Mode(context.Background(), testURL)
+	require.NoError(t, err)
+	assert.Equal(t, getter.ModeDir, mode)
+}
+
+func TestCASGetterGetFile(t *testing.T) {
+	t.Parallel()
+
+	g := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})
+	err := g.GetFile(context.Background(), &getter.Request{})
+	require.Error(t, err)
+	assert.Equal(t, "GetFile not implemented", err.Error())
+}
+
+func TestCASGetterDetect(t *testing.T) {
+	t.Parallel()
+
+	g := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})
+
+	tests := []struct {
+		name     string
+		src      string
+		pwd      string
+		expected bool
+	}{
+		{
+			name:     "GitHub repository",
+			src:      "github.com/gruntwork-io/terragrunt",
+			expected: true,
+		},
+		{
+			name:     "Invalid URL",
+			src:      "not-a-valid-url",
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			req := &getter.Request{
+				Src: tt.src,
+				Pwd: tt.pwd,
+			}
+			ok, err := g.Detect(req)
+			require.NoError(t, err)
+			assert.Equal(t, tt.expected, ok)
+		})
+	}
+}
+
+func TestCASGetterGet(t *testing.T) {
+	t.Parallel()
+
+	c, err := cas.New(cas.Options{})
+	require.NoError(t, err)
+
+	opts := &cas.CloneOptions{
+		Branch: "main",
+	}
+
+	l := log.New()
+
+	g := cas.NewCASGetter(&l, c, opts)
+	client := getter.Client{
+		Getters: []getter.Getter{g},
+	}
+
+	tests := []struct {
+		name      string
+		url       string
+		queryRef  string
+		expectRef string
+	}{
+		{
+			name:      "Basic URL without ref",
+			url:       "github.com/gruntwork-io/terragrunt",
+			expectRef: "",
+		},
+		{
+			name:      "URL with ref parameter",
+			url:       "github.com/gruntwork-io/terragrunt?ref=v0.75.0",
+			expectRef: "v0.75.0",
+		},
+		{
+			name:      "URL as SSH",
+			url:       "git@github.com:gruntwork-io/terragrunt.git",
+			expectRef: "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			tmpDir := t.TempDir()
+
+			res, err := client.Get(
+				context.TODO(),
+				&getter.Request{
+					Src: tt.url,
+					Dst: tmpDir,
+				},
+			)
+			require.NoError(t, err)
+
+			assert.Equal(t, tmpDir, res.Dst)
+		})
+	}
+}
diff --git a/internal/cas/git_test.go b/internal/cas/git_test.go
new file mode 100644
index 00000000..7db6398e
--- /dev/null
+++ b/internal/cas/git_test.go
@@ -0,0 +1,230 @@
+package cas_test
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGitRunner_LsRemote(t *testing.T) {
+	t.Parallel()
+	runner := cas.NewGitRunner()
+
+	ctx := context.Background()
+
+	t.Run("valid repository", func(t *testing.T) {
+		t.Parallel()
+		results, err := runner.LsRemote(ctx, "https://github.com/gruntwork-io/terragrunt.git", "HEAD")
+		require.NoError(t, err)
+		require.NotEmpty(t, results)
+		assert.Regexp(t, "^[0-9a-f]{40}$", results[0].Hash)
+		assert.Equal(t, "HEAD", results[0].Ref)
+	})
+
+	t.Run("invalid repository", func(t *testing.T) {
+		t.Parallel()
+		_, err := runner.LsRemote(ctx, "https://github.com/nonexistent/repo.git", "HEAD")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrCommandSpawn)
+	})
+
+	t.Run("nonexistent reference", func(t *testing.T) {
+		t.Parallel()
+		_, err := runner.LsRemote(ctx, "https://github.com/gruntwork-io/terragrunt.git", "nonexistent-branch")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrNoMatchingReference)
+	})
+}
+
+func TestGitRunner_Clone(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+
+	t.Run("shallow clone", func(t *testing.T) {
+		t.Parallel()
+		cloneDir := t.TempDir()
+		runner := cas.NewGitRunner().WithWorkDir(cloneDir)
+		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
+		require.NoError(t, err)
+
+		// Verify it's a git repository
+		_, err = os.Stat(filepath.Join(cloneDir, "HEAD"))
+		require.NoError(t, err)
+	})
+
+	t.Run("clone without workdir fails", func(t *testing.T) {
+		t.Parallel()
+		runner := cas.NewGitRunner()
+		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrNoWorkDir)
+	})
+
+	t.Run("invalid repository", func(t *testing.T) {
+		t.Parallel()
+		cloneDir := t.TempDir()
+		runner := cas.NewGitRunner().WithWorkDir(cloneDir)
+		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt-fake.git", false, 1, "")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrGitClone)
+	})
+}
+
+func TestCreateTempDir(t *testing.T) {
+	t.Parallel()
+	git := cas.NewGitRunner()
+	dir, cleanup, err := git.CreateTempDir()
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		assert.NoError(t, cleanup())
+	})
+
+	// Verify directory exists
+	_, err = os.Stat(dir)
+	require.NoError(t, err)
+
+	// Verify it's empty
+	entries, err := os.ReadDir(dir)
+	require.NoError(t, err)
+	assert.Empty(t, entries)
+}
+
+func TestGetRepoName(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name string
+		repo string
+		want string
+	}{
+		{
+			name: "simple repo",
+			repo: "https://github.com/user/repo.git",
+			want: "repo",
+		},
+		{
+			name: "no .git suffix",
+			repo: "https://github.com/user/repo",
+			want: "repo",
+		},
+		{
+			name: "with path",
+			repo: "/path/to/repo.git",
+			want: "repo",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			assert.Equal(t, tt.want, cas.GetRepoName(tt.repo))
+		})
+	}
+}
+
+func TestGitRunner_LsTree(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+
+	t.Run("valid repository", func(t *testing.T) {
+		t.Parallel()
+		cloneDir := t.TempDir()
+		runner := cas.NewGitRunner().WithWorkDir(cloneDir)
+
+		// First clone a repository
+		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
+		require.NoError(t, err)
+
+		// Then try to ls-tree HEAD
+		tree, err := runner.LsTree(ctx, "HEAD", ".")
+		require.NoError(t, err)
+		require.NotEmpty(t, tree.Entries())
+
+		// Verify some common files exist in the tree
+		found := false
+		for _, entry := range tree.Entries() {
+			if entry.Path == "README.md" {
+				found = true
+				assert.Equal(t, "blob", entry.Type)
+				assert.Equal(t, "100644", entry.Mode)
+				assert.Regexp(t, "^[0-9a-f]{40}$", entry.Hash)
+				break
+			}
+		}
+		assert.True(t, found, "README.md should exist in the repository")
+	})
+
+	t.Run("ls-tree without workdir fails", func(t *testing.T) {
+		t.Parallel()
+		runner := cas.NewGitRunner()
+		_, err := runner.LsTree(ctx, "HEAD", ".")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrNoWorkDir)
+	})
+
+	t.Run("invalid reference", func(t *testing.T) {
+		t.Parallel()
+		cloneDir := t.TempDir()
+		runner := cas.NewGitRunner().WithWorkDir(cloneDir)
+
+		// First clone a repository
+		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
+		require.NoError(t, err)
+
+		// Try to ls-tree an invalid reference
+		_, err = runner.LsTree(ctx, "nonexistent", ".")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrReadTree)
+	})
+
+	t.Run("invalid repository", func(t *testing.T) {
+		t.Parallel()
+		runner := cas.NewGitRunner().WithWorkDir(t.TempDir())
+
+		// Try to ls-tree in an empty directory
+		_, err := runner.LsTree(ctx, "HEAD", ".")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrReadTree)
+	})
+}
+
+func TestGitRunner_RequiresWorkDir(t *testing.T) {
+	t.Parallel()
+
+	t.Run("with workdir", func(t *testing.T) {
+		t.Parallel()
+		runner := cas.NewGitRunner().WithWorkDir(t.TempDir())
+		err := runner.RequiresWorkDir()
+		assert.NoError(t, err)
+	})
+
+	t.Run("without workdir", func(t *testing.T) {
+		t.Parallel()
+		runner := cas.NewGitRunner()
+		err := runner.RequiresWorkDir()
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrNoWorkDir)
+	})
+}
diff --git a/internal/cas/integration_test.go b/internal/cas/integration_test.go
new file mode 100644
index 00000000..0a8dc808
--- /dev/null
+++ b/internal/cas/integration_test.go
@@ -0,0 +1,145 @@
+package cas_test
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIntegration_CloneAndReuse(t *testing.T) {
+	t.Parallel()
+
+	l := log.New()
+
+	t.Run("clone same repo twice uses store", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+		storePath := filepath.Join(tempDir, "store")
+
+		// First clone
+		firstClonePath := filepath.Join(tempDir, "first")
+		cas1, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		require.NoError(t, err)
+		require.NoError(t, cas1.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir: firstClonePath,
+		}, "https://github.com/gruntwork-io/terragrunt.git"))
+
+		// Get info about first clone
+		firstReadme := filepath.Join(firstClonePath, "README.md")
+		firstStat, err := os.Stat(firstReadme)
+		require.NoError(t, err)
+
+		// Second clone
+		secondClonePath := filepath.Join(tempDir, "second")
+		cas2, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		require.NoError(t, err)
+		require.NoError(t, cas2.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir: secondClonePath,
+		}, "https://github.com/gruntwork-io/terragrunt.git"))
+
+		// Get info about second clone
+		secondReadme := filepath.Join(secondClonePath, "README.md")
+		secondStat, err := os.Stat(secondReadme)
+		require.NoError(t, err)
+
+		// Verify both files exist
+		assert.FileExists(t, firstReadme)
+		assert.FileExists(t, secondReadme)
+
+		// Verify they're hard links using os.SameFile instead of comparing entire Stat_t
+		assert.True(t, os.SameFile(firstStat, secondStat))
+	})
+
+	t.Run("clone with nonexistent branch fails gracefully", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+
+		c, err := cas.New(cas.Options{
+			StorePath: filepath.Join(tempDir, "store"),
+		})
+		require.NoError(t, err)
+
+		err = c.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir:    filepath.Join(tempDir, "repo"),
+			Branch: "nonexistent-branch",
+		}, "https://github.com/gruntwork-io/terragrunt.git")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrNoMatchingReference)
+	})
+
+	t.Run("clone with invalid repository fails gracefully", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+
+		c, err := cas.New(cas.Options{
+			StorePath: filepath.Join(tempDir, "store"),
+		})
+		require.NoError(t, err)
+
+		err = c.Clone(context.TODO(), &l, &cas.CloneOptions{
+			Dir: filepath.Join(tempDir, "repo"),
+		}, "https://github.com/yhakbar/nonexistent-repo.git")
+		require.Error(t, err)
+		var wrappedErr *cas.WrappedError
+		require.ErrorAs(t, err, &wrappedErr)
+		assert.ErrorIs(t, wrappedErr.Err, cas.ErrCommandSpawn)
+	})
+}
+
+func TestIntegration_TreeStorage(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+
+	l := log.New()
+
+	t.Run("stores tree objects", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+		storePath := filepath.Join(tempDir, "store")
+
+		// First clone to populate store
+		c, err := cas.New(cas.Options{
+			StorePath: storePath,
+		})
+		require.NoError(t, err)
+		require.NoError(t, c.Clone(ctx, &l, &cas.CloneOptions{
+			Dir: filepath.Join(tempDir, "repo"),
+		}, "https://github.com/gruntwork-io/terragrunt.git"))
+
+		// Get the commit hash
+		git := cas.NewGitRunner().WithWorkDir(filepath.Join(tempDir, "repo"))
+		results, err := git.LsRemote(ctx, "https://github.com/gruntwork-io/terragrunt.git", "HEAD")
+		require.NoError(t, err)
+		require.NotEmpty(t, results)
+		commitHash := results[0].Hash
+
+		// 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")
+
+		// Verify we can read the tree content
+		content := cas.NewContent(store)
+		treeData, err := content.Read(commitHash)
+		require.NoError(t, err)
+
+		// Parse the tree data to confirm it's valid
+		tree, err := cas.ParseTree(string(treeData), "")
+		require.NoError(t, err)
+		assert.NotEmpty(t, tree.Entries(), "Tree should have entries")
+	})
+}
diff --git a/internal/cas/store_test.go b/internal/cas/store_test.go
new file mode 100644
index 00000000..72a310ac
--- /dev/null
+++ b/internal/cas/store_test.go
@@ -0,0 +1,66 @@
+package cas_test
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestStore(t *testing.T) {
+	t.Parallel()
+
+	t.Run("custom path", func(t *testing.T) {
+		t.Parallel()
+		tempDir := t.TempDir()
+		customPath := filepath.Join(tempDir, "custom-store")
+
+		store := cas.NewStore(customPath)
+		assert.Equal(t, customPath, store.Path())
+	})
+}
+
+func TestStore_NeedsWrite(t *testing.T) {
+	t.Parallel()
+	tempDir := t.TempDir()
+	store := cas.NewStore(tempDir)
+
+	// Create a fake content file
+	testHash := "abcdef123456"
+	// Create partition directory
+	partitionDir := filepath.Join(store.Path(), testHash[:2])
+	err := os.MkdirAll(partitionDir, 0755)
+	require.NoError(t, err, "Failed to create partition directory")
+
+	testPath := filepath.Join(partitionDir, testHash)
+	err = os.WriteFile(testPath, []byte("test"), 0644)
+	require.NoError(t, err, "Failed to create test file")
+
+	tests := []struct {
+		name string
+		hash string
+		want bool
+	}{
+		{
+			name: "existing content",
+			hash: testHash,
+			want: false,
+		},
+		{
+			name: "non-existing content",
+			hash: "nonexistent",
+			want: true,
+		},
+	}
+
+	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()))
+		})
+	}
+}
diff --git a/internal/cas/tree_test.go b/internal/cas/tree_test.go
new file mode 100644
index 00000000..18769997
--- /dev/null
+++ b/internal/cas/tree_test.go
@@ -0,0 +1,289 @@
+package cas_test
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/cas"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseTreeEntry(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		input   string
+		want    cas.TreeEntry
+		wantErr bool
+	}{
+		{
+			name:  "regular file",
+			input: "100644 blob a1b2c3d4 README.md",
+			want: cas.TreeEntry{
+				Mode: "100644",
+				Type: "blob",
+				Hash: "a1b2c3d4",
+				Path: "README.md",
+			},
+		},
+		{
+			name:  "executable file",
+			input: "100755 blob e5f6g7h8 scripts/test.sh",
+			want: cas.TreeEntry{
+				Mode: "100755",
+				Type: "blob",
+				Hash: "e5f6g7h8",
+				Path: "scripts/test.sh",
+			},
+		},
+		{
+			name:  "directory",
+			input: "040000 tree i9j0k1l2 src",
+			want: cas.TreeEntry{
+				Mode: "040000",
+				Type: "tree",
+				Hash: "i9j0k1l2",
+				Path: "src",
+			},
+		},
+		{
+			name:  "path with spaces",
+			input: "100644 blob m3n4o5p6 path with spaces.txt",
+			want: cas.TreeEntry{
+				Mode: "100644",
+				Type: "blob",
+				Hash: "m3n4o5p6",
+				Path: "path with spaces.txt",
+			},
+		},
+		{
+			name:    "invalid format",
+			input:   "invalid format",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got, err := cas.ParseTreeEntry(tt.input)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestParseTree(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    string
+		path     string
+		wantLen  int
+		wantPath string
+		wantErr  bool
+	}{
+		{
+			name: "multiple entries",
+			input: `100644 blob a1b2c3d4 README.md
+100755 blob e5f6g7h8 scripts/test.sh
+040000 tree i9j0k1l2 src`,
+			path:     "test-repo",
+			wantLen:  3,
+			wantPath: "test-repo",
+		},
+		{
+			name:     "empty input",
+			input:    "",
+			path:     "empty-repo",
+			wantLen:  0,
+			wantPath: "empty-repo",
+		},
+		{
+			name: "invalid entry",
+			input: `100644 blob a1b2c3d4 README.md
+invalid format`,
+			path:    "invalid-repo",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got, err := cas.ParseTree(tt.input, tt.path)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Len(t, got.Entries(), tt.wantLen)
+			assert.Equal(t, tt.wantPath, got.Path())
+		})
+	}
+}
+
+func TestLinkTree(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name       string
+		setupStore func(t *testing.T) (*cas.Store, string)
+		treeData   string
+		wantFiles  []struct {
+			path    string
+			content []byte
+			isDir   bool
+			hash    string
+		}
+		wantErr bool
+	}{
+		{
+			name: "basic tree with files and directories",
+			setupStore: func(t *testing.T) (*cas.Store, string) {
+				t.Helper()
+
+				storeDir := t.TempDir()
+				store := cas.NewStore(storeDir)
+				content := cas.NewContent(store)
+
+				// Create test content
+				testData := []byte("test content")
+				testHash := "a1b2c3d4"
+				err := content.Store(nil, testHash, testData)
+				require.NoError(t, err)
+
+				// Create and store the src directory tree data
+				srcTreeData := `100644 blob a1b2c3d4 README.md`
+				srcTreeHash := "i9j0k1l2"
+				err = content.Store(nil, srcTreeHash, []byte(srcTreeData))
+				require.NoError(t, err)
+
+				return store, testHash
+			},
+			treeData: `100644 blob a1b2c3d4 README.md
+100755 blob a1b2c3d4 scripts/test.sh
+040000 tree i9j0k1l2 src`,
+			wantFiles: []struct {
+				path    string
+				content []byte
+				isDir   bool
+				hash    string
+			}{
+				{
+					path:    "README.md",
+					content: []byte("test content"),
+					isDir:   false,
+					hash:    "a1b2c3d4",
+				},
+				{
+					path:    "scripts/test.sh",
+					content: []byte("test content"),
+					isDir:   false,
+					hash:    "a1b2c3d4",
+				},
+				{
+					path:  "src",
+					isDir: true,
+				},
+				{
+					path:    "src/README.md",
+					content: []byte("test content"),
+					isDir:   false,
+					hash:    "a1b2c3d4",
+				},
+			},
+		},
+		{
+			name: "empty tree",
+			setupStore: func(t *testing.T) (*cas.Store, string) {
+				t.Helper()
+
+				storeDir := t.TempDir()
+				store := cas.NewStore(storeDir)
+				return store, ""
+			},
+			treeData: "",
+			wantFiles: []struct {
+				path    string
+				content []byte
+				isDir   bool
+				hash    string
+			}{},
+		},
+		{
+			name: "tree with missing content",
+			setupStore: func(t *testing.T) (*cas.Store, string) {
+				t.Helper()
+
+				storeDir := t.TempDir()
+				store := cas.NewStore(storeDir)
+				return store, ""
+			},
+			treeData: `100644 blob missing123 README.md`,
+			wantErr:  true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			// Setup store
+			store, _ := tt.setupStore(t)
+
+			// Parse the tree
+			tree, err := cas.ParseTree(tt.treeData, "test-repo")
+			require.NoError(t, err)
+
+			// Create target directory
+			targetDir := t.TempDir()
+
+			// Link the tree
+			err = tree.LinkTree(context.Background(), store, targetDir)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			// Verify all expected files and directories
+			for _, want := range tt.wantFiles {
+				path := filepath.Join(targetDir, want.path)
+
+				// Check if file/directory exists
+				info, err := os.Stat(path)
+				require.NoError(t, err)
+				assert.Equal(t, want.isDir, info.IsDir())
+
+				if !want.isDir {
+					// Check file content
+					data, err := os.ReadFile(path)
+					require.NoError(t, err)
+					assert.Equal(t, want.content, data)
+
+					dataStat, err := os.Stat(path)
+					require.NoError(t, err)
+
+					// Verify hard link by comparing content.
+					// We don't compare inode numbers because the test might be running on Windows.
+					storePath := filepath.Join(store.Path(), want.hash[:2], want.hash)
+					storeStat, err := os.Stat(storePath)
+					require.NoError(t, err)
+
+					assert.True(t, os.SameFile(dataStat, storeStat))
+
+				}
+			}
+		})
+	}
+}
diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go
new file mode 100644
index 00000000..14a9b469
--- /dev/null
+++ b/internal/discovery/discovery_test.go
@@ -0,0 +1,140 @@
+package discovery_test
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/internal/discovery"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDiscovery(t *testing.T) {
+	t.Parallel()
+
+	// Create a temporary directory for testing
+	tmpDir := t.TempDir()
+
+	// Create test directory structure
+	testDirs := []string{
+		"unit1",
+		"unit2",
+		"stack1",
+		".hidden/hidden-unit",
+		"nested/unit4",
+	}
+
+	for _, dir := range testDirs {
+		err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	// Create test files
+	testFiles := map[string]string{
+		"unit1/terragrunt.hcl":               "",
+		"unit2/terragrunt.hcl":               "",
+		"stack1/terragrunt.stack.hcl":        "",
+		".hidden/hidden-unit/terragrunt.hcl": "",
+		"nested/unit4/terragrunt.hcl":        "",
+	}
+
+	for path, content := range testFiles {
+		err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	tests := []struct {
+		name          string
+		discovery     *discovery.Discovery
+		wantUnits     []string
+		wantStacks    []string
+		errorExpected bool
+	}{
+		{
+			name:       "basic discovery without hidden",
+			discovery:  discovery.NewDiscovery(tmpDir),
+			wantUnits:  []string{"unit1", "unit2", "nested/unit4"},
+			wantStacks: []string{"stack1"},
+		},
+		{
+			name:       "discovery with hidden",
+			discovery:  discovery.NewDiscovery(tmpDir).WithHidden(),
+			wantUnits:  []string{"unit1", "unit2", ".hidden/hidden-unit", "nested/unit4"},
+			wantStacks: []string{"stack1"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			configs, err := tt.discovery.Discover()
+			if !tt.errorExpected {
+				require.NoError(t, err)
+			}
+
+			units := configs.Filter(discovery.ConfigTypeUnit).Paths()
+			stacks := configs.Filter(discovery.ConfigTypeStack).Paths()
+
+			assert.ElementsMatch(t, units, tt.wantUnits)
+			assert.ElementsMatch(t, stacks, tt.wantStacks)
+		})
+	}
+}
+
+func TestDiscoveredConfigsSort(t *testing.T) {
+	t.Parallel()
+
+	// Setup
+	configs := discovery.DiscoveredConfigs{
+		{Path: "c", Type: discovery.ConfigTypeUnit},
+		{Path: "a", Type: discovery.ConfigTypeUnit},
+		{Path: "b", Type: discovery.ConfigTypeStack},
+	}
+
+	// Act
+	sorted := configs.Sort()
+
+	// Assert
+	require.Len(t, sorted, 3)
+	assert.Equal(t, "a", sorted[0].Path)
+	assert.Equal(t, "b", sorted[1].Path)
+	assert.Equal(t, "c", sorted[2].Path)
+}
+
+func TestDiscoveredConfigsFilter(t *testing.T) {
+	t.Parallel()
+
+	// Setup
+	configs := discovery.DiscoveredConfigs{
+		{Path: "a", Type: discovery.ConfigTypeUnit},
+		{Path: "b", Type: discovery.ConfigTypeStack},
+		{Path: "c", Type: discovery.ConfigTypeUnit},
+	}
+
+	// Test unit filtering
+	t.Run("filter units", func(t *testing.T) {
+		t.Parallel()
+
+		units := configs.Filter(discovery.ConfigTypeUnit)
+		require.Len(t, units, 2)
+		assert.Equal(t, discovery.ConfigTypeUnit, units[0].Type)
+		assert.Equal(t, discovery.ConfigTypeUnit, units[1].Type)
+		assert.ElementsMatch(t, []string{"a", "c"}, units.Paths())
+	})
+
+	// Test stack filtering
+	t.Run("filter stacks", func(t *testing.T) {
+		t.Parallel()
+
+		stacks := configs.Filter(discovery.ConfigTypeStack)
+		require.Len(t, stacks, 1)
+		assert.Equal(t, discovery.ConfigTypeStack, stacks[0].Type)
+		assert.Equal(t, "b", stacks[0].Path)
+	})
+}
diff --git a/test/fixtures/destroy-order/hello/main.tf b/test/fixtures/destroy-order/hello/main.tf
index 99c7116f..9c801939 100755
--- a/test/fixtures/destroy-order/hello/main.tf
+++ b/test/fixtures/destroy-order/hello/main.tf
@@ -1,15 +1,29 @@
-data "template_file" "test" {
-  template = "${module.hello.hello}, ${var.name}"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 variable "name" {
   description = "Specify a name"
-}
-
-output "test" {
-  value = data.template_file.test.rendered
+  type        = string
 }
 
 module "hello" {
   source = "./hello"
 }
+
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo '${module.hello.hello}, ${var.name}'"
+  }
+}
+
+output "test" {
+  value = "${module.hello.hello}, ${var.name}"
+}
diff --git a/test/fixtures/download/hello-world-no-remote/main.tf b/test/fixtures/download/hello-world-no-remote/main.tf
index 2235fa7a..f05f283a 100644
--- a/test/fixtures/download/hello-world-no-remote/main.tf
+++ b/test/fixtures/download/hello-world-no-remote/main.tf
@@ -1,15 +1,29 @@
-data "template_file" "test" {
-  template = "${module.hello.hello}, ${var.name}"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 variable "name" {
   description = "Specify a name"
-}
-
-output "test" {
-  value = data.template_file.test.rendered
+  type        = string
 }
 
 module "hello" {
   source = "..//hello-world/hello"
-}
\ No newline at end of file
+}
+
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo '${module.hello.hello}, ${var.name}'"
+  }
+}
+
+output "test" {
+  value = "${module.hello.hello}, ${var.name}"
+}
diff --git a/test/fixtures/download/hello-world-with-backend/main.tf b/test/fixtures/download/hello-world-with-backend/main.tf
index f05e9acf..970ec99c 100644
--- a/test/fixtures/download/hello-world-with-backend/main.tf
+++ b/test/fixtures/download/hello-world-with-backend/main.tf
@@ -1,16 +1,28 @@
-data "template_file" "test" {
-  template = "hello, ${var.name}"
+terraform {
+  # These settings will be filled in by Terragrunt
+  backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 variable "name" {
   description = "Specify a name"
+  type        = string
 }
 
-output "test" {
-  value = data.template_file.test.rendered
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo 'hello, ${var.name}'"
+  }
 }
 
-terraform {
-  # These settings will be filled in by Terragrunt
-  backend "s3" {}
-}
\ No newline at end of file
+output "test" {
+  value = "hello, ${var.name}"
+}
diff --git a/test/fixtures/download/hello-world/main.tf b/test/fixtures/download/hello-world/main.tf
index 31582d6e..74c1e12b 100755
--- a/test/fixtures/download/hello-world/main.tf
+++ b/test/fixtures/download/hello-world/main.tf
@@ -1,19 +1,33 @@
-data "template_file" "test" {
-  template = "${module.hello.hello}, ${var.name}"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 variable "name" {
   description = "Specify a name"
-}
-
-output "test" {
-  value = data.template_file.test.rendered
+  type        = string
 }
 
 module "hello" {
   source = "./hello"
 }
 
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo '${module.hello.hello}, ${var.name}'"
+  }
+}
+
+output "test" {
+  value = "${module.hello.hello}, ${var.name}"
+}
+
 module "remote" {
   source = "github.com/gruntwork-io/terragrunt.git//test/fixture-download/hello-world?ref=v0.9.9"
   name   = var.name
diff --git a/test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/main.tf b/test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/main.tf
index 31582d6e..74c1e12b 100644
--- a/test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/main.tf
+++ b/test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/main.tf
@@ -1,19 +1,33 @@
-data "template_file" "test" {
-  template = "${module.hello.hello}, ${var.name}"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 variable "name" {
   description = "Specify a name"
-}
-
-output "test" {
-  value = data.template_file.test.rendered
+  type        = string
 }
 
 module "hello" {
   source = "./hello"
 }
 
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo '${module.hello.hello}, ${var.name}'"
+  }
+}
+
+output "test" {
+  value = "${module.hello.hello}, ${var.name}"
+}
+
 module "remote" {
   source = "github.com/gruntwork-io/terragrunt.git//test/fixture-download/hello-world?ref=v0.9.9"
   name   = var.name
diff --git a/test/fixtures/download/stdout/main.tf b/test/fixtures/download/stdout/main.tf
index 84fab5fd..ec2f8df2 100644
--- a/test/fixtures/download/stdout/main.tf
+++ b/test/fixtures/download/stdout/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "foo" {
-  template = "foo"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "foo" {
+  provisioner "local-exec" {
+    command = "echo foo"
+  }
 }
 
 output "foo" {
-  value = data.template_file.foo.rendered
-}
\ No newline at end of file
+  value = "foo"
+}
diff --git a/test/fixtures/engine/engine-dependencies/.gitignore b/test/fixtures/engine/engine-dependencies/.gitignore
new file mode 100644
index 00000000..1648d919
--- /dev/null
+++ b/test/fixtures/engine/engine-dependencies/.gitignore
@@ -0,0 +1,2 @@
+backend.gen.tf
+test.txt
diff --git a/test/fixtures/engine/engine-dependencies/app1/main.tf b/test/fixtures/engine/engine-dependencies/app1/main.tf
new file mode 100644
index 00000000..423e784f
--- /dev/null
+++ b/test/fixtures/engine/engine-dependencies/app1/main.tf
@@ -0,0 +1,3 @@
+output "value" {
+  value = "app1-test"
+}
diff --git a/test/fixtures/engine/engine-dependencies/app1/terragrunt.hcl b/test/fixtures/engine/engine-dependencies/app1/terragrunt.hcl
new file mode 100644
index 00000000..204f9c04
--- /dev/null
+++ b/test/fixtures/engine/engine-dependencies/app1/terragrunt.hcl
@@ -0,0 +1,9 @@
+include "root" {
+  path = find_in_parent_folders("root.hcl")
+}
+
+engine {
+  source  = "github.com/gruntwork-io/terragrunt-engine-opentofu"
+  version = "v0.0.16"
+  type    = "rpc"
+}
diff --git a/test/fixtures/engine/engine-dependencies/app2/main.tf b/test/fixtures/engine/engine-dependencies/app2/main.tf
new file mode 100644
index 00000000..4b000a86
--- /dev/null
+++ b/test/fixtures/engine/engine-dependencies/app2/main.tf
@@ -0,0 +1,8 @@
+variable "app1_output" {
+  type = string
+}
+
+resource "local_file" "test" {
+  content  = var.app1_output
+  filename = "${path.module}/test.txt"
+}
diff --git a/test/fixtures/engine/engine-dependencies/app2/terragrunt.hcl b/test/fixtures/engine/engine-dependencies/app2/terragrunt.hcl
new file mode 100644
index 00000000..871bb092
--- /dev/null
+++ b/test/fixtures/engine/engine-dependencies/app2/terragrunt.hcl
@@ -0,0 +1,17 @@
+include "root" {
+  path = find_in_parent_folders("root.hcl")
+}
+
+engine {
+  source  = "github.com/gruntwork-io/terragrunt-engine-opentofu"
+  version = "v0.0.16"
+  type    = "rpc"
+}
+
+dependency "app1" {
+  config_path = "../app1"
+}
+
+inputs = {
+  app1_output = dependency.app1.outputs.value
+}
diff --git a/test/fixtures/engine/engine-dependencies/root.hcl b/test/fixtures/engine/engine-dependencies/root.hcl
new file mode 100644
index 00000000..69453871
--- /dev/null
+++ b/test/fixtures/engine/engine-dependencies/root.hcl
@@ -0,0 +1,10 @@
+remote_state {
+  backend = "local"
+  generate = {
+    path = "backend.gen.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+  config = {
+    path = "${get_terragrunt_dir()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixtures/find/basic/stack/terragrunt.stack.hcl b/test/fixtures/find/basic/stack/terragrunt.stack.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixtures/find/basic/stack/terragrunt.stack.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixtures/find/basic/unit/terragrunt.hcl b/test/fixtures/find/basic/unit/terragrunt.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixtures/find/basic/unit/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixtures/find/hidden/.hide/unit/terragrunt.hcl b/test/fixtures/find/hidden/.hide/unit/terragrunt.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixtures/find/hidden/.hide/unit/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixtures/find/hidden/stack/terragrunt.stack.hcl b/test/fixtures/find/hidden/stack/terragrunt.stack.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixtures/find/hidden/stack/terragrunt.stack.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixtures/find/hidden/unit/terragrunt.hcl b/test/fixtures/find/hidden/unit/terragrunt.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixtures/find/hidden/unit/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixtures/gcs-byo-bucket/main.tf b/test/fixtures/gcs-byo-bucket/main.tf
index 8f52ed68..634a02ba 100644
--- a/test/fixtures/gcs-byo-bucket/main.tf
+++ b/test/fixtures/gcs-byo-bucket/main.tf
@@ -1,12 +1,19 @@
 terraform {
   backend "gcs" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "test" {
-  template = "Hello, I am a template. My sample_var value = $${sample_var}"
-
-  vars = {
-    sample_var = var.sample_var
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
   }
 }
diff --git a/test/fixtures/gcs-byo-bucket/outputs.tf b/test/fixtures/gcs-byo-bucket/outputs.tf
deleted file mode 100644
index af9fb11e..00000000
--- a/test/fixtures/gcs-byo-bucket/outputs.tf
+++ /dev/null
@@ -1,3 +0,0 @@
-output "rendered_template" {
-  value = data.template_file.test.rendered
-}
diff --git a/test/fixtures/gcs-byo-bucket/vars.tf b/test/fixtures/gcs-byo-bucket/vars.tf
deleted file mode 100644
index 8c0b97ee..00000000
--- a/test/fixtures/gcs-byo-bucket/vars.tf
+++ /dev/null
@@ -1,5 +0,0 @@
-# Configure these variables
-variable "sample_var" {
-  description = "A sample_var to pass to the template."
-  default     = "hello"
-}
diff --git a/test/fixtures/gcs/main.tf b/test/fixtures/gcs/main.tf
index 8f52ed68..dd912877 100644
--- a/test/fixtures/gcs/main.tf
+++ b/test/fixtures/gcs/main.tf
@@ -1,12 +1,18 @@
 terraform {
   backend "gcs" {}
-}
 
-# Create an arbitrary local resource
-data "template_file" "test" {
-  template = "Hello, I am a template. My sample_var value = $${sample_var}"
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
 
-  vars = {
-    sample_var = var.sample_var
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
   }
 }
diff --git a/test/fixtures/gcs/outputs.tf b/test/fixtures/gcs/outputs.tf
deleted file mode 100644
index af9fb11e..00000000
--- a/test/fixtures/gcs/outputs.tf
+++ /dev/null
@@ -1,3 +0,0 @@
-output "rendered_template" {
-  value = data.template_file.test.rendered
-}
diff --git a/test/fixtures/gcs/terragrunt.hcl b/test/fixtures/gcs/terragrunt.hcl
index 827a95b6..5171db95 100644
--- a/test/fixtures/gcs/terragrunt.hcl
+++ b/test/fixtures/gcs/terragrunt.hcl
@@ -6,7 +6,7 @@ remote_state {
     project  = "__FILL_IN_PROJECT__"
     location = "__FILL_IN_LOCATION__"
     bucket   = "__FILL_IN_BUCKET_NAME__"
-    prefix   = "terraform.tfstate"
+    prefix   = "tofu.tfstate"
 
     gcs_bucket_labels = {
       owner = "terragrunt_test"
diff --git a/test/fixtures/gcs/vars.tf b/test/fixtures/gcs/vars.tf
deleted file mode 100644
index 8c0b97ee..00000000
--- a/test/fixtures/gcs/vars.tf
+++ /dev/null
@@ -1,5 +0,0 @@
-# Configure these variables
-variable "sample_var" {
-  description = "A sample_var to pass to the template."
-  default     = "hello"
-}
diff --git a/test/fixtures/hooks/after-only/main.tf b/test/fixtures/hooks/after-only/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/after-only/main.tf
+++ b/test/fixtures/hooks/after-only/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/all/after-only/main.tf b/test/fixtures/hooks/all/after-only/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/all/after-only/main.tf
+++ b/test/fixtures/hooks/all/after-only/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/all/before-only/main.tf b/test/fixtures/hooks/all/before-only/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/all/before-only/main.tf
+++ b/test/fixtures/hooks/all/before-only/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/bad-arg-action/empty-command-list/main.tf b/test/fixtures/hooks/bad-arg-action/empty-command-list/main.tf
index 15d67cca..1ae2641f 100644
--- a/test/fixtures/hooks/bad-arg-action/empty-command-list/main.tf
+++ b/test/fixtures/hooks/bad-arg-action/empty-command-list/main.tf
@@ -1,7 +1,16 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
-output "example" {
-  value = data.template_file.example.rendered
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
 }
diff --git a/test/fixtures/hooks/bad-arg-action/empty-string-command/main.tf b/test/fixtures/hooks/bad-arg-action/empty-string-command/main.tf
index 15d67cca..fb8a5e67 100644
--- a/test/fixtures/hooks/bad-arg-action/empty-string-command/main.tf
+++ b/test/fixtures/hooks/bad-arg-action/empty-string-command/main.tf
@@ -1,5 +1,18 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
 }
 
 output "example" {
diff --git a/test/fixtures/hooks/before-after-and-error-merge/qa/my-app/main.tf b/test/fixtures/hooks/before-after-and-error-merge/qa/my-app/main.tf
index 7cc7209a..b13eb4ed 100644
--- a/test/fixtures/hooks/before-after-and-error-merge/qa/my-app/main.tf
+++ b/test/fixtures/hooks/before-after-and-error-merge/qa/my-app/main.tf
@@ -1,11 +1,22 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
-data "template_file" "example" {
-  template = "hello, world"
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/before-after-and-on-error/main.tf b/test/fixtures/hooks/before-after-and-on-error/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/before-after-and-on-error/main.tf
+++ b/test/fixtures/hooks/before-after-and-on-error/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/before-and-after/main.tf b/test/fixtures/hooks/before-and-after/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/before-and-after/main.tf
+++ b/test/fixtures/hooks/before-and-after/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/before-only/main.tf b/test/fixtures/hooks/before-only/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/before-only/main.tf
+++ b/test/fixtures/hooks/before-only/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/init-once/base-module/main.tf b/test/fixtures/hooks/init-once/base-module/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/init-once/base-module/main.tf
+++ b/test/fixtures/hooks/init-once/base-module/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/init-once/no-source-no-backend/main.tf b/test/fixtures/hooks/init-once/no-source-no-backend/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/init-once/no-source-no-backend/main.tf
+++ b/test/fixtures/hooks/init-once/no-source-no-backend/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/init-once/no-source-with-backend/main.tf b/test/fixtures/hooks/init-once/no-source-with-backend/main.tf
index 7cc7209a..a2099546 100644
--- a/test/fixtures/hooks/init-once/no-source-with-backend/main.tf
+++ b/test/fixtures/hooks/init-once/no-source-with-backend/main.tf
@@ -1,11 +1,18 @@
 terraform {
   backend "s3" {}
-}
 
-data "template_file" "example" {
-  template = "hello, world"
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
-output "example" {
-  value = data.template_file.example.rendered
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
 }
diff --git a/test/fixtures/hooks/interpolations/main.tf b/test/fixtures/hooks/interpolations/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/interpolations/main.tf
+++ b/test/fixtures/hooks/interpolations/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/one-arg-action/main.tf b/test/fixtures/hooks/one-arg-action/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/one-arg-action/main.tf
+++ b/test/fixtures/hooks/one-arg-action/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/hooks/skip-on-error/main.tf b/test/fixtures/hooks/skip-on-error/main.tf
index 15d67cca..91e61e8d 100644
--- a/test/fixtures/hooks/skip-on-error/main.tf
+++ b/test/fixtures/hooks/skip-on-error/main.tf
@@ -1,7 +1,20 @@
-data "template_file" "example" {
-  template = "hello, world"
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
+resource "null_resource" "example" {
+  provisioner "local-exec" {
+    command = "echo hello, world"
+  }
 }
 
 output "example" {
-  value = data.template_file.example.rendered
+  value = "hello, world"
 }
diff --git a/test/fixtures/include/qa/my-app/main.tf b/test/fixtures/include/qa/my-app/main.tf
index 5d4e2b52..53a91000 100644
--- a/test/fixtures/include/qa/my-app/main.tf
+++ b/test/fixtures/include/qa/my-app/main.tf
@@ -1,9 +1,25 @@
+terraform {
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
+}
+
 # Create an arbitrary local resource
-data "template_file" "test" {
-  template = "Hello, I am a template."
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
+}
+
+variable "reflect" {
+  type = string
 }
 
-variable "reflect" {}
 output "reflect" {
   value = var.reflect
 }
diff --git a/test/fixtures/include/stage/my-app/main.tf b/test/fixtures/include/stage/my-app/main.tf
index 30425d44..2f49f360 100644
--- a/test/fixtures/include/stage/my-app/main.tf
+++ b/test/fixtures/include/stage/my-app/main.tf
@@ -1,13 +1,28 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "test" {
-  template = "Hello, I am a template."
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
 }
 
-variable "reflect" {}
+variable "reflect" {
+  type = string
+}
+
+
 output "reflect" {
   value = var.reflect
 }
diff --git a/test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/main.tf b/test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/main.tf
index dabe32d1..8c0baffa 100644
--- a/test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/main.tf
+++ b/test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/main.tf
@@ -1,8 +1,19 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "test" {
-  template = "Hello, I am a template."
-}
\ No newline at end of file
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
+}
diff --git a/test/fixtures/stack/mgmt/bastion-host/main.tf b/test/fixtures/stack/mgmt/bastion-host/main.tf
index b83465aa..b05288b8 100644
--- a/test/fixtures/stack/mgmt/bastion-host/main.tf
+++ b/test/fixtures/stack/mgmt/bastion-host/main.tf
@@ -1,18 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a bastion-host template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a bastion host template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a bastion host template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]"
 }
 
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "vpc" {
diff --git a/test/fixtures/stack/mgmt/kms-master-key/main.tf b/test/fixtures/stack/mgmt/kms-master-key/main.tf
index 8e6a44aa..f09172dc 100644
--- a/test/fixtures/stack/mgmt/kms-master-key/main.tf
+++ b/test/fixtures/stack/mgmt/kms-master-key/main.tf
@@ -1,12 +1,23 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a kms-master-key template.]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a kms-master-key template.]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a kms-master-key template.]"
 }
diff --git a/test/fixtures/stack/mgmt/vpc/main.tf b/test/fixtures/stack/mgmt/vpc/main.tf
index f0cb1225..032d5d35 100644
--- a/test/fixtures/stack/mgmt/vpc/main.tf
+++ b/test/fixtures/stack/mgmt/vpc/main.tf
@@ -1,12 +1,23 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a mgmt vpc template. I have no dependencies.]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a mgmt vpc template. I have no dependencies.]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a mgmt vpc template. I have no dependencies.]"
 }
diff --git a/test/fixtures/stack/stage/backend-app/main.tf b/test/fixtures/stack/stage/backend-app/main.tf
index 6792d083..2f7b0492 100644
--- a/test/fixtures/stack/stage/backend-app/main.tf
+++ b/test/fixtures/stack/stage/backend-app/main.tf
@@ -1,18 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a backend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, mysql = ${data.terraform_remote_state.mysql.outputs.text}, search-app = ${data.terraform_remote_state.search_app.outputs.text}]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a backend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, mysql = ${data.terraform_remote_state.mysql.outputs.text}, search-app = ${data.terraform_remote_state.search_app.outputs.text}]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a backend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, mysql = ${data.terraform_remote_state.mysql.outputs.text}, search-app = ${data.terraform_remote_state.search_app.outputs.text}]"
 }
 
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "vpc" {
diff --git a/test/fixtures/stack/stage/frontend-app/main.tf b/test/fixtures/stack/stage/frontend-app/main.tf
index 1a9ce74f..8f158538 100644
--- a/test/fixtures/stack/stage/frontend-app/main.tf
+++ b/test/fixtures/stack/stage/frontend-app/main.tf
@@ -1,18 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a frontend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, backend-app = ${data.terraform_remote_state.backend_app.outputs.text}]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a frontend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, backend-app = ${data.terraform_remote_state.backend_app.outputs.text}]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a frontend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, backend-app = ${data.terraform_remote_state.backend_app.outputs.text}]"
 }
 
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "vpc" {
@@ -33,7 +45,6 @@ data "terraform_remote_state" "backend_app" {
   }
 }
 
-
 data "terraform_remote_state" "bastion_host" {
   backend = "s3"
   config = {
diff --git a/test/fixtures/stack/stage/mysql/main.tf b/test/fixtures/stack/stage/mysql/main.tf
index 6c1d78d0..7def4a9c 100644
--- a/test/fixtures/stack/stage/mysql/main.tf
+++ b/test/fixtures/stack/stage/mysql/main.tf
@@ -1,18 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a mysql template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a mysql template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a mysql template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]"
 }
 
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "vpc" {
diff --git a/test/fixtures/stack/stage/redis/main.tf b/test/fixtures/stack/stage/redis/main.tf
index 3307441c..813c35b2 100644
--- a/test/fixtures/stack/stage/redis/main.tf
+++ b/test/fixtures/stack/stage/redis/main.tf
@@ -1,18 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a redis template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a redis template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a redis template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]"
 }
 
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "vpc" {
diff --git a/test/fixtures/stack/stage/search-app/example-module/main.tf b/test/fixtures/stack/stage/search-app/example-module/main.tf
index 2368e3ba..0a1656fc 100644
--- a/test/fixtures/stack/stage/search-app/example-module/main.tf
+++ b/test/fixtures/stack/stage/search-app/example-module/main.tf
@@ -1,8 +1,10 @@
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "Example text from a module"
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo Hello, World!"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am an example module template.]"
 }
diff --git a/test/fixtures/stack/stage/search-app/main.tf b/test/fixtures/stack/stage/search-app/main.tf
index 45db4328..e3e3c87a 100644
--- a/test/fixtures/stack/stage/search-app/main.tf
+++ b/test/fixtures/stack/stage/search-app/main.tf
@@ -1,22 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a search-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, redis = ${data.terraform_remote_state.redis.outputs.text}, example_module = ${module.example_module.text}]"
+resource "null_resource" "test" {
+  provisioner "local-exec" {
+    command = "echo '[I am a search-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, redis = ${data.terraform_remote_state.redis.outputs.text}, example_module = ${module.example_module.text}]'"
+  }
 }
 
 module "example_module" {
   source = "./example-module"
 }
 
-output "text" {
-  value = data.template_file.text.rendered
-}
-
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "vpc" {
@@ -36,3 +44,7 @@ data "terraform_remote_state" "redis" {
     key    = "stage/redis/terraform.tfstate"
   }
 }
+
+output "text" {
+  value = "[I am a search-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, redis = ${data.terraform_remote_state.redis.outputs.text}, example_module = ${module.example_module.text}]"
+}
diff --git a/test/fixtures/stack/stage/vpc/main.tf b/test/fixtures/stack/stage/vpc/main.tf
index c15ddccd..95b700af 100644
--- a/test/fixtures/stack/stage/vpc/main.tf
+++ b/test/fixtures/stack/stage/vpc/main.tf
@@ -1,18 +1,30 @@
 terraform {
   backend "s3" {}
+
+  required_version = ">= 1.5.7"
+
+  required_providers {
+    null = {
+      source  = "hashicorp/null"
+      version = "3.2.3"
+    }
+  }
 }
 
 # Create an arbitrary local resource
-data "template_file" "text" {
-  template = "[I am a stage vpc template. Data from my dependencies: vpc = ${data.terraform_remote_state.mgmt_vpc.outputs.text}]"
+resource "null_resource" "text" {
+  provisioner "local-exec" {
+    command = "echo '[I am a stage vpc template. Data from my dependencies: vpc = ${data.terraform_remote_state.mgmt_vpc.outputs.text}]'"
+  }
 }
 
 output "text" {
-  value = data.template_file.text.rendered
+  value = "[I am a stage vpc template. Data from my dependencies: vpc = ${data.terraform_remote_state.mgmt_vpc.outputs.text}]"
 }
 
 variable "terraform_remote_state_s3_bucket" {
   description = "The name of the S3 bucket where Terraform remote state is stored"
+  type        = string
 }
 
 data "terraform_remote_state" "mgmt_vpc" {
diff --git a/test/integration_catalog_test.go b/test/integration_catalog_test.go
index ee1f8a2d..9506b92c 100644
--- a/test/integration_catalog_test.go
+++ b/test/integration_catalog_test.go
@@ -22,10 +22,10 @@ func TestCatalogGitRepoUpdate(t *testing.T) {
 
 	tempDir := t.TempDir()
 
-	_, err := module.NewRepo(ctx, log.New(), "github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false)
+	_, err := module.NewRepo(ctx, log.New(), "github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false, false)
 	require.NoError(t, err)
 
-	_, err = module.NewRepo(ctx, log.New(), "github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false)
+	_, err = module.NewRepo(ctx, log.New(), "github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false, false)
 	require.NoError(t, err)
 }
 
@@ -36,7 +36,7 @@ func TestScaffoldGitRepo(t *testing.T) {
 
 	tempDir := t.TempDir()
 
-	repo, err := module.NewRepo(ctx, log.New(), "github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false)
+	repo, err := module.NewRepo(ctx, log.New(), "github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false, false)
 	require.NoError(t, err)
 
 	modules, err := repo.FindModules(ctx)
@@ -51,7 +51,7 @@ func TestScaffoldGitModule(t *testing.T) {
 
 	tempDir := t.TempDir()
 
-	repo, err := module.NewRepo(ctx, log.New(), "https://github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false)
+	repo, err := module.NewRepo(ctx, log.New(), "https://github.com/gruntwork-io/terraform-fake-modules.git", tempDir, false, false)
 	require.NoError(t, err)
 
 	modules, err := repo.FindModules(ctx)
@@ -89,7 +89,7 @@ func TestScaffoldGitModuleHttps(t *testing.T) {
 
 	tempDir := t.TempDir()
 
-	repo, err := module.NewRepo(ctx, log.New(), "https://github.com/gruntwork-io/terraform-fake-modules", tempDir, false)
+	repo, err := module.NewRepo(ctx, log.New(), "https://github.com/gruntwork-io/terraform-fake-modules", tempDir, false, false)
 	require.NoError(t, err)
 
 	modules, err := repo.FindModules(ctx)
diff --git a/test/integration_engine_test.go b/test/integration_engine_test.go
index 443b67c6..754cbe93 100644
--- a/test/integration_engine_test.go
+++ b/test/integration_engine_test.go
@@ -3,7 +3,11 @@
 package test_test
 
 import (
+	"context"
 	"fmt"
+
+	"github.com/hashicorp/go-getter/v2"
+
 	"os"
 	"path/filepath"
 	"runtime"
@@ -20,6 +24,7 @@ import (
 )
 
 const (
+	testFixtureEngineDependency     = "fixtures/engine/engine-dependencies"
 	testFixtureLocalEngine          = "fixtures/engine/local-engine"
 	testFixtureRemoteEngine         = "fixtures/engine/remote-engine"
 	testFixtureOpenTofuEngine       = "fixtures/engine/opentofu-engine"
@@ -29,7 +34,11 @@ const (
 	envVarExperimental = "TG_EXPERIMENTAL_ENGINE"
 )
 
-var LocalEngineBinaryPath = "terragrunt-iac-engine-opentofu_rpc_" + testEngineVersion() + "_" + runtime.GOOS + "_" + runtime.GOARCH
+var (
+	engineAssetName    = "terragrunt-iac-engine-opentofu_rpc_" + testEngineVersion() + "_" + runtime.GOOS + "_" + runtime.GOARCH
+	engineAssetArchive = engineAssetName + ".zip"
+	downloadUrl        = fmt.Sprintf("https://github.com/gruntwork-io/terragrunt-engine-opentofu/releases/download/%s/%s", testEngineVersion(), engineAssetArchive)
+)
 
 func TestEngineLocalPlan(t *testing.T) {
 	rootPath := setupLocalEngine(t)
@@ -37,9 +46,10 @@ func TestEngineLocalPlan(t *testing.T) {
 	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s --terragrunt-log-level trace", rootPath))
 	require.NoError(t, err)
 
-	assert.Contains(t, stderr, LocalEngineBinaryPath)
-	assert.Contains(t, stderr, "[INFO]  plugin process exited:")
-	assert.Contains(t, stderr, "plugin process exited:")
+	assert.Contains(t, stderr, engineAssetName)
+	assert.Contains(t, stderr, "Tofu Initialization started")
+	assert.Contains(t, stderr, "Tofu Initialization completed")
+	assert.Contains(t, stderr, "Tofu Shutdown completed")
 	assert.Contains(t, stdout, "1 to add, 0 to change, 0 to destroy.")
 }
 
@@ -49,9 +59,10 @@ func TestEngineLocalApply(t *testing.T) {
 	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
-	assert.Contains(t, stderr, LocalEngineBinaryPath)
-	assert.Contains(t, stderr, "[INFO]  plugin process exited:")
-	assert.Contains(t, stderr, "plugin process exited:")
+	assert.Contains(t, stderr, engineAssetName)
+	assert.Contains(t, stderr, "Tofu Initialization started")
+	assert.Contains(t, stderr, "Tofu Initialization completed")
+	assert.Contains(t, stderr, "Tofu Shutdown completed")
 	assert.Contains(t, stdout, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
 }
 
@@ -65,8 +76,9 @@ func TestEngineOpentofu(t *testing.T) {
 	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
-	assert.Contains(t, stderr, "[INFO]  plugin process exited:")
-	assert.Contains(t, stderr, "plugin process exited:")
+	assert.Contains(t, stderr, "Tofu Initialization started")
+	assert.Contains(t, stderr, "Tofu Initialization completed")
+	assert.Contains(t, stderr, "Tofu Shutdown completed")
 	assert.Contains(t, stdout, "OpenTofu has been successfully initialized")
 	assert.Contains(t, stdout, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
 }
@@ -81,8 +93,7 @@ func TestEngineRunAllOpentofu(t *testing.T) {
 	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
-	assert.Contains(t, stderr, "[INFO]  plugin process exited")
-	assert.Contains(t, stderr, "plugin process exited:")
+	assert.Contains(t, stderr, "Tofu Initialization started")
 	assert.Contains(t, stdout, "resource \"local_file\" \"test\"")
 	assert.Contains(t, stdout, "filename             = \"./test.txt\"\n")
 	assert.Contains(t, stdout, "OpenTofu has been successful")
@@ -98,8 +109,6 @@ func TestEngineRunAllOpentofuCustomPath(t *testing.T) {
 	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
-	assert.Contains(t, stderr, "[INFO]  plugin process exited:")
-	assert.Contains(t, stderr, "plugin process exited:")
 	assert.Contains(t, stdout, "OpenTofu has been successful")
 	assert.Contains(t, stderr, "Tofu Shutdown completed")
 	assert.Contains(t, stdout, "Apply complete!")
@@ -127,8 +136,9 @@ func TestEngineDownloadOverHttp(t *testing.T) {
 	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
-	assert.Contains(t, stderr, "[INFO]  plugin process exited:")
-	assert.Contains(t, stderr, "plugin process exited:")
+	assert.Contains(t, stderr, "Tofu Initialization started")
+	assert.Contains(t, stderr, "Tofu Initialization completed")
+	assert.Contains(t, stderr, "Tofu Shutdown completed")
 	assert.Contains(t, stdout, "OpenTofu has been successfully initialized")
 	assert.Contains(t, stdout, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
 }
@@ -217,6 +227,32 @@ func TestEngineOpentofuLatestRunAll(t *testing.T) {
 	assert.Contains(t, stdout, "Apply complete!")
 }
 
+func TestEngineDependency(t *testing.T) {
+	t.Setenv(envVarExperimental, "1")
+
+	helpers.CleanupTerraformFolder(t, testFixtureEngineDependency)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureEngineDependency)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureEngineDependency)
+
+	terragruntCmd := "terragrunt apply -log-level debug -no-color -auto-approve --non-interactive --tf-forward-stdout --working-dir %s"
+
+	// Run apply in app1, make sure it uses engine
+	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(terragruntCmd, util.JoinPath(rootPath, "app1")))
+	require.NoError(t, err)
+	assert.Contains(t, stderr, "Using engine to run command: tofu apply -auto-approve -no-color")
+	assert.Contains(t, stdout, "Changes to Outputs:")
+	assert.Contains(t, stdout, "value = \"app1-test\"")
+
+	// Run apply in app2, make sure it uses engine for both app1 output and apply
+	stdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(terragruntCmd, util.JoinPath(rootPath, "app2")))
+	require.NoError(t, err)
+	assert.Contains(t, stderr, "prefix=../app1 msg=Using engine to run command: tofu output -json")
+	assert.Contains(t, stderr, "msg=Using engine to run command: tofu apply -auto-approve -no-color")
+	assert.Contains(t, stdout, "resource \"local_file\" \"test\"")
+	assert.Contains(t, stdout, "content              = \"app1-test\"")
+	assert.Contains(t, stdout, "filename             = \"./test.txt\"\n")
+}
+
 func TestEngineLogLevel(t *testing.T) {
 	t.Setenv(envVarExperimental, "1")
 
@@ -249,12 +285,16 @@ func setupLocalEngine(t *testing.T) string {
 	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalEngine)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureLocalEngine)
 
-	// get pwd
-	pwd, err := os.Getwd()
+	// download engine to local directory
+	engineDir := util.JoinPath(rootPath, "engine")
+	if err := os.MkdirAll(engineDir, 0755); err != nil {
+		require.NoError(t, err)
+	}
+	_, err := getter.GetAny(context.TODO(), engineDir, downloadUrl)
 	require.NoError(t, err)
 
 	helpers.CopyAndFillMapPlaceholders(t, util.JoinPath(testFixtureLocalEngine, "terragrunt.hcl"), util.JoinPath(rootPath, config.DefaultTerragruntConfigPath), map[string]string{
-		"__engine_source__": pwd + "/../" + LocalEngineBinaryPath,
+		"__engine_source__": filepath.Join(engineDir, engineAssetName),
 	})
 	return rootPath
 }
@@ -263,7 +303,7 @@ func setupLocalEngine(t *testing.T) string {
 func testEngineVersion() string {
 	value, found := os.LookupEnv("TOFU_ENGINE_VERSION")
 	if !found {
-		return "v0.0.1"
+		return "v0.0.16"
 	}
 	return value
 }
diff --git a/test/integration_find_test.go b/test/integration_find_test.go
new file mode 100644
index 00000000..d2df1ba1
--- /dev/null
+++ b/test/integration_find_test.go
@@ -0,0 +1,78 @@
+package test_test
+
+import (
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/test/helpers"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	testFixtureFindBasic  = "fixtures/find/basic"
+	testFixtureFindHidden = "fixtures/find/hidden"
+)
+
+func TestFindBasic(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureFindBasic)
+
+	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt find --experiment cli-redesign --no-color --working-dir "+testFixtureFindBasic)
+	require.NoError(t, err)
+
+	assert.Empty(t, stderr)
+	assert.Equal(t, "stack\nunit\n", stdout)
+}
+
+func TestFindBasicJSON(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureFindBasic)
+
+	stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt find --experiment cli-redesign --no-color --working-dir "+testFixtureFindBasic+" --json")
+	require.NoError(t, err)
+
+	assert.Empty(t, stderr)
+	assert.JSONEq(t, `[{"type": "stack", "path": "stack"}, {"type": "unit", "path": "unit"}]`, stdout)
+}
+
+func TestFindHidden(t *testing.T) {
+	t.Parallel()
+
+	tc := []struct {
+		name     string
+		hidden   bool
+		expected string
+	}{
+		{
+			name:     "visible",
+			expected: "stack\nunit\n",
+		},
+		{
+			name:     "hidden",
+			hidden:   true,
+			expected: ".hide/unit\nstack\nunit\n",
+		},
+	}
+
+	for _, tt := range tc {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			helpers.CleanupTerraformFolder(t, testFixtureFindHidden)
+
+			cmd := "terragrunt find --experiment cli-redesign --no-color --working-dir " + testFixtureFindHidden
+
+			if tt.hidden {
+				cmd += " --hidden"
+			}
+
+			stdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)
+			require.NoError(t, err)
+
+			assert.Empty(t, stderr)
+			assert.Equal(t, tt.expected, stdout)
+		})
+	}
+}
diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go
index 66969c27..e882902a 100644
--- a/test/integration_serial_test.go
+++ b/test/integration_serial_test.go
@@ -667,12 +667,12 @@ func TestReadTerragruntAuthProviderCmdRemoteState(t *testing.T) {
 	secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
 
 	// I'm not sure why, but this test doesn't work with tenv
-	os.Setenv("AWS_ACCESS_KEY_ID", "")     //nolint: tenv
-	os.Setenv("AWS_SECRET_ACCESS_KEY", "") //nolint: tenv
+	os.Setenv("AWS_ACCESS_KEY_ID", "")     //nolint: tenv,usetesting
+	os.Setenv("AWS_SECRET_ACCESS_KEY", "") //nolint: tenv,usetesting
 
 	defer func() {
-		os.Setenv("AWS_ACCESS_KEY_ID", accessKeyID)
-		os.Setenv("AWS_SECRET_ACCESS_KEY", secretAccessKey)
+		os.Setenv("AWS_ACCESS_KEY_ID", accessKeyID)         //nolint: tenv,usetesting
+		os.Setenv("AWS_SECRET_ACCESS_KEY", secretAccessKey) //nolint: tenv,usetesting
 	}()
 
 	credsConfig := util.JoinPath(rootPath, "creds.config")