Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
869f6b6
chore(AI): Init OpenSpec skills, wire into AGENTS.md
MaxymVlasov Jul 23, 2026
3707561
f
MaxymVlasov Jul 23, 2026
0051ab7
Merge branch 'master' into openspec
MaxymVlasov Jul 24, 2026
f053b65
commit part that makes +- sense
MaxymVlasov Jul 23, 2026
3df47b5
And commit a second part
MaxymVlasov Jul 23, 2026
c95abe6
Archive spec
MaxymVlasov Jul 23, 2026
e24e21b
Drop extralinks, as #1003 discarded
MaxymVlasov Jul 24, 2026
4c23c39
Apply suggestions from code review - docs
MaxymVlasov Jul 28, 2026
2e303f6
Rename function resolve_tool_version to resolve_tool_path
MaxymVlasov Jul 28, 2026
2764a0e
Apply suggestions from code review - code, 1st round
MaxymVlasov Jul 28, 2026
c915e7b
Apply suggestions from code review
MaxymVlasov Jul 28, 2026
b0bc55b
Apply review suggestions and little improvements
MaxymVlasov Jul 28, 2026
a5e62cc
chore: untrack local openspec.ai tooling files
MaxymVlasov Jul 28, 2026
4438ee8
Still ask AI to generate and use openspec, but do not commit skills
MaxymVlasov Jul 28, 2026
b85f444
fix(hooks): stop masking tool_path resolution failures
MaxymVlasov Jul 28, 2026
a432cff
revert: drop OpenSpec-related changes, moved to #1005
MaxymVlasov Jul 28, 2026
a475d47
fix(hooks): check terragrunt syntax version against tool_path, not $PATH
MaxymVlasov Jul 28, 2026
4dfc05c
Merge branch 'master' into downloadable_and_version_controlled_hooks
MaxymVlasov Jul 28, 2026
243e6f9
Apply suggestion from @MaxymVlasov
MaxymVlasov Jul 28, 2026
1c881c1
Add table from PR review comments
MaxymVlasov Jul 28, 2026
2ed31af
Fix numbering
MaxymVlasov Jul 28, 2026
cc08a68
Merge branch 'master' into downloadable_and_version_controlled_hooks
MaxymVlasov Aug 12, 2026
ccb0e11
feat(tests): Cover `--hook-config=--tool-version` resolution with pyt…
MaxymVlasov Aug 18, 2026
6078e48
Merge branch 'master' into downloadable_and_version_controlled_hooks
MaxymVlasov Aug 18, 2026
43bf759
fix(hooks): Make cache-miss download race-safe (#1012)
MaxymVlasov Aug 20, 2026
dd02688
Update tools/install/_common.sh
MaxymVlasov Aug 21, 2026
7892df6
fix(tool-version): Address Copilot review feedback
MaxymVlasov Aug 21, 2026
277e7cd
chore(docker): Silence hadolint DL3064 false positives
MaxymVlasov Aug 21, 2026
fb8c3f8
fix(ci): wire GITHUB_TOKEN into docker image builds
MaxymVlasov Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/AI_POLICY.md
Comment thread
MaxymVlasov marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ The contributor — not the AI — is the author of the contribution. Using an L

The LLM types for you. It doesn't think for you.

## Recommended tooling

If you're using an AI coding agent, install [OpenSpec](https://openspec.dev/) (`npm install -g @fission-ai/openspec@latest`, then `openspec init`) and use its spec-driven workflow (`/opsx:propose` → `/opsx:apply`) to scope the change into a reviewable proposal + spec + tasks *before* any code gets written. `openspec init` provisions the matching skills/commands locally — see [`AGENTS.md`](../AGENTS.md#skill-routing).

These skills are not installed in this repo — `.agents/skills/openspec-*`, and `.agents/commands/opsx/` are generated on your own machine by `openspec init` and must never be committed. Provision them locally, keep them out of your diffs and PRs.

Scoping the change up front makes it easier to meet the ownership expectations above: you review and understand the plan before the AI starts typing, instead of reverse-engineering intent from a pile of already-generated code.

## Disclosure

You are encouraged (but not required) to disclose when AI tools contributed to your work. A short note in the PR description is enough — e.g. *"Tests were initially drafted with an LLM and then reviewed and adjusted by me."*
Expand Down
3 changes: 3 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ You can use [this PR](https://github.com/antonbabenko/pre-commit-terraform/pull/
* `docker build -t pre-commit --build-arg INSTALL_ALL=true .`
* `docker build -t pre-commit --build-arg <NEW_HOOK>_VERSION=latest .`
* `docker build -t pre-commit --build-arg <NEW_HOOK>_VERSION=<1.2.3> .`

> [!NOTE]
> `tools/install/<tool>.sh` has a second call site besides the Dockerfile: if the new tool is distributed as a GitHub (or HashiCorp) release binary, `hooks/_common.sh::common::resolve_tool_path` invokes this same script at hook run-time to support [`--hook-config=--tool-version=`](../README.md#most-hooks-pin-a-specific-tool-version). Wire your new hook to it the same way the existing binary-wrapping hooks are (look at how `terraform_tflint.sh` or `terrascan.sh` call `common::resolve_tool_path`), unless the tool has a different distribution model (e.g. pip, like `checkov`) - in that case, skip this and leave a comment explaining why, as `terraform_checkov.sh` does.
2. Add Docker structure tests to [`.github/.container-structure-test-config.yaml`](.container-structure-test-config.yaml)
3. Add new hook to [`.pre-commit-hooks.yaml`](../.pre-commit-hooks.yaml)
4. Create hook file. Don't forget to make it executable via `chmod +x /path/to/hook/file`.
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,7 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python

tests/results/*

# Drop openspec.ai generated files to not clutter the repo but still be able to use it locally
.agents/commands/opsx/*
.agents/skills/openspec-*
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,9 @@ Load these skills when their triggers match.
| --- | --- |
| Creating a new skill: registering in AGENTS.md, choosing script vs SKILL.md | `adding-skills` |
| Create a commit, GitHub issue, or PR | `git-workflow` |
| Propose a new OpenSpec change (design, specs, tasks in one step) | `openspec-propose` |
| Think through an idea/problem before or during an OpenSpec change | `openspec-explore` |
| Implement tasks from an existing OpenSpec change | `openspec-apply-change` |
| Revise an OpenSpec change's planning artifacts after edits/new decisions | `openspec-update-change` |
| Sync delta specs from a change into main specs (without archiving) | `openspec-sync-specs` |
| Archive a completed OpenSpec change | `openspec-archive-change` |
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ If you want to support the development of `pre-commit-terraform` and [many other
* [All hooks: Set env vars inside hook at runtime](#all-hooks-set-env-vars-inside-hook-at-runtime)
* [All hooks: Disable color output](#all-hooks-disable-color-output)
* [All hooks: Log levels](#all-hooks-log-levels)
* [Most hooks: Pin a specific tool version](#most-hooks-pin-a-specific-tool-version)
* [Keeping pinned versions up-to-date using Renovate](#keeping-pinned-versions-up-to-date-using-renovate)
* [Many hooks: Parallelism](#many-hooks-parallelism)
* [checkov (deprecated) and terraform\_checkov](#checkov-deprecated-and-terraform_checkov)
* [infracost\_breakdown](#infracost_breakdown)
Expand All @@ -76,6 +78,7 @@ If you want to support the development of `pre-commit-terraform` and [many other
* [About Docker image security](#about-docker-image-security)
* [File Permissions](#file-permissions)
* [Download Terraform modules from private GitHub repositories](#download-terraform-modules-from-private-github-repositories)
* [Mount tools cache directory](#mount-tools-cache-directory)
* [GitHub Actions](#github-actions)
* [Authors](#authors)
* [License](#license)
Expand Down Expand Up @@ -433,6 +436,79 @@ PCT_LOG=trace pre-commit run -a

Less verbose log levels will be implemented in [#562](https://github.com/antonbabenko/pre-commit-terraform/issues/562).

### Most hooks: Pin a specific tool version

> All hooks, which wrap a tool distributed as a downloadable release asset. Not supported for `checkov`/`terraform_checkov` (distributed via PyPi) and for deprecated `terraform_docs_replace` hook.

1. You can pin a specific version of the wrapped tool per hook, independent of whatever is on your `$PATH` or baked into the Docker image. If that version isn't already cached locally, it's downloaded from the tool's GitHub releases on first use, then reused (without re-downloading) on every subsequent run.

@coderabbitai coderabbitai Bot Jul 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid claiming every binary is downloaded from GitHub releases.

Terraform/OpenTofu resolution uses HashiCorp release installers, so this should say “the tool’s upstream release assets” (or similar) rather than specifically “GitHub releases.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 443, Update the version-pinning description near the
wrapped-tool download behavior to replace the GitHub-specific wording with “the
tool’s upstream release assets” or equivalent, while preserving the existing
caching and first-use download behavior.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see an issue here. "Github release" is more undersandable, IMO.
@yermulnik ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"GitHub releases" describes the feature, but e.g. "GitHub releases page" (or with capitalized "Releases" to mark the section on the web-site) describes the actual location where we download assets from.
So I'd add "page" after "releases" to provide clarity. However, tech-savvies do indeed often say "Github releases" meaning "Github Releases page" =)
Up to you 👍

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


Config example:

```yaml
- id: terraform_tflint
args:
- --hook-config=--tool-version=0.50.0
Comment thread
Copilot marked this conversation as resolved.
```

2. The same `--tool-version` key also works for `terraform_validate`, `terraform_fmt` and `terraform_providers_lock`, which resolve their Terraform/OpenTofu binary through [`--tf-path`](#11-custom-terraform-binaries-and-opentofu-support) - by default it downloads/uses whichever of `terraform`/`opentofu` binary the rest of that precedence chain would otherwise have picked (`terraform`, unless it's missing from `$PATH` while `tofu` is present, in which case `opentofu`). To pick explicitly instead of relying on the auto-detection mechanism, set `--tf-path` to the literal value of `terraform`, `opentofu` or `tofu`:

```yaml
- id: terraform_validate
args:
- --hook-config=--tf-path=opentofu
- --hook-config=--tool-version=1.12.0
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

> [!TIP]
> 3. Since this resolves to a version-specific cached binary rather than mutating `$PATH`, the same hook can be listed multiple times with different pinned versions, e.g. to test compatibility across tool versions in one run:
>
> ```yaml
> - id: terraform_tflint
> args:
> - --hook-config=--tool-version=0.50.0
> - id: terraform_tflint
> args:
> - --hook-config=--tool-version=0.55.0
> ```

4. By default, if a different version of the tool is already on `$PATH`, the pinned version still wins (with a warning message logged) based on `--hook-config=--tool-version-mode=strict`. Set it to `prefer-local` (as opposite to `strict`) to invert that: if the tool already resolves via `$PATH`, this local binary is used as-is and no download is attempted; the pinned version is only downloaded/used as a fallback when nothing is found locally.

```yaml
- id: terraform_tflint
args:
- --hook-config=--tool-version=0.50.0
- --hook-config=--tool-version-mode=prefer-local
```

5. By default, downloaded binaries are cached under `$XDG_CACHE_HOME/pre-commit-terraform/` (or `$HOME/.cache/pre-commit-terraform/` if `XDG_CACHE_HOME` is unset). Override this location with the `PCT_TOOL_CACHE_DIR` environment variable - see [Mount tools cache directory](#mount-tools-cache-directory) for the Docker case.

6. If a `GITHUB_TOKEN` environment variable is set, it's inherited automatically to authenticate GitHub API requests made during version resolution, the same way it already is utilized for [building your own Docker image](#docker-usage).

#### Keeping pinned versions up-to-date using Renovate

Neither Renovate's built-in [`pre-commit` manager](https://docs.renovatebot.com/modules/manager/pre-commit/) (which only understands `repo:`/`rev:` and `additional_dependencies` for Go/Node/Python) nor its `dockerfileVersions` preset can "look" inside hook's `args:`, so the `--tool-version` pin needs its own [`customManagers`](https://docs.renovatebot.com/modules/manager/regex/) entry in your own `renovate.json5` config file, using the same `# renovate: datasource=... depName=...` annotation convention commonly used for Dockerfile `ARG *_VERSION` pins:

```yaml
- id: terraform_tflint
args:
# renovate: datasource=github-releases depName=terraform-linters/tflint
- --hook-config=--tool-version=0.50.0
```

```json5
{
customManagers: [
{
customType: "regex",
managerFilePatterns: ["/\\.pre-commit-config\\.ya?ml$/"],
matchStrings: [
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)\\s+-\\s+--hook-config=--tool-version=(?<currentValue>\\S+)",
],
},
],
}
```

### Many hooks: Parallelism

> All, except deprecated hooks: `checkov`, `terraform_docs_replace` and hooks which can't be paralleled this way: `infracost_breakdown`, `terraform_wrapper_module_for_each`.
Expand Down Expand Up @@ -1276,6 +1352,21 @@ Finally, you can execute `docker run` with an additional volume mount so that th
docker run --rm -e "USERID=$(id -u):$(id -g)" -v ~/.netrc:/root/.netrc -v $(pwd):/lint -w /lint ghcr.io/antonbabenko/pre-commit-terraform:latest run -a
```

### Mount tools cache directory

A container's own filesystem is discarded after `docker run` exits, so a version downloaded via [`--tool-version`](#most-hooks-pin-a-specific-tool-version) would otherwise be re-downloaded on every single run. Mount the cache directory as a volume to persist it across runs, the same way you would for [`TF_PLUGIN_CACHE_DIR`](https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache):

```bash
TAG=latest
docker run \
-e "USERID=$(id -u):$(id -g)" \
-v ~/.cache/pre-commit-terraform:/root/.cache/pre-commit-terraform \
-v $(pwd):/lint -w /lint \
ghcr.io/antonbabenko/pre-commit-terraform:$TAG run -a
```

If you set `PCT_TOOL_CACHE_DIR` to a custom location, mount that path instead (and pass the same env var to the container with `-e PCT_TOOL_CACHE_DIR=...`).

## GitHub Actions

You can use this hook in your GitHub Actions workflow together with [pre-commit](https://pre-commit.com). To easy up
Expand Down
Loading
Loading