CLI tool
hashicorp/setup-terraform avatar
hashicorp/setup-terraform

setup-terraform does three things, and the wrapper script is the one that surprises people

Sets up Terraform CLI in your GitHub Actions workflow.

1,594 stars288 forksJavaScriptMPL-2.0

At a glance

What is it?
hashicorp/setup-terraform installs the Terraform CLI on a GitHub Actions runner, writes CLI configuration for HCP Terraform or Terraform Enterprise, and installs a wrapper that republishes each command's output as step outputs. That third behaviour is why a plan can be echoed, commented on a pull request, or silently altered if you do not expect a shim in front of the binary.
Who is it for?
setup-terraform fits a team whose pull requests should show the plan output as a comment, since republishing stdout as a step output is the feature that enables that, and who run on ubuntu-latest, windows-latest or macos-latest runners. It does not fit a job that needs the real terraform binary with no interception, which is solved by setting terraform_wrapper to false, and self-hosted runners must already have Node installed because this is a JavaScript action.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three responsibilities, one action

The README states what the action does in three bullets, and the decomposition matters because each has its own failure mode.

First, it downloads a specific version of Terraform CLI and puts it on the PATH. Second, it configures the Terraform CLI configuration file with an HCP Terraform or Terraform Enterprise hostname and an API token. Third, it installs a wrapper script that wraps subsequent calls of the terraform binary and exposes its standard output, standard error and exit code as outputs named `stdout`, `stderr` and `exitcode`.

That third item is the mechanism behind nearly everything else in this README. Once outputs exist as step outputs, ordinary Terraform commands can be run with the normal `run` syntax and their results can be consumed later in the job.

The default usage is one line:

yaml
steps:
- uses: hashicorp/setup-terraform@v4

After that, any later step in the same job can run Terraform commands as it would on your own machine. The action is versioned by major tag, `@v4`, and the pinned version inside the action is 4.0.1.

The repository structure reflects the build. There is a `wrapper/` directory and a `dist/` directory, and the build script shows why both are committed: `ncc build wrapper/terraform.js --out wrapper/dist` and `ncc build index.js --out dist`. Compiled bundles are checked in, which is the requirement GitHub places on JavaScript actions.

Pinning the version, or inheriting whatever is newest

The default installs the latest version of Terraform CLI. If you want a specific one, the input is `terraform_version`:

yaml
steps:
- uses: hashicorp/setup-terraform@v4
  with:
    terraform_version: "1.14.6"

That distinction is the practical difference between a workflow that is reproducible and one that is not. A pull request run today and the same run next month may use different Terraform versions, and a plan that changes because the tool changed is a confusing failure to debug. Pinning costs one line.

Version resolution is handled by the semver package as a dependency, alongside @hashicorp/js-releases, which is the client used to find and download releases. So the action is not guessing versions from a hardcoded list; it queries the release feed.

The other inputs configure the remote backend. For HCP Terraform at app.terraform.io, the token alone is enough:

yaml
steps:
- uses: hashicorp/setup-terraform@v4
  with:
    cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

For a self-hosted Terraform Enterprise instance you also need the hostname:

yaml
steps:
- uses: hashicorp/setup-terraform@v4
  with:
    cli_config_credentials_hostname: 'terraform.example.com'
    cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

Both come from GitHub secrets, which is the right place for them and is worth checking in your own workflows, since a token pasted into a workflow file is a token in your git history.

stdout as an output, and the 65535 character wall

Here is the minimal pattern that uses the wrapper:

yaml
steps:
- uses: hashicorp/setup-terraform@v4

- run: terraform init

- id: plan
  run: terraform plan -no-color

- run: echo ${{ steps.plan.outputs.stdout }}
- run: echo ${{ steps.plan.outputs.stderr }}
- run: echo ${{ steps.plan.outputs.exitcode }}

Note the `id` on the plan step. Outputs only exist for steps that have one, and `terraform init` in the example has none precisely because nothing needs its output.

The realistic use is commenting the plan on a pull request, and the README is direct about the failure mode there. GitHub limits a comment to 65535 characters, so a large plan can fail the workflow run even though the plan itself succeeded. The README offers the alternative: append the plan to the `$GITHUB_STEP_SUMMARY` environment variable, which supports markdown and has no such small limit.

That is a better default for most repositories, and the comment approach is better when reviewers need the plan in the discussion thread where they already are. What the README does not offer is truncation, so if you choose comments you are choosing a threshold you did not set.

If a step should fail without stopping the job, the examples pair it with `continue-on-error: true`, which is how fmt and plan are marked in both complete workflows, letting the comment report success and failure of each command side by side rather than aborting at the first problem.

Turning the wrapper off, and choosing the right runner

The wrapper is optional, and the README says so. Setting `terraform_wrapper` to false skips installing it:

yaml
steps:
- uses: hashicorp/setup-terraform@v4
  with:
    terraform_wrapper: false

That matters if a later step invokes terraform in a way the wrapper does not handle, or if you are debugging and want the real binary's behaviour. The trade is that you lose the three outputs, so nothing downstream can read the plan text.

Runner support is stated plainly: ubuntu-latest, windows-latest and macos-latest. On windows-latest the shell must be set to Bash, which is a one-line job-level setting and not something the action will do for you.

Self-hosted runners carry an extra requirement, and it is the one most people miss. Because this is a JavaScript action, Node must already be installed on a self-hosted runner at the version specified in action.yml. Your self-hosted runner may need you to install Node.js in advance.

Another documented behaviour worth knowing: there is no per-step working directory setting on the action itself, so the complete workflows in the README set a job-level default instead, pointing `defaults.run.working-directory` at `${{ env.tf_actions_working_dir }}`. That is how they run Terraform against a configuration that is not in the repository root.

Maintenance signals in the repository itself

The package manifest carries more information about the project's governance than the README does.

The declared Node requirement is `>=24.0.0`, which is a notable floor and is consistent with the action being maintained for current runners. The publisher field is set to hashicorp, and the licence is MPL-2.0, which is the file-explained licence Mozilla Public License, a weak copyleft that applies to the action's own files. In practice that matters for the compiled bundles in `dist/`, since those are what your workflow actually executes.

Release notes are managed with changie, configured in .changie.yaml with entries under a .changes/ directory, and licence headers are managed with copywrite via .copywrite.hcl. CHANGELOG.md sits at the root. Version tags show v3.1.2 on 2024-08-19, v4.0.0 on 2026-02-24 and v4.0.1 on 2026-05-12, with the last push on 2026-10-13.

Two further details from package.json are worth a mention because they are security-relevant in an action. There is an overrides block forcing specific versions of minimatch, glob, undici, test-exclude and babel-plugin-istanbul, which is the mechanism for pinning transitive dependencies that ship advisories. And semistandard ignores `**/dist/**` during linting, which is correct, since the compiled bundles are generated.

Tests are Jest with coverage, plus nock for HTTP mocking, and the test script runs the linter first: `semistandard --env jest && jest --coverage`. A formatting check script exists and prints that it is unimplemented for actions and reusable-workflows basic validation, which is an honest statement about what is not yet covered.

Editorial conclusion

setup-terraform fits a team whose pull requests should show the plan output as a comment, since republishing stdout as a step output is the feature that enables that, and who run on ubuntu-latest, windows-latest or macos-latest runners. It does not fit a job that needs the real terraform binary with no interception, which is solved by setting terraform_wrapper to false, and self-hosted runners must already have Node installed because this is a JavaScript action. Verify first that you pin terraform_version explicitly, since the default installs the latest release, and set permissions to pull-requests: write only on the job that actually comments.

Frequently asked questions

How do I pin a Terraform version in setup-terraform?

Pass the terraform_version input, for example `terraform_version: "1.14.6"`. Without it the action installs the latest version of Terraform CLI, so a workflow can change behaviour between runs.

What do the setup-terraform wrapper outputs contain?

The wrapper republishes the terraform command's standard output, standard error and exit code as the step outputs `stdout`, `stderr` and `exitcode`. A step needs an id for those outputs to be addressable, as in `- id: plan`.

Can I stop setup-terraform from wrapping the terraform binary?

Yes. Set the terraform_wrapper input to false. The action still installs and configures Terraform CLI, but subsequent steps can no longer read its output as step outputs.

Which runners does setup-terraform support?

ubuntu-latest, windows-latest and macos-latest. On windows-latest the shell has to be set to Bash, and on self-hosted runners Node must already be installed at the version specified in action.yml, because the action is a JavaScript action.

Why does commenting the Terraform plan on a pull request sometimes fail?

GitHub limits comments to 65535 characters, so a large plan can fail the workflow run even when the plan succeeded. The README suggests appending the plan to the GITHUB_STEP_SUMMARY environment variable instead, which supports markdown and has no such limit.

How do I configure HCP Terraform or Terraform Enterprise credentials?

Set cli_config_credentials_token to your API token secret for HCP Terraform at app.terraform.io, and also set cli_config_credentials_hostname when using a self-hosted Terraform Enterprise instance.

Official sources

  1. hashicorp/setup-terraform on GitHub
  2. License: MPL-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hashicorp-setup-terraform.svg)](https://hysenlabs.com/projects/hashicorp-setup-terraform)

Was this useful?