tfv

tfv CLI

Install, switch and pin Terraform, OpenTofu and Terragrunt versions on macOS, Linux and Windows, without sudo.

tfv installs, switches and manages several versions of Terraform, OpenTofu and Terragrunt, without sudo. Every binary lives in ~/.tfv/, so upgrading tfv itself never removes the versions you installed.

Installing Terraform, OpenTofu and Terragrunt, switching, auto-switch and the cd hook, pin, exec, targeted plans, plugin cache, doctor and prune. All output is from tfv 6.3.0.

Install

npm install -g tfv

On install, tfv adds ~/.tfv/bin to your PATH (shell configs, fish conf.d, or the Windows User PATH). Restart your terminal once afterwards.

If your package manager skips install scripts (pnpm 10, Bun, Yarn Berry, --ignore-scripts) or you installed with sudo, the same setup runs the first time you run tfv.

How it works

npm install -g tfv
       │
       ▼
postInstall.js  (or first `tfv` run, if install scripts were skipped)
  ├── creates ~/.tfv/bin/, ~/.tfv/store/, ~/.tfv/cache/
  └── adds ~/.tfv/bin to PATH (shell configs / Windows registry)
       │
       ▼
tfv install 1.9.0
  ├── fetches version list  →  ~/.tfv/cache/terraform-versions.json  (1hr TTL)
  ├── downloads zip         →  system temp dir  (with progress bar)
  ├── verifies SHA256       →  HashiCorp / OpenTofu checksums
  ├── extracts binary only  →  ~/.tfv/store/terraform/1.9.0
  └── records arch          →  ~/.tfv/store/terraform/arch.json
       │
       ▼
tfv use 1.9.0
  ├── copies ~/.tfv/store/terraform/1.9.0  →  ~/.tfv/bin/terraform
  └── writes active version  →  ~/.tfv/active.json
       │
       ▼
terraform plan   ← resolves to ~/.tfv/bin/terraform  (same binary as tfv)
tfv plan         ← spawns ~/.tfv/bin/terraform directly (no PATH lookup)

terraform and tfv always run the exact same binary.

Store layout

~/.tfv/
  bin/
    terraform          ← active terraform binary (no sudo, no symlinks)
    tofu               ← active opentofu binary
    terragrunt         ← active terragrunt binary
  store/
    terraform/
      1.9.0            ← installed versions (binary renamed to version number)
      1.7.3
      arch.json        ← { "1.9.0": "arm64", "1.7.3": "amd64" }
    opentofu/
      1.8.0
      arch.json
    terragrunt/
      1.2.1
      arch.json
  cache/
    terraform-versions.json   ← remote list cached for 1 hour
    opentofu-versions.json
  active.json          ← { "terraform": "1.9.0", "opentofu": null, "terragrunt": "1.2.1" }

Install a version

tfv install latest                   # latest stable
tfv install 1.9.0                    # exact version
tfv install 1.9.^                    # latest 1.9.x patch
tfv install 1.8.0-beta1              # explicit pre-release (warns you)
tfv install latest --provider tofu   # OpenTofu
tfv install 1.9.0 --arch amd64       # override architecture

Alias: tfv i.

Switch version

tfv use 1.9.0
tfv use latest                       # latest installed version
tfv use 1.8.0 --provider tofu

No sudo: it copies the binary to ~/.tfv/bin/terraform.

Auto-switch

Switches to the version your project needs, and installs it first if it isn't in the store.

tfv auto-switch
tfv as                               # alias
tfv as --provider tofu

It reads the version from, in order:

  1. A version file in the current directory:
    • Terraform: .terraform-version, .tfswitchrc, .tfswitch.toml, or a terraform line in .tool-versions (asdf / mise)
    • OpenTofu: .opentofu-version, .tofu-version, an opentofu or tofu line in .tool-versions, then .terraform-version
  2. terraform.tfstate → the terraform_version field
  3. required_version in any .tf file. All constraint operators work: =, >=, >, <=, <, !=, ~>, and compound ones like ">= 1.3, < 2.0"

Switch on cd

The shell hook runs whenever you change directory (it uses your shell's own hooks, such as zsh's chpwd), so entering a Terraform project switches the version for you. Other folders cost one filesystem check and no subprocess.

A folder counts as a Terraform project if it has:

  • a version file (.terraform-version, .opentofu-version, .tofu-version, .tfswitchrc, .tfswitch.toml), or
  • a .tool-versions file with a terraform, opentofu or tofu line, or
  • any *.tf files

It also switches OpenTofu when the project pins it with its own file (.opentofu-version, .tofu-version or .tool-versions).

cd ~/Downloads           # no .tf files → nothing happens
cd ~/projects/react-app  # no .tf files → nothing happens
cd ~/projects/infra      # has main.tf  → auto-switch runs silently
cd ~/projects/platform   # has .terraform-version → auto-switch runs silently

Set it up once:

# Bash
printf '\neval "$(tfv shell-init bash)"\n' >> ~/.bashrc

# Zsh
printf '\neval "$(tfv shell-init zsh)"\n' >> ~/.zshrc

# Fish
printf '\ntfv shell-init fish | source\n' >> ~/.config/fish/config.fish

# PowerShell
Add-Content $PROFILE "`nInvoke-Expression (tfv shell-init powershell | Out-String)"

shell-init also adds tab completion: tfv <tab> completes commands, tfv use <tab> completes installed versions, and tfv --provider <tab> completes provider names.

List versions

tfv list                             # installed versions (default)
tfv ls --remote                      # all available versions from HashiCorp
tfv ls --remote --provider tofu      # all available OpenTofu versions

The active version is marked with 🚀.

Current version

tfv current                          # active version + PATH status
tfv which                            # alias
tfv current --provider tofu
Active terraform version: 1.9.0
Binary: /Users/you/.tfv/bin/terraform
Reported: Terraform v1.9.0
PATH OK — 'terraform' resolves to tfv-managed binary

Pin a version

Writes a version file in the current folder, so teammates get the same version with tfv auto-switch. Each tool has its own file, so pinning one never switches another.

tfv pin                              # pin the active version → .terraform-version
tfv pin 1.9.0                        # pin a specific version
tfv pin --provider tofu              # → .opentofu-version
tfv pin --provider terragrunt        # → .terragrunt-version

Upgrade

tfv upgrade                          # active 1.6.3 → latest 1.6.x patch
tfv upgrade 1.9                      # install + use latest 1.9.x
tfv upgrade latest                   # install + use the absolute latest
tfv upgrade --provider tofu

It also moves ~/.tfv/bin back to the front of PATH, ahead of any system-installed Terraform.

Remove and prune

tfv remove 1.7.3
tfv rm 1.7.3 1.6.6                   # remove several
tfv rm 1.8.0 --provider tofu

remove warns you if the version is the active one. To free space, prune removes every version except the active one:

tfv prune                            # keep only the active version
tfv prune --keep 2                   # keep the 2 most recent + the active version
tfv prune --provider tofu
tfv prune --yes                      # skip the confirmation prompt

The active version is always kept, whatever --keep says.

Run one command with another version

exec runs a single command with an installed version, without changing the active one. Handy for cross-version testing and CI. Put Terraform/OpenTofu flags after --.

tfv exec 1.9.0 -- version
tfv exec 1.8.0 -- plan -var="env=prod"
tfv exec 1.7.3 --provider tofu -- validate

Doctor

A full health check: store folders, active binaries, PATH order, PATH conflicts and shell config. It exits with code 1 if anything is wrong.

tfv doctor
tfv doctor

Store
  ✔  ~/.tfv/store/terraform/ exists
  ✔  ~/.tfv/store/opentofu/ exists

Active versions
  ✔  terraform: active version is 1.9.0
  ✔  terraform: binary exists
  ✔  terraform: executes  (Terraform v1.9.0)
  –  tofu: not set up (no versions installed)

PATH
  ✔  ~/.tfv/bin is in PATH
  ✔  ~/.tfv/bin precedes system dirs in PATH
  ✔  'terraform' resolves to tfv-managed binary
  ✔  PATH block found in shell config (~/.zshrc)

Cache
  ✔  terraform version cache exists
  –  opentofu version cache not yet created

All checks passed. tfv is healthy.

Terraform commands through tfv

These use the tfv-managed binary. Extra Terraform flags go after --.

tfv init
tfv validate
tfv fmt
tfv fmt -- -recursive

tfv plan
tfv plan --file main.tf                        # targets taken from a file
tfv plan --file main.tf --file network.tf      # several files
tfv plan --file main.tf -- -var="env=prod"     # with extra terraform flags

tfv apply --file main.tf -- -auto-approve
tfv destroy --file main.tf -- -auto-approve

--file reads .tf files and adds a -target for every resource, data and module block it finds. Comments are stripped first. Every command takes --provider tofu to use OpenTofu.

OpenTofu

Every command works with OpenTofu through --provider tofu (or --provider opentofu):

tfv install latest --provider tofu
tfv use 1.8.0 --provider tofu
tfv list --remote --provider tofu
tfv current --provider tofu
tfv plan --provider tofu

OpenTofu is stored apart from Terraform, in ~/.tfv/store/opentofu/, and runs as ~/.tfv/bin/tofu.

Terragrunt

Terragrunt works the same way, with --provider terragrunt (or -p tg):

tfv install latest -p tg
tfv use 1.2.1 -p tg
tfv list --remote -p tg
tfv pin -p tg                        # writes .terragrunt-version
tfv auto-switch -p tg

tfv auto-switch -p tg (and the shell hook) reads, in order: .terragrunt-version, .tgswitchrc, a terragrunt line in .tool-versions, then terragrunt_version_constraint in terragrunt.hcl / root.hcl.

Terraform also reads terraform_version_constraint from terragrunt.hcl / root.hcl, so in a Terragrunt project one cd switches both tools:

# terragrunt.hcl
terraform_version_constraint  = "~> 1.9"
terragrunt_version_constraint = ">= 1.0, < 2.0"

Terragrunt lives in ~/.tfv/store/terragrunt/ and runs as ~/.tfv/bin/terragrunt.

Provider plugin cache

Share downloaded providers across all your projects, so each provider version downloads once instead of once per project:

tfv plugin-cache on                  # enable
tfv plugin-cache status              # show what's set
tfv plugin-cache off                 # remove what tfv added

This sets plugin_cache_dir in your Terraform CLI config (~/.terraformrc, or %APPDATA%\terraform.rc on Windows), with the cache in ~/.terraform.d/plugin-cache. OpenTofu reads the same file unless you have a ~/.tofurc, which tfv then updates too. tfv never changes a plugin_cache_dir you set yourself.

Mirrors, proxies and offline use

Download from an internal mirror (Artifactory, Nexus, …) instead of the public sites. Each variable replaces one host and keeps the rest of the path:

Variable Replaces Used for
TFV_HASHICORP_MIRROR https://releases.hashicorp.com Terraform (downloads and version list)
TFV_GITHUB_MIRROR https://github.com OpenTofu and Terragrunt downloads
export TFV_HASHICORP_MIRROR=https://artifactory.example.com/artifactory/hashicorp
export TFV_GITHUB_MIRROR=https://user:token@nexus.example.com/repository/github

Plain http:// mirrors and credentials in the URL work. Checksums are still checked against the mirrored SHA256SUMS file.

OpenTofu and Terragrunt version lists come from the GitHub API. If it can't be reached, installing an exact version (tfv install 1.8.0 -p tofu) still works; latest needs the list. Set GITHUB_TOKEN to raise GitHub's limit of 60 requests an hour.

Windows

Everything works on Windows without administrator rights:

  • Store: %USERPROFILE%\.tfv\
  • PATH: set with [Environment]::SetEnvironmentVariable("PATH", ..., "User") (User scope, no admin)
  • Shell hook: PowerShell profile, via tfv shell-init powershell

Help

tfv --help
tfv <command> --help

Based on README.md in the tfv repo.