Repository: gruntwork-io/terragrunt

Issue:
Terragrunt scaffold does not support boilerplate dependency and/or partials functionality when template is not part of terraform module
## Describe the bug

Boilerplate has useful functionality for dependencies and partials, which help in DRY'ing up templates if you are managing more than one.

When adding a boilerplate template to a terraform module (in a `.boilerplate` folder), the boilerplate dependency and partials functionality is fully functional. However, when providing the boilerplate template either as a cli input, or in the catalog config, the functionality is unavailable. 

The reason for this is that partials and dependencies are defined in a folder separate from the template folder. For [example](https://github.com/tgeijg/bp-demo):
```
├── dependency
│   ├── boilerplate.yml
│   └── dependency.hcl
├── partials
│   └── demo.hcl
└── template
    └── boilerplate.yml

```

When defining the partials and/or dependency folders within the terraform module, they are going to be available when scaffolding as the whole terraform module repo will be cloned, including the relevant partials and dependency folders.

When providing the template folder as a cli input or in the catalog config, the above folder structure - which is also used in the boilerplate docs - causes issues, since the code relies on go-getter to download the template folder and go-getter subsequently only downloads the template folder, not the partials and dependency folders. Or to be more precise, go-getter first clones the whole template repo to a temporary folder and then discards anything but the specified folder path. 

## Steps To Reproduce

As an example:

Running 
```
tg scaffold git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git git@github.com:tgeijg/bp-demo.git//template
```
results in the following error:
```
10:04:01.076 ERROR  Could not find boilerplate.yml in /var/folders/s3/zm9trzfs4cd6900ws1qhvfdh0000gp/T/dependency/boilerplate.yml and the missing-config-action flag is set to exit
10:04:01.325 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1
```

One might be tempted to try and specify the template repo as an input
```
tg scaffold git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git git@github.com:tgeijg/bp-demo.git//.
```
but that also fails, as scaffold needs to be told in what folder the template/`boilerplate.yml` file is located:
```
09:59:12.855 ERROR  Could not find boilerplate.yml in /var/folders/s3/zm9trzfs4cd6900ws1qhvfdh0000gp/T/template2632958686/boilerplate.yml and the missing-config-action flag is set to exit
09:59:12.980 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1
```


## Expected behavior

All boilerplate features - especially dependencies and partials - should be supported when creating templates for terragrunt scaffold, regardless of the way in which the template is provided as a scaffold input. The scaffold docs currently don't list any missing and/or unsupported boilerplate features, so the expectation is that anything mentioned in the boilerplate docs is available when drafting a boilerplate template.

## Additional context
As mentioned above, the problem exists because go-getter only downloads the specified template sub-folder, and not any supporting folders. I personally think it should in fact download the whole template repo/local folder instead.

Discussion:
@yhakbar: Worth addressing, but can't be prioritized by maintainers now. Would review community contributions to address.

Solution patch:
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index d2ee574f..60e629c6 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -28,7 +28,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
       - name: Setup Ruby
-        uses: ruby/setup-ruby@cb0fda56a307b8c78d38320cd40d9eb22a3bf04e # v1.242.0
+        uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0
         with:
           ruby-version: '3.3'
           bundler-cache: true
diff --git a/cli/commands/scaffold/scaffold.go b/cli/commands/scaffold/scaffold.go
index b76c539a..85b62014 100644
--- a/cli/commands/scaffold/scaffold.go
+++ b/cli/commands/scaffold/scaffold.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/url"
 	"os"
+	"path/filepath"
 	"regexp"
 	"strings"
 
@@ -236,33 +237,50 @@ func generateDefaultTemplate(boilerplateDir string) (string, error) {
 	return boilerplateDir, nil
 }
 
-// downloadTemplate - parse URL and download files
+// downloadTemplate - parse URL, download files, and handle subfolders
 func downloadTemplate(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, templateURL string, tempDir string) (string, error) {
 	parsedTemplateURL, err := tf.ToSourceURL(templateURL, tempDir)
 	if err != nil {
 		return "", errors.New(err)
 	}
 
-	parsedTemplateURL, err = rewriteTemplateURL(ctx, l, opts, parsedTemplateURL)
+	// Split the processed URL to get the base URL and subfolder
+	baseURL, subFolder, err := tf.SplitSourceURL(l, parsedTemplateURL)
 	if err != nil {
 		return "", errors.New(err)
 	}
-	// regenerate template url with all changes
-	templateURL = parsedTemplateURL.String()
 
-	// prepare temporary directory for template
-	templateDir, err := os.MkdirTemp("", "template")
+	// Go-getter expects a pathspec or . for file paths
+	if baseURL.Scheme == "" || baseURL.Scheme == "file" {
+		baseURL.Path = filepath.ToSlash(strings.TrimSuffix(baseURL.Path, "/")) + "//."
+	}
+
+	baseURL, err = rewriteTemplateURL(ctx, l, opts, baseURL)
 	if err != nil {
 		return "", errors.New(err)
 	}
 
-	// downloading templateURL
-	l.Infof("Using template from %s", templateURL)
+	templateDir, err := os.MkdirTemp(tempDir, "template")
+	if err != nil {
+		return "", errors.New(err)
+	}
 
-	if _, err := getter.GetAny(ctx, templateDir, templateURL); err != nil {
+	l.Infof("Downloading template from %s into %s", baseURL.String(), templateDir)
+	// Downloading baseURL to support boilerplate dependencies and partials. Go-getter discards all but specified folder if one is provided.
+	if _, err := getter.GetAny(ctx, templateDir, baseURL.String()); err != nil {
 		return "", errors.New(err)
 	}
 
+	// Add subfolder to templateDir if provided, as scaffold needs path to boilerplate.yml file
+	if subFolder != "" {
+		subFolder = strings.TrimPrefix(subFolder, "/")
+		templateDir = filepath.Join(templateDir, subFolder)
+		// Verify that subfolder exists
+		if _, err := os.Stat(templateDir); os.IsNotExist(err) {
+			return "", errors.Errorf("subfolder \"//%s\" not found in downloaded template from %s", subFolder, templateURL)
+		}
+	}
+
 	return templateDir, nil
 }
 
@@ -298,7 +316,7 @@ func prepareBoilerplateFiles(ctx context.Context, l log.Logger, opts *options.Te
 
 			boilerplateDir = tempTemplateDir
 		} else {
-			defaultTempDir, err := os.MkdirTemp("", "boilerplate")
+			defaultTempDir, err := os.MkdirTemp(tempDir, "boilerplate")
 			if err != nil {
 				return "", errors.New(err)
 			}
diff --git a/config/config.go b/config/config.go
index d1bc4549..eea8079b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1189,6 +1189,10 @@ func ParseConfigFile(ctx *ParsingContext, l log.Logger, configPath string, inclu
 
 		fileInfo, err := os.Stat(configPath)
 		if err != nil {
+			if os.IsNotExist(err) {
+				return TerragruntConfigNotFoundError{Path: configPath}
+			}
+
 			return errors.Errorf("failed to get file info: %w", err)
 		}
 
diff --git a/config/config_partial.go b/config/config_partial.go
index e389d176..0d5d8a6c 100644
--- a/config/config_partial.go
+++ b/config/config_partial.go
@@ -284,6 +284,10 @@ func PartialParseConfigFile(ctx *ParsingContext, l log.Logger, configPath string
 
 	fileInfo, err := os.Stat(configPath)
 	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, TerragruntConfigNotFoundError{Path: configPath}
+		}
+
 		return nil, errors.New(err)
 	}
 
diff --git a/config/errors.go b/config/errors.go
index 9048c612..d61e0d51 100644
--- a/config/errors.go
+++ b/config/errors.go
@@ -142,7 +142,7 @@ type TerragruntConfigNotFoundError struct {
 }
 
 func (err TerragruntConfigNotFoundError) Error() string {
-	return fmt.Sprintf("Terragrunt config %s not found", err.Path)
+	return fmt.Sprintf("You attempted to run terragrunt in a folder that does not contain a terragrunt.hcl file. Please add a terragrunt.hcl file and try again.\n\nPath: %q", err.Path)
 }
 
 type InvalidSourceURLError struct {
diff --git a/go.mod b/go.mod
index 8dba8621..525991f6 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	cloud.google.com/go/storage v1.54.0
 	dario.cat/mergo v1.0.1
 	github.com/NYTimes/gziphandler v1.1.1
-	github.com/ProtonMail/go-crypto v1.2.0
+	github.com/ProtonMail/go-crypto v1.3.0
 	github.com/aws/aws-sdk-go v1.55.7
 	github.com/aws/aws-sdk-go-v2 v1.36.3
 	github.com/charmbracelet/bubbles v0.21.0
@@ -88,7 +88,7 @@ require (
 	github.com/invopop/jsonschema v0.13.0
 	github.com/xeipuuv/gojsonschema v1.2.0
 	go.uber.org/mock v0.5.2
-	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
+	golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b
 )
 
 require (
@@ -255,9 +255,11 @@ require (
 	github.com/pquerna/otp v1.4.0 // indirect
 	github.com/pterm/pterm v0.12.80 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryanuber/go-glob v1.0.0 // indirect
 	github.com/sahilm/fuzzy v0.1.1 // indirect
+	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/sourcegraph/go-lsp v0.0.0-20240223163137-f80c5dd31dfd // indirect
 	github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect
@@ -290,7 +292,7 @@ require (
 	golang.org/x/crypto v0.38.0 // indirect
 	golang.org/x/net v0.40.0 // indirect
 	golang.org/x/time v0.11.0 // indirect
-	golang.org/x/tools v0.29.0 // indirect
+	golang.org/x/tools v0.33.0 // indirect
 	google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
diff --git a/go.sum b/go.sum
index dda97c31..3f1e9c36 100644
--- a/go.sum
+++ b/go.sum
@@ -716,8 +716,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
-github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
 github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
@@ -1613,8 +1613,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
@@ -1627,8 +1627,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
-github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -1848,8 +1848,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
-golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
-golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
+golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -2248,8 +2248,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
-golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/cas/.gitignore b/internal/cas/.gitignore
new file mode 100644
index 00000000..9ed3b07c
--- /dev/null
+++ b/internal/cas/.gitignore
@@ -0,0 +1 @@
+*.test
diff --git a/internal/cas/cas.go b/internal/cas/cas.go
index 2dd57062..4fd1399f 100644
--- a/internal/cas/cas.go
+++ b/internal/cas/cas.go
@@ -66,9 +66,14 @@ func New(opts Options) (*CAS, error) {
 
 	store := NewStore(opts.StorePath)
 
+	git, err := NewGitRunner()
+	if err != nil {
+		return nil, err
+	}
+
 	return &CAS{
 		store: store,
-		git:   NewGitRunner(),
+		git:   git,
 		opts:  opts,
 	}, nil
 }
@@ -161,6 +166,27 @@ func (c *CAS) cloneAndStoreContent(ctx context.Context, l log.Logger, opts *Clon
 }
 
 func (c *CAS) storeRootTree(ctx context.Context, l log.Logger, hash string, opts *CloneOptions) error {
+	// Get all blobs recursively in a single git ls-tree -r call at the root
+	allBlobsTree, err := c.git.LsTreeRecursive(ctx, hash, ".")
+	if err != nil {
+		return err
+	}
+
+	// Collect all blobs from recursive listing
+	var allBlobs []TreeEntry
+
+	for _, entry := range allBlobsTree.Entries() {
+		if entry.Type == "blob" {
+			allBlobs = append(allBlobs, entry)
+		}
+	}
+
+	// Store all blobs concurrently (single batch from recursive call)
+	if err := c.storeBlobs(ctx, allBlobs); err != nil {
+		return err
+	}
+
+	// Now store the tree structure (which won't need to handle blobs again)
 	if err := c.storeTree(ctx, l, hash, ""); err != nil {
 		return err
 	}
@@ -213,80 +239,32 @@ func (c *CAS) storeTree(ctx context.Context, l log.Logger, hash, prefix string)
 		return nil
 	}
 
+	// Get tree structure (no recursive blobs needed - they're already stored)
 	tree, err := c.git.LsTree(ctx, hash, ".")
 	if err != nil {
 		return err
 	}
 
-	// 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
-	)
+	// Only collect immediate tree entries (blobs are already handled at root)
+	var immediateTrees []TreeEntry
 
 	for _, entry := range tree.Entries() {
 		if prefix != "" {
 			entry.Path = filepath.Join(prefix, entry.Path)
 		}
 
-		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 entry.Type == "tree" {
+			immediateTrees = append(immediateTrees, entry)
 		}
 	}
 
-	if len(errs) > 0 {
-		return errors.Join(errs...)
+	// Store tree objects recursively
+	if err := c.storeTrees(ctx, l, immediateTrees, prefix); err != nil {
+		return err
 	}
 
+	// Store the current tree object
 	content := NewContent(c.store)
-
 	if err := content.Ensure(l, hash, tree.Data()); err != nil {
 		return err
 	}
@@ -294,8 +272,8 @@ func (c *CAS) storeTree(ctx context.Context, l log.Logger, hash, prefix string)
 	return nil
 }
 
-// storeBlobs concurrently stores blobs in the CAS
-func (c *CAS) storeBlobs(ctx context.Context, entries []TreeEntry) error {
+// 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
@@ -310,9 +288,8 @@ func (c *CAS) storeBlobs(ctx context.Context, entries []TreeEntry) error {
 		go func(hash string) {
 			defer wg.Done()
 
-			if err := c.ensureBlob(ctx, hash); err != nil {
+			if err := c.storeTree(ctx, l, hash, prefix); err != nil {
 				ch <- err
-
 				return
 			}
 
@@ -321,10 +298,9 @@ func (c *CAS) storeBlobs(ctx context.Context, entries []TreeEntry) error {
 	}
 
 	wg.Wait()
-
 	close(ch)
 
-	errs := []error{}
+	var errs []error
 
 	for err := range ch {
 		if err != nil {
@@ -339,8 +315,8 @@ func (c *CAS) storeBlobs(ctx context.Context, entries []TreeEntry) error {
 	return nil
 }
 
-// storeTrees concurrently stores trees in the CAS
-func (c *CAS) storeTrees(ctx context.Context, l log.Logger, entries []TreeEntry, prefix string) error {
+// 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
@@ -355,7 +331,7 @@ func (c *CAS) storeTrees(ctx context.Context, l log.Logger, entries []TreeEntry,
 		go func(hash string) {
 			defer wg.Done()
 
-			if err := c.storeTree(ctx, l, hash, prefix); err != nil {
+			if err := c.ensureBlob(ctx, hash); err != nil {
 				ch <- err
 
 				return
diff --git a/internal/cas/getter.go b/internal/cas/getter.go
index fb715705..ad694019 100644
--- a/internal/cas/getter.go
+++ b/internal/cas/getter.go
@@ -61,8 +61,8 @@ func (g *CASGetter) Get(ctx context.Context, req *getter.Request) error {
 	// 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://")
+	if after, ok := strings.CutPrefix(urlStr, "ssh://"); ok {
+		urlStr = after
 		// Replace the first slash with a colon
 		urlStr = strings.Replace(urlStr, "/", ":", 1)
 	}
diff --git a/internal/cas/git.go b/internal/cas/git.go
index a261e73d..82c44ba9 100644
--- a/internal/cas/git.go
+++ b/internal/cas/git.go
@@ -18,20 +18,30 @@ const (
 
 // GitRunner handles git command execution
 type GitRunner struct {
+	GitPath string
 	WorkDir string
 }
 
 // NewGitRunner creates a new GitRunner instance
-func NewGitRunner() *GitRunner {
-	return &GitRunner{}
+func NewGitRunner() (*GitRunner, error) {
+	gitPath, err := exec.LookPath("git")
+	if err != nil {
+		return nil, &WrappedError{
+			Op:      "git",
+			Context: "git not found",
+			Err:     ErrCommandSpawn,
+		}
+	}
+
+	return &GitRunner{GitPath: gitPath}, nil
 }
 
 // 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,
-	}
+	copy := *g
+	copy.WorkDir = workDir
+
+	return &copy
 }
 
 // RequiresWorkDir returns an error if no working directory is set
@@ -206,6 +216,32 @@ func (g *GitRunner) LsTree(ctx context.Context, reference, path string) (*Tree,
 	return ParseTree(stdout.String(), path)
 }
 
+// LsTreeRecursive runs git ls-tree -r and returns all blobs recursively
+// This eliminates the need for multiple separate ls-tree calls on subtrees
+func (g *GitRunner) LsTreeRecursive(ctx context.Context, reference, path string) (*Tree, error) {
+	if err := g.RequiresWorkDir(); err != nil {
+		return nil, err
+	}
+
+	// Use recursive ls-tree to get all blobs in a single command
+	cmd := g.prepareCommand(ctx, "ls-tree", "-r", 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_recursive",
+			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 {
@@ -240,5 +276,5 @@ func (g *GitRunner) SetWorkDir(dir string) {
 }
 
 func (g *GitRunner) prepareCommand(ctx context.Context, name string, args ...string) *exec.Cmd {
-	return exec.CommandContext(ctx, "git", append([]string{name}, args...)...)
+	return exec.CommandContext(ctx, g.GitPath, append([]string{name}, args...)...)
 }
diff --git a/internal/cas/tree.go b/internal/cas/tree.go
index 4f5ecc4a..62b5d95f 100644
--- a/internal/cas/tree.go
+++ b/internal/cas/tree.go
@@ -6,14 +6,10 @@ import (
 	"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
@@ -101,96 +97,41 @@ func ParseTree(output, path string) (*Tree, error) {
 
 // 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)
+	content := NewContent(store)
 
 	for _, entry := range t.entries {
-		wg.Add(1)
+		// Check for cancellation
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
 
-		go func(entry TreeEntry) {
-			defer wg.Done()
+		entryPath := filepath.Join(targetDir, entry.Path)
+		if err := os.MkdirAll(filepath.Dir(entryPath), DefaultDirPerms); err != nil {
+			return wrapError("mkdir_all", entryPath, err)
+		}
 
-			select {
-			case <-ctx.Done():
-				return
-			default:
+		switch entry.Type {
+		case "blob":
+			if err := content.Link(entry.Hash, entryPath); err != nil {
+				return wrapError("link_blob", entryPath, err)
 			}
-
-			select {
-			case semaphore <- struct{}{}:
-				defer func() { <-semaphore }()
-			case <-ctx.Done():
-				return
+		case "tree":
+			treeData, err := content.Read(entry.Hash)
+			if err != nil {
+				return wrapError("read_tree", entry.Hash, err)
 			}
 
-			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
+			subTree, err := ParseTree(string(treeData), entryPath)
+			if err != nil {
+				return wrapError("parse_tree", entry.Hash, err)
 			}
 
-			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
-				}
+			if err := subTree.LinkTree(ctx, store, entryPath); err != nil {
+				return wrapError("link_subtree", entryPath, err)
 			}
-		}(entry)
-	}
-
-	wg.Wait()
-
-	if len(errs) > 0 {
-		return errors.Join(errs...)
+		}
 	}
 
 	return nil



Test patch:
diff --git a/internal/cas/benchmark_test.go b/internal/cas/benchmark_test.go
index 17711cfc..e5f41172 100644
--- a/internal/cas/benchmark_test.go
+++ b/internal/cas/benchmark_test.go
@@ -24,6 +24,8 @@ func BenchmarkClone(b *testing.B) {
 
 		b.ResetTimer()
 		for i := 0; b.Loop(); i++ {
+			b.StopTimer()
+
 			storePath := filepath.Join(tempDir, "store", strconv.Itoa(i))
 			targetPath := filepath.Join(tempDir, "repo", strconv.Itoa(i))
 
@@ -34,6 +36,8 @@ func BenchmarkClone(b *testing.B) {
 				b.Fatal(err)
 			}
 
+			b.StartTimer()
+
 			if err := c.Clone(b.Context(), l, &cas.CloneOptions{
 				Dir: targetPath,
 			}, repo); err != nil {
@@ -60,7 +64,10 @@ func BenchmarkClone(b *testing.B) {
 		}
 
 		b.ResetTimer()
+
 		for i := 0; b.Loop(); i++ {
+			b.StopTimer()
+
 			targetPath := filepath.Join(tempDir, "repo", strconv.Itoa(i))
 
 			c, err := cas.New(cas.Options{
@@ -70,6 +77,8 @@ func BenchmarkClone(b *testing.B) {
 				b.Fatal(err)
 			}
 
+			b.StartTimer()
+
 			if err := c.Clone(b.Context(), l, &cas.CloneOptions{
 				Dir: targetPath,
 			}, repo); err != nil {
@@ -91,7 +100,12 @@ func BenchmarkContent(b *testing.B) {
 
 	b.Run("store", func(b *testing.B) {
 		for i := 0; b.Loop(); i++ {
-			hash := fmt.Sprintf("benchmark%d", i)
+			b.StopTimer()
+
+			hash := "benchmark" + strconv.Itoa(i)
+
+			b.StartTimer()
+
 			if err := content.Store(l, hash, testData); err != nil {
 				b.Fatal(err)
 			}
@@ -127,7 +141,11 @@ func BenchmarkContent(b *testing.B) {
 func BenchmarkGitOperations(b *testing.B) {
 	// Setup a git repository for testing
 	repoDir := b.TempDir()
-	git := cas.NewGitRunner().WithWorkDir(repoDir)
+	git, err := cas.NewGitRunner()
+	if err != nil {
+		b.Fatal(err)
+	}
+	git = git.WithWorkDir(repoDir)
 
 	ctx := b.Context()
 
@@ -136,7 +154,12 @@ func BenchmarkGitOperations(b *testing.B) {
 	}
 
 	b.Run("ls-remote", func(b *testing.B) {
-		git := cas.NewGitRunner() // No workDir needed for ls-remote
+		git, err := cas.NewGitRunner()
+		if err != nil {
+			b.Fatal(err)
+		}
+		git = git.WithWorkDir(repoDir)
+
 		b.ResetTimer()
 		for b.Loop() {
 			_, err := git.LsRemote(ctx, "https://github.com/gruntwork-io/terragrunt.git", "HEAD")
diff --git a/internal/cas/git_test.go b/internal/cas/git_test.go
index d3304308..ee266b74 100644
--- a/internal/cas/git_test.go
+++ b/internal/cas/git_test.go
@@ -12,7 +12,8 @@ import (
 
 func TestGitRunner_LsRemote(t *testing.T) {
 	t.Parallel()
-	runner := cas.NewGitRunner()
+	runner, err := cas.NewGitRunner()
+	require.NoError(t, err)
 
 	ctx := t.Context()
 
@@ -52,8 +53,10 @@ func TestGitRunner_Clone(t *testing.T) {
 	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")
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		runner = runner.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
@@ -63,8 +66,9 @@ func TestGitRunner_Clone(t *testing.T) {
 
 	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")
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		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)
@@ -74,8 +78,10 @@ func TestGitRunner_Clone(t *testing.T) {
 	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, "")
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		runner = runner.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)
@@ -85,7 +91,8 @@ func TestGitRunner_Clone(t *testing.T) {
 
 func TestCreateTempDir(t *testing.T) {
 	t.Parallel()
-	git := cas.NewGitRunner()
+	git, err := cas.NewGitRunner()
+	require.NoError(t, err)
 	dir, cleanup, err := git.CreateTempDir()
 	require.NoError(t, err)
 	t.Cleanup(func() {
@@ -142,10 +149,12 @@ func TestGitRunner_LsTree(t *testing.T) {
 	t.Run("valid repository", func(t *testing.T) {
 		t.Parallel()
 		cloneDir := t.TempDir()
-		runner := cas.NewGitRunner().WithWorkDir(cloneDir)
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		runner = runner.WithWorkDir(cloneDir)
 
 		// First clone a repository
-		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
+		err = runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
 		require.NoError(t, err)
 
 		// Then try to ls-tree HEAD
@@ -169,8 +178,10 @@ func TestGitRunner_LsTree(t *testing.T) {
 
 	t.Run("ls-tree without workdir fails", func(t *testing.T) {
 		t.Parallel()
-		runner := cas.NewGitRunner()
-		_, err := runner.LsTree(ctx, "HEAD", ".")
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+
+		_, err = runner.LsTree(ctx, "HEAD", ".")
 		require.Error(t, err)
 		var wrappedErr *cas.WrappedError
 		require.ErrorAs(t, err, &wrappedErr)
@@ -180,10 +191,12 @@ func TestGitRunner_LsTree(t *testing.T) {
 	t.Run("invalid reference", func(t *testing.T) {
 		t.Parallel()
 		cloneDir := t.TempDir()
-		runner := cas.NewGitRunner().WithWorkDir(cloneDir)
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		runner = runner.WithWorkDir(cloneDir)
 
 		// First clone a repository
-		err := runner.Clone(ctx, "https://github.com/gruntwork-io/terragrunt.git", true, 1, "main")
+		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
@@ -196,10 +209,12 @@ func TestGitRunner_LsTree(t *testing.T) {
 
 	t.Run("invalid repository", func(t *testing.T) {
 		t.Parallel()
-		runner := cas.NewGitRunner().WithWorkDir(t.TempDir())
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		runner = runner.WithWorkDir(t.TempDir())
 
 		// Try to ls-tree in an empty directory
-		_, err := runner.LsTree(ctx, "HEAD", ".")
+		_, err = runner.LsTree(ctx, "HEAD", ".")
 		require.Error(t, err)
 		var wrappedErr *cas.WrappedError
 		require.ErrorAs(t, err, &wrappedErr)
@@ -212,15 +227,18 @@ func TestGitRunner_RequiresWorkDir(t *testing.T) {
 
 	t.Run("with workdir", func(t *testing.T) {
 		t.Parallel()
-		runner := cas.NewGitRunner().WithWorkDir(t.TempDir())
-		err := runner.RequiresWorkDir()
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		runner = runner.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()
+		runner, err := cas.NewGitRunner()
+		require.NoError(t, err)
+		err = runner.RequiresWorkDir()
 		require.Error(t, err)
 		var wrappedErr *cas.WrappedError
 		require.ErrorAs(t, err, &wrappedErr)
diff --git a/internal/cas/integration_test.go b/internal/cas/integration_test.go
index 9ca045da..84e386b0 100644
--- a/internal/cas/integration_test.go
+++ b/internal/cas/integration_test.go
@@ -120,7 +120,10 @@ func TestIntegration_TreeStorage(t *testing.T) {
 		}, "https://github.com/gruntwork-io/terragrunt.git"))
 
 		// Get the commit hash
-		git := cas.NewGitRunner().WithWorkDir(filepath.Join(tempDir, "repo"))
+		git, err := cas.NewGitRunner()
+		require.NoError(t, err)
+
+		git = git.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)
diff --git a/test/fixtures/scaffold/custom-default-template/root.hcl b/test/fixtures/scaffold/custom-default-template/root.hcl
index 87758c50..004ca2d4 100644
--- a/test/fixtures/scaffold/custom-default-template/root.hcl
+++ b/test/fixtures/scaffold/custom-default-template/root.hcl
@@ -1,3 +1,3 @@
 catalog {
-  default_template = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template"
+  default_template = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template/template"
 }
diff --git a/test/fixtures/scaffold/external-template/boilerplate.yml b/test/fixtures/scaffold/external-template/boilerplate.yml
deleted file mode 100644
index 870ee34a..00000000
--- a/test/fixtures/scaffold/external-template/boilerplate.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-# boilerplate config
-variables:
diff --git a/test/fixtures/scaffold/external-template/dependency/boilerplate.yml b/test/fixtures/scaffold/external-template/dependency/boilerplate.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixtures/scaffold/external-template/dependency/dependency.txt b/test/fixtures/scaffold/external-template/dependency/dependency.txt
new file mode 100644
index 00000000..57c70e8b
--- /dev/null
+++ b/test/fixtures/scaffold/external-template/dependency/dependency.txt
@@ -0,0 +1 @@
+# Dependency
\ No newline at end of file
diff --git a/test/fixtures/scaffold/external-template/template/boilerplate.yml b/test/fixtures/scaffold/external-template/template/boilerplate.yml
new file mode 100644
index 00000000..7b75a8a5
--- /dev/null
+++ b/test/fixtures/scaffold/external-template/template/boilerplate.yml
@@ -0,0 +1,6 @@
+# boilerplate config
+
+dependencies:
+  - name: test-dependency
+    template-url: ../dependency
+    output-folder: dependency/
diff --git a/test/fixtures/scaffold/external-template/external-template.txt b/test/fixtures/scaffold/external-template/template/external-template.txt
similarity index 100%
rename from test/fixtures/scaffold/external-template/external-template.txt
rename to test/fixtures/scaffold/external-template/template/external-template.txt
diff --git a/test/fixtures/scaffold/external-template/terragrunt.hcl b/test/fixtures/scaffold/external-template/template/terragrunt.hcl
similarity index 100%
rename from test/fixtures/scaffold/external-template/terragrunt.hcl
rename to test/fixtures/scaffold/external-template/template/terragrunt.hcl
diff --git a/test/integration_scaffold_ssh_test.go b/test/integration_scaffold_ssh_test.go
index 7f9869b2..8fda023b 100644
--- a/test/integration_scaffold_ssh_test.go
+++ b/test/integration_scaffold_ssh_test.go
@@ -22,6 +22,13 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+const (
+	testScaffoldModuleGit                 = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/scaffold-module"
+	testScaffoldTemplateModule            = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/module-with-template"
+	testScaffoldExternalTemplateModule    = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template/template"
+	testScaffoldWithCustomDefaultTemplate = "fixtures/scaffold/custom-default-template"
+)
+
 func TestSSHScaffoldWithCustomDefaultTemplate(t *testing.T) {
 	t.Parallel()
 
@@ -52,6 +59,7 @@ func TestSSHScaffoldModuleExternalTemplate(t *testing.T) {
 	assert.Contains(t, stderr, "Scaffolding completed")
 	// check that exists file from external template
 	assert.FileExists(t, tmpEnvPath+"/external-template.txt")
+	assert.FileExists(t, tmpEnvPath+"/dependency/dependency.txt")
 }
 
 func TestSSHScaffoldModuleDifferentRevisionAndSSH(t *testing.T) {
@@ -69,7 +77,6 @@ func TestSSHScaffoldModuleSSH(t *testing.T) {
 	t.Parallel()
 
 	tmpEnvPath := t.TempDir()
-
 	_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s", tmpEnvPath, testScaffoldModuleGit))
 	require.NoError(t, err)
 	assert.Contains(t, stderr, "Scaffolding completed")
diff --git a/test/integration_scaffold_test.go b/test/integration_scaffold_test.go
index 8e14c29e..8e210471 100644
--- a/test/integration_scaffold_test.go
+++ b/test/integration_scaffold_test.go
@@ -14,15 +14,11 @@ import (
 )
 
 const (
-	testScaffoldModuleURL                 = "https://github.com/gruntwork-io/terragrunt.git//test/fixtures/scaffold/scaffold-module"
-	testScaffoldModuleGit                 = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/scaffold-module"
-	testScaffoldModuleShort               = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs"
-	testScaffoldTemplateModule            = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/module-with-template"
-	testScaffoldExternalTemplateModule    = "git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template"
-	testScaffoldLocalModulePath           = "fixtures/scaffold/scaffold-module"
-	testScaffoldWithRootHCL               = "fixtures/scaffold/root-hcl"
-	testScaffold3rdPartyModulePath        = "git::https://github.com/Azure/terraform-azurerm-avm-res-compute-virtualmachine.git//.?ref=v0.15.0"
-	testScaffoldWithCustomDefaultTemplate = "fixtures/scaffold/custom-default-template"
+	testScaffoldModuleURL          = "https://github.com/gruntwork-io/terragrunt.git//test/fixtures/scaffold/scaffold-module"
+	testScaffoldModuleShort        = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs"
+	testScaffoldLocalModulePath    = "fixtures/scaffold/scaffold-module"
+	testScaffoldWithRootHCL        = "fixtures/scaffold/root-hcl"
+	testScaffold3rdPartyModulePath = "git::https://github.com/Azure/terraform-azurerm-avm-res-compute-virtualmachine.git//.?ref=v0.15.0"
 )
 
 func TestScaffoldModule(t *testing.T) {