Home
Blog
Terraform Path Values: path.module, path.root, and path.cwd Explained

Terraform Path Values: path.module, path.root, and path.cwd Explained

Zeen Rachidi
Product Marketing
Abstract isometric art with a compass, cubes, and a radar scope
with special guest
Mitchell
Hashimoto
Mitchell Hashimoto headshot

Terraform gives you three built-in values for finding your way around the filesystem: path.module, path.root, and path.cwd. They look interchangeable right up until a module gets called from somewhere unexpected, a pipeline runs from a different directory than your laptop does, or someone reaches for the -chdir flag for the first time. Pick the wrong one and a module that worked perfectly on your machine breaks the moment someone else calls it.

This guide covers what each value actually resolves to, where they diverge, and the specific mistakes that break portability the moment a module gets reused, including a couple of gotchas that HashiCorp's own reference docs flag more directly than most third-party writeups do.

What are Terraform path values?

path.module, path.root, and path.cwd belong to a small set of named values Terraform calls filesystem and workspace info. You reference them directly in any expression, the same way you'd reference a resource attribute or an input variable. No function call is required. You just write path.module (or path.root, or path.cwd) wherever you need a directory path.

ValueResolves toChanges with module locationChanges with where you run TerraformBest used for
path.moduleDirectory of the module where the expression is writtenYesNoFiles packaged with a module: templates, scripts, policy documents
path.rootDirectory of the root module of the configurationNoNoFiles that belong to the overall configuration, not a specific module
path.cwdThe shell's working directory when Terraform started, before -chdirNoYesRare cases where you deliberately need the invocation directory, not the config directory

In most Terraform code, you want path.module. It's the only one of the three considered safe inside a reusable module, because it's the only one anchored to the module's own location rather than to how or where Terraform happens to be invoked.

path.module in practice

Say a child module ships its own template file next to its configuration:

my_project/
├── main.tf
├── variables.tf
└── modules/
└── web_tier/
├── main.tf
└── templates/
└── nginx.conf.tftpl

Inside modules/web_tier/main.tf:

resource "aws_instance" "web" {
ami = data.aws_ami.app.id
instance_type = "t3.micro"

user_data = templatefile("${path.module}/templates/nginx.conf.tftpl", {
port = var.port
})
}

No matter where web_tier gets called from (the root module, a nested wrapper module, a completely separate project that vendors it in) path.module still resolves to modules/web_tier, so templatefile() finds nginx.conf.tftpl sitting right next to it. Once you have that path in hand, functions like dirname(), basename(), and fileset() cover most of what you'd want to do with it next.

HashiCorp's own reference docs are more cautious here than most blog coverage: path.module is fine for reads, but avoid it for writes. Consider a module called with for_each:

module "site" {
source = "./modules/web_tier"
for_each = var.sites
name = each.key
}

Inside web_tier, something like this looks safe because each.key differs per instance:

resource "local_file" "manifest" {
filename = "${path.module}/generated/manifest.json"
content = jsonencode(local.manifest)
}

But path.module doesn't vary by instance. Every invocation of a local module shares the same source directory on disk, so every instance in that for_each is writing the exact same file at the exact same time. That's the race condition HashiCorp's docs warn about directly, and it tends not to show up until two or more instances of the module are actually running concurrently. If you need a reminder on how count and for_each behave on module blocks, our Terraform Modules guide covers the basics.

path.root in practice

path.root always points to the directory of the root module, the configuration Terraform was pointed at (or -chdir'd into) for this run, regardless of which module the expression lives in:

locals {
shared_tags = jsondecode(file("${path.root}/tags.json"))
}

This resolves the same way whether the expression sits in the root module or three levels deep in a child module. path.root doesn't move. If you're still deciding what belongs in the root configuration versus a child module in the first place, our Terraform Files and Folder Structure guide covers that layout question directly.

HashiCorp's guidance goes a step further than most coverage of path.root: aside from path.module, they recommend keeping these filesystem and workspace values out of any module meant to be called by someone else, and reserving them for the root module only. The same caution applies to terraform.workspace. A shared module that bakes in an assumption about its caller's root directory, or its workspace name, stops being portable the same way a hardcoded absolute path would:

# Inside a reusable module: avoid this
resource "aws_s3_bucket" "logs" {
bucket = "logs-${terraform.workspace}"
}

# Take it as an input instead, and let the caller decide
variable "name_prefix" {
type = string
}

resource "aws_s3_bucket" "logs" {
bucket = "logs-${var.name_prefix}"
}

The fix is the same one you'd reach for with any hardcoded value in a shared module: turn it into an input variable and let the calling configuration decide, rather than letting the module infer it from context that changes depending on who's calling it.

path.cwd and the -chdir flag

path.cwd is the one value of the three that has nothing to do with your configuration's structure. It's simply the directory your shell was in when you ran the terraform command, captured before any -chdir argument is applied:

$ pwd
/home/dev/repo
$ terraform -chdir=environments/prod plan

Given outputs like these inside environments/prod:

output "root" {
value = path.root
}

output "cwd" {
value = path.cwd
}

path.root resolves to /home/dev/repo/environments/prod, following -chdir the way path.module would too. path.cwd still resolves to /home/dev/repo, the directory you were actually standing in when you typed the command.

That gap is exactly why HashiCorp recommends avoiding path.cwd where path.root or path.module would do the job. path.cwd encodes where the terminal happened to be, which a CI runner, a teammate's machine, or a wrapper script one directory removed from yours won't reproduce. If a resource argument depends on it, moving the invocation changes that value even though nothing in the actual configuration changed, and Terraform reads that as a diff, not a no-op.

Does OpenTofu handle these the same way?

Yes. OpenTofu's own reference documentation defines path.module, path.root, and path.cwd in nearly identical language to Terraform's, down to the same recommendation to prefer path.root or path.module over path.cwd. If you're running a mixed Terraform and OpenTofu estate, or migrating between the two, path values aren't something you need to re-verify per binary. The behavior didn't diverge when OpenTofu forked.

Managing Terraform paths with env zero

Two mechanics in env zero make these path values worth getting right rather than something you patch around after a deployment fails.

  • The Working Directory field sets your effective root module. If your Terraform configuration lives in a subdirectory of the repository, you point env zero at it from the Working Directory field under a template's Advanced settings. That subdirectory becomes the root module for the deployment, so path.root (and any path.module reference at that top level) resolves relative to it, not to the repository root. A module that assumes path.root means "the repo root" rather than "wherever this deployment's working directory points" runs into that assumption the first time the field gets set or changed.
  • Remote runners don't persist a working copy between deployments. env zero's Ad Hoc Tasks pull a fresh git clone for each run, and standard deployments work the same way. Terraform code that leans on path.cwd to reach something outside the checked-out configuration, an absolute path baked in during a prior local run, a file a teammate left in a directory above the repo, has nothing to find once that clone starts fresh. path.module and path.root both resolve from what's actually inside the checkout, which is one more reason they're the values worth defaulting to.

Best practices when using Terraform path values

  • Default to path.module inside anything meant to be called more than once.
  • Reserve path.root for genuinely root-level concerns (shared config, an environments/ folder) and keep it out of modules meant for reuse.
  • Treat path.cwd as an escape hatch, not a default. If you're reaching for it, check whether path.root would do the same job more portably first.
  • Avoid writing files into path.module or path.root from provisioners without a clear ownership story. Multiple instances of the same module can share the exact same path.module.
  • If a shared module needs a unique name or prefix, take it as an input variable. Don't derive it from terraform.workspace or path.root inside the module itself.
  • Test path-dependent code from more than one working directory, and with -chdir if you use it, before calling it done. A single successful local run doesn't confirm portability.

Frequently asked questions

Q. What's the difference between path.module, path.root, and path.cwd?

path.module is the directory of the module where the expression is written, and it changes depending on which module you're in. path.root is the directory of the root module for the whole configuration, and it stays the same no matter which module references it. path.cwd is the directory you were in when you ran the terraform command, before any -chdir flag, and it has nothing to do with your configuration's structure at all.

Q. Is path.module an absolute path or a relative path?

Absolute. All three path values, path.module, path.root, and path.cwd, resolve to absolute filesystem paths.

Q. Does path.root change inside a child module?

No. path.root always points to the root module's directory, even when it's referenced from several levels deep inside a child module.

Q. Do OpenTofu and Terraform handle path.module, path.root, and path.cwd the same way?

Yes. OpenTofu's reference documentation defines all three with the same behavior and the same guidance to prefer path.root or path.module over path.cwd. This is not something that diverged when OpenTofu forked from Terraform.

Q. Does path.module change across instances of a for_each or count module call?

No, and this is a common source of surprise. Multiple instances of the same local module call share the same source directory, so they share the same path.module value. That's fine for reads, but HashiCorp's own docs specifically warn against writing to it in that situation, since concurrent instances can end up racing to write the same path at the same time.

Q. Why is path.cwd considered risky in CI/CD or remote runs?

Because it depends on the directory you happened to invoke Terraform from, not on your configuration's structure. A CI runner, a teammate's machine, or an orchestration platform that re-clones your repository for every run can each have a different working directory than your laptop does, which makes anything derived from path.cwd unreliable outside one consistent environment.

Key points

path.module, path.root, and path.cwd solve the same underlying problem, finding a file reliably regardless of how or where Terraform runs, but only two of them are safe defaults. path.module keeps a module's own files self-contained and portable. path.root is the right choice for genuinely root-level concerns, kept out of shared modules. path.cwd is worth reaching for only when you specifically mean the invocation directory, and even then it's worth testing from more than one working directory before you trust it.

Schedule a technical demo
See env zero in action
Schedule demo

Related Content

All articles
Abstract isometric art with a compass, cubes, and a radar scope
The OpenTofu and Terraform logo marks next to each other
Long strings comprised of cubes
Green cube surrounded by white cubes