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.
Install
npm install -g tfvOn 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 architectureAlias: tfv i.
Switch version
tfv use 1.9.0
tfv use latest # latest installed version
tfv use 1.8.0 --provider tofuNo 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 tofuIt reads the version from, in order:
- A version file in the current directory:
- Terraform:
.terraform-version,.tfswitchrc,.tfswitch.toml, or aterraformline in.tool-versions(asdf / mise) - OpenTofu:
.opentofu-version,.tofu-version, anopentofuortofuline in.tool-versions, then.terraform-version
- Terraform:
terraform.tfstate→ theterraform_versionfieldrequired_versionin any.tffile. 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-versionsfile with aterraform,opentofuortofuline, or - any
*.tffiles
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 silentlySet 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 versionsThe active version is marked with 🚀.
Current version
tfv current # active version + PATH status
tfv which # alias
tfv current --provider tofuActive terraform version: 1.9.0
Binary: /Users/you/.tfv/bin/terraform
Reported: Terraform v1.9.0
PATH OK — 'terraform' resolves to tfv-managed binaryPin 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-versionUpgrade
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 tofuIt 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 tofuremove 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 promptThe 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 -- validateDoctor
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 doctortfv 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 tofuOpenTofu 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 tgtfv 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 addedThis 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/githubPlain 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> --helpBased on README.md in the tfv repo.
