Terraform path

Terraform Infrastructure as Code Troubleshooting

What is Terraform path?

Terraform path refers to one of three built-in expressions: path.module, path.root, and path.cwd, which return filesystem paths you can reference inside a resource, a local value, or a data source. They're the closest thing HCL has to showing where you are at any point in the code. The same search term also turns up the Terraform Plugin Framework's unrelated path concept and the operating system's PATH variable, both covered below.

Basic Terraform path syntax

Terraform exposes path.module, path.root, and path.cwd through a built-in path module that needs no import and no declaration; they're just available wherever an expression can go, the same way any other named value is.

# main.tf
output "path_module_demo" {
value = path.module
}

Run terraform apply from the directory containing this file, and Terraform prints path_module_demo as the filesystem path it resolved for the module where this expression lives.

Swap in path.root or path.cwd and the syntax is identical; only the resolution logic changes, which the rest of this page covers.

What Terraform path actually does

The Plugin Framework's schema-attribute path is only important if you're building a provider, and the PATH environment variable is a one-time shell setup step covered in the FAQ below.

The three path.* expressions themselves each answer a slightly different question, and the differences run deeper than the family resemblance suggests.

path.module

The module path path.module is the filesystem path of the module containing the file where you wrote the expression. Write it in a root-level .tf file, and it points at the root module's directory; write the same expression inside a child module, and it points at that module's directory instead. It resolves to wherever the expression can be found.

It commonly resolves as a relative path (a root module's own path.module is simply ., not an absolute location), mirroring how the module's source was written. HashiCorp's documentation calls path.cwd an absolute path but makes no equivalent claim for path.module.

path.root

The root path path.root answers a fixed question regardless of where you ask it. It's always the root module's directory, which is usually the directory you ran the command from, but if you're using the -chdir flag, path.root follows the -chdir target, not wherever you happened to be.

A child module referencing path.root gets the same path the root configuration would get; it's the one value in this trio that doesn't shift based on module location, whether the referencing expression lives in the root configuration or several levels deep inside a child module.

path.cwd

path.cwd differs in kind, not just scope. It's the original working directory Terraform was invoked from, before any -chdir argument, and unlike the other two, it is genuinely an absolute path carrying real filesystem-structure detail.

path.root and path.module cover most legitimate use cases; use path.cwd in advanced, invocation-sensitive cases where you genuinely want to understand where the shell happened to be.

An example of Terraform path in action

The cleanest way to see how a terraform module path value actually behaves is to write the same expression twice: once in the root module and once inside a child module it calls.

# main.tf (root module)
output "root_level_path" {
value = path.module
}
module "notify" {
source = "./modules/notify"
}
output "module_level_path" {
value = module.notify.module_level_path
}
# modules/notify/main.tf (child module)
output "module_level_path" {
value = path.module
}

Run terraform apply from the project root, and root_level_path resolves to ., because the expression sits in the root module, whose directory Terraform is already standing in.

The child module's module_level_path resolves to ./modules/notify instead, the actual relative path to that module's own files. It's the same function call each time; the result simply follows wherever the Terraform code lives, which is exactly the source of confusion when someone assumes it means the root of the project everywhere it appears.

Module source shifts this too.

The example above assumes a local module sourced from a relative directory in the same repository. Source a module from a registry or a git URL instead, and path.module resolves to wherever Terraform cloned that module during init, a hidden subdirectory of the working directory (commonly .terraform/modules), not a path you wrote or control.

Reading a file the module ships with still works either way; once you treat that download location as stable, local and remote sources diverge.

Common mistakes users make with Terraform path

path.module can produce different behavior and lead to race conditions in write operations, without ever showing the failure mode. Here it is, worked through directly.

# modules/report/main.tf
variable "region" {
type = string
}
resource "local_file" "summary" {
filename = "${path.module}/summary.json"
content = jsonencode({ region = var.region })
}
# main.tf
module "report" {
source = "./modules/report"
for_each = toset(["us-east-1", "eu-west-1"])
region = each.key
}

for_each creates two instances of the same module, sourced from the same directory, not two separate copies of it. Both instances resolve path.module to the identical path, ./modules/report, so both local_file resources target the same file, and whichever apply finishes last wins.

A module's directory stops being a unique identifier once that module runs more than once, and a write operation turns that ambiguity into lost data instead of a confusing debug session.

The portability trap is a related issue.

Bake path.cwd or path.module into a resource argument or anything else Terraform tracks as state, and re-running that configuration from a different directory or computer produces a path string that looks like a real change, even though nothing about the resource moved.

The same caution extends to resource names or other identifiers meant to stay stable: if a value can shift because of where someone ran a Terraform command, it doesn't belong anywhere Terraform treats as meaningful state.

The nearest alternative to Terraform path values

The safer tool for a stable name or path, one that has to survive being called more than once or run from a different location, isn't a path.* expression at all: it's an explicit input variable the calling module sets.

The established pattern for the analogous case, where a shared module derives a naming prefix from terraform.workspace, makes the same point.

Define an input variable for the prefix and let the caller decide it, rather than deriving a name from wherever the module happens to sit. The same logic holds for any path used in a write operation or anything else Terraform tracks as state.

Stategraph's perspective on Terraform path

Everything above holds for one engineer on one checkout. Enterprise-scale infrastructure runs differently: applies span many CI runners and pipelines, touching more than one state at once.

path.cwd was never meant to survive that shift; it's a snapshot of wherever a given invocation happened to start. path.root fares better, since it stays anchored to the root module regardless of invocation directory, but it still assumes one obvious root module per run, an assumption a cross-state pipeline coordinating several configurations at once can't always make cleanly.

Stategraph Velocity approaches the same coordination problem from the other direction: instead of reasoning about where a checkout landed, its graph-aware execution tracks resource and dependency identity directly in a database, so a change plans and applies against the actual dependency graph rather than wherever a runner placed a repository on disk.

It offers a sturdier way to settle where something belongs, and it's worth considering once path-based assumptions start needing workarounds rather than occasional care.

If that challenge sounds familiar, try Stategraph free and see what tracking resource identity through a graph, rather than wherever a checkout lands, changes about how applies run.

How do I add Terraform to PATH on Windows?

Open System Properties, go to Environment Variables, and edit the Path entry under your user or system variables. Add an entry pointing at the folder containing terraform.exe, save, then open a new terminal (existing ones won't pick up the change).

Run terraform -version to confirm. Adding the binary to PATH is entirely separate from path.module, path.root, and path.cwd; it only affects whether typing terraform finds it.

Related Terraform terms

  • Terraform filespath.module and path.root both resolve to directories full of .tf files; this covers how those files and the root module directory are typically organized.
  • Terraform workspaceterraform.workspace is the other named value HashiCorp's docs group alongside path.module, path.root, and path.cwd, and it carries the same portability warning.
  • Terraform CLI – The -chdir flag that changes what path.root and path.cwd resolve to is a Terraform CLI flag, not a path.* expression itself.
  • The dependency graph – Child modules and their paths are only important because Terraform builds a dependency graph out of them first; this covers how that graph is built and where it breaks down at scale.