Tool provisioning¶
Guardrails orchestrates a lot of tools — ruff, mypy, shellcheck, shfmt, yamllint, actionlint, kubeconform, terraform, tflint, trivy, golangci-lint and more. You don't install any of them. Guardrails provisions pinned, checksum-verified copies on demand, so a host needs little more than the language runtime you already code in.
How a tool is found¶
For every tool, Guardrails tries, in order:
- The project's own environment. Python tools run from the project's
.venvwhen it has them, so pytest and mypy see your dependencies. - Your
PATH. If you installed a tool yourself, that copy wins (unlesstools.prefer: managed). - The managed copy. The pinned version in
${CLAUDE_PLUGIN_DATA}/tools/<name>/<version>/, installed on first use.
flowchart LR
A[check needs ruff] --> B{.venv/bin/ruff?}
B -- yes --> R[run it]
B -- no --> C{on PATH?}
C -- yes --> R
C -- no --> D{managed copy installed?}
D -- yes --> R
D -- no --> E[download pinned version<br/>verify SHA-256<br/>install] --> R
Never inside the edit budget¶
Per-edit checks have a two-second budget, so they never wait on a download:
- SessionStart detects which languages the repo uses and starts a
background install of the tools they need. You see one line:
installing pinned tools in the background: ruff, mypy, shellcheck. - PostToolUse skips a check whose tool is still installing, rather than blocking Claude for something that isn't its fault.
- Stop waits for any install to finish, so the gate always runs in full.
Where tools come from¶
Every tool is pinned in
internal/tools/tools.yaml
and locked in tools.lock.json, which records the exact URL and SHA-256 of
the artifact for each platform (Linux and macOS, amd64 and arm64). Downloads
whose checksum doesn't match are rejected.
| Source | Used for | Integrity |
|---|---|---|
| Release archive | shellcheck, actionlint, kubeconform, tflint, trivy, terraform, tofu, golangci-lint, gofumpt | SHA-256 pinned in the lock |
| PyPI wheel | uv, shfmt | SHA-256 pinned in the lock, cross-checked against PyPI's digest |
| uv | ruff, mypy, yamllint | A hash-pinned requirements file per tool, installed with uv pip install --require-hashes into an isolated venv |
go install |
goimports, govulncheck | The Go checksum database |
uv also provides a Python interpreter for Python-based tools when the host has none.
The lock is generated, never hand-edited: go run ./internal/tools/lockgen
downloads every artifact, hashes it and checks the executable is inside. A
weekly workflow regenerates it and opens a pull request, and CI installs
every tool on all four platforms so a broken upstream artifact fails there,
not on your machine. (It has already caught one: a corrupt macOS wheel
published on PyPI.)
What you still need¶
| Need | When |
|---|---|
| Go | Go repositories (to build and test) — the tools around it are managed |
| A Python project environment | Running your tests; Guardrails uses uv run or .venv |
| A Docker daemon | Only for the optional docker build --check; every other Docker check works without one |
| Network access on first use | To download tools; afterwards everything is cached |
Configuration¶
tools:
install: auto # auto | never — never uses only what's installed
prefer: path # path | managed — managed always uses the pinned versions
Use prefer: managed when you want results that match CI and teammates
exactly, regardless of what's on each machine. Use install: never on
air-gapped machines; missing tools are then reported to you, and required
ones fail the gate.
Commands¶
guardrails tools list # every tool, pinned version, where it resolves
guardrails tools install # install everything for this platform
guardrails tools install ruff # install specific tools
guardrails tools path shellcheck # print the command Guardrails will run
TOOL PINNED SOURCE RESOLVES TO
actionlint 1.7.12 archive managed
golangci-lint 2.15.0 archive /usr/local/bin/golangci-lint (PATH)
mypy 2.4.0 uv managed
ruff 0.17.1 uv managed
shellcheck 0.11.0 archive managed
terraform 1.16.6 archive managed
...
Why not containers?¶
Running each tool in a container sounds simpler, but it breaks the inner loop: container start-up eats most of the two-second per-edit budget, it needs a running daemon, and bind mounts make paths, permissions and caches awkward. Static binaries and uv-managed venvs start in milliseconds and work the same on every machine.