Auto-detection
When you omit provider (or set provider=auto), Clover infers the provider and its parameters from the content of the target line. Common cases need almost no annotation.
# @clover: constraint=minor
FROM redis:7.2.0
Here Clover recognizes a Docker image reference on the line and resolves it with the Docker provider, inferring repository=redis. A line that names a GitHub repository resolves with the GitHub provider instead.
The @clover shorthand
The canonical spelling of an auto marker is a bare @clover comment. The @ sigil stands for provider=auto, and the colon returns when other keys follow.
# @clover
FROM redis:7.2.0
# @clover: constraint=minor
FROM redis:7.2.0
The longhand clover: provider=auto means the same thing and stays valid. The shorthand is what clover annotate writes, and clover format rewrites the longhand into it. A redundant @clover: provider=<name> unfolds back to longhand, since an explicit provider always wins.
Recognized shapes
Auto-detection recognizes these shapes:
| Shape | File | Provider | Inference and notes |
|---|---|---|---|
A registry tool released only on crates.io (magika, …) | mise, asdf | crates.io | package |
FROM instruction (tag or digest) | Dockerfile, Containerfile | Docker | registry, repository |
image: mapping (tag or digest) | YAML | Docker | registry, repository |
uses: docker:// container job (tag or digest) | Workflow YAML | Docker | registry, repository |
Digest pin with a floating tag (nonroot, latest) | Any Docker shape above | Docker | Infers track, so the digest stays fresh while the tag text stays put |
uses: reference (commit SHA or tag) | GitHub Actions YAML | GitHub | repository |
Version input of a toolchain setup action (go-version:, node-version:, python-version:, swift-version:, terraform_version:) | Workflow YAML, action.yml | Go, Node.js, Python, Swift, HashiCorp | The input name identifies the toolchain, so the step’s uses: is not read and the same pin resolves as a workflow env: value. Not claimed: go-version-file: (a file, not a version), an expression (${{ matrix.go }}), a block value, or a build-matrix entry - whether spelled as a flow list (python-version: ['3.13', '3.14']) or as a scalar under matrix:, since a matrix’s oldest entry is deliberate and bumping it would drop support. The matrix refusal binds auto-detection only; an explicit provider= marker still tracks the line |
github: or ubi: backend, or a GitHub-released registry tool (tofu, ripgrep, …) | mise, asdf | GitHub | repository |
rev: in a repos entry | .pre-commit-config.yaml | GitHub, GitLab, Gitea | repository. Reads the sibling repo: by parsing the file as YAML, and the forge that URL names selects the provider. A local or meta pseudo-repository, or a host Clover reaches only with an extra key (a self-hosted forge, a non-default Gitea flavor), infers nothing rather than guessing. The frozen form pre-commit autoupdate --freeze writes (a commit SHA with a # frozen: v1.2.3 comment) is recognized too, and its SHA and comment are rewritten together. Rewriting one needs the commit a tag points at, which only GitHub resolves, so a frozen rev on another forge is left for explicit annotation rather than annotated into a marker that could never resolve |
<TOOL>_VERSION variable (ARG GO_VERSION=1.24.0, SOPS_VERSION: 3.9.0) | Dockerfile, Containerfile, workflow YAML, CI YAML | Whichever tracks the named tool | The whole prefix must name a tool Clover knows - a runtime, a HashiCorp product, or a mise registry tool - and supplies repository, product, or package accordingly. A name it cannot place infers nothing, so APP_VERSION, API_VERSION and the like are left alone. The prefix is never read segment by segment: API_NODE_VERSION is a service’s API version, not a Node.js pin. A build-matrix entry is refused as for a setup input. The shape is claimed in these files only, which is what leaves an Xcode project’s SWIFT_VERSION alone: that names the Swift language mode, which accepts 5.0 or 6.0 and nothing between them, so resolving it against a toolchain release would write a value Xcode rejects |
<TOOL>_SHA256 variable (ARG GO_SHA256=<64 hex>, also _SHA256SUM, _CHECKSUM, _SUM, _SHA) | Dockerfile, Containerfile, workflow YAML, CI YAML | Follows the sibling <TOOL>_VERSION | The line must carry a 64-character hex sum, so a commit or any other value is declined by its shape rather than its name. It is paired with the sibling <TOOL>_VERSION variable naming the same tool, which earns an id, and the sum becomes a value=sha256 follower of it - so it is refreshed only when the version it belongs to actually changed. pattern is read from the line that downloads the asset, the only place in the file that names it (RUN curl … go${GO_VERSION}.linux-amd64.tar.gz supplies go<version>.linux-amd64.tar.gz), in the shell or ${{ env.… }} spelling. Infers nothing when no download names the asset, when the filename still holds a variable after substitution (the multi-arch ${TARGETARCH} case), when two downloads disagree about it, or when either half of the pair appears more than once |
component: include (gitlab.com/group/project/name@1.0.0) | GitLab CI/CD YAML | GitLab | repository, plus host on a self-managed instance |
go (or golang) tool version | mise, asdf | Go | - |
go and toolchain directives | go.mod, go.work | Go | Resolved from the go.dev download index |
terraform = "1.9.8" tool version | mise, asdf | HashiCorp | product |
required_version constraint | .tf (Terraform) | HashiCorp | product=terraform, bumped in place so "~> 1.11.0" keeps its operator and precision. In a .tofu file it tracks OpenTofu through the GitHub provider instead. In .tf OpenTofu files, set provider explicitly |
version in a dependencies entry | Chart.yaml (Helm) | Helm | Reads the sibling name and repository by parsing the file as YAML. repository becomes registry, so an https://, http://, or oci:// chart resolves and a file:// subchart infers nothing. The chart’s own top-level version is left alone |
node (or the mise alias nodejs) tool version | mise, asdf | Node.js | - |
.node-version or .nvmrc pin (whole line) | nodenv, nvm | Node.js | No comment syntax, so tracked through a sidecar or clover run --infer. An alias like lts/* or node infers nothing |
A registry tool released only on npm (prettier, wrangler, …) | mise, asdf | npm | package |
A registry tool released only on PyPI (ansible, yamllint, …) | mise, asdf | PyPI | package |
Quoted dependency specifier ("uv_build>=0.8.24") | pyproject.toml dependencies, requires, or dependency-group array | PyPI | package. Bumped in place with any trailing environment marker left untouched. Only ==, >=, ~=, === are recognized, since bumping an exclusion (!=) or a cap (<, <=) would invert its meaning. A range (">=1.26,<2.1") or a multi-specifier line is skipped as ambiguous |
python tool version | mise, asdf | Python | - |
.python-version pin (whole line) | pyenv | Python | No comment syntax, so tracked through a sidecar or clover run --infer. An implementation-prefixed pin like pypy3.10-7.3.12 infers nothing |
target-version (compact py314 form) | pyproject.toml (ruff, black, mypy) | Python | - |
requires-python floor | pyproject.toml | Python | Bumped in place, so ">=3.14" keeps its operator and precision. A range like ">=3.10,<4" carries two tokens and is skipped as ambiguous |
rust tool version | mise, asdf | Rust | - |
channel = "1.97.0" | rust-toolchain.toml | Rust | A named channel (stable, nightly-2026-07-11) carries no version and infers nothing |
| Bare toolchain pin (whole line) | rust-toolchain (legacy) | Rust | No comment syntax, so tracked through a sidecar or clover run --infer. A named channel (stable, nightly-2026-07-11) infers nothing |
rust-version floor | Cargo.toml | Rust | Bumped in place with precision preserved, so "1.70" advances only when a new minor line ships |
swift tool version | mise, asdf | Swift | - |
.swift-version pin (whole line) | swiftly | Swift | No comment syntax, so tracked through a sidecar or clover run --infer. A snapshot pin like 6.1-snapshot-2026-06-29 infers nothing |
swift-tools-version declaration | Package.swift | Swift | The declaration has to stay at the top - SwiftPM below 6.0 rejects one that is not the first line - so a directive comment has nowhere to sit above it and it is tracked through a sidecar or clover run --infer. The floor keeps the precision it is written with, so a two-component 6.0 advances to 6.3. A version-specific Package@swift-5.9.swift is not claimed: it exists to serve an older toolchain. This is the one shape read from a comment line, which auto-detection otherwise skips as documentation |
version in a module block | .tf (Terraform) | Terraform | Reads the sibling source by parsing the file as HCL. Only a plain registry address (terraform-aws-modules/vpc/aws) is claimed. A dot or colon in the first segment marks it a hostname, which rules out both a host-qualified address (needing a host key inference cannot supply) and the forge shorthands Terraform reads as git sources (github.com/org/repo). A local path, a git:: URL, or an archive is not a registry address either |
version constraint in a required_providers entry | .tf (Terraform) | Terraform | Reads the sibling source by parsing the file as HCL, the one inference that looks beyond the target line. A version outside required_providers infers nothing. In a .tofu file it resolves with provider=opentofu against the OpenTofu registry. In .tf OpenTofu files, set provider=opentofu explicitly |
zig tool version | mise, asdf | Zig | - |
The mise rows cover any of the shapes mise reads (mise.toml, .mise.toml, a mise.local.toml or mise.<env>.toml variant, mise/config.toml, .config/mise/config.toml, a .config/mise/conf.d fragment) and asdf’s .tool-versions file, which mise reads with the same tool names. The tool maps are generated from the mise registry. A tool with a GitHub-shaped backend tracks its repository, and one released only through pipx, npm, or cargo tracks the package that ecosystem installs.
When to be explicit
Auto-detection covers the obvious cases. Set provider explicitly when:
- the line is ambiguous, or the value you want to track is not the most obvious token on it,
- you are tracking something that is not literally written on the target line,
- you want the annotation to document intent regardless of the line’s contents.
Inference only fills in what you leave out, and any key you set yourself always wins.
Generating annotations
To add @clover directives across an existing codebase rather than write them by hand, run clover annotate. It scans for the same lines auto-detection recognizes and inserts a directive above each, so onboarding a repository is a single command.
To skip annotations entirely, run clover run --infer: it updates every recognized line directly, as if each carried a bare @clover, writing no comments at all. Annotations remain the way to pin selection rules (constraint, include, cooldown, …) to a line.