Tally is a Dockerfile/Containerfile linter and formatter designed to keep build files clean, modern, and consistent. It leverages BuildKit's official parser and checks, ensuring compatibility with the latest features while providing a fast, daemon-less experience that integrates seamlessly into CI pipelines.
Key Features:
Comprehensive Rule Coverage: Combines Docker’s official BuildKit checks, Hadolint-compatible rules, and custom rules for modern syntax like heredocs and mount options.
PowerShell and Windows Support: Parses full PowerShell syntax and understands Windows container specifics, including paths and shells.
Safe Auto-Fix Engine: Applies mechanical rewrites with --fix and offers opt-in AI-driven fixes via ACP for complex transformations.
Performance and Cross-Platform Availability: Runs as a single Go binary, installable via winget, Homebrew, npm, pip, and RubyGems, supporting Linux and Windows environments.
Editor Integration: Features a VS Code extension with LSP support, enhancing developer productivity.
CI-Friendly Outputs: Supports JSON, SARIF, and GitHub Actions annotations for seamless integration.
Audience & Benefit:
Ideal for developers, DevOps engineers, and teams using Docker or containerization. Tally ensures build files are clean and consistent, reduces errors, improves maintainability, and enhances productivity with automated fixes and CI support.
README
tally
tally is a production-grade Dockerfile/Containerfile linter + formatter that keeps build files clean, modern, and consistent.
It uses BuildKit's official parser and checks (the same foundation behind docker buildx) plus a safe auto-fix engine. It runs fast,
doesn't require Docker Desktop or a daemon, and fits neatly into CI.
# Lint everything in the repo (recursive)
tally lint .
# Apply all safe fixes automatically
tally lint --fix Dockerfile
Why tally
Modern Dockerfiles deserve modern tooling. tally is opinionated in the right places:
BuildKit-native: understands modern syntax like heredocs, RUN --mount=..., COPY --link, and ADD --checksum=....
Fixes, not just findings: --fix applies safe, mechanical rewrites; --fix-unsafe unlocks opt-in risky fixes (including AI).
Modernizes on purpose: converts eligible RUN/COPY instructions to heredocs, prefers ADD --extract, and more.
Broad rule coverage: combines Docker's official BuildKit checks, embedded ShellCheck for shell snippets, Hadolint-compatible rules, and
tally-specific rules.
PowerShell-aware: parses full PowerShell syntax for semantic tokens and rule analysis, so PowerShell RUN instructions are treated as real
code instead of opaque strings.
Windows-container aware: detects Windows container OS, understands Windows paths and default shells, and recognizes cmd.exe and
PowerShell-specific build patterns.
Registry-aware without Docker: uses a Podman-compatible registry client for image metadata checks (no daemon required).
Editor + CI friendly: VS Code extension (wharflab.tally, powered by tally lsp) and outputs for JSON, SARIF, and GitHub Actions annotations.
Easy to install anywhere: Homebrew, WinGet, Go, npm, pip, and RubyGems.
Written in Go: single fast binary, built on production-grade libraries.
Dockerfile linting usually means picking a compromise:
Hadolint is popular and battle-tested, but it uses its own Dockerfile parser, so support for newer BuildKit features can lag behind. It also
is commonly consumed as a prebuilt binary, and it focuses on reporting — not fixing.
docker buildx --check runs Docker's official BuildKit checks, but it requires the Docker/buildx toolchain and can be heavier than a pure
static linter (and not always available if you're using Podman/Finch/other runtimes).
Roadmap: more auto-fixes, more Hadolint parity, richer registry-aware checks, higher-level rules (cache & tmpfs mount recommendations,
tooling-aware checks for uv/bun, line-length and layer optimizations), and dedicated tally/powershell/* plus tally/windows/* rule families
for Windows-container and PowerShell-specific mistakes. That is a category of Dockerfile linting that Hadolint and other mainstream linters do
not cover today.
Optional: AI AutoFix via ACP
tally supports opt-in AI AutoFix for the kinds of improvements that are hard to express as a deterministic rewrite.
Instead of asking you for an API key, tally integrates with ACP (Agent Client Protocol) so you can use the agent you already trust (Gemini CLI,
OpenCode, GitHub Copilot CLI, and more), while tally keeps linting fast and validates proposed changes before applying them.
AI fixes are rule-driven (one narrow transformation at a time) and verified (re-parse + re-lint) before anything is applied.
Official images are published to GitHub Container Registry at ghcr.io/wharflab/tally.
All published images are signed with cosign (keyless/OIDC).
The Linux image is distroless, non-root, and shell-free. The Windows image is built on Nano Server.
ghcr.io/wharflab/tally:latest is the multi-platform image index. It auto-selects a distroless Linux image on linux/amd64
and linux/arm64, and a Nano Server image on windows/amd64.
ghcr.io/wharflab/tally:v is the versioned multi-platform image index (e.g., v1.2.3).
Per-platform tags are also published: v-linux-amd64, v-linux-arm64, and v-windows-amd64.
Linux example with Docker:
docker run --rm ghcr.io/wharflab/tally:latest version
docker run --rm -v "$PWD:/work" -w /work ghcr.io/wharflab/tally:latest lint Dockerfile
Linux example with Podman:
podman run --rm ghcr.io/wharflab/tally:latest version
podman run --rm -v "$PWD:/work:Z" -w /work ghcr.io/wharflab/tally:latest lint .
Windows container example:
docker run --rm ghcr.io/wharflab/tally:latest version
docker run --rm -v "${PWD}:C:\work" -w C:\work ghcr.io/wharflab/tally:latest lint Dockerfile
copy-ignored-file: Detects when COPY or ADD commands reference files that would be excluded by .dockerignore. This helps catch mistakes
where files are copied but won't actually be included in the build.
# .dockerignore contains: *.log
# This will trigger a warning:
COPY app.log /app/ # File matches .dockerignore pattern
# Heredoc sources are exempt (they're inline, not from context):
COPY < environment variables > config file > defaults.
**See [Configuration Guide](docs/guide/configuration.md) for full reference.**
## Output Formats
tally supports multiple output formats for different use cases.
### Text (default)
Human-readable output with colors and source code snippets:
```bash
tally lint Dockerfile
WARNING: StageNameCasing - https://docs.docker.com/go/dockerfile/rule/stage-name-casing/
Stage name 'Builder' should be lowercase
Dockerfile:2
────────────────────
1 │ FROM alpine
>>>2 │ FROM ubuntu AS Builder
3 │ RUN echo "hello"
────────────────────
JSON
Machine-readable format with summary statistics and scan metadata:
Control which severity levels cause a non-zero exit code:
# Fail only on errors (ignore warnings)
tally lint --fail-level error Dockerfile
# Never fail (useful for CI reporting without blocking)
tally lint --fail-level none --format sarif Dockerfile > results.sarif
# Fail on any violation including style issues (default behavior)
tally lint --fail-level style Dockerfile
Available levels (from most to least severe): error, warning, info, style (default), none
IDE Integration
VS Code
Install the official tally extension from the Visual Studio Marketplace for
real-time linting and diagnostics.
JetBrains IDEs
Install the official Tally plugin from JetBrains Marketplace for IDE integration in
IntelliJ-based editors.
Other Editors
Any editor that supports the Language Server Protocol can use tally's built-in LSP server
over stdio:
npx -y tally-cli lsp --stdio
Development
Running Tests
# Run all tests
make test
# Run linting
make lint
# Run copy/paste detection (CPD)
make cpd
Code Quality
This project uses:
golangci-lint for Go linting
PMD CPD for copy/paste detection (minimum 100 tokens)
Copy/paste detection runs automatically in CI and helps identify duplicate code patterns.