Back to Blog

How Terraform Installs Providers for the Terraform Associate (004) Exam: The CLI Config File, Implied & Explicit Installation Methods, Filesystem & Network Mirrors, the Plugin Cache, dev_overrides, h1 vs zh Lock-File Hashes & terraform providers lock/mirror

A practitioner's guide to objective 2a of the Terraform Associate (004) exam, 'Install and version Terraform providers': what terraform init really does, where the CLI configuration file lives (.terraformrc, terraform.rc, TF_CLI_CONFIG_FILE), the implied filesystem mirrors, the provider_installation block with direct, filesystem_mirror, network_mirror and dev_overrides, include/exclude patterns, packed vs unpacked mirror layouts, the provider network mirror protocol, the plugin cache and its lock-file rule, zh: vs h1: hashes and trust on first use, cross-platform lock files with terraform providers lock -platform, terraform providers mirror for air-gapped installs, and init -upgrade, -lockfile=readonly and -plugin-dir.

By Sailor Team , October 24, 2026

Objective 2a of the Terraform Associate (004) exam is a single line: install and version Terraform providers. Most candidates prepare for it by learning the required_providers block, version constraint operators and the fact that .terraform.lock.hcl should be committed. Our providers and provider block guide covers exactly that, including terraform init -upgrade and the one-paragraph mention that mirrors and a plugin cache exist for offline use.

This article is about the plumbing that paragraph skipped. Where does terraform init look for a provider, in what order, and what stops it from using a cached copy? Why does a lock file created on a Mac break a Linux CI run only when the team uses a mirror? What does dev_overrides actually bypass? The exam asks these as scenario questions (“the team must install providers without internet access”, “a developer wants to test an unreleased provider build”, “init fails with a checksum error on a new platform”), and the answers are all in the CLI configuration file and the lock file’s hash scheme.

What terraform init Actually Does With Providers

When you run terraform init, the provider part of the job is a pipeline:

  1. Discover requirements. Terraform walks the root module and every child module for required_providers entries and implied provider references, and merges the constraints. terraform providers prints this tree without installing anything, which is the fastest way to find out which module dragged in an unexpected provider.
  2. Select a version. If the lock file already records a version for that provider, Terraform reselects it even when newer versions satisfy the constraints. If there is no entry, Terraform picks the newest version that satisfies every constraint. -upgrade throws away the recorded selection and chooses the newest again.
  3. Pick an installation method. This is governed by the CLI configuration file. With no configuration, Terraform checks a set of implied local mirror directories and then falls back to the provider’s origin registry (the direct method). With a provider_installation block, it uses whatever you configured.
  4. Fetch the package from that method, or reuse a copy from the plugin cache if one is allowed.
  5. Verify checksums against the lock file. A package that matches none of the recorded hashes fails init with a “doesn’t match the checksums recorded in the lock file” error.
  6. Write the package under the working directory’s .terraform data directory (relocatable with TF_DATA_DIR) and update the lock file, printing a reminder to review and commit it if anything changed.

Every exam scenario about installation is a question about which of those six steps is being changed.

The CLI Configuration File

The CLI configuration file holds per-user settings that apply to every working directory. It is not Terraform configuration: nothing in it goes in a .tf file, and nothing in a .tf file can set it.

PlatformDefault location
Windowsterraform.rc in the %APPDATA% directory
macOS, Linux and other systems.terraformrc in the home directory
AnyOverride with the TF_CLI_CONFIG_FILE environment variable (use a *.tfrc name)

The file uses HCL. The settings you should recognise:

SettingWhat it controls
credentials "hostname" block with tokenAPI token for HCP Terraform or Terraform Enterprise; must be a user or team token, one block per host
TF_TOKEN_<hostname> environment variable (v1.2+)Same purpose, dots in the hostname become underscores, takes precedence over the file
credentials_helper "name" blockExternal program consulted for hosts with no credentials block; at most one, Terraform ships none
disable_checkpointTurns off upgrade and security bulletin checks
disable_checkpoint_signatureKeeps the checks but drops the anonymous de-duplication ID
plugin_cache_dirEnables the provider plugin cache at the given, pre-existing directory
plugin_cache_may_break_dependency_lock_fileLets the cache be used even when the lock file has no matching hash
provider_installation blockReplaces the default installation methods

A providers block in this file is the Terraform 0.12 mechanism and is deprecated; provider_installation replaced it in 0.13.

Implied Behaviour: What Happens With No Configuration

With no provider_installation block, Terraform behaves as if you had written an implied configuration with a set of filesystem mirrors first and direct last. The implied mirror directories are:

PlatformImplied filesystem mirror directories
Windows%APPDATA%/terraform.d/plugins and %APPDATA%/HashiCorp/Terraform/plugins
macOS$HOME/.terraform.d/plugins, ~/Library/Application Support/io.terraform/plugins, /Library/Application Support/io.terraform/plugins
Linux and Unix$HOME/.terraform.d/plugins plus terraform/plugins under the XDG data directories (defaults ~/.local/share, /usr/local/share, /usr/share)
Allterraform.d/plugins in the current working directory (the launch directory when using -chdir)

Any provider found in an implied mirror is automatically excluded from the implied direct method, so dropping a provider package into ~/.terraform.d/plugins is enough to make Terraform stop contacting the registry for it. That automatic exclusion does not apply when you write an explicit block; there you must add exclude yourself.

Explicit Installation Methods: provider_installation

An explicit block lists one or more methods. Terraform tries all matching methods for a provider and selects the newest version available across them that satisfies the configuration’s constraints.

# ~/.terraformrc
provider_installation {
  filesystem_mirror {
    path    = "/opt/terraform/providers"
    include = ["registry.terraform.io/hashicorp/*", "example.com/*/*"]
  }
  network_mirror {
    url     = "https://mirror.example.com/providers/"
    exclude = ["example.com/*/*"]
  }
  direct {
    exclude = ["registry.terraform.io/hashicorp/*", "example.com/*/*"]
  }
}
MethodArgumentPurpose
directnoneDownload from each provider’s origin registry
filesystem_mirrorpathRead packages from a local directory in the mirror layout; multiple blocks allowed
network_mirrorurl (must be https: and end in a slash)Fetch from a server implementing the provider network mirror protocol
dev_overridesmap of source address to local directoryUse a locally built provider binary, bypassing version and checksum checks

Every method except dev_overrides accepts include and exclude lists of source-address patterns. Exclusions take priority over inclusions. A pattern may omit the registry.terraform.io/ prefix, so */* means every provider on the public registry. Terraform warns against untrusted network mirrors because a mirror can serve modified packages.

Pattern you will see on the exam: air-gapped installation

“The security team blocks outbound internet from the CI runners. How should the team install providers?” The answer is a filesystem mirror (or an internal network mirror) declared in the CLI configuration, populated with terraform providers mirror, with direct either omitted or excluded for every provider. Omitting direct entirely makes the registry unreachable by design.

provider_installation {
  filesystem_mirror {
    path    = "/opt/terraform/providers"
    include = ["*/*"]
  }
}

A one-off alternative is terraform init -plugin-dir=/opt/terraform/providers, which treats the directory as if it were the only filesystem_mirror and ignores the configured methods for that run. It is meant for testing, not for a standing policy.

Mirror Directory Layouts

A filesystem mirror holds providers in one of two layouts, and both may be mixed in one directory:

LayoutPath patternNotes
PackedHOSTNAME/NAMESPACE/TYPE/terraform-provider-TYPE_VERSION_TARGET.zipThe original distribution zip from the registry; Terraform always extracts it
UnpackedHOSTNAME/NAMESPACE/TYPE/VERSION/TARGET/The extracted contents; Terraform may symlink to it instead of copying

VERSION is a plain version such as 5.70.0; TARGET is the platform, such as linux_amd64 or darwin_arm64. The hostname is the provider’s origin hostname (for example registry.terraform.io), not the mirror’s, which is why one mirror can serve providers from several registries.

Building the mirror with terraform providers mirror

terraform providers mirror \
  -platform=linux_amd64 \
  -platform=darwin_arm64 \
  /opt/terraform/providers

The command reads the current configuration’s requirements, downloads the matching packages for each -platform (defaulting to the platform you run it on) and writes them in the packed layout. It also writes the index.json and VERSION.json index files that the network mirror protocol needs, so the same directory can be uploaded to any static HTTPS host and used as a network_mirror; when used as a filesystem mirror the JSON files are ignored. Re-running it against the same directory adds packages and merges indexes rather than wiping them, and running it from several configurations into one directory produces one combined mirror. By default (-lock-file=true) it respects the versions in the lock file if the directory has been initialised.

The network mirror protocol in one paragraph

Terraform makes two kinds of request under the mirror’s base URL: HOSTNAME/NAMESPACE/TYPE/index.json to list available versions, and HOSTNAME/NAMESPACE/TYPE/VERSION.json to list the archives per platform. Each archive entry has a url (relative to the JSON file) and an optional hashes list of h1: values. If hashes are present Terraform verifies the download against them; if not, it installs unverified, which is why mirrors must be trusted. Credentials configured for the mirror’s hostname are sent to the index requests but not to the archive downloads. The protocol has existed since Terraform 0.13.2, and it is deliberately static-file friendly.

dev_overrides: Testing a Provider You Built Locally

provider_installation {
  dev_overrides {
    "hashicorp/null" = "/home/dev/go/bin"
  }
  direct {}
}

Rules that turn into exam distractors:

  • dev_overrides must be the first method in the block and wins over every other method for the providers it lists.
  • The directory must contain an executable named terraform-provider-TYPE (optionally with a version suffix).
  • For an overridden provider Terraform disables version constraint checking and checksum verification. You get a warning on every command.
  • terraform init still selects and records a published version in the lock file; it is the other commands (plan, apply, and so on) that ignore the lock entry and use the local binary. So “dev_overrides lets me skip init” is false, and “the lock file still shows the registry version” is expected.
  • If you omit direct {} from the block, only the overridden providers are installable.
  • It works in Terraform 0.14 and later, is documented as a temporary measure, and should never appear in shared CI configuration.

The Plugin Cache and Its Lock-File Rule

Without a cache, every working directory downloads its own copy of each provider. The cache is a shared directory that init copies from (or symlinks to) instead of hitting the network:

plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"

or, for one shell session, export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache". The directory must already exist, must not also be a filesystem mirror directory, is never cleaned by Terraform, and is not guaranteed to be safe for concurrent init runs.

The behaviour that trips people in CI is the interaction with the lock file. Terraform uses a cached package only if it matches at least one checksum already recorded in the lock file for that provider. On the first init of a configuration with no lock entry, Terraform downloads from upstream even though the cache has the package, so that it can record the registry’s signed checksums for all platforms. If you want the cache used regardless, set plugin_cache_may_break_dependency_lock_file = true (or the TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE environment variable to anything other than empty or 0). The cost is in the name: the resulting lock entry may contain only an h1: hash for the current OS and architecture, which breaks the next teammate on a different platform. The documentation reserves it for unusual situations.

Lock-File Hashes: zh: vs h1: and Trust on First Use

.terraform.lock.hcl lives in the root of the configuration, is created and updated only by init (and providers lock), and tracks providers only; module versions are never locked, so an exact module constraint is the only way to pin a module. Our state management guide explains why the lock file belongs in version control next to the backend configuration.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.70.0"
  constraints = "~> 5.70"
  hashes = [
    "h1:3b2p6LqG3nQ8rCzQy1vRq6eC3dD1cQ9mK2wE7hL5xYo=",
    "zh:0b1a2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5",
    "zh:1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  ]
}
FieldMeaning
versionThe exact version init selected
constraintsThe constraints in force when it was selected; informational only
hashesEvery checksum Terraform will accept for that version, across platforms

Two hash schemes coexist:

SchemeComputed fromCan verify
zh: (“zip hash”)SHA-256 of the official .zip from the registryPackages installed from a registry, which still publishes this scheme
h1: (“hash scheme 1”)SHA-256 over the package contentsThe official zip, an unpacked mirror directory, or a re-zipped copy; the preferred scheme going forward

The security model is trust on first use. When a provider is added, Terraform pre-populates hashes from the developer’s signed checksums published by the registry, which are mostly zh: values for every platform the developer released. From then on, any package that matches none of the recorded hashes is rejected. init prints the signing key ID so you can verify who signed what, and h1: hashes are added opportunistically whenever a newly installed package already matches a recorded hash.

Why a mirror breaks cross-platform lock files

Registry installs record signed zh: hashes for all platforms. A mirror install cannot do that: a filesystem mirror in the unpacked layout has no official zip to hash, and no mirror can supply the developer’s signature. So an init against a mirror records only the h1: hash for the platform where it ran. The next init on another platform finds a package whose hash is not in the file and fails with the checksum error. The fix is to pre-populate the lock file from the origin registry for every platform the team uses:

terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64 \
  -platform=windows_amd64

terraform providers lock adds selection and checksum information without installing anything. Run with no provider addresses it covers every provider in the configuration; name specific addresses to update only those entries. -fs-mirror=PATH and -net-mirror=URL make it read from a mirror instead of the registry (recording only the mirror’s hashes for the requested platforms, with no signed checksums), which is how you lock an in-house provider that exists only in your mirror. -enable-plugin-cache is off by default because the cache is not an authoritative source.

init flags that touch the lock file

FlagEffect
terraform init -upgradeIgnore recorded selections, pick the newest versions matching the constraints, rewrite the lock file
terraform init -lockfile=readonlyVerify checksums against the lock file but never modify it; conflicts with -upgrade; the right flag for CI so a run cannot silently change dependencies
terraform init -plugin-dir=PATHInstall only from that directory, as a one-time filesystem_mirror
terraform init on Terraform 1.1+Removes lock entries for providers no longer referenced by configuration or state; v1.0 and earlier left stale entries, which produce a “missing or corrupted provider plugins” error until a newer init cleans them

Removed providers that are later re-added are treated as new, so the selected version and hashes may differ.

Decision Table: Scenario to Mechanism

RequirementMechanism
Stop re-downloading the same provider for every project on a laptopplugin_cache_dir or TF_PLUGIN_CACHE_DIR
Cache is populated but init still downloads on a fresh projectExpected: no lock entry yet; or set plugin_cache_may_break_dependency_lock_file and accept a single-platform lock
No outbound internet from runnersterraform providers mirror into a directory, filesystem_mirror (or network_mirror) in the CLI config, no direct
Internal providers from an internal mirror, public ones from the registryfilesystem_mirror or network_mirror with include, direct with matching exclude
Test an unreleased provider build locallydev_overrides first in provider_installation, keep direct {} for everything else
Lock file fails checksum on Linux after being created on macOS via a mirrorterraform providers lock -platform=linux_amd64 -platform=darwin_arm64
CI must fail rather than mutate the lock fileterraform init -lockfile=readonly
Move to newer provider versions deliberatelyterraform init -upgrade, review the diff, commit the lock file
Lock a provider that exists only in an internal mirrorterraform providers lock -fs-mirror=PATH or -net-mirror=URL
Install from a directory just once without editing configterraform init -plugin-dir=PATH
See which module requires which providerterraform providers
Machine-readable resource and attribute schemasterraform providers schema -json after init

Commands Cheat Sheet

# Show provider requirements by module (no installation)
terraform providers

# Record versions and signed checksums for three platforms without installing
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64

# Lock an internal provider from a filesystem mirror
terraform providers lock -fs-mirror=/opt/terraform/providers example.com/platform/internal

# Populate a mirror (packed zips + network-mirror JSON indexes)
terraform providers mirror -platform=linux_amd64 /opt/terraform/providers

# One-off install from a directory, ignoring configured methods
terraform init -plugin-dir=/opt/terraform/providers

# CI: verify against the lock file, never change it
terraform init -lockfile=readonly

# Deliberately move to newer versions within constraints
terraform init -upgrade

# Enable the plugin cache for this shell
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"

# Point Terraform at a non-default CLI config file
export TF_CLI_CONFIG_FILE="$HOME/ci.tfrc"

The rest of the day-to-day command set is in our Terraform commands cheat sheet, and the init to apply sequence these flags modify is covered in the core workflow guide.

Exam Scenario Drill

ScenarioCorrect answerTrap answer
Team wants every developer to use the same provider versionsCommit .terraform.lock.hclPin exact versions in required_providers only (works, but the lock file is the mechanism Terraform provides)
init on a Linux runner fails with a checksum mismatch after a developer created the lock on macOS using a mirrorterraform providers lock -platform=linux_amd64 … and commitDelete the lock file
Runners have no internetprovider_installation with a mirror populated by terraform providers mirrorPut the provider binary in the module directory
A developer needs to run plan against a provider build from sourcedev_overrides in the CLI configurationEdit required_providers to point at a local path (not valid)
After enabling the plugin cache, a brand-new project still downloadsNormal: nothing in the lock file matches yetThe cache path is wrong
Where is the CLI configuration file on Linux?~/.terraformrc (or TF_CLI_CONFIG_FILE)terraform.tfvars, backend.tf
What does the lock file record for modules?Nothing; only providersModule versions and hashes
What does -upgrade do to the lock file?Re-selects newest versions within constraints and rewrites entriesDeletes it
What does -lockfile=readonly do?Checks hashes, refuses to change the fileSkips hash verification
Which hash scheme can a mirror in unpacked layout produce?h1: onlyzh:
A network_mirror URL starts with http://Rejected; must be https: with a trailing slashAccepted with a warning
dev_overrides listed after direct {}Invalid; it must come firstFine

Common Mistakes

  1. Putting installation settings in .tf files. plugin_cache_dir, provider_installation and credentials only exist in the CLI configuration file or their environment variables.
  2. Expecting the cache to be used on first init. It is skipped until the lock file holds a matching hash, unless you opt into the “may break” setting.
  3. Treating dev_overrides as a way to skip init. init still runs and still records a registry version; only later commands use the local binary.
  4. Assuming a mirror install produces a portable lock file. It records one platform’s h1: hash; run terraform providers lock -platform=… for the others.
  5. Deleting the lock file to “fix” a checksum error. That discards trust on first use; add the missing platform hashes instead.
  6. Omitting exclude in an explicit block. Automatic exclusion from direct only happens for implied mirrors.
  7. Using an http: network mirror or forgetting the trailing slash. Both are rejected.
  8. Running -upgrade in CI. Use -lockfile=readonly so dependency changes only happen through a reviewed commit.
  9. Believing the lock file pins modules. It does not; use exact module version constraints.
  10. Sharing a plugin cache directory as a mirror. The documentation requires them to be separate directories.

Frequently Asked Questions

Is the CLI configuration file the same as a .tfvars or backend file?

No. .tfvars files set input variables for a configuration; the backend block lives in .tf files; the CLI configuration file (~/.terraformrc or %APPDATA%\terraform.rc) sets per-user CLI behaviour such as credentials, the plugin cache and provider installation methods. It is HCL, but it is not Terraform configuration and is never read as part of a module.

Do I still need terraform init when using dev_overrides?

Yes. init selects a published version and records it in the lock file as usual. The override is honoured by plan, apply and the other commands, which ignore the lock entry for that provider and run the local binary, printing a warning.

Why does my lock file have many zh: lines but only one h1: line?

Registry installs record the developer’s signed checksums, which are zh: hashes for every platform they released. Terraform computes an h1: hash only for the package it actually installed on your platform, and adds more h1: hashes as other platforms install and match. Running terraform providers lock -platform=… records both schemes for each platform you name.

Can terraform providers mirror build a network mirror?

Yes. Besides the packed zips, it writes the index.json and per-version JSON files the provider network mirror protocol expects, so the directory can be served from any static HTTPS host and referenced as a network_mirror.

Does terraform init -upgrade also upgrade modules?

Yes. It re-selects providers to the newest versions within constraints and also refreshes modules to the newest version their source constraints allow, since module versions are never locked.

What is the difference between -plugin-dir and filesystem_mirror?

-plugin-dir is a one-run flag on init that behaves like a single filesystem_mirror and ignores the configured methods for that run. filesystem_mirror is persistent configuration in the CLI configuration file, can be combined with include and exclude, and is the right tool for a standing air-gapped policy.

Conclusion

Objective 2a looks small until you list what terraform init actually decides: which version, from which source, verified against which hash, written to which directory, recorded in which file. The CLI configuration file controls the source (implied mirrors and direct by default, or an explicit provider_installation block with filesystem and network mirrors and dev_overrides), the plugin cache controls whether the network is touched at all, and the lock file’s zh: and h1: hashes with trust on first use control whether the result is accepted. Knowing that a mirror install records a single-platform h1: hash, that terraform providers lock -platform=… fixes it, that the cache is skipped until the lock file matches, and that dev_overrides bypasses checksums but not init, answers most of the scenario questions in this area.

For the rest of the exam, Sailor.sh’s HashiCorp Terraform Associate (004) Mock Exam Bundle gives you eight full-length, 60-minute practice exams with explanations that spell out why the -upgrade answer, the “delete the lock file” answer or the .tf-file answer is wrong. Pair it with our Terraform Associate exam guide and the 30-day study plan, then run through the Terraform Associate practice questions to confirm the installation mechanics have stuck.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now