# AI AutoFix (ACP)
Source: https://tally.wharflab.com/guides/ai-autofix
Use ACP-compatible coding agents to fix complex Dockerfile issues that are too risky for deterministic rewrites.
tally supports **opt-in AI AutoFix** for the kinds of Dockerfile improvements that are hard to express as a purely mechanical rewrite — or too risky
to apply without extra validation.
Instead of asking you for an API key, tally integrates with
[**ACP (Agent Client Protocol)**](https://agentclientprotocol.com/get-started/introduction) — a protocol created by the [Zed editor](https://zed.dev/)
to standardize how tools talk to coding agents.
This means:
* You choose **which agent** you want to use (Gemini CLI, OpenCode, GitHub Copilot CLI, and more).
* You keep **credentials and model choice** inside that agent.
* tally stays a **linter first** — fast and deterministic — and uses AI only when you explicitly opt in.
## How it works
tally treats AI AutoFix as a normal part of its existing fix pipeline:
1. A rule detects a violation and attaches a **SuggestedFix** marked as async.
2. tally builds a **prompt** containing the Dockerfile text and structured rule evidence.
3. tally runs your configured agent via **ACP over stdio**.
4. The agent returns a **unified diff patch** targeting the exact Dockerfile bytes from the prompt.
5. tally **validates** the patch: parses it, re-lints the result, and checks invariants.
6. If valid, the patch is applied. If not, tally skips the fix and continues linting.
Linting always works even when AI is misconfigured or unavailable.
## Quick start
Choose an ACP-capable CLI agent. Any of these work out of the box:
* [Gemini CLI](https://agentclientprotocol.com/agents/gemini-cli) (native ACP)
* [OpenCode](https://opencode.ai/docs/acp/) (native ACP)
* [GitHub Copilot CLI](https://docs.github.com/en/copilot/reference/acp-server) (native ACP)
* [Cline CLI v2](https://docs.cline.bot/cline-cli/acp-editor-integrations) (native ACP)
* [Kiro CLI](https://kiro.dev/docs/cli/acp/) (native ACP)
* [Docker agent](https://docker.github.io/docker-agent/features/acp/) (native ACP)
Browse the full registry at [agentclientprotocol.com/get-started/registry](https://agentclientprotocol.com/get-started/registry).
Create or update your `.tally.toml`. The example below uses Gemini CLI with MCP servers disabled for lower latency:
```toml theme={null}
[ai]
enabled = true
timeout = "90s"
max-input-bytes = 262144
redact-secrets = true
command = [
"gemini",
"--experimental-acp",
"--allowed-mcp-server-names=none",
"--model=gemini-3-flash-preview",
]
```
`--allowed-mcp-server-names` is an allowlist. Passing a name you don't have configured (like `none`) effectively disables all MCP servers. tally doesn't provide any MCP servers to the agent today, so enabling MCP is usually just extra startup and latency overhead.
AI fixes are intentionally marked **unsafe** and require both `--fix` and `--fix-unsafe`. For best results, narrow the scope to a single rule:
```bash theme={null}
tally lint \
--fix --fix-unsafe \
--fix-rule tally/prefer-multi-stage-build \
path/to/Dockerfile
```
To prevent AI fixes from running accidentally, set the rule's fix mode to `"explicit"` in your config:
```toml theme={null}
[rules.tally.prefer-multi-stage-build]
fix = "explicit"
```
## Recommended setup (low latency)
Dockerfiles are a mature domain that most modern models understand well. For AI fixes, you usually don't need external tools or context servers — you
want fast, predictable transformations.
**Recommended:**
* A fast or smaller model with solid general reasoning.
* Disable agent-side tool integrations (MCP servers) unless you know you need them.
```bash theme={null}
# Gemini CLI — fast model, no MCP overhead
gemini --experimental-acp --allowed-mcp-server-names=none --model=gemini-3-flash-preview
```
## Configuration reference
### Config file (`.tally.toml`)
All AI settings live under `[ai]`:
```toml theme={null}
[ai]
enabled = false # Default: false
command = ["gemini", "--experimental-acp", "--allowed-mcp-server-names=none", "--model=gemini-3-flash-preview"]
timeout = "90s" # Per-fix timeout
max-input-bytes = 262144 # Prompt size limit (bytes)
redact-secrets = true # Redact obvious secrets (default: true)
```
| Setting | Default | Description |
| -------------------- | --------- | ---------------------------------------------------- |
| `ai.enabled` | `false` | Master kill-switch for AI features |
| `ai.command` | *(empty)* | ACP agent argv (stdio). If empty, AI fixes can't run |
| `ai.timeout` | `"90s"` | Per-fix timeout for the ACP interaction |
| `ai.max-input-bytes` | `262144` | Maximum prompt size to send to the agent |
| `ai.redact-secrets` | `true` | Redact obvious secrets in prompts (best-effort) |
### Environment variables
```bash theme={null}
TALLY_AI_ENABLED=true
TALLY_ACP_COMMAND="gemini --experimental-acp --allowed-mcp-server-names=none --model=gemini-3-flash-preview"
TALLY_AI_TIMEOUT=90s
TALLY_AI_MAX_INPUT_BYTES=262144
TALLY_AI_REDACT_SECRETS=true
```
### CLI flags
```bash theme={null}
--ai # Enable AI (when ai.command is already in .tally.toml)
--acp-command "..." # Set the ACP agent command line (also enables AI)
--ai-timeout 90s # Override ai.timeout
--ai-max-input-bytes 262144 # Override ai.max-input-bytes
--ai-redact-secrets=false # Override ai.redact-secrets
```
If your agent command needs complex quoting, prefer `ai.command = ["arg1", "arg2", ...]` in `.tally.toml` rather than `--acp-command`.
## Supported ACP agents
### Native ACP agents
| Agent | Link |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Gemini CLI | [agentclientprotocol.com/agents/gemini-cli](https://agentclientprotocol.com/agents/gemini-cli) |
| OpenCode | [opencode.ai/docs/acp/](https://opencode.ai/docs/acp/) |
| GitHub Copilot CLI | [docs.github.com/en/copilot/reference/acp-server](https://docs.github.com/en/copilot/reference/acp-server) |
| Kiro CLI | [kiro.dev/docs/cli/acp/](https://kiro.dev/docs/cli/acp/) |
| Cline CLI v2 | [docs.cline.bot/cline-cli/acp-editor-integrations](https://docs.cline.bot/cline-cli/acp-editor-integrations) |
| Docker agent | [docker.github.io/docker-agent/features/acp/](https://docker.github.io/docker-agent/features/acp/) |
| QwenCode | [qwenlm.github.io/qwen-code-docs](https://qwenlm.github.io/qwen-code-docs/en/users/integration-zed/#install-from-acp-registry-recommend) |
### Zed-maintained adapters
| Agent | Adapter |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| Claude Code | [agentclientprotocol.com/agents/claude-code](https://agentclientprotocol.com/agents/claude-code) |
| OpenAI Codex CLI | [agentclientprotocol.com/agents/codex](https://agentclientprotocol.com/agents/codex) |
## Rules with AI AutoFix
Today tally routes AI AutoFix to two rule objectives. Each objective owns its own prompt, validators, and acceptance criteria:
| Rule | Objective | What it does |
| ------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [`tally/prefer-multi-stage-build`](/rules/tally/prefer-multi-stage-build) | `prefer-multi-stage-build` | Converts single-stage Dockerfiles into a builder + runtime split, preserving final-stage runtime invariants. |
| [`tally/gpu/prefer-uv-over-conda`](/rules/tally/gpu/prefer-uv-over-conda) | `prefer-uv-over-conda` | Migrates narrow GPU Python Dockerfiles from conda/mamba/micromamba to uv, preserving final-stage runtime invariants. |
Each rule emits a whole-file rewrite, so only one AI fix can land per `--fix` invocation. If a single Dockerfile triggers both rules, run
`--fix --fix-unsafe --fix-rule ` twice, once per rule, to compose the rewrites.
## Security and privacy
ACP is a protocol, **not a sandbox**. If you run a local agent process that can access your machine, it can still do so outside of ACP. Treat the agent like any other executable you run locally.
tally adds multiple guardrails for AI fixes:
* **Explicit opt-in** — AI is off unless you set `ai.enabled = true`.
* **Unsafe gating** — AI fixes require `--fix-unsafe` in addition to `--fix`.
* **Minimal capabilities** — tally advertises no filesystem and no terminal capabilities via ACP.
* **Secret redaction** — prompts are best-effort redacted before being sent to the agent (controlled by `ai.redact-secrets`).
* **Strict output contract** — the agent must return a small, targeted diff patch that applies cleanly to the exact Dockerfile bytes tally sent.
* **Validation loop** — tally re-parses, re-lints, and checks runtime invariants before accepting any proposed change.
## Troubleshooting: "Skipped N fixes"
Common reasons a fix is skipped:
| Reason | Fix |
| ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `--fix` not passed | Add `--fix` to your command |
| `--fix-unsafe` not passed | AI fixes always require `--fix-unsafe` |
| `--fix-rule` set, but the rule didn't trigger | The rule had no violations for this Dockerfile |
| `tally/prefer-multi-stage-build` not triggering | This rule only fires for Dockerfiles with exactly **one `FROM`** |
| Agent timed out | Increase `--ai-timeout` or check stderr for the error message |
| Agent failed | tally prints the reason on stderr and keeps stdout clean for JSON/SARIF output |
## Why ACP instead of API keys
Many tools bolt AI onto a linter by asking for an OpenAI or Anthropic API key. That approach comes with trade-offs:
* **Provider lock-in** — the linter becomes a mini "AI platform" that must track models, pricing, retries, and auth.
* **Secret sprawl** — API keys end up in dotfiles, CI secrets, and team docs.
* **Enterprise friction** — organizations often standardize on a specific gateway, proxy, or provider policy.
* **Inconsistent experience** — your editor agent knows your preferences, but your linter uses a completely different stack.
ACP inverts this: tally stays agent-agnostic, you bring your own agent and existing auth setup, and you can switch models or providers without waiting
for tally to add a new integration.
# Auto-fix
Source: https://tally.wharflab.com/guides/auto-fix
Apply safe and unsafe auto-fixes to Dockerfiles using tally lint --fix.
tally can apply fixes automatically. Fixes are designed to be:
* **Atomic** — a fix applies fully or not at all.
* **Conflict-aware** — overlapping edits are skipped rather than producing a corrupted file.
* **Configurable** — per-rule fix modes let you control exactly which fixes run and when.
## Basic usage
Apply all safe fixes:
```bash theme={null}
tally lint --fix Dockerfile
```
Apply unsafe fixes too (includes AI-powered fixes when enabled):
```bash theme={null}
tally lint --fix --fix-unsafe Dockerfile
```
`--fix` is not supported when the lint entrypoint is a Bake or Compose file. Lint the Dockerfile directly when you want tally to modify files.
Limit fixes to specific rules:
```bash theme={null}
tally lint --fix --fix-unsafe \
--fix-rule hadolint/DL3008 \
--fix-rule tally/prefer-copy-heredoc \
Dockerfile
```
Use `--fix-rule` to limit the blast radius when applying fixes for the first time. Start with one rule at a time to review changes before committing.
## Safe vs. unsafe fixes
| Mode | Flag | Description |
| ------ | ------------------------ | -------------------------------------------------------------------------------- |
| Safe | `--fix` | Applies mechanical rewrites with high confidence (🔧 in the rules reference) |
| Unsafe | `--fix` + `--fix-unsafe` | Also applies riskier fixes that may change semantics, including AI-powered fixes |
The 🔧 marker in the rules reference indicates a rule has auto-fix support. Safe fixes are things like adding a missing newline, reordering
instructions, or converting `RUN echo` to a `COPY` heredoc. Unsafe fixes may restructure stages or change command arguments.
## Per-rule fix modes
Control when fixes are allowed per rule in `.tally.toml`:
```toml theme={null}
[rules.tally.prefer-copy-heredoc]
fix = "always" # Apply whenever --fix is set (default)
[rules.tally.prefer-multi-stage-build]
fix = "explicit" # Only when --fix-rule includes this rule explicitly
[rules.tally.no-trailing-spaces]
fix = "unsafe-only" # Only when --fix-unsafe is also set
[rules.tally.eol-last]
fix = "never" # Never auto-fix this rule
```
Valid fix mode values:
| Value | Behavior |
| ------------- | ---------------------------------------------------------- |
| `always` | Apply whenever `--fix` is passed (default) |
| `never` | Never auto-fix, even when `--fix` is set |
| `explicit` | Only when `--fix-rule ` is explicitly specified |
| `unsafe-only` | Only when `--fix-unsafe` is also set |
## How conflict resolution works
When two fixes would modify overlapping lines, tally skips the conflicting fix rather than applying a partial or corrupted change. The skipped fix is
reported to stderr:
```text theme={null}
Skipped 1 fixes
note: skipped fix tally/prefer-copy-heredoc (Dockerfile): overlapping edit at line 14
```
Fix skips are informational — linting continues and the violation is still reported so you can address it manually.
## Examples of fixable rules
Rules marked 🔧 in the rules reference support auto-fix. Some notable examples:
| Rule | Fix type | What it does |
| ------------------------------------ | ----------- | --------------------------------------------------------- |
| `tally/prefer-copy-heredoc` | Safe | Converts `RUN echo`/`cat`/`printf` to `COPY` heredoc |
| `tally/prefer-copy-chmod` | Safe | Converts `COPY` + `RUN chmod` to `COPY --chmod` |
| `tally/no-trailing-spaces` | Safe | Removes trailing whitespace |
| `tally/eol-last` | Safe | Adds missing newline at end of file |
| `tally/sort-packages` | Safe | Sorts package lists alphabetically |
| `tally/epilogue-order` | Safe | Reorders `STOPSIGNAL`, `HEALTHCHECK`, `ENTRYPOINT`, `CMD` |
| `tally/curl-should-follow-redirects` | Safe | Adds `-L` to `curl` commands |
| `tally/prefer-multi-stage-build` | Unsafe (AI) | Converts single-stage builds to multi-stage |
| `tally/prefer-package-cache-mounts` | Unsafe (AI) | Adds BuildKit cache mounts for package installs |
## AI AutoFix
Some fixes are too complex to implement deterministically. For those, tally supports an opt-in AI resolver via ACP (Agent Client Protocol).
AI fixes are marked unsafe and require both `--fix` and `--fix-unsafe`. See the [AI AutoFix (ACP)](/guides/ai-autofix) guide for setup instructions.
# Build invocations
Source: https://tally.wharflab.com/guides/build-invocations
Lint Dockerfiles through Docker Buildx Bake and Docker Compose entrypoints.
A build invocation is one planned build of one Dockerfile. When you lint a Bake or Compose file, tally resolves each target or service into the
Dockerfile, build context, build args, platform, target stage, named contexts, and runtime metadata declared by that orchestrator.
Use invocation-aware linting when the Dockerfile is not the whole source of truth for the build.
Lint the targets from `docker-bake.hcl` or `docker-bake.json`.
Lint services with a `build:` section from `compose.yaml`.
Use the orchestrator's context instead of repeating `--context`.
Attach Bake or Compose context to Dockerfile diagnostics in LSP clients.
## Supported entrypoints
`tally lint` still uses one command. The behavior depends on the explicit path you pass.
| Input | Behavior |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| `tally lint .` | Recursively discovers Dockerfiles and Containerfiles. No orchestrator discovery is performed. |
| `tally lint Dockerfile` | Lints one Dockerfile directly. |
| `tally lint docker-bake.hcl` | Resolves Bake targets and lints each resulting build invocation. |
| `tally lint docker-bake.json` | Resolves Bake JSON targets and lints each resulting build invocation. |
| `tally lint compose.yaml` | Resolves Compose services with `build:` sections and lints each resulting build invocation. |
Orchestrator mode requires one explicit regular file. Do not mix a Bake or Compose file with other lint inputs in the same command.
Directory and glob inputs keep their Dockerfile discovery behavior. This means `tally lint .` does not search for `compose.yaml` or
`docker-bake.hcl`; pass those files explicitly when they are the build source of truth.
## Dockerfile mode and `--context`
For direct Dockerfile linting, use `--context` when rules need access to files in the build context:
```bash theme={null}
tally lint --context . Dockerfile
```
This is useful for checks that depend on `.dockerignore` or on files copied into the image.
When you lint a Bake or Compose entrypoint, do not pass `--context`. tally derives the primary build context from the selected target or service.
```bash theme={null}
# Correct: context comes from docker-bake.hcl
tally lint docker-bake.hcl
# Error: --context is only for direct Dockerfile mode
tally lint --context . docker-bake.hcl
```
## Docker Buildx Bake
Lint all targets in the Bake default group:
```bash theme={null}
tally lint docker-bake.hcl
```
Select specific targets or groups with `--target`:
```bash theme={null}
tally lint docker-bake.hcl --target api --target worker
```
Example Bake file:
```hcl docker-bake.hcl theme={null}
group "default" {
targets = ["api", "worker"]
}
target "api" {
context = "./services/api"
dockerfile = "Dockerfile"
target = "runtime"
platforms = ["linux/amd64"]
args = {
NODE_VERSION = "22"
}
contexts = {
base = "target:base"
}
}
target "worker" {
context = "./services/worker"
dockerfile = "Containerfile"
}
```
tally uses the resolved target data when linting:
* `context` becomes the primary build context.
* `dockerfile` selects the file to lint.
* `args` with concrete values are available to Dockerfile analysis.
* `target` selects the effective final stage.
* `platforms` are available to platform-aware checks.
* `contexts` are preserved as named build contexts.
### Bake limitations
| Feature | Behavior |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dockerfile-inline` | Error. tally needs a real Dockerfile path for diagnostics and fixes. |
| Multi-file Bake setups | Error when sibling Bake files are detected. Pass one explicit file that contains the full target definition, or lint the resolved Dockerfiles directly. |
| Remote Bake definition URLs | Not supported as lint entrypoints. Pass a local Bake file. |
| Non-local primary contexts | Supported as invocation metadata, but tally cannot inspect remote files or resolve a relative Dockerfile from a remote context. |
## Docker Compose
Lint all active services with a `build:` section:
```bash theme={null}
tally lint compose.yaml
```
Select specific services with `--service`:
```bash theme={null}
tally lint compose.yaml --service api --service worker
```
Example Compose file:
```yaml compose.yaml theme={null}
services:
api:
build:
context: ./services/api
dockerfile: Dockerfile
target: runtime
args:
NODE_VERSION: "22"
additional_contexts:
base: service:base
ports:
- "8080:8080"
environment:
NODE_ENV: production
worker:
build:
context: ./services/worker
dockerfile: Containerfile
redis:
image: redis:7
```
In this example, `api` and `worker` produce lint invocations. `redis` does not, because it has no `build:` section.
tally preserves both build-time and selected runtime metadata:
* `build.context`, `build.dockerfile`, `build.args`, `build.platforms`, `build.target`, and `build.additional_contexts`
* service `platform` when `build.platforms` is not set
* service `environment`, `ports`, `expose`, `labels`, `networks`, `secrets`, `healthcheck`, `entrypoint`, `command`, `user`, `working_dir`, and
`stop_signal`
Some rules do not use all of this metadata yet. The data is still part of the invocation so current and future checks agree on the same planned
build.
### Compose limitations
| Feature | Behavior |
| --------------------------------------- | ------------------------------------------------------------------------ |
| `build.dockerfile_inline` | Error. tally needs a real Dockerfile path. |
| Profile-gated build services | Error. tally does not guess which non-default profiles should be active. |
| Selected service without `build:` | Error. `--service` must name a buildable service. |
| Image-only services | Allowed, but they do not produce lint invocations. |
| Unsaved Compose file edits in an editor | Ignored until the file is saved. |
## Build contexts
The primary context is the context used for regular `COPY` and `ADD` sources.
| Context kind | Example | Local file inspection |
| ----------------------- | --------------------------------------------- | ------------------------ |
| Local directory | `context = "."`, `build.context: ./app` | Yes |
| Local tar archive | `context = "./context.tar"` | No |
| Git repository | `context = "https://github.com/acme/app.git"` | No |
| Remote URL | `context = "https://example.com/context"` | No |
| Empty context | `context = "-"` | No |
| Named image context | `docker-image://alpine:3.20` | No |
| Bake target context | `target:base` | No local file inspection |
| Compose service context | `service:base` | No local file inspection |
Only local directory contexts let tally evaluate `.dockerignore` and read files copied by `COPY` or `ADD`. Remote and non-directory contexts are
valid invocation metadata, but tally does not fetch them during linting.
Named contexts are tracked separately from the primary context. They are available to rules that need to know that a name was declared, but they do
not make files locally readable unless a future rule explicitly supports that context kind.
## Output and attribution
When an orchestrator produces more than one invocation, tally keeps diagnostics separate even if multiple invocations point at the same Dockerfile.
Text output groups findings by invocation:
```text theme={null}
[bake target: api]
services/api/Dockerfile:12: buildkit/UndefinedVar - Usage of undefined variable '$NODE_VERSION'
[bake target: worker]
services/worker/Containerfile:8: tally/max-lines - File has 520 lines, exceeding maximum of 500
Summary: 2 Dockerfiles, 2 invocations, 2 violations.
```
JSON output includes invocation metadata on each violation and reports both file and invocation counts:
```json theme={null}
{
"files": [
{
"file": "services/api/Dockerfile",
"violations": [
{
"rule": "buildkit/UndefinedVar",
"message": "Usage of undefined variable '$NODE_VERSION'",
"invocation": {
"kind": "bake",
"file": "/repo/docker-bake.hcl",
"name": "api"
}
}
]
}
],
"summary": {
"total": 1,
"files": 1,
"invocations": 1
},
"files_scanned": 1,
"invocations_scanned": 1
}
```
SARIF stores invocation metadata in result properties. GitHub Actions annotations prefix the message with the invocation label. Markdown output adds
an `Invocation` column.
## Fixes
`--fix` is not supported when the lint entrypoint is a Bake or Compose file:
```bash theme={null}
tally lint --fix compose.yaml
```
This exits with code `2`. A single Dockerfile can be linted under multiple invocation contexts, and applying one text edit through an orchestrator
entrypoint could be wrong for another target or service.
To apply fixes, lint the Dockerfile directly:
```bash theme={null}
tally lint --fix services/api/Dockerfile
```
Review the fix against the relevant Bake or Compose invocation before committing it.
## LSP and editor use
The LSP server remains Dockerfile-document-centric. To attach orchestrator context to Dockerfile diagnostics, configure invocation entrypoints in
your editor or LSP client.
User-facing setting:
```json theme={null}
{
"tally.invocationEntrypoints": [
"compose.yaml",
"docker-bake.hcl"
]
}
```
Protocol payload shape used by LSP clients:
```json theme={null}
{
"tally": {
"workspaces": [
{
"uri": "file:///repo",
"settings": {
"invocationEntrypoints": [
"compose.yaml",
"docker-bake.hcl"
]
}
}
]
}
}
```
For each open Dockerfile, tally finds invocations from those entrypoints whose resolved Dockerfile path matches the document. Diagnostics use source
labels such as `tally/bake:api` or `tally/compose:worker`.
Mutating code actions are only shown when the requested diagnostic or range belongs to one invocation context. Broad fix-all actions are disabled
when the selected diagnostics span multiple invocation contexts.
## Exit codes
Orchestrator entrypoints use the same exit-code family as Dockerfile linting:
| Code | Orchestrator meaning |
| ---- | ------------------------------------------------------------------------------------------------------ |
| `0` | Clean run, or a valid Bake/Compose file with no lintable invocations. |
| `1` | Violations were found at or above `--fail-level`. |
| `2` | CLI misuse, parse/load failure, unsupported orchestrator feature, or invalid target/service selection. |
| `4` | A referenced Dockerfile has fatal syntax errors. |
Exit code `3` remains the Dockerfile discovery "no files found" case for directory and glob inputs. A valid orchestrator file with zero buildable
targets or services exits `0`, not `3`.
# CI/CD integration
Source: https://tally.wharflab.com/guides/ci-cd
Integrate tally into GitHub Actions, GitLab CI, and pre-commit hooks for automated Dockerfile linting.
tally is designed to run fast in CI without requiring Docker Desktop or a daemon. It produces output in formats that native CI systems understand
natively, including GitHub Actions annotations and SARIF for code scanning.
## Quick tips
* Use `--fail-level` to control which severities fail CI (for example, fail on `warning` but not on `style`).
* Use `--exclude` to skip generated or vendor trees.
* Commit a `.tally.toml` to keep CI and local runs consistent.
* Use `--format github-actions` for inline PR annotations on GitHub.
* Use `--format sarif` to upload results to GitHub Code Scanning or Azure DevOps.
* Lint `docker-bake.hcl` or `compose.yaml` directly when those files define the real build.
### Basic lint step
Add tally to any workflow that touches Dockerfiles:
```yaml theme={null}
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
tally:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install tally
run: npm install -g tally-cli
- name: Lint Dockerfiles
run: tally lint --format github-actions .
```
The `github-actions` format emits `::warning` and `::error` annotations that GitHub renders inline in the PR diff.
### SARIF upload to Code Scanning
Upload results to GitHub Code Scanning for a persistent view of findings across commits:
```yaml theme={null}
name: Lint
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * 1" # Weekly on Monday
jobs:
tally:
runs-on: ubuntu-latest
permissions:
security-events: write # Required for SARIF upload
steps:
- uses: actions/checkout@v4
- name: Install tally
run: npm install -g tally-cli
- name: Run tally
run: |
tally lint \
--format sarif \
--output tally.sarif \
--fail-level none \
.
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: tally.sarif
```
Use `--fail-level none` when uploading SARIF so the step doesn't fail before the upload runs. Code Scanning will surface the findings separately.
### Matrix strategy for multiple Dockerfiles
Lint different Dockerfiles in parallel using a matrix:
```yaml theme={null}
jobs:
tally:
runs-on: ubuntu-latest
strategy:
matrix:
dockerfile:
- Dockerfile
- Dockerfile.dev
- services/api/Dockerfile
steps:
- uses: actions/checkout@v4
- name: Install tally
run: npm install -g tally-cli
- name: Lint ${{ matrix.dockerfile }}
run: tally lint --format github-actions ${{ matrix.dockerfile }}
```
### Lint step
```yaml theme={null}
tally:lint:
image: node:lts-alpine
stage: lint
script:
- npm install -g tally-cli
- tally lint --format text .
rules:
- changes:
- "**Dockerfile*"
- "**Containerfile*"
- .tally.toml
```
### With SARIF artifact
GitLab supports SARIF reports as Code Quality artifacts:
```yaml theme={null}
tally:lint:
image: node:lts-alpine
stage: lint
script:
- npm install -g tally-cli
- tally lint --format sarif --output gl-sast-report.sarif --fail-level none . || true
artifacts:
reports:
sast: gl-sast-report.sarif
when: always
expire_in: 1 week
```
### Using tally as a pre-commit hook
Add tally to your `.pre-commit-config.yaml`. Because tally is distributed via npm, pip, and RubyGems, you can use `language: system` with a globally installed binary:
```yaml theme={null}
repos:
- repo: local
hooks:
- id: tally
name: tally (Dockerfile linter)
language: system
entry: tally lint
args: ["--format", "text"]
files: '(Dockerfile|Containerfile)(\..*)?$|.*\.(Dockerfile|Containerfile)$'
pass_filenames: true
```
Install pre-commit and the hook:
```bash theme={null}
pip install pre-commit
npm install -g tally-cli
pre-commit install
```
Run manually against all files:
```bash theme={null}
pre-commit run tally --all-files
```
### Fail level for pre-commit
To only block commits on errors (not warnings or style issues), add `--fail-level error`:
```yaml theme={null}
- id: tally
name: tally (Dockerfile linter)
language: system
entry: tally lint
args: ["--fail-level", "error"]
files: '(Dockerfile|Containerfile)(\..*)?$|.*\.(Dockerfile|Containerfile)$'
pass_filenames: true
```
## Lint Bake or Compose in CI
If your CI builds images through Bake or Compose, lint the same entrypoint rather than rediscovering Dockerfiles:
```bash theme={null}
# Docker Buildx Bake
tally lint --format github-actions docker-bake.hcl
# Docker Compose
tally lint --format github-actions compose.yaml
```
Select only the build that changed:
```bash theme={null}
tally lint --format github-actions docker-bake.hcl --target api
tally lint --format github-actions compose.yaml --service api
```
Do not use `--fix` in orchestrator CI jobs. Orchestrator runs can represent multiple builds of the same Dockerfile, so fixes are only available when
linting a Dockerfile directly. See [Build invocations](/guides/build-invocations) for the full behavior.
## Output format recommendations
| CI system | Recommended format | Why |
| ---------------------------- | -------------------- | ----------------------------------- |
| GitHub Actions (annotations) | `github-actions` | Inline PR diff annotations |
| GitHub Code Scanning | `sarif` | Persistent findings in Security tab |
| GitLab Code Quality | `sarif` | SAST artifact support |
| Azure DevOps | `sarif` | SARIF is natively supported |
| Terminal / local | `text` (default) | Human-readable with source snippets |
| AI agents / scripts | `json` or `markdown` | Machine-readable or token-efficient |
## Related guides
* [Configuration](/guides/configuration) — set `fail-level`, `format`, and `exclude` in `.tally.toml`
* [Output formats](/guides/output-formats) — full reference for all output formats
* [Exit codes](/guides/exit-codes) — how to handle each exit code in scripts
# Configuration
Source: https://tally.wharflab.com/guides/configuration
Complete reference for tally's config file, environment variables, CLI flags, and inline directives.
tally supports configuration via TOML config files, environment variables, and CLI flags. Sources cascade in a predictable order so you can set
project defaults while allowing per-run overrides.
## Priority order
Configuration sources are applied highest-priority first:
1. **CLI flags** — `--fail-level error`
2. **Environment variables** — `TALLY_OUTPUT_FAIL_LEVEL=error`
3. **Config file** — `.tally.toml` or `tally.toml`
4. **Built-in defaults**
## Config file
### File names
tally looks for these config file names, in order:
1. `.tally.toml` (hidden file, recommended)
2. `tally.toml`
### Discovery
tally uses cascading config discovery similar to [Ruff](https://docs.astral.sh/ruff/configuration/):
1. Starting from the Dockerfile's directory, walks up the filesystem.
2. Stops at the first `.tally.toml` or `tally.toml` found.
3. Uses that config — no merging with parent configs.
This allows monorepo setups with per-directory configurations:
```text theme={null}
monorepo/
├── .tally.toml # Default config for most services
├── services/
│ ├── api/
│ │ └── Dockerfile # Uses monorepo/.tally.toml
│ └── legacy/
│ ├── .tally.toml # Override for legacy service
│ └── Dockerfile # Uses services/legacy/.tally.toml
```
### Explicit config path
Override discovery with `--config`:
```bash theme={null}
tally lint --config /path/to/.tally.toml Dockerfile
```
***
## Config file reference
Controls how tally reports violations.
```toml theme={null}
[output]
format = "text" # text, json, sarif, github-actions, markdown
path = "stdout" # stdout, stderr, or a file path
show-source = true # Show source code snippets
fail-level = "style" # Minimum severity for exit code 1
```
| Option | Default | Description |
| ------------- | ---------- | --------------------------------------------------------------------------------------- |
| `format` | `"text"` | Output format: `text`, `json`, `sarif`, `github-actions`, `markdown` |
| `path` | `"stdout"` | Output destination: `stdout`, `stderr`, or a file path |
| `show-source` | `true` | Show source code snippets alongside violations |
| `fail-level` | `"style"` | Minimum severity that produces exit code 1: `error`, `warning`, `info`, `style`, `none` |
Controls auto-fix safety when fixes are requested.
```toml theme={null}
unsafe-fixes = true
```
| Option | Default | Description |
| -------------- | ------- | ------------------------------------------------------- |
| `unsafe-fixes` | unset | Enable application of unsafe fixes when `--fix` is used |
Controls which rules are enabled and how they are configured.
### Rule selection
Use glob patterns to include or exclude rules by namespace or by specific rule code:
```toml theme={null}
[rules]
include = ["buildkit/*", "tally/*", "hadolint/*"]
exclude = [
"buildkit/MaintainerDeprecated",
"hadolint/DL3008",
]
```
#### Per-rule configuration
Configure individual rules with `severity` and rule-specific options:
```toml theme={null}
# Syntax: [rules..]
[rules.tally.max-lines]
severity = "error"
max = 500
skip-blank-lines = true
skip-comments = true
[rules.buildkit.StageNameCasing]
severity = "info" # Downgrade from warning to info
[rules.hadolint.DL3026]
severity = "warning"
trusted-registries = ["docker.io", "gcr.io", "ghcr.io"]
```
#### Severity levels
| Severity | Description |
| ----------- | ----------------------------------------- |
| `"off"` | Disable the rule |
| `"error"` | Critical issues that should block CI |
| `"warning"` | Important issues that should be addressed |
| `"info"` | Informational suggestions |
| `"style"` | Style preferences |
#### Enabling off-by-default rules
Some rules are disabled by default (for example, experimental rules). Enable them by providing configuration:
```toml theme={null}
# DL3026 is off by default — providing config auto-enables it
[rules.hadolint.DL3026]
trusted-registries = ["docker.io", "ghcr.io"]
# Or set severity explicitly
[rules.tally.prefer-copy-heredoc]
severity = "style"
```
Controls how inline ignore comments are processed.
```toml theme={null}
[inline-directives]
enabled = true # Process inline directives (default: true)
warn-unused = false # Warn about unused directives (default: false)
validate-rules = false # Warn about unknown rule codes (default: false)
require-reason = false # Require reason= on all ignore directives (default: false)
```
| Option | Default | Description |
| ---------------- | ------- | -------------------------------------------------------- |
| `enabled` | `true` | Process inline directives |
| `warn-unused` | `false` | Warn about directives that don't suppress any violations |
| `validate-rules` | `false` | Warn about unknown rule codes in directives |
| `require-reason` | `false` | Require `reason=` on all ignore directives |
Configuration for AI AutoFix via ACP. See [AI AutoFix (ACP)](/guides/ai-autofix) for the full guide.
```toml theme={null}
[ai]
enabled = false
command = ["gemini", "--experimental-acp", "--allowed-mcp-server-names=none", "--model=gemini-3-flash-preview"]
timeout = "90s"
max-input-bytes = 262144
redact-secrets = true
```
| Setting | Default | Description |
| ----------------- | --------- | ---------------------------------------------------- |
| `enabled` | `false` | Master kill-switch for AI features |
| `command` | *(empty)* | ACP agent argv (stdio). If empty, AI fixes can't run |
| `timeout` | `"90s"` | Per-fix timeout for the ACP interaction |
| `max-input-bytes` | `262144` | Maximum prompt size to send to the agent |
| `redact-secrets` | `true` | Redact obvious secrets in prompts (best-effort) |
Pre-parse file validation. Useful for rejecting unexpectedly large files before linting.
```toml theme={null}
[file-validation]
max-file-size = 102400 # bytes; 0 = unlimited
```
| Option | Default | Description |
| --------------- | ----------------- | --------------------------------------------------------------------------------------------------------- |
| `max-file-size` | `102400` (100 KB) | Maximum file size in bytes. Files above this limit are rejected before parsing. Set to `0` for unlimited. |
Controls registry-aware and other slow checks that require network access.
```toml theme={null}
[slow-checks]
mode = "auto" # auto, on, off
timeout = "20s"
fail-fast = true
```
| Option | Default | Description |
| ----------- | -------- | ---------------------------------------------------------------------------------------- |
| `mode` | `"auto"` | `auto` skips slow checks in CI; `on` always runs them; `off` always skips them |
| `timeout` | `"20s"` | Timeout for slow checks |
| `fail-fast` | `true` | Skip slow checks for files that already have `error`-severity violations from fast rules |
You can also control this via CLI:
```bash theme={null}
tally lint --slow-checks=on --slow-checks-timeout=30s Dockerfile
```
***
## Environment variables
| Variable | Description |
| -------------------------- | -------------------------------------------------------------------- |
| `TALLY_OUTPUT_FORMAT` | Output format: `text`, `json`, `sarif`, `github-actions`, `markdown` |
| `TALLY_FORMAT` | Alias for `TALLY_OUTPUT_FORMAT` |
| `TALLY_OUTPUT_PATH` | Output destination: `stdout`, `stderr`, or file path |
| `TALLY_OUTPUT_SHOW_SOURCE` | Show source snippets: `true` / `false` |
| `TALLY_OUTPUT_FAIL_LEVEL` | Minimum severity for non-zero exit |
| `NO_COLOR` | Disable colored output (standard env var) |
| Variable | Description |
| ---------------------------------------- | ------------------------------------------------- |
| `TALLY_RULES_MAX_LINES_MAX` | Maximum lines allowed |
| `TALLY_RULES_MAX_LINES_SKIP_BLANK_LINES` | Exclude blank lines: `true` / `false` |
| `TALLY_RULES_MAX_LINES_SKIP_COMMENTS` | Exclude comment lines: `true` / `false` |
| `TALLY_RULES_SELECT` | Enable specific rules (comma-separated patterns) |
| `TALLY_RULES_IGNORE` | Disable specific rules (comma-separated patterns) |
| Variable | Description |
| --------------------------- | -------------------------------------------------------- |
| `TALLY_EXCLUDE` | Glob pattern(s) to exclude files (comma-separated) |
| `TALLY_CONTEXT` | Build context directory for direct Dockerfile linting |
| `TALLY_SLOW_CHECKS` | Slow checks mode: `auto`, `on`, `off` |
| `TALLY_SLOW_CHECKS_TIMEOUT` | Timeout for slow checks (e.g. `20s`) |
| `TALLY_FIX` | Apply safe fixes automatically: `true` / `false` |
| `TALLY_FIX_UNSAFE` | Also apply unsafe fixes: `true` / `false` |
| `TALLY_UNSAFE_FIXES` | Config-shaped alias for `unsafe-fixes`: `true` / `false` |
| `TALLY_FIX_RULE` | Limit fixes to specific rules (comma-separated) |
| Variable | Description |
| ---------------------------------------- | -------------------------------------------------------- |
| `TALLY_NO_INLINE_DIRECTIVES` | Disable inline directive processing: `true` / `false` |
| `TALLY_INLINE_DIRECTIVES_WARN_UNUSED` | Warn about unused directives: `true` / `false` |
| `TALLY_INLINE_DIRECTIVES_REQUIRE_REASON` | Require `reason=` on ignore directives: `true` / `false` |
| Variable | Description |
| -------------------------- | -------------------------------------------------------- |
| `TALLY_AI_ENABLED` | Enable AI AutoFix: `true` / `false` |
| `TALLY_ACP_COMMAND` | ACP agent command line |
| `TALLY_AI_TIMEOUT` | Per-fix timeout (e.g. `90s`) |
| `TALLY_AI_MAX_INPUT_BYTES` | Maximum prompt size in bytes |
| `TALLY_AI_REDACT_SECRETS` | Redact secrets before sending to agent: `true` / `false` |
***
## CLI flags
| Flag | Description |
| -------------- | ------------------------------------------------------------------ |
| `--config, -c` | Path to config file (overrides discovery) |
| `--no-config` | Skip config file discovery and use defaults plus env/CLI overrides |
| `--exclude` | Glob pattern(s) to exclude files (repeatable) |
| `--context` | Build context directory for direct Dockerfile linting |
| `--target` | Bake target or group to lint (repeatable; Bake entrypoints only) |
| `--service` | Compose service to lint (repeatable; Compose entrypoints only) |
| `--select` | Enable specific rules (repeatable) |
| `--ignore` | Disable specific rules (repeatable) |
| Flag | Description |
| --------------- | -------------------------------------------------------------------- |
| `--format, -f` | Output format: `text`, `json`, `sarif`, `github-actions`, `markdown` |
| `--output, -o` | Output destination: `stdout`, `stderr`, or file path |
| `--no-color` | Disable colored output |
| `--show-source` | Show source code snippets (default: true) |
| `--hide-source` | Hide source code snippets |
| `--fail-level` | Minimum severity for non-zero exit |
| Flag | Description |
| -------------------- | ----------------------------------------------- |
| `--max-lines, -l` | Maximum number of lines allowed (0 = unlimited) |
| `--skip-blank-lines` | Exclude blank lines from the line count |
| `--skip-comments` | Exclude comment lines from the line count |
| Flag | Description |
| -------------------------- | ------------------------------------------------------------ |
| `--no-inline-directives` | Disable processing of inline ignore directives |
| `--warn-unused-directives` | Warn about directives that don't suppress any violations |
| `--require-reason` | Warn about ignore directives without a `reason=` explanation |
| Flag | Description |
| ---------------------- | ------------------------------------------------- |
| `--fix` | Apply safe auto-fixes automatically |
| `--fix-rule` | Only fix specific rules (repeatable) |
| `--fix-unsafe` | Also apply unsafe fixes (requires `--fix`) |
| `--ai` | Enable AI AutoFix (requires an ACP agent command) |
| `--acp-command` | ACP agent command line |
| `--ai-timeout` | Per-fix AI timeout (e.g. `90s`) |
| `--ai-max-input-bytes` | Maximum prompt size in bytes |
| `--ai-redact-secrets` | Redact secrets before sending to agent |
***
## Build context and invocation flags
`--context` applies only when you lint Dockerfiles directly:
```bash theme={null}
tally lint --context . Dockerfile
```
When you pass a Bake or Compose file, tally reads the build context from the selected target or service instead:
```bash theme={null}
tally lint docker-bake.hcl --target api
tally lint compose.yaml --service api
```
Do not combine `--context` with a Bake or Compose entrypoint. Use `--target` only with Bake, and `--service` only with Compose. See
[Build invocations](/guides/build-invocations) for the full entrypoint behavior.
***
## Inline directives
Suppress specific violations using inline comment directives directly in your Dockerfile.
Suppress violations on the **next line**:
```dockerfile theme={null}
# tally ignore=StageNameCasing
FROM alpine AS Build
```
Suppress multiple rules with comma-separated values:
```dockerfile theme={null}
# tally ignore=StageNameCasing,DL3006
FROM Ubuntu AS Build
```
Suppress all rules on a line:
```dockerfile theme={null}
# tally ignore=all
FROM Ubuntu AS Build
```
Suppress violations throughout the **entire file**:
```dockerfile theme={null}
# tally global ignore=max-lines
FROM alpine
# ... rest of file is not checked for max-lines
```
Document why a rule is suppressed using `;reason=`:
```dockerfile theme={null}
# tally ignore=DL3006;reason=Using older base image for compatibility
FROM ubuntu:16.04
# tally global ignore=max-lines;reason=Generated file, size is expected
```
Use `--require-reason` (or `require-reason = true` in `.tally.toml`) to enforce that all ignore directives include an explanation.
tally supports directive formats from other linters, making migration easy:
```dockerfile theme={null}
# hadolint ignore=DL3006
FROM ubuntu
# hadolint global ignore=DL3008
FROM alpine
# check=skip=StageNameCasing
FROM alpine AS Build
```
***
## Example configurations
### Strict CI
```toml theme={null}
# .tally.toml - Strict settings for CI
[output]
format = "sarif"
path = "tally-results.sarif"
fail-level = "warning"
[rules]
include = ["buildkit/*", "tally/*", "hadolint/*"]
[rules.tally.max-lines]
max = 50
skip-blank-lines = true
skip-comments = true
[inline-directives]
require-reason = true
warn-unused = true
```
### Relaxed development
```toml theme={null}
# .tally.toml - Relaxed settings for development
[output]
format = "text"
show-source = true
fail-level = "error"
[rules]
include = ["buildkit/*", "tally/*"]
exclude = ["buildkit/MaintainerDeprecated"]
[rules.tally.max-lines]
severity = "warning"
max = 200
```
### Monorepo setup
Place a root `.tally.toml` with shared defaults, then override for specific services:
```text theme={null}
monorepo/
├── .tally.toml # Shared defaults
├── services/
│ ├── api/
│ │ └── Dockerfile # Inherits root config
│ └── legacy/
│ ├── .tally.toml # Legacy overrides
│ └── Dockerfile
```
```toml theme={null}
# services/legacy/.tally.toml - Gradual migration from hadolint
[output]
format = "text"
fail-level = "error"
[rules]
# Start with just BuildKit rules; add hadolint rules gradually
include = ["buildkit/*"]
[rules.buildkit.StageNameCasing]
severity = "info" # Downgrade during migration
```
# Exit codes
Source: https://tally.wharflab.com/guides/exit-codes
Reference for tally's exit codes and how to handle them in scripts and CI pipelines.
tally uses distinct exit codes so scripts and CI pipelines can react to different outcomes with precision.
## Exit code reference
| Code | Name | Meaning |
| ---- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | Success | No violations found, all violations are below the configured `--fail-level`, or a valid orchestrator file has no lintable invocations |
| `1` | Violations | One or more violations at or above the configured `--fail-level` |
| `2` | Error | Configuration, parse, I/O, CLI usage, or unsupported orchestrator error |
| `3` | No files | No Dockerfiles to lint from directory or glob discovery (missing file, empty glob, empty directory) |
| `4` | Syntax error | Dockerfile has fatal syntax issues (unknown instructions, malformed directives), including Dockerfiles referenced by an orchestrator |
## How `--fail-level` affects exit code 1
By default, tally exits with code `1` when any violation at `style` severity or above is found. Use `--fail-level` to raise or lower that threshold:
```bash theme={null}
# Fail only on errors (ignore warnings, info, and style)
tally lint --fail-level error Dockerfile
# Fail on warnings or worse
tally lint --fail-level warning Dockerfile
# Never exit 1 due to violations (useful for SARIF upload workflows)
tally lint --fail-level none --format sarif . > results.sarif
# Default: fail on any violation including style issues
tally lint --fail-level style Dockerfile
```
Available levels from most to least severe: `error`, `warning`, `info`, `style` (default), `none`.
## Orchestrator entrypoints
Bake and Compose entrypoints use the same exit-code family, with two differences from directory discovery:
* A valid Bake or Compose file with no lintable targets or services exits `0`, not `3`.
* Invalid target/service selection, unsupported inline Dockerfiles, profile-gated build services, multi-file Bake setups, and invalid flag
combinations exit `2`.
Examples:
```bash theme={null}
# Code 0 when compose.yaml is valid but only contains image-only services
tally lint compose.yaml
# Code 2: --target is only valid for Bake entrypoints
tally lint compose.yaml --target api
# Code 2: --fix is not supported for orchestrator entrypoints
tally lint --fix docker-bake.hcl
```
See [Build invocations](/guides/build-invocations) for supported entrypoint behavior.
## Script examples
### Basic check
```bash theme={null}
tally lint .
echo $? # 0 = clean, 1 = violations found
```
### Handling each exit code
```bash theme={null}
tally lint Dockerfile.prod
status=$?
case "$status" in
0) echo "No issues found." ;;
1) echo "Violations found — review and fix before merging."; exit 1 ;;
2) echo "Config or parse error — check your .tally.toml."; exit 2 ;;
3) echo "No Dockerfiles found — check the path."; exit 3 ;;
4) echo "Dockerfile has syntax errors — fix before linting."; exit 4 ;;
*) echo "Unknown exit code: $status"; exit "$status" ;;
esac
```
### Distinguish "nothing to lint" from real errors
Exit code `3` lets you detect a missing or empty path separately from a configuration error (code `2`):
```bash theme={null}
tally lint Dockerfile.prod
status=$?
if [ "$status" -eq 3 ]; then
echo "No Dockerfiles found — skipping lint step."
elif [ "$status" -ne 0 ]; then
exit "$status"
fi
```
### Detect syntax errors before linting
```bash theme={null}
tally lint Dockerfile
status=$?
if [ "$status" -eq 4 ]; then
echo "Dockerfile has fatal syntax errors — fix typos or invalid instructions first."
exit 4
fi
```
### CI: fail only on errors
```bash theme={null}
# In CI, treat warnings as informational rather than blocking
tally lint --fail-level error --format github-actions .
```
## CI/CD tips
* Use `--fail-level error` to allow warnings without blocking the build.
* Use `--fail-level none` when uploading SARIF so the upload step always runs even when violations exist.
* Exit code `3` distinguishes "the path was wrong" from "the config is broken" (code `2`), useful in matrix CI jobs where not every service has a
Dockerfile.
* Exit code `4` indicates the Dockerfile itself is malformed — fix those before addressing lint violations.
See [CI/CD integration](/guides/ci-cd) for complete pipeline examples.
# IDE integration
Source: https://tally.wharflab.com/guides/ide-integration
Set up stack-aware Dockerfile linting, formatting, and auto-fix in VS Code, JetBrains IDEs, and any LSP-compatible editor.
tally provides real-time diagnostics in your editor via a built-in Language Server Protocol (LSP) server. Official extensions are available for VS
Code and JetBrains IDEs. Any other editor that supports LSP can connect using `tally lsp --stdio`.
Install the official `wharflab.tally` extension from the Visual Studio Marketplace.
Install Tally in PhpStorm, RubyMine, Rider, and other JetBrains IDEs.
Run `tally lsp --stdio` to connect any LSP-compatible editor.
***
## VS Code
Install the official extension from the Visual Studio Marketplace:
**Extension ID:** `wharflab.tally`
**[Install from Marketplace](https://marketplace.visualstudio.com/items?itemName=wharflab.tally)**
The extension provides:
* Real-time linting diagnostics as you type.
* Inline violation messages with rule codes and doc links.
* Squiggles and Problems panel integration.
* Picks up `.tally.toml` config files automatically from your workspace.
The extension uses `tally lsp --stdio` under the hood. Marketplace packages
include a platform-specific `tally` binary, which is used automatically when no
compatible project or environment installation is available.
***
## JetBrains IDEs
Install the official plugin from JetBrains Marketplace:
**Plugin ID:** `30255-tally`
**[Install from JetBrains Marketplace](https://plugins.jetbrains.com/plugin/30255-tally)**
The plugin works in IntelliJ-based IDEs including **PhpStorm**, **RubyMine**, Rider, IntelliJ IDEA, GoLand, PyCharm, and WebStorm.
It provides:
* Stack-aware Dockerfile diagnostics for PHP, Ruby, PowerShell, shell, and Windows containers.
* Modern BuildKit guidance for heredocs, cache mounts, multi-stage builds, and security.
* Safe quick fixes, rule documentation links, and Fix All on Save.
* Dockerfile and Containerfile formatting through **Reformat Code**.
* Automatic `.tally.toml` / `tally.toml` discovery and a bundled Tally binary.
### Stack-specific analysis
Tally analyzes the tools and runtimes used by each stage rather than treating every `RUN` instruction as generic shell text.
* **PhpStorm:** Composer production dependencies, OPcache, Xdebug, PHP package installs, cache mounts, and security.
* **RubyMine:** Bundler deployment mode and cache mounts, Rails asset compilation and health checks, YJIT, runtime state paths, and secrets.
* **Rider and Windows projects:** PowerShell semantics, Windows container behavior, effective `SHELL`, and PowerShell security rules.
BuildKit and Hadolint-compatible checks remain available alongside these stack-specific rules.
### Configure the plugin
Open **Settings → Tools → Tally** to configure the executable, an explicit configuration file, unsafe fixes, formatting, and Fix All on Save. The
plugin discovers the nearest `.tally.toml` or `tally.toml` by default.
***
## Invocation-aware diagnostics
By default, the LSP server lints the Dockerfile document you are editing. If your workspace builds that Dockerfile through Bake or Compose, configure
invocation entrypoints so diagnostics use the same build args, target stage, platform, and build context as the real build.
User-facing setting:
```json theme={null}
{
"tally.invocationEntrypoints": ["compose.yaml", "docker-bake.hcl"]
}
```
For generic LSP clients that send the full tally settings envelope, the payload shape is:
```json theme={null}
{
"tally": {
"workspaces": [
{
"uri": "file:///repo",
"settings": {
"invocationEntrypoints": ["compose.yaml", "docker-bake.hcl"]
}
}
]
}
}
```
When a configured entrypoint resolves to the open Dockerfile, diagnostics use source labels such as `tally/bake:api` or `tally/compose:worker`.
If a code action request spans diagnostics from more than one invocation context, mutating quick fixes and fix-all actions are hidden. This prevents
one edit from being applied as if it were valid for every target or service.
See [Build invocations](/guides/build-invocations) for CLI behavior and orchestrator limitations.
***
## Generic LSP
Any editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) can use tally's built-in LSP server
over stdio.
### Start the LSP server
If tally is installed globally:
```bash theme={null}
tally lsp --stdio
```
If you prefer to use the npm package without a global install:
```bash theme={null}
npx -y tally-cli lsp --stdio
```
### Editor configuration examples
The exact configuration depends on your editor's LSP client. The general pattern is to configure your LSP client to run `tally lsp --stdio` for
Dockerfile files (`dockerfile` language ID).
```lua Neovim (nvim-lspconfig) theme={null}
-- In your Neovim config (lua)
require('lspconfig').tally.setup({
cmd = { 'tally', 'lsp', '--stdio' },
filetypes = { 'dockerfile' },
root_dir = require('lspconfig.util').root_pattern('.tally.toml', 'tally.toml', '.git'),
})
```
```toml Helix (languages.toml) theme={null}
[[language]]
name = "dockerfile"
language-servers = ["tally"]
[language-server.tally]
command = "tally"
args = ["lsp", "--stdio"]
```
```json Zed (settings.json) theme={null}
{
"lsp": {
"tally": {
"binary": {
"path": "tally",
"arguments": ["lsp", "--stdio"]
}
}
}
}
```
tally's LSP server uses cascading config discovery — it finds the nearest `.tally.toml` relative
to each Dockerfile being edited, the same way the CLI does.
# Installing BuildKit for Windows Containers
Source: https://tally.wharflab.com/guides/installing-buildkit-windows-containers
Set up a standalone BuildKit daemon for Windows containers and connect docker buildx with the remote driver.
This guide sets up a standalone BuildKit daemon for Windows containers and connects `docker buildx` to it with the `remote` driver.
It is based on a working setup validated on **April 1, 2026** with real Windows container builds.
## Known-good versions
* `containerd v1.7.30`
* `buildkit v0.29.0`
* `buildx v0.33.0`
Do **not** use `containerd v2.2.2` for this setup. In our Windows validation, `buildkitd` failed to start against it with:
```text theme={null}
unknown service containerd.services.leases.v1.Leases
```
## Prerequisites
* Windows with Docker Desktop installed
* Docker Desktop switched to **Windows containers**
* An **elevated PowerShell** session
* Windows container prerequisites enabled (`Hyper-V`, Windows Containers, and the default `nat` network)
## Install standalone BuildKit
Run this in an elevated PowerShell window:
```powershell theme={null}
$ErrorActionPreference = 'Stop'
$containerdVersion = '1.7.30'
$buildkitVersion = 'v0.29.0'
$buildxVersion = 'v0.33.0'
$arch = 'amd64'
$temp = Join-Path $env:TEMP 'buildkit-win-upgrade'
New-Item -ItemType Directory -Force -Path $temp | Out-Null
Set-Location $temp
Write-Host 'Checking nat network...'
$nat = Get-HnsNetwork | Where-Object { $_.Name -eq 'nat' }
if ($null -eq $nat) {
throw "NAT network not found. Make sure Windows Containers / Hyper-V are enabled and Docker is in Windows containers mode."
}
$gateway = $nat.Subnets[0].GatewayAddress
$subnet = $nat.Subnets[0].AddressPrefix
Write-Host 'Installing CNI config...'
$cniConfPath = "$env:ProgramFiles\containerd\cni\conf"
$cniBinDir = "$env:ProgramFiles\containerd\cni\bin"
$cniVersion = '0.3.0'
New-Item -ItemType Directory -Force -Path $cniConfPath, $cniBinDir | Out-Null
curl.exe -fSL "https://github.com/microsoft/windows-container-networking/releases/download/v$cniVersion/windows-container-networking-cni-amd64-v$cniVersion.zip" -o "windows-container-networking-cni-amd64-v$cniVersion.zip"
tar.exe xvf "windows-container-networking-cni-amd64-v$cniVersion.zip" -C $cniBinDir
$natConfig = @"
{
"cniVersion": "$cniVersion",
"name": "nat",
"type": "nat",
"master": "Ethernet",
"ipam": {
"subnet": "$subnet",
"routes": [
{
"gateway": "$gateway"
}
]
},
"capabilities": {
"portMappings": true,
"dns": true
}
}
"@
Set-Content -Path "$cniConfPath\0-containerd-nat.conf" -Value $natConfig
Write-Host 'Installing containerd...'
curl.exe -fSL "https://github.com/containerd/containerd/releases/download/v$containerdVersion/containerd-$containerdVersion-windows-$arch.tar.gz" -o "containerd-$containerdVersion-windows-$arch.tar.gz"
tar.exe xvf "containerd-$containerdVersion-windows-$arch.tar.gz"
if (Get-Service containerd -ErrorAction SilentlyContinue) {
Stop-Service containerd -ErrorAction SilentlyContinue
.\bin\containerd.exe --unregister-service
}
.\bin\containerd.exe --register-service
Start-Service containerd
Write-Host 'Installing BuildKit...'
curl.exe -fSL "https://github.com/moby/buildkit/releases/download/$buildkitVersion/buildkit-$buildkitVersion.windows-$arch.tar.gz" -o "buildkit-$buildkitVersion.windows-$arch.tar.gz"
tar.exe xvf "buildkit-$buildkitVersion.windows-$arch.tar.gz"
$buildkitPath = "$env:ProgramFiles\buildkit"
New-Item -ItemType Directory -Force -Path $buildkitPath | Out-Null
Copy-Item -Path ".\bin\*" -Destination $buildkitPath -Force
if (Get-Service buildkitd -ErrorAction SilentlyContinue) {
Stop-Service buildkitd -ErrorAction SilentlyContinue
& "$buildkitPath\buildkitd.exe" --unregister-service
}
Write-Host 'Registering buildkitd service...'
& "$buildkitPath\buildkitd.exe" --register-service `
--addr "npipe:////./pipe/buildkitd" `
--containerd-cni-config-path="$cniConfPath\0-containerd-nat.conf" `
--containerd-cni-binary-dir="$cniBinDir"
Start-Service buildkitd
Write-Host 'Installing buildx plugin...'
$pluginsDir = "$env:USERPROFILE\.docker\cli-plugins"
New-Item -ItemType Directory -Force -Path $pluginsDir | Out-Null
curl.exe -fSL "https://github.com/docker/buildx/releases/download/$buildxVersion/buildx-$buildxVersion.windows-$arch.exe" -o "$pluginsDir\docker-buildx.exe"
Write-Host 'Waiting for buildkitd...'
for ($i = 0; $i -lt 30; $i++) {
Start-Sleep -Seconds 2
& "$buildkitPath\buildctl.exe" --addr "npipe:////./pipe/buildkitd" debug info *> $null
if ($LASTEXITCODE -eq 0) { break }
}
& "$buildkitPath\buildctl.exe" --addr "npipe:////./pipe/buildkitd" debug info
Write-Host 'Creating buildx builder...'
docker buildx rm buildkit-windows 2>$null
docker buildx create --name buildkit-windows --driver remote "npipe:////./pipe/buildkitd" --use
docker buildx inspect --bootstrap buildkit-windows
docker buildx version
```
## Allow non-admin clients to use the BuildKit pipe
If `buildctl` works in the elevated shell but fails in a normal shell with `Access is denied`, re-register `buildkitd` like this:
```powershell theme={null}
Stop-Service buildkitd
& "$env:ProgramFiles\buildkit\buildkitd.exe" --unregister-service
& "$env:ProgramFiles\buildkit\buildkitd.exe" --register-service `
--addr "npipe:////./pipe/buildkitd" `
--group "Users" `
--containerd-cni-config-path="$env:ProgramFiles\containerd\cni\conf\0-containerd-nat.conf" `
--containerd-cni-binary-dir="$env:ProgramFiles\containerd\cni\bin"
Start-Service buildkitd
```
## Verify the builder
Run these in a normal PowerShell session:
```powershell theme={null}
docker buildx version
docker buildx inspect buildkit-windows --bootstrap
& "$env:ProgramFiles\buildkit\buildctl.exe" --addr "npipe:////./pipe/buildkitd" debug info
```
Expected checks:
* `docker buildx version` shows `v0.33.0`
* `docker buildx inspect buildkit-windows --bootstrap` shows `Driver: remote`
* the node reports `BuildKit version: v0.29.0`
* `buildctl debug info` succeeds through `\\.\pipe\buildkitd`
## Smoke test with a real Windows build
```powershell theme={null}
$testDir = Join-Path $env:TEMP 'buildkit-win-test'
New-Item -ItemType Directory -Force -Path $testDir | Out-Null
@'
FROM mcr.microsoft.com/windows/nanoserver:ltsc2025
RUN cmd /S /C echo upgraded-buildkit> C:\buildkit.txt
'@ | Set-Content -Path (Join-Path $testDir 'Dockerfile')
docker buildx build --builder buildkit-windows --platform windows/amd64 --progress plain --load -t bk-win-test $testDir
docker run --rm bk-win-test cmd /S /C type C:\buildkit.txt
```
If the setup is healthy, the container prints:
```text theme={null}
upgraded-buildkit
```
## Troubleshooting
`buildkitd` fails to start with `unknown service containerd.services.leases.v1.Leases`
* You are likely using an incompatible `containerd` version such as `2.2.2`.
* Reinstall `containerd v1.7.30`.
`buildx inspect buildkit-windows --bootstrap` times out
* Check that the `buildkitd` service is running.
* Check that `buildctl debug info` works from the same shell.
* If only the elevated shell works, re-register `buildkitd` with `--group "Users"`.
`buildctl` says `Access is denied`
* The named pipe permissions are too restrictive.
* Re-register `buildkitd` with `--group "Users"` as shown above.
Windows build starts but fails during execution
* Make sure Docker Desktop is actually in Windows containers mode.
* Confirm the builder reports `Platforms: windows/amd64`.
* Re-run the smoke test before debugging your own Dockerfile.
# Output formats
Source: https://tally.wharflab.com/guides/output-formats
Reference for all five tally output formats: text, json, sarif, github-actions, and markdown.
tally supports five output formats so it fits into both terminals and automation pipelines. Select a format with `--format` or the `format` key in
`.tally.toml`.
## Output options
| Flag | Description |
| --------------- | -------------------------------------------------------------------- |
| `--format, -f` | Output format: `text`, `json`, `sarif`, `github-actions`, `markdown` |
| `--output, -o` | Output destination: `stdout`, `stderr`, or a file path |
| `--no-color` | Disable colored output (also respects the `NO_COLOR` env var) |
| `--show-source` | Show source code snippets (default: `true`) |
| `--hide-source` | Hide source code snippets |
***
## Invocation-aware output
When you lint a Bake or Compose entrypoint, tally may run more than one invocation for the same Dockerfile. Output formats preserve that attribution:
| Format | Invocation behavior |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `text` | Groups findings under labels such as `[bake target: api]` or `[compose service: worker]` and prints an invocation summary. |
| `json` | Adds an `invocation` object to each orchestrator-derived violation and includes `invocations_scanned`. |
| `sarif` | Stores invocation metadata in each result's properties. |
| `github-actions` | Prefixes annotation messages with the invocation label. |
| `markdown` | Adds an `Invocation` column when invocation metadata is present. |
See [Build invocations](/guides/build-invocations) for CLI examples and supported entrypoints.
***
## text (default)
Human-readable output with colors and source code snippets. Best for local development.
```bash theme={null}
tally lint Dockerfile
```
Example output:
```text theme={null}
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"
────────────────────
```
### Source snippets
Source snippets are shown by default. Toggle them with flags:
```bash theme={null}
# Hide source snippets (cleaner for long files)
tally lint --hide-source Dockerfile
# Explicitly show (useful when config sets show-source = false)
tally lint --show-source Dockerfile
```
In `.tally.toml`:
```toml theme={null}
[output]
show-source = false
```
## json
Machine-readable format with full violation details, summary statistics, and scan metadata. Best for scripts, dashboards, and custom reporting.
```bash theme={null}
tally lint --format json Dockerfile
```
Example output:
```json theme={null}
{
"files": [
{
"file": "Dockerfile",
"violations": [
{
"location": {
"file": "Dockerfile",
"start": { "line": 2, "column": 0 }
},
"rule": "buildkit/StageNameCasing",
"message": "Stage name 'Builder' should be lowercase",
"severity": "warning",
"docUrl": "https://docs.docker.com/go/dockerfile/rule/stage-name-casing/"
}
]
}
],
"summary": {
"total": 1,
"errors": 0,
"warnings": 1,
"info": 0,
"style": 0,
"files": 1
},
"files_scanned": 1,
"rules_enabled": 41
}
```
Top-level fields:
| Field | Description |
| --------------------- | ------------------------------------------------------------------------------------ |
| `files` | Array of files with their violations |
| `summary` | Aggregate counts: `total`, `errors`, `warnings`, `info`, `style`, `files` |
| `files_scanned` | Total number of files scanned |
| `invocations_scanned` | Total number of build invocations scanned; omitted or `0` for direct Dockerfile runs |
| `rules_enabled` | Number of active rules with a non-`"off"` default severity |
Orchestrator-derived violations also include:
| Field | Description |
| ----------------- | ----------------------------------------- |
| `invocation.kind` | `bake` or `compose` |
| `invocation.file` | Absolute path to the Bake or Compose file |
| `invocation.name` | Target or service name |
Write JSON to a file:
```bash theme={null}
tally lint --format json --output results.json .
```
## sarif
[SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/) format for static analysis tools and CI code scanning integrations. Best for GitHub Code Scanning, Azure DevOps, and similar platforms.
```bash theme={null}
tally lint --format sarif --output results.sarif .
```
Or redirect to a file:
```bash theme={null}
tally lint --format sarif . > results.sarif
```
SARIF output includes rule metadata, help URIs, and per-result location data that code scanning tools use to render findings in pull requests and security dashboards.
### GitHub Code Scanning
```bash theme={null}
# Generate SARIF (use --fail-level none so the step doesn't fail before upload)
tally lint --format sarif --output tally.sarif --fail-level none .
# Upload with the CodeQL action
# uses: github/codeql-action/upload-sarif@v3
# with:
# sarif_file: tally.sarif
```
See [CI/CD integration](/guides/ci-cd) for a complete GitHub Actions workflow example.
## github-actions
Emits GitHub Actions [workflow commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions) (`::warning` and `::error`) for inline PR annotations.
```bash theme={null}
tally lint --format github-actions .
```
Example output:
```text theme={null}
::warning file=Dockerfile,line=2,title=StageNameCasing::Stage name 'Builder' should be lowercase
```
GitHub renders these as inline annotations in the PR diff and in the Actions run summary. No upload step is needed — the annotations appear automatically when the format is used in a GitHub Actions workflow.
Severity mapping:
| tally severity | GitHub command |
| -------------- | -------------- |
| `error` | `::error` |
| `warning` | `::warning` |
| `info` | `::notice` |
| `style` | `::notice` |
## markdown
Concise Markdown tables optimized for AI agents, PR comments, and token-efficient reporting.
```bash theme={null}
tally lint --format markdown Dockerfile
```
Example output:
```markdown theme={null}
**2 issues** in `Dockerfile`
| Line | Issue |
| ---- | ------------------------------------------- |
| 2 | ⚠️ Stage name 'Builder' should be lowercase |
| 10 | ❌ Use absolute WORKDIR |
```
Features:
* Summary line with total issue count upfront.
* Violations sorted by invocation, file, line, column, and rule code.
* Emoji severity indicators: ❌ error, ⚠️ warning, ℹ️ info, 💅 style.
* No rule codes or doc URLs — optimized for token efficiency.
* Adds a `File` column automatically when linting multiple files.
* Adds an `Invocation` column automatically when linting Bake or Compose entrypoints.
Pipe output into a file for use as a PR comment or report artifact:
```bash theme={null}
tally lint --format markdown . > lint-report.md
```
# Installation
Source: https://tally.wharflab.com/installation
Install tally via Homebrew, mise, WinGet, npm, Bun, uv, pip, RubyGems, Docker, Go, or from source.
tally is a single self-contained binary. Pick the method that fits your environment.
```bash Homebrew theme={null}
brew trust --formula wharflab/tap/tally
brew install --require-sha wharflab/tap/tally
```
```bash mise theme={null}
mise use -g github:wharflab/tally@latest
```
```powershell WinGet theme={null}
winget install --id Wharflab.Tally
```
```bash npm theme={null}
npm install -g tally-cli
```
```bash Bun theme={null}
bun add -g tally-cli
```
```bash uv theme={null}
uv tool install tally-cli
```
```bash pip theme={null}
pip install tally-cli
```
```bash RubyGems theme={null}
gem install tally-cli
```
```bash Go theme={null}
go install github.com/wharflab/tally@latest
```
## Homebrew (macOS/Linux)
```bash theme={null}
brew trust --formula wharflab/tap/tally
brew install --require-sha wharflab/tap/tally
```
Run `tally register-docker-plugin` if you want Docker to discover the plugin without editing `~/.docker/config.json`.
Homebrew also installs a `docker-lint` symlink under Homebrew's Docker plugin directory. See
[Docker CLI plugin](/integrations/docker-cli-plugin) if you prefer to configure Docker's `cliPluginsExtraDirs`.
## mise
```bash theme={null}
mise use -g github:wharflab/tally@latest
```
This installs the latest tally release from GitHub and exposes the `tally` binary through mise's global tool shims.
Run `tally register-docker-plugin` after installation to register the plugin with Docker.
## WinGet (Windows)
```powershell theme={null}
winget install --id Wharflab.Tally
```
Run `tally register-docker-plugin` after installation to register the plugin with Docker.
## npm
```bash theme={null}
npm install -g tally-cli
```
Use a global npm install for Docker CLI plugin setup. Project-local `node_modules` installs are rejected by `tally register-docker-plugin`.
## Bun
```bash theme={null}
bun add -g tally-cli
```
Use a global Bun install for Docker CLI plugin setup. `bunx` and project-local `node_modules` launches are rejected by
`tally register-docker-plugin`.
## uv
```bash theme={null}
uv tool install tally-cli
```
Use `uv tool install` for Python-managed Docker CLI plugin setup. Active Python virtual environments are rejected by
`tally register-docker-plugin`.
## pip
```bash theme={null}
pip install tally-cli
```
## RubyGems
```bash theme={null}
gem install tally-cli
```
## Docker / Podman
Official images are published to GitHub Container Registry at `ghcr.io/wharflab/tally`. All images are signed with
[cosign](https://github.com/sigstore/cosign) (keyless/OIDC).
The Linux image is distroless, non-root, and shell-free. It contains only the `tally` binary. The Windows image is built on Nano Server.
### Image tags
| Tag | Description |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `ghcr.io/wharflab/tally:latest` | Multi-platform image index: auto-selects `linux/amd64`, `linux/arm64`, or `windows/amd64` |
| `ghcr.io/wharflab/tally:v` | Versioned multi-platform image index (e.g., `v1.2.3`) |
| `ghcr.io/wharflab/tally:v-linux-amd64` | Per-platform tag |
| `ghcr.io/wharflab/tally:v-linux-arm64` | Per-platform tag |
| `ghcr.io/wharflab/tally:v-windows-amd64` | Per-platform tag |
### Docker (Linux)
```bash theme={null}
# Check the version
docker run --rm ghcr.io/wharflab/tally:latest version
# Lint a Dockerfile in the current directory
docker run --rm -v "$PWD:/work" -w /work ghcr.io/wharflab/tally:latest lint Dockerfile
```
### Podman (Linux)
```bash theme={null}
# Check the version
podman run --rm ghcr.io/wharflab/tally:latest version
# Lint all Dockerfiles recursively (note the :Z label for SELinux)
podman run --rm -v "$PWD:/work:Z" -w /work ghcr.io/wharflab/tally:latest lint .
```
### Docker (Windows containers)
```powershell theme={null}
# Check the version
docker run --rm ghcr.io/wharflab/tally:latest version
# Lint a Dockerfile
docker run --rm -v "${PWD}:C:\work" -w C:\work ghcr.io/wharflab/tally:latest lint Dockerfile
```
### Verify image signatures
All published images are signed with cosign via keyless/OIDC. Verify before use:
```bash theme={null}
cosign verify --certificate-identity-regexp='https://github.com/wharflab/tally' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/wharflab/tally:latest
```
## Go
Requires Go 1.21 or later.
```bash theme={null}
go install github.com/wharflab/tally@latest
```
## From source
```bash theme={null}
git clone https://github.com/wharflab/tally.git
cd tally
go build .
```
## Verify your installation
After installing, confirm tally is on your `PATH`:
```bash theme={null}
tally --help
tally lint --help
```
To enable the Docker CLI plugin, run the registration command from a global tally install:
```bash theme={null}
tally register-docker-plugin
```
Then verify that Docker can discover it:
```bash theme={null}
docker lint --help
```
Check the installed version:
```bash theme={null}
# Human-readable
tally version
# Machine-readable JSON (includes ShellCheck version)
tally version --json
```
If the command is not found, make sure the install location is in your `PATH` (for Go installs, this is usually `$GOPATH/bin` or `$HOME/go/bin`).
Run `tally lint .` from your project root to immediately lint every Dockerfile and Containerfile in the repo.
# Docker CLI plugin
Source: https://tally.wharflab.com/integrations/docker-cli-plugin
Run tally as docker lint from the Docker CLI.
The Docker CLI plugin lets you run `tally lint` as `docker lint`. It is a client-side Docker CLI plugin, not a Docker Engine plugin, so it does not
install anything into the Docker daemon.
`docker lint` maps to the lint command only. Use `tally lsp --stdio` for editor integrations and `tally version` for full standalone version details.
## Install
Install tally globally, make sure `docker info` works, then ask tally to register the `docker-lint` plugin with Docker.
```bash Homebrew theme={null}
brew trust --formula wharflab/tap/tally
brew install --require-sha wharflab/tap/tally
tally register-docker-plugin
```
```bash mise theme={null}
mise use -g github:wharflab/tally@latest
tally register-docker-plugin
```
```powershell WinGet theme={null}
winget install --id Wharflab.Tally
tally register-docker-plugin
```
```bash npm theme={null}
npm install -g tally-cli
tally register-docker-plugin
```
```bash Bun theme={null}
bun add -g tally-cli
tally register-docker-plugin
```
```bash uv theme={null}
uv tool install tally-cli
tally register-docker-plugin
```
The command refuses project-local or temporary launches such as `node_modules`, `bunx`, an active Python virtual environment, and `go run`. Use a
global install before running it.
During registration, tally runs `docker info --format json`, checks the Docker CLI version, inspects existing Docker CLI plugins, and verifies the
registration after writing `docker-lint`. If Docker already has a non-tally `lint` plugin, tally stops instead of shadowing it. If an older tally
plugin is present, tally reports the upgrade. If a newer tally plugin is already registered, tally does not overwrite it unless you pass `--force`.
```text theme={null}
Found Docker CLI version 29.4.1
Upgrading tally lint plugin from version 0.5.6 to 0.7.19
```
Verify the plugin:
```bash theme={null}
docker lint --help
```
Use `--dry-run` to inspect the Docker CLI version, source, target path, and existing plugin decision before making changes:
```bash theme={null}
tally register-docker-plugin --dry-run
```
## Manual fallback
If you cannot use `tally register-docker-plugin`, register the same binary as `docker-lint`.
```bash macOS/Linux theme={null}
mkdir -p ~/.docker/cli-plugins
ln -sf "$(command -v tally)" ~/.docker/cli-plugins/docker-lint
docker lint --help
```
```powershell Windows theme={null}
New-Item -ItemType Directory -Force "$env:USERPROFILE\.docker\cli-plugins"
Copy-Item (Get-Command tally.exe).Source "$env:USERPROFILE\.docker\cli-plugins\docker-lint.exe" -Force
docker lint --help
```
On Windows, repeat the copy after upgrading tally unless your installer manages the plugin file for you.
Homebrew also installs a `docker-lint` symlink under its own Docker plugin directory. If you prefer to use that Homebrew-managed file, add Homebrew's
plugin directory to `~/.docker/config.json`:
```json theme={null}
{
"cliPluginsExtraDirs": [
"/opt/homebrew/lib/docker/cli-plugins"
]
}
```
Use `/usr/local/lib/docker/cli-plugins` on Intel macOS, or the matching Linuxbrew prefix on Linux.
## Usage
Use `docker lint` the same way you use `tally lint`:
```bash theme={null}
docker lint Dockerfile
docker lint .
docker lint --format json Dockerfile
docker lint --fix Dockerfile
docker lint docker-bake.hcl --target web
docker lint compose.yaml --service api
```
Docker global flags go before `lint`. tally flags go after `lint`:
```bash theme={null}
docker --context production lint Dockerfile
docker lint --context . Dockerfile
```
The first command selects Docker's current context. The second command passes tally's build context option.
## Troubleshooting
### `docker: 'lint' is not a docker command`
Docker did not find `docker-lint`. Check that the file exists in one of Docker's CLI plugin directories or in a directory listed by
`cliPluginsExtraDirs`.
```bash theme={null}
ls ~/.docker/cli-plugins/docker-lint
```
Run `tally register-docker-plugin --dry-run` to confirm where tally will register `docker-lint`.
### Invalid plugin metadata
Run the metadata command directly:
```bash theme={null}
~/.docker/cli-plugins/docker-lint docker-cli-plugin-metadata
```
The output should be JSON with `"SchemaVersion": "0.1.0"` and `"Vendor": "Wharflab"`. If it prints normal tally help instead, the file is not named
`docker-lint`.
### Wrong architecture binary
Use a tally binary that matches your operating system and CPU architecture. This matters when copying binaries manually between machines or between
Intel and Apple Silicon Macs.
### Missing execute bit on macOS/Linux
Make the plugin executable:
```bash theme={null}
chmod +x ~/.docker/cli-plugins/docker-lint
```
### Command name conflict
`lint` is a generic plugin command name. If another `docker-lint` exists earlier in Docker's plugin search path, Docker may run that plugin instead.
Remove the conflicting file or adjust `cliPluginsExtraDirs`.
## Limitations
`docker lint` exposes linting only. It does not expose `tally lsp` or `tally version` as Docker subcommands.
WinGet exposes the `tally` command only. Run `tally register-docker-plugin` after installing or upgrading tally through WinGet.
# EditorConfig integration
Source: https://tally.wharflab.com/integrations/editorconfig
Use EditorConfig settings to keep tally formatting fixes aligned with your repository style.
tally reads `.editorconfig` for formatting rules that need repository style preferences. Use `.editorconfig` for shared whitespace and indentation
settings, and use `.tally.toml` for enabling, disabling, or changing the severity of lint rules.
This page will collect tally features that integrate with EditorConfig as they are added.
## Recommended Dockerfile settings
Add this section to your repository's `.editorconfig` to keep Dockerfiles and Containerfiles consistent:
```ini theme={null}
[{Containerfile,Containerfile.,Dockerfile,Dockerfile.}*]
indent_style = tab
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
```
The pattern matches `Dockerfile`, `Dockerfile.*`, `Containerfile`, and `Containerfile.*`.
## Rules that use EditorConfig
* [`tally/prefer-formatted-heredocs`](/rules/tally/prefer-formatted-heredocs) formats `COPY` and `ADD` heredoc bodies for JSON, YAML, TOML, XML, and
INI files, plus shell heredocs from executable `RUN` heredocs and `COPY` heredocs with a shell shebang or `.sh` destination. PowerShell heredocs
are formatted through PSScriptAnalyzer and do not use EditorConfig today.
For structured heredoc payloads, tally resolves EditorConfig settings using a virtual filename next to the Dockerfile and the heredoc destination
basename. For example, a heredoc in `services/api/Dockerfile` targeting `/etc/app/config.yaml` is resolved as `services/api/config.yaml`, so
`*.yaml` and `config.yaml` sections apply.
`COPY` shell heredocs use that same destination-basename lookup. For example, a heredoc targeting `/usr/local/bin/entrypoint.sh` is resolved as
`entrypoint.sh`, so `*.sh` and `entrypoint.sh` sections apply.
For `RUN` shell heredocs, tally uses a virtual filename next to the Dockerfile named `Dockerfile.heredoc.`, such as
`Dockerfile.heredoc.sh`, `Dockerfile.heredoc.bash`, or `Dockerfile.heredoc.zsh`.
## Supported Properties
The heredoc formatter currently reads these EditorConfig properties:
| Property | Behavior |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `indent_style` | Uses tabs when set to `tab`; otherwise uses spaces. |
| `indent_size` | Sets the number of spaces per indent level when it is a positive integer. |
| `tab_width` | Used as the fallback indent width when `indent_size` is not a positive integer. |
| `max_line_length` | YAML and shell heredocs. For YAML, a positive integer is used as the preferred scalar wrapping width. For shell heredocs, a positive integer is used as the preferred command wrapping width; unset and invalid values use `100`, and `off` disables the shell line-width pass. This is a formatting preference, not a hard maximum. |
| `binary_next_line` | Shell heredocs only. Mirrors `shfmt --binary-next-line`. |
| `switch_case_indent` | Shell heredocs only. Mirrors `shfmt --case-indent`. |
| `space_redirects` | Shell heredocs only. Mirrors `shfmt --space-redirects`. |
| `keep_padding` | Shell heredocs only. Mirrors `shfmt --keep-padding`. |
| `function_next_line` | Shell heredocs only. Mirrors `shfmt --func-next-line`. |
| `simplify` | Shell heredocs only. Mirrors `shfmt --simplify`. |
| `minify` | Shell heredocs only. Mirrors `shfmt --minify` and implies `simplify`. |
Other EditorConfig properties are intentionally not interpreted by the heredoc formatter today:
| Property | Reason |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `insert_final_newline` | Heredoc bodies always need a newline before the terminator, so formatted payloads always end with exactly one newline regardless of this setting. |
| `end_of_line` | Heredoc payloads are embedded in the Dockerfile and should follow the parent Dockerfile's line-ending convention rather than a separate virtual target setting. |
| `charset` | tally already reads Dockerfiles as text and does not transcode heredoc payloads during formatting. |
| `trim_trailing_whitespace` | Formatted output for the currently supported structured formats does not emit incidental trailing whitespace. tally does not read this property or run a separate trimming pass, because whitespace may be payload data in some formats. |
# Introduction
Source: https://tally.wharflab.com/introduction
tally is a BuildKit-native Dockerfile and Containerfile linter and formatter with safe auto-fix, LSP support, and SARIF output.
tally is a production-grade **Dockerfile and Containerfile linter + formatter** built on BuildKit's official parser — the same foundation behind
`docker buildx`. It catches issues before they reach production, modernizes syntax automatically, and fits cleanly into any CI pipeline or editor
workflow.
```bash theme={null}
# Lint everything in your repo (recursive)
tally lint .
# Apply all safe fixes automatically
tally lint --fix Dockerfile
```
Install via Homebrew, mise, WinGet, npm, Bun, uv, pip, RubyGems, Docker, or Go.
Lint your first Dockerfile in under a minute.
Rules across BuildKit, tally, Hadolint, and ShellCheck namespaces.
Configure rules, output formats, and fix modes with `.tally.toml`.
Lint Dockerfiles through Bake targets and Compose services.
## Why tally?
Dockerfile linting has traditionally meant 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 focuses
on reporting — not fixing.
* **`docker buildx --check`** runs Docker's official BuildKit checks, but it requires the Docker/buildx toolchain and is not available if you are
using Podman, Finch, or another runtime.
tally takes a different approach:
| Capability | tally | Hadolint | docker buildx --check |
| -------------------------------------------------- | ----- | --------------- | --------------------- |
| Uses BuildKit's official parser | Yes | No (own parser) | Yes |
| Understands heredocs, `RUN --mount`, `COPY --link` | Yes | Partial | Yes |
| Safe auto-fix (`--fix`) | Yes | No | No |
| AI-powered fixes (`--fix-unsafe`) | Yes | No | No |
| Bake and Compose entrypoints | Yes | No | Partial (Bake only) |
| No Docker daemon required | Yes | Yes | No |
| SARIF output | Yes | Yes | No |
| LSP / editor integration | Yes | Limited | No |
| Windows-container aware | Yes | No | Partial |
| PowerShell semantic analysis | Yes | No | No |
## Key capabilities
### BuildKit-native parsing
tally uses BuildKit's official parser — the same one that `docker buildx` uses. It correctly handles modern syntax like heredocs,
`RUN --mount=type=cache`, `COPY --link`, and `ADD --checksum` without lagging behind new Docker features.
### Rules across four namespaces
| Namespace | Source |
| ------------- | ----------------------------------------------------- |
| `buildkit/` | Docker's official Dockerfile checks |
| `tally/` | Custom rules including secret detection with gitleaks |
| `hadolint/` | Hadolint-compatible Dockerfile rules |
| `shellcheck/` | Shell script analysis within `RUN` instructions |
Rules are grouped by category: correctness, security, performance, style, and maintainability. See the [rules reference](/rules/overview) for the full
list.
### Safe auto-fix
`--fix` applies safe, mechanical rewrites — things like canonicalizing `STOPSIGNAL`, adding `--chown` flags, and converting `RUN echo` patterns to
`COPY` heredocs. Nothing is applied that could change build behavior.
### Build invocation context
tally can lint Dockerfiles through Docker Buildx Bake and Docker Compose entrypoints. When you run `tally lint docker-bake.hcl` or
`tally lint compose.yaml`, tally resolves each target or service into the Dockerfile, build context, build args, platform, and selected target stage.
See [Build invocations](/guides/build-invocations) for the full workflow.
### AI AutoFix via ACP
`--fix-unsafe` unlocks opt-in AI AutoFix for improvements that are hard to express as a deterministic rewrite. Instead of requiring an API key, tally
integrates with **ACP (Agent Client Protocol)** so you can use the AI agent you already trust — Gemini CLI, OpenCode, GitHub Copilot CLI, and more.
All AI fixes are rule-driven (one narrow transformation at a time) and verified by re-parsing and re-linting before anything is written to disk. See
the [AI AutoFix guide](/guides/ai-autofix) for details.
### LSP and editor support
`tally lsp` exposes a full Language Server Protocol server over stdio, giving you real-time diagnostics in any LSP-compatible editor. Official
extensions are available for [VS Code](https://marketplace.visualstudio.com/items?itemName=wharflab.tally) and
[JetBrains IDEs](https://plugins.jetbrains.com/plugin/30255-tally).
### CI-ready output formats
tally supports text, JSON, SARIF, GitHub Actions annotations, and Markdown output. SARIF integrates with GitHub Code Scanning, Azure DevOps, and other
tools. The Markdown format is optimized for AI agents and token efficiency.
## Get started
Choose your package manager: Homebrew, mise, WinGet, npm, Bun, uv, pip, RubyGems, Docker, or Go.
Lint a Dockerfile, read the output, apply fixes, and set up `.tally.toml`.
Use the build definition your project already uses.
# Quick start
Source: https://tally.wharflab.com/quickstart
Lint your first Dockerfile in under a minute.
Pick your preferred package manager:
```bash Homebrew theme={null}
brew trust --formula wharflab/tap/tally
brew install --require-sha wharflab/tap/tally
```
```bash mise theme={null}
mise use -g github:wharflab/tally@latest
```
```bash npm theme={null}
npm install -g tally-cli
```
```bash Bun theme={null}
bun add -g tally-cli
```
```bash uv theme={null}
uv tool install tally-cli
```
```bash pip theme={null}
pip install tally-cli
```
```bash Go theme={null}
go install github.com/wharflab/tally@latest
```
See [Installation](/installation) for WinGet, RubyGems, Docker, and from-source options.
Run tally against a single file or an entire directory (recursive):
```bash theme={null}
# Single file
tally lint Dockerfile
# All Dockerfiles in the current repo
tally lint .
```
tally discovers files matching `Dockerfile`, `Dockerfile.*`, `*.Dockerfile`, `Containerfile`, and `Containerfile.*`.
To make the same lint command available through Docker, register the Docker CLI plugin:
```bash theme={null}
tally register-docker-plugin
docker lint Dockerfile
```
If your build is defined by Docker Buildx Bake or Docker Compose, pass that file directly:
```bash theme={null}
# Lint Bake targets from the default group
tally lint docker-bake.hcl
# Lint Compose services with build sections
tally lint compose.yaml
```
Use `--target` for Bake targets or groups and `--service` for Compose services:
```bash theme={null}
tally lint docker-bake.hcl --target api
tally lint compose.yaml --service api
```
See [Build invocations](/guides/build-invocations) for context handling, output attribution, and limitations.
By default tally prints human-readable output with source snippets:
```text theme={null}
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"
────────────────────
```
Exit code `1` means violations were found. Exit code `0` means clean.
Many rules are auto-fixable. Apply all safe fixes with `--fix`:
```bash theme={null}
tally lint --fix Dockerfile
```
`--fix` modifies files in place. Commit your changes before running it, or review the diff afterward.
Create `.tally.toml` in your project root to set defaults for your whole team:
```toml .tally.toml theme={null}
[output]
format = "text"
fail-level = "warning"
[rules]
include = ["buildkit/*", "tally/*", "hadolint/*"]
```
Config is discovered automatically — no `--config` flag needed.
In direct Dockerfile mode, pass `--context` to unlock rules that check `.dockerignore` interactions:
```bash theme={null}
tally lint --context . Dockerfile
```
Bake and Compose entrypoints derive the build context from the selected target or service, so they do not use `--context`.
Use an inline directive to suppress a specific rule on the next line:
```dockerfile theme={null}
# tally ignore=StageNameCasing
FROM alpine AS Build
```
Add a reason to document why:
```dockerfile theme={null}
# tally ignore=DL3007;reason=Pinning at CI level via Renovate
FROM ubuntu:latest
```
## Lint from stdin
Pass `-` as the filename to read from stdin. Useful in pipelines:
```bash theme={null}
cat Dockerfile | tally lint -
```
With `--fix`, the fixed content is written to stdout:
```bash theme={null}
cat Dockerfile | tally lint --fix - > Dockerfile.fixed
```
## Next steps
Full reference for `.tally.toml`, environment variables, and CLI flags.
Lint Dockerfiles through Bake targets and Compose services.
Learn about safe and unsafe fixes, and per-rule fix modes.
Browse all rules across BuildKit, tally, Hadolint, and ShellCheck namespaces.
Add tally to GitHub Actions, GitLab CI, and other pipelines.
# buildkit/ConsistentInstructionCasing
Source: https://tally.wharflab.com/rules/buildkit/ConsistentInstructionCasing
Instruction keywords should use consistent casing throughout the Dockerfile.
Instruction keywords should use consistent casing throughout the Dockerfile.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Instruction keywords should use consistent casing (all lowercase or all
uppercase). Using a case that mixes uppercase and lowercase, such as PascalCase
or snakeCase, results in poor readability.
## Examples
Bad:
```dockerfile theme={null}
From alpine
Run echo hello > /greeting.txt
EntRYpOiNT ["cat", "/greeting.txt"]
```
Good (all uppercase):
```dockerfile theme={null}
FROM alpine
RUN echo hello > /greeting.txt
ENTRYPOINT ["cat", "/greeting.txt"]
```
Good (all lowercase):
```dockerfile theme={null}
from alpine
run echo hello > /greeting.txt
entrypoint ["cat", "/greeting.txt"]
```
## Auto-fix
The fix changes instruction keywords to match the majority casing in the
Dockerfile.
```dockerfile theme={null}
# Before (majority is uppercase)
FROM alpine
run echo hello
# After (with --fix)
FROM alpine
RUN echo hello
```
## Reference
* [buildkit/ConsistentInstructionCasing](https://docs.docker.com/reference/build-checks/consistent-instruction-casing/)
# buildkit/CopyIgnoredFile
Source: https://tally.wharflab.com/rules/buildkit/CopyIgnoredFile
Attempting to Copy file that is excluded by .dockerignore.
Attempting to Copy file that is excluded by .dockerignore.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
## Description
When you use the `ADD` or `COPY` instructions in a Dockerfile, you should
ensure that the files to be copied into the image do not match a pattern present
in `.dockerignore`. Files which match the patterns in a `.dockerignore` file are
not present in the context of the image when it is built. Trying to copy or add
a file which is missing from the context will result in a build error.
## Build context requirement
This rule needs build context information. In direct Dockerfile mode, pass a local context directory:
```bash theme={null}
tally lint --context . Dockerfile
```
When linting a Bake or Compose entrypoint, tally uses the local `context` declared by the selected target or service:
```bash theme={null}
tally lint docker-bake.hcl --target api
tally lint compose.yaml --service api
```
Remote, tar, git, and empty contexts are valid build declarations, but tally does not fetch or unpack them during linting. For those contexts, this
rule only reports problems that can be determined without reading context files.
## Examples
Given a `.dockerignore` containing `*/tmp/*`:
Bad:
```dockerfile theme={null}
FROM scratch
COPY ./tmp/helloworld.txt /helloworld.txt
```
Good:
```dockerfile theme={null}
FROM scratch
COPY ./forever/helloworld.txt /helloworld.txt
```
## Reference
* [buildkit/CopyIgnoredFile](https://docs.docker.com/reference/build-checks/copy-ignored-file/)
# buildkit/DuplicateStageName
Source: https://tally.wharflab.com/rules/buildkit/DuplicateStageName
Stage names in a multi-stage build should be unique.
Stage names in a multi-stage build should be unique.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
Defining multiple stages with the same name results in an error because the
builder is unable to uniquely resolve the stage name reference.
## Examples
Bad:
```dockerfile theme={null}
FROM debian:latest AS builder
RUN apt-get update; apt-get install -y curl
FROM golang:latest AS builder
```
Good:
```dockerfile theme={null}
FROM debian:latest AS deb-builder
RUN apt-get update; apt-get install -y curl
FROM golang:latest AS go-builder
```
## Supersedes
* [hadolint/DL3024](../hadolint/DL3024)
## Reference
* [buildkit/DuplicateStageName](https://docs.docker.com/reference/build-checks/duplicate-stage-name/)
# buildkit/ExposeInvalidFormat
Source: https://tally.wharflab.com/rules/buildkit/ExposeInvalidFormat
EXPOSE instruction should not define an IP address or host-port mapping.
EXPOSE instruction should not define an IP address or host-port mapping.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
## Description
The `EXPOSE` instruction in a Dockerfile is used to indicate which ports the
container listens on at runtime. It should not include an IP address or
host-port mapping.
Including an IP address or host-port mapping in the `EXPOSE` instruction does
not actually publish the port and can be misleading. Use `docker run -p` or
`docker compose` port mappings to bind host ports at runtime.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine
EXPOSE 127.0.0.1:80:80
```
Good:
```dockerfile theme={null}
FROM alpine
EXPOSE 80
```
Bad:
```dockerfile theme={null}
FROM alpine
EXPOSE 80:80
```
Good:
```dockerfile theme={null}
FROM alpine
EXPOSE 80
```
## Reference
* [buildkit/ExposeInvalidFormat](https://docs.docker.com/reference/build-checks/expose-invalid-format/)
# buildkit/ExposeProtoCasing
Source: https://tally.wharflab.com/rules/buildkit/ExposeProtoCasing
Protocol names in `EXPOSE` instructions should be lowercase.
Protocol names in `EXPOSE` instructions should be lowercase.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Protocol names in the `EXPOSE` instruction should be specified in lowercase to
maintain consistency and readability.
## Examples
Bad:
```dockerfile theme={null}
EXPOSE 80/TcP
```
Good:
```dockerfile theme={null}
EXPOSE 80/tcp
```
## Auto-fix
The fix lowercases the protocol in `EXPOSE` port specs.
```dockerfile theme={null}
# Before
EXPOSE 8080/TCP
# After (with --fix)
EXPOSE 8080/tcp
```
## Reference
* [buildkit/ExposeProtoCasing](https://docs.docker.com/reference/build-checks/expose-proto-casing/)
# buildkit/FromAsCasing
Source: https://tally.wharflab.com/rules/buildkit/FromAsCasing
The `AS` and `FROM` keywords' casing should match.
The `AS` and `FROM` keywords' casing should match.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
While Dockerfile keywords can be either uppercase or lowercase, mixing case
styles is not recommended for readability. This rule reports violations where
mixed case style occurs for a `FROM` instruction with an `AS` keyword declaring
a stage name.
## Examples
Bad:
```dockerfile theme={null}
FROM debian:latest as builder
```
`FROM` is uppercase but `as` is lowercase.
Good:
```dockerfile theme={null}
FROM debian:latest AS deb-builder
```
```dockerfile theme={null}
from debian:latest as deb-builder
```
Both keywords use the same casing.
## Auto-fix
The fix changes the `AS` keyword casing to match the `FROM` keyword.
```dockerfile theme={null}
# Before
FROM alpine as builder
# After (with --fix)
FROM alpine AS builder
```
## Related
* [buildkit/ConsistentInstructionCasing](./ConsistentInstructionCasing)
## Reference
* [buildkit/FromAsCasing](https://docs.docker.com/reference/build-checks/from-as-casing/)
# buildkit/FromPlatformFlagConstDisallowed
Source: https://tally.wharflab.com/rules/buildkit/FromPlatformFlagConstDisallowed
FROM `--platform` flag should not use a constant value.
FROM `--platform` flag should not use a constant value.
| Property | Value |
| -------- | -------------------------------------------------------------------------------- |
| Severity | Off |
| Category | Best Practice |
| Default | Disabled (superseded by [`tally/platform-mismatch`](../tally/platform-mismatch)) |
## Tally behavior deviation
Tally disables this rule by default because hardcoding `--platform` is
legitimate in several real-world scenarios:
* **ARM-only services.** Deployments targeting AWS Graviton or other ARM-only
infrastructure use `FROM --platform=linux/arm64` to ensure the correct
architecture regardless of where the build runs.
* **Windows containers.** Windows Dockerfiles use
`FROM --platform=windows/amd64 mcr.microsoft.com/...` to explicitly target
Windows, which is necessary when the builder could be multi-platform.
* **Cross-compilation.** Go and Rust projects commonly use
`FROM --platform=linux/amd64 golang:1.22` for a specific builder stage while
the final image targets a different architecture.
Rather than discouraging constant `--platform` values, tally validates them
against the registry with [`tally/platform-mismatch`](../tally/platform-mismatch).
This catches provable errors (image doesn't publish the requested platform)
without flagging intentional platform pinning.
You can re-enable this rule via configuration if you prefer the BuildKit
behavior:
```yaml theme={null}
rules:
buildkit/FromPlatformFlagConstDisallowed:
severity: warning
```
## Description
When the `--platform` flag appears with a hardcoded value, it restricts image
building to a single target platform, preventing multi-platform images.
The recommended strategy involves:
* Removing `FROM --platform` and applying `--platform` at build time.
* Substituting `$BUILDPLATFORM` or comparable variable expressions.
* Naming stages to reflect platform when containing platform-specific
operations.
## Examples
Bad:
```dockerfile theme={null}
FROM --platform=linux/amd64 alpine AS base
RUN apk add --no-cache git
```
Good (default platform):
```dockerfile theme={null}
FROM alpine AS base
RUN apk add --no-cache git
```
Good (meta variable):
```dockerfile theme={null}
FROM --platform=${BUILDPLATFORM} alpine AS base
RUN apk add --no-cache git
```
Good (multi-stage build with target architecture):
```dockerfile theme={null}
FROM --platform=linux/amd64 alpine AS build_amd64
...
FROM --platform=linux/arm64 alpine AS build_arm64
...
FROM build_${TARGETARCH} AS build
...
```
## Supersedes
* [hadolint/DL3029](../hadolint/DL3029)
## See also
* [`tally/platform-mismatch`](../tally/platform-mismatch) — validates explicit `--platform` against the registry instead of discouraging it
* [buildkit/FromPlatformFlagConstDisallowed](https://docs.docker.com/reference/build-checks/from-platform-flag-const-disallowed/)
# buildkit/InvalidBaseImagePlatform
Source: https://tally.wharflab.com/rules/buildkit/InvalidBaseImagePlatform
Validates that the platform of an external base image matches the expected target platform.
Validates that the platform of an external base image matches the expected target platform.
| Property | Value |
| -------- | -------------------------------------------------------------------------------- |
| Severity | Off |
| Category | Correctness |
| Default | Disabled (superseded by [`tally/platform-mismatch`](../tally/platform-mismatch)) |
## Tally behavior deviation
Tally disables this rule by default because its host-dependent design is
fundamentally broken for a static linter:
* **Non-deterministic results across machines.** The rule compares resolved
image platforms against the host's default platform (via `runtime.GOARCH`).
The same Dockerfile produces different violations on `linux/amd64` CI vs
`macOS/arm64` developer laptops.
* **False positives on Windows containers.** The expected OS is hardcoded to
`"linux"`, so any Windows base image (e.g.,
`mcr.microsoft.com/windows/servercore`) is always flagged as a mismatch.
* **Fires without explicit intent.** When no `--platform` flag is set on
`FROM`, the rule still compares against the host — even though the builder
will pick the correct platform at build time.
Use [`tally/platform-mismatch`](../tally/platform-mismatch) instead. That
rule only fires when `--platform` is explicitly set on `FROM` and the registry
does not provide the requested platform, producing deterministic results
regardless of host.
You can re-enable this rule via configuration if you prefer the BuildKit
behavior:
```yaml theme={null}
rules:
buildkit/InvalidBaseImagePlatform:
severity: error
```
## Description
When using `--platform` or `$TARGETPLATFORM`, this rule checks that the base
image actually supports the requested platform by resolving image metadata from
the registry.
This is an async rule that runs with `--slow-checks`, as it requires resolving
image metadata from the registry.
## Examples
Bad (image not available for requested platform):
```dockerfile theme={null}
FROM --platform=linux/s390x ubuntu:22.04
# Error if ubuntu:22.04 is not available for linux/s390x
```
Good:
```dockerfile theme={null}
FROM --platform=linux/amd64 ubuntu:22.04
```
The error message includes available platforms:
```text theme={null}
Base image ubuntu:22.04 was pulled with platform "linux/arm64", expected "linux/s390x" for current build
```
## See also
* [`tally/platform-mismatch`](../tally/platform-mismatch) — deterministic replacement that only validates explicit `--platform` flags
# buildkit/InvalidDefaultArgInFrom
Source: https://tally.wharflab.com/rules/buildkit/InvalidDefaultArgInFrom
Using the global ARGs with default values should produce a valid build.
Using the global ARGs with default values should produce a valid build.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
An `ARG` used in an image reference should be valid when no build arguments are
used. An image build should not require `--build-arg` to produce a valid build.
If a global `ARG` has no default value and is interpolated into a `FROM`
instruction, the resulting image reference may be invalid when the argument is
not supplied at build time.
## Examples
Bad:
```dockerfile theme={null}
ARG TAG
FROM busybox:${TAG}
```
Good:
```dockerfile theme={null}
ARG TAG=latest
FROM busybox:${TAG}
```
Good (empty ARG is OK if image is valid with it empty):
```dockerfile theme={null}
ARG VARIANT
FROM busybox:stable${VARIANT}
```
Good (default value syntax):
```dockerfile theme={null}
ARG TAG
FROM alpine:${TAG:-3.14}
```
## Reference
* [https://docs.docker.com/reference/build-checks/invalid-default-arg-in-from/](https://docs.docker.com/reference/build-checks/invalid-default-arg-in-from/)
# buildkit/InvalidDefinitionDescription
Source: https://tally.wharflab.com/rules/buildkit/InvalidDefinitionDescription
Comments for build stages or arguments should follow the description format.
Comments for build stages or arguments should follow the description format.
| Property | Value |
| -------- | ------------------ |
| Severity | Info |
| Category | Style |
| Default | Off (experimental) |
| Auto-fix | Yes (`--fix`) |
## Description
Comments for build stages or arguments should follow the format:
`# `. If a comment is not intended to be a
description, add an empty line or comment between the instruction and the
comment.
The `--call=outline` and `--call=targets` flags for the `docker build` command
print descriptions for build targets and arguments. The descriptions are
generated from Dockerfile comments that immediately precede the `FROM` or `ARG`
instruction and that begin with the name of the build stage or argument.
## Examples
Bad:
```dockerfile theme={null}
# a non-descriptive comment
FROM scratch AS base
# another non-descriptive comment
ARG VERSION=1
```
Good (empty line separating):
```dockerfile theme={null}
# a non-descriptive comment
FROM scratch AS base
# another non-descriptive comment
ARG VERSION=1
```
Good (proper description format):
```dockerfile theme={null}
# base is a stage for compiling source
FROM scratch AS base
# VERSION This is the version number.
ARG VERSION=1
```
## Auto-fix
The fix inserts an empty line between the non-description comment and the
instruction.
```dockerfile theme={null}
# Before
# Some comment
FROM alpine AS builder
# After (with --fix)
# Some comment
FROM alpine AS builder
```
## Reference
* [buildkit/InvalidDefinitionDescription](https://docs.docker.com/reference/build-checks/invalid-definition-description/)
# buildkit/JSONArgsRecommended
Source: https://tally.wharflab.com/rules/buildkit/JSONArgsRecommended
JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals.
JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals.
| Property | Value |
| -------- | ------------- |
| Severity | Info |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
`ENTRYPOINT` and `CMD` instructions both support shell form and exec form.
When you use shell form, the executable runs as a child process to a shell,
which doesn't pass signals. This means that the program running in the
container can't detect OS signals like `SIGTERM` and `SIGKILL` and respond to
them correctly.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine
ENTRYPOINT my-program start
# entrypoint becomes: /bin/sh -c my-program start
```
Good:
```dockerfile theme={null}
FROM alpine
ENTRYPOINT ["my-program", "start"]
# entrypoint becomes: my-program start
```
### Workarounds
If you need shell features (variable expansion, piping, command chaining), you
can:
1. Create a wrapper script:
```dockerfile theme={null}
FROM alpine
RUN apk add bash
COPY --chmod=755 <
# After (with --fix)
LABEL org.opencontainers.image.authors="John Doe "
```
## Supersedes
* [hadolint/DL4000](../hadolint/DL4000)
## Reference
* [buildkit/MaintainerDeprecated](https://docs.docker.com/reference/build-checks/maintainer-deprecated/)
# buildkit/MultipleInstructionsDisallowed
Source: https://tally.wharflab.com/rules/buildkit/MultipleInstructionsDisallowed
Multiple CMD instructions should not be used in the same stage because only the last one will be used.
Multiple CMD instructions should not be used in the same stage because only the last one will be used.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
If you have multiple `CMD`, `HEALTHCHECK`, or `ENTRYPOINT` instructions in your
Dockerfile, only the last occurrence is used. An image can only ever have one
`CMD`, one `HEALTHCHECK`, and one `ENTRYPOINT`.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine
ENTRYPOINT ["echo", "Hello, Norway!"]
ENTRYPOINT ["echo", "Hello, Sweden!"]
# Only "Hello, Sweden!" will be printed
```
Good:
```dockerfile theme={null}
FROM alpine
ENTRYPOINT ["echo", "Hello, Norway!\nHello, Sweden!"]
```
You can have both a regular `CMD` and a separate `CMD` for `HEALTHCHECK`:
```dockerfile theme={null}
FROM python:alpine
RUN apk add curl
HEALTHCHECK --interval=1s --timeout=3s \
CMD ["curl", "-f", "http://localhost:8080"]
CMD ["python", "-m", "http.server", "8080"]
```
## Auto-fix
The fix comments out duplicate `CMD`/`ENTRYPOINT`/`HEALTHCHECK` instructions,
keeping only the last one in each stage.
```dockerfile theme={null}
# Before
CMD echo "first"
CMD echo "second"
# After (with --fix)
# [commented out by tally - Docker will ignore all but last CMD]: CMD echo "first"
CMD echo "second"
```
## Supersedes
* [hadolint/DL3012](../hadolint/DL3012) (multiple `HEALTHCHECK`)
* [hadolint/DL4003](../hadolint/DL4003) (multiple `CMD`)
* [hadolint/DL4004](../hadolint/DL4004) (multiple `ENTRYPOINT`)
## Reference
* [buildkit/MultipleInstructionsDisallowed](https://docs.docker.com/reference/build-checks/multiple-instructions-disallowed/)
# buildkit/NoEmptyContinuation
Source: https://tally.wharflab.com/rules/buildkit/NoEmptyContinuation
Empty continuation lines are deprecated and will cause errors in future Dockerfile syntax versions.
Empty continuation lines are deprecated and will cause errors in future Dockerfile syntax versions.
| Property | Value |
| -------- | ------------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Support for empty continuation (`\`) lines has been deprecated and will
generate errors in future versions of the Dockerfile syntax. Empty continuation
lines are empty lines following a newline escape.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine
EXPOSE \
80
```
Good:
```dockerfile theme={null}
FROM alpine
EXPOSE \
# Port
80
```
Bad:
```dockerfile theme={null}
FROM alpine
RUN apk add \
gnupg \
curl
```
Good (empty lines removed):
```dockerfile theme={null}
FROM alpine
RUN apk add \
gnupg \
curl
```
## Auto-fix
The fix removes empty continuation lines from multi-line commands.
## Reference
* [buildkit/NoEmptyContinuation](https://docs.docker.com/reference/build-checks/no-empty-continuation/)
# buildkit/RedundantTargetPlatform
Source: https://tally.wharflab.com/rules/buildkit/RedundantTargetPlatform
Setting platform to predefined `$TARGETPLATFORM` in FROM is redundant as this is the default behavior.
Setting platform to predefined `$TARGETPLATFORM` in FROM is redundant as this is the default behavior.
| Property | Value |
| -------- | ------------- |
| Severity | Info |
| Category | Best Practice |
| Default | Enabled |
## Description
A custom platform can be used for a base image. The default platform is the
same platform as the target output, so setting the platform to
`$TARGETPLATFORM` is redundant and unnecessary.
## Examples
Bad:
```dockerfile theme={null}
FROM --platform=$TARGETPLATFORM alpine AS builder
RUN apk add --no-cache git
```
Good:
```dockerfile theme={null}
FROM alpine AS builder
RUN apk add --no-cache git
```
## Reference
* [buildkit/RedundantTargetPlatform](https://docs.docker.com/reference/build-checks/redundant-target-platform/)
# buildkit/ReservedStageName
Source: https://tally.wharflab.com/rules/buildkit/ReservedStageName
`scratch` is reserved and should not be used as a stage name.
`scratch` is reserved and should not be used as a stage name.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
Reserved words should not be used as names for stages in multi-stage builds.
The reserved words are: `context`, `scratch`.
Using a reserved word as a stage name can conflict with built-in BuildKit
behavior and produce confusing build errors.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine AS scratch
FROM alpine AS context
```
Good:
```dockerfile theme={null}
FROM alpine AS builder
```
## Reference
* [buildkit/ReservedStageName](https://docs.docker.com/reference/build-checks/reserved-stage-name/)
# buildkit/SecretsUsedInArgOrEnv
Source: https://tally.wharflab.com/rules/buildkit/SecretsUsedInArgOrEnv
Potentially sensitive data should not be used in the ARG or ENV commands.
Potentially sensitive data should not be used in the ARG or ENV commands.
| Property | Value |
| -------- | -------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
## Description
While it is common to pass secrets to running processes through environment
variables during local development, setting secrets in a Dockerfile using `ENV`
or `ARG` is insecure because they persist in the final image. This rule reports
violations where `ENV` and `ARG` keys indicate that they contain sensitive data.
Instead of `ARG` or `ENV`, you should use secret mounts, which expose secrets
to your builds in a secure manner and do not persist in the final image or its
metadata.
## Examples
Bad:
```dockerfile theme={null}
FROM scratch
ARG AWS_SECRET_ACCESS_KEY
```
Good (use secret mounts instead):
```dockerfile theme={null}
FROM scratch
RUN --mount=type=secret,id=aws_key \
AWS_SECRET_ACCESS_KEY=$(cat /run/secrets/aws_key) \
aws s3 cp ...
```
See also: [tally/secrets-in-code](../tally/secrets-in-code) complements this
rule by detecting actual secret *values* (not just variable names).
## Reference
* [buildkit/SecretsUsedInArgOrEnv](https://docs.docker.com/reference/build-checks/secrets-used-in-arg-or-env/)
* [Build secrets](https://docs.docker.com/build/building/secrets/)
# buildkit/StageNameCasing
Source: https://tally.wharflab.com/rules/buildkit/StageNameCasing
Stage names in multi-stage builds should be lowercase.
Stage names in multi-stage builds should be lowercase.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Stage name should be lowercase. To help distinguish Dockerfile instruction
keywords from identifiers, this rule forces names of stages in a multi-stage
Dockerfile to be all lowercase.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine AS BuilderBase
```
Good:
```dockerfile theme={null}
FROM alpine AS builder-base
```
## Auto-fix
The fix renames the stage to lowercase and updates all references (`FROM`,
`COPY --from`).
```dockerfile theme={null}
# Before
FROM alpine AS Builder
COPY --from=Builder /app .
# After (with --fix)
FROM alpine AS builder
COPY --from=builder /app .
```
## Reference
* [buildkit/StageNameCasing](https://docs.docker.com/reference/build-checks/stage-name-casing/)
# buildkit/UndefinedArgInFrom
Source: https://tally.wharflab.com/rules/buildkit/UndefinedArgInFrom
FROM argument is not declared.
FROM argument is not declared.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
## Description
This rule warns for cases where you are consuming an undefined build argument
in `FROM` instructions.
Interpolating build arguments in `FROM` instructions can be a good way to add
flexibility to your build. However, if the argument is never declared with
`ARG`, the variable silently resolves to an empty string, which is almost
certainly not the intended behavior.
This check also tries to detect and warn when a `FROM` instruction references
misspelled built-in build arguments, like `BUILDPLATFORM`.
## Examples
Bad:
```dockerfile theme={null}
FROM node:22${VARIANT} AS jsbuilder
```
Good:
```dockerfile theme={null}
ARG VARIANT="-alpine3.20"
FROM node:22${VARIANT} AS jsbuilder
```
You can also pass the argument at build time:
```console theme={null}
docker buildx build --build-arg ALPINE_VERSION=edge .
```
## Reference
* [buildkit/UndefinedArgInFrom](https://docs.docker.com/reference/build-checks/undefined-arg-in-from/)
# buildkit/UndefinedVar
Source: https://tally.wharflab.com/rules/buildkit/UndefinedVar
Usage of undefined variable.
Usage of undefined variable.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
## Description
This check ensures that environment variables and build arguments are correctly
declared before being used. While undeclared variables might not cause an
immediate build failure, they can lead to unexpected behavior. It also detects
common mistakes like typos in variable names.
This check does not evaluate undefined variables for `RUN`, `CMD`, and
`ENTRYPOINT` instructions where you use the shell form, because when you use
shell form, variables are resolved by the command shell.
## Examples
Bad:
```dockerfile theme={null}
FROM alpine AS base
COPY $foo .
```
Good:
```dockerfile theme={null}
FROM alpine AS base
ARG foo
COPY $foo .
```
Bad (typo detection):
```dockerfile theme={null}
FROM alpine
ENV PATH=$PAHT:/app/bin
```
Output: `Usage of undefined variable '$PAHT' (did you mean $PATH?)`
## Supersedes
* [hadolint/DL3044](../hadolint/DL3044)
## Reference
* [buildkit/UndefinedVar](https://docs.docker.com/reference/build-checks/undefined-var/)
# buildkit/WorkdirRelativePath
Source: https://tally.wharflab.com/rules/buildkit/WorkdirRelativePath
Relative workdir can have unexpected results if the base image changes.
Relative workdir can have unexpected results if the base image changes.
| Property | Value |
| -------- | ---------------------------------------------------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (suggestion `--fix-unsafe`; safe with `--slow-checks`) |
## Description
When specifying `WORKDIR` in a build stage, you can use an absolute path, like
`/build`, or a relative path, like `./build`. Using a relative path means that
the working directory is relative to whatever the previous working directory was.
This rule warns if you use `WORKDIR` with a relative path without first
specifying an absolute path in the same Dockerfile. The rationale is that using
a relative working directory for a base image built externally is prone to
breaking, since the working directory may change upstream without warning.
## Examples
Bad (assumes `WORKDIR` in base image is `/`):
```dockerfile theme={null}
FROM nginx AS web
WORKDIR usr/share/nginx/html
COPY public .
```
Good:
```dockerfile theme={null}
FROM nginx AS web
WORKDIR /usr/share/nginx/html
COPY public .
```
## Auto-fix
Replaces the relative `WORKDIR` with an absolute path.
* **Fast path** (no registry access): resolves against `/` as a default (`FixSuggestion`, requires `--fix-unsafe`).
* **With `--slow-checks`**: resolves against the base image's actual `WORKDIR` from the registry (`FixSafe`, applied with `--fix`). Chained relative
WORKDIRs are resolved cumulatively.
```dockerfile theme={null}
# Before
FROM nginx AS web
WORKDIR usr/share/nginx/html
# After (with --fix-unsafe, fast path — assumes base WORKDIR is /)
FROM nginx AS web
WORKDIR /usr/share/nginx/html
# After (with --fix --slow-checks, base image has WORKDIR /etc/nginx)
FROM nginx AS web
WORKDIR /etc/nginx/usr/share/nginx/html
```
## Supersedes
* [hadolint/DL3000](../hadolint/DL3000)
## Reference
* [buildkit/WorkdirRelativePath](https://docs.docker.com/reference/build-checks/workdir-relative-path/)
# BuildKit rules
Source: https://tally.wharflab.com/rules/buildkit/overview
tally supports all 22 BuildKit checks, including reimplementations of build-time checks as pure static analysis.
tally supports **22/22** BuildKit checks. 5 are captured directly from BuildKit's parser; 17 are reimplemented as static rules so they work without running Docker/BuildKit.
BuildKit checks come from Docker's official [build checks reference](https://docs.docker.com/reference/build-checks/). tally integrates them in two
ways:
* **Implemented by tally** — BuildKit normally runs these during LLB conversion (i.e., when actually building). tally reimplements them as pure static
checks so they catch issues without a Docker daemon.
* **Captured from BuildKit parser** — These are emitted by BuildKit during Dockerfile parsing and forwarded directly to tally's diagnostic pipeline.
All 11 auto-fixable rules are marked with 🔧. Run `tally lint --fix` to apply safe fixes automatically.
***
## Implemented by tally
These 17 checks correspond to BuildKit's LLB-conversion checks. tally runs them statically, so you get full coverage without Docker or BuildKit
installed.
All commands within the Dockerfile should use the same casing (either upper or lower).
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [consistent-instruction-casing](https://docs.docker.com/reference/build-checks/consistent-instruction-casing/)
```dockerfile theme={null}
# Violation: mixed casing
FROM alpine
run apk add curl
COPY . /app
cmd ["./app"]
```
Attempting to COPY a file that is excluded by `.dockerignore`.
**Severity:** Warning (enabled by default) · **Auto-fixable:** No
**Docker docs:** [copy-ignored-file](https://docs.docker.com/reference/build-checks/copy-ignored-file/)
```dockerfile theme={null}
# .dockerignore contains: *.log
# Violation: copying a file excluded by .dockerignore
COPY app.log /app/app.log
```
Stage names should be unique.
**Severity:** Error (enabled by default) · **Auto-fixable:** No
**Docker docs:** [duplicate-stage-name](https://docs.docker.com/reference/build-checks/duplicate-stage-name/)
```dockerfile theme={null}
# Violation: two stages share the name "builder"
FROM alpine AS builder
RUN echo "first"
FROM ubuntu AS builder
RUN echo "second"
```
IP address and host-port mapping should not be used in EXPOSE. This will become an error in a future release.
**Severity:** Warning (enabled by default) · **Auto-fixable:** No
**Docker docs:** [expose-invalid-format](https://docs.docker.com/reference/build-checks/expose-invalid-format/)
```dockerfile theme={null}
# Violation: host-port mapping in EXPOSE
EXPOSE 0.0.0.0:8080
```
Protocol in EXPOSE instruction should be lowercase.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [expose-proto-casing](https://docs.docker.com/reference/build-checks/expose-proto-casing/)
```dockerfile theme={null}
# Violation: protocol in uppercase
EXPOSE 80/TCP
EXPOSE 443/UDP
```
`FROM --platform` flag should not use a constant value.
**Severity:** Off by default · **Auto-fixable:** No
**Docker docs:** [from-platform-flag-const-disallowed](https://docs.docker.com/reference/build-checks/from-platform-flag-const-disallowed/)
```dockerfile theme={null}
# Violation: hardcoded platform instead of using build arg
FROM --platform=linux/amd64 alpine
```
Base image platform does not match expected target platform.
**Severity:** Off by default · **Auto-fixable:** No
```dockerfile theme={null}
# Violation: explicit platform conflicts with the target platform
FROM --platform=linux/arm64 node:20 AS builder
```
Default value for a global ARG results in an empty or invalid base image name.
**Severity:** Error (enabled by default) · **Auto-fixable:** No
**Docker docs:** [invalid-default-arg-in-from](https://docs.docker.com/reference/build-checks/invalid-default-arg-in-from/)
```dockerfile theme={null}
# Violation: ARG default is empty, making the image name invalid
ARG BASE_IMAGE=""
FROM ${BASE_IMAGE}
```
JSON arguments are recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals.
**Severity:** Info (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [json-args-recommended](https://docs.docker.com/reference/build-checks/json-args-recommended/)
```dockerfile theme={null}
# Violation: shell form — PID 1 is /bin/sh, not the process
ENTRYPOINT myapp --config /etc/myapp.conf
CMD myapp
```
Legacy key/value format with whitespace separator should not be used.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [legacy-key-value-format](https://docs.docker.com/reference/build-checks/legacy-key-value-format/)
```dockerfile theme={null}
# Violation: legacy whitespace-separated ENV and LABEL syntax
ENV MY_VAR my_value
LABEL maintainer John Doe
```
Multiple instructions of the same type should not be used in the same stage.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [multiple-instructions-disallowed](https://docs.docker.com/reference/build-checks/multiple-instructions-disallowed/)
```dockerfile theme={null}
FROM alpine
# Violation: two CMD instructions; only the last takes effect
CMD ["echo", "first"]
CMD ["echo", "second"]
```
Setting `--platform` to the predefined `$TARGETPLATFORM` in FROM is redundant as it is the default behavior.
**Severity:** Warning (enabled by default) · **Auto-fixable:** No
**Docker docs:** [redundant-target-platform](https://docs.docker.com/reference/build-checks/redundant-target-platform/)
```dockerfile theme={null}
# Violation: $TARGETPLATFORM is already the default
FROM --platform=$TARGETPLATFORM alpine
```
Reserved words should not be used as stage names.
**Severity:** Error (enabled by default) · **Auto-fixable:** No
**Docker docs:** [reserved-stage-name](https://docs.docker.com/reference/build-checks/reserved-stage-name/)
```dockerfile theme={null}
# Violation: "scratch" is a reserved stage name
FROM alpine AS scratch
RUN echo "building"
```
Sensitive data should not be used in the ARG or ENV commands.
**Severity:** Warning (enabled by default) · **Auto-fixable:** No
**Docker docs:** [secrets-used-in-arg-or-env](https://docs.docker.com/reference/build-checks/secrets-used-in-arg-or-env/)
```dockerfile theme={null}
# Violation: secret value baked into the image layer
ARG AWS_SECRET_ACCESS_KEY
ENV DATABASE_PASSWORD=supersecret
```
FROM command must use declared ARGs.
**Severity:** Warning (enabled by default) · **Auto-fixable:** No
**Docker docs:** [undefined-arg-in-from](https://docs.docker.com/reference/build-checks/undefined-arg-in-from/)
```dockerfile theme={null}
# Violation: IMAGE_TAG is not declared before use in FROM
FROM alpine:${IMAGE_TAG}
```
Variables should be defined before their use.
**Severity:** Warning (enabled by default) · **Auto-fixable:** No
**Docker docs:** [undefined-var](https://docs.docker.com/reference/build-checks/undefined-var/)
```dockerfile theme={null}
FROM alpine
# Violation: $APP_DIR was never defined with ARG or ENV
COPY . $APP_DIR
```
Relative WORKDIR without a prior absolute WORKDIR can have unexpected results if the base image changes.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [workdir-relative-path](https://docs.docker.com/reference/build-checks/workdir-relative-path/)
```dockerfile theme={null}
FROM alpine
# Violation: relative path depends on the base image's CWD
WORKDIR app
```
***
## Captured from BuildKit parser
These 5 checks are emitted by BuildKit during Dockerfile parsing. tally captures them directly and includes them in its diagnostic output alongside
statically implemented rules.
The `as` keyword should match the case of the `from` keyword.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [from-as-casing](https://docs.docker.com/reference/build-checks/from-as-casing/)
```dockerfile theme={null}
# Violation: FROM is uppercase but AS is lowercase (or vice versa)
FROM alpine as builder
# or
from alpine AS builder
```
Comments for build stages or arguments should follow the format: `# `. If this is not intended to be a description comment, add an empty line or comment between the instruction and the comment.
**Severity:** Warning · **Default:** Off (experimental) · **Auto-fixable:** Yes
**Docker docs:** [invalid-definition-description](https://docs.docker.com/reference/build-checks/invalid-definition-description/)
```dockerfile theme={null}
# Violation: comment does not match the stage name immediately below
# This is the production builder
FROM alpine AS builder
```
The MAINTAINER instruction is deprecated; use a label instead to define an image author.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [maintainer-deprecated](https://docs.docker.com/reference/build-checks/maintainer-deprecated/)
```dockerfile theme={null}
FROM alpine
# Violation: MAINTAINER is deprecated
MAINTAINER Jane Doe
```
Empty continuation lines will become errors in a future release.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [no-empty-continuation](https://docs.docker.com/reference/build-checks/no-empty-continuation/)
```dockerfile theme={null}
FROM alpine
# Violation: trailing backslash on an otherwise empty continuation line
RUN echo "hello" \
\
&& echo "world"
```
Stage names should be lowercase.
**Severity:** Warning (enabled by default) · **Auto-fixable:** Yes
**Docker docs:** [stage-name-casing](https://docs.docker.com/reference/build-checks/stage-name-casing/)
```dockerfile theme={null}
# Violation: stage name uses mixed or uppercase letters
FROM alpine AS Builder
FROM ubuntu AS PROD
```
# hadolint/DL3000
Source: https://tally.wharflab.com/rules/hadolint/DL3000
Use absolute WORKDIR.
Use absolute WORKDIR.
> **Superseded by [`buildkit/WorkdirRelativePath`](../buildkit/WorkdirRelativePath)**, which provides the same check with improved diagnostics.
## Description
Using a relative `WORKDIR` without first setting an absolute path can lead to unexpected results if the base image changes its working directory.
## Examples
### Problematic code
```dockerfile theme={null}
FROM nginx
WORKDIR usr/share/nginx/html
```
### Correct code
```dockerfile theme={null}
FROM nginx
WORKDIR /usr/share/nginx/html
```
## Reference
* [hadolint/DL3000](https://github.com/hadolint/hadolint/wiki/DL3000)
# hadolint/DL3001
Source: https://tally.wharflab.com/rules/hadolint/DL3001
Command does not make sense in a container.
Command does not make sense in a container.
| Property | Value |
| -------- | ------------- |
| Severity | Info |
| Category | Best Practice |
| Default | Enabled |
## Description
For some POSIX commands it makes no sense to run them in a Docker container because they are bound to the host or are otherwise dangerous (like
`shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`). Interactive utilities also don't make much sense (`nano`, `vim`).
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
RUN top
```
### Correct code
```dockerfile theme={null}
FROM busybox
```
## Reference
* [hadolint/DL3001](https://github.com/hadolint/hadolint/wiki/DL3001)
# hadolint/DL3002
Source: https://tally.wharflab.com/rules/hadolint/DL3002
Last user should not be root.
Last user should not be root.
| Property | Value |
| -------- | -------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
## Description
Switching to the root `USER` opens up certain security risks if an attacker gets access to the container. In order to mitigate this, switch back to a
non-privileged user after running the commands you need as root.
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
USER root
RUN ...
```
### Correct code
```dockerfile theme={null}
FROM busybox
USER root
RUN ...
USER guest
```
## Reference
* [hadolint/DL3002](https://github.com/hadolint/hadolint/wiki/DL3002)
# hadolint/DL3003
Source: https://tally.wharflab.com/rules/hadolint/DL3003
Use WORKDIR to switch to a directory.
Use WORKDIR to switch to a directory.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Only use `cd` in a subshell. Most commands can work with absolute paths. Docker provides the `WORKDIR` instruction if you really need to change the
current working directory.
When executed in a subshell, `cd` only affects the single `RUN` instruction, not any subsequent instructions. This can be an advantage over `WORKDIR`
which affects all subsequent instructions.
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
RUN cd /usr/src/app && git clone git@github.com:lukasmartinelli/hadolint.git
```
### Correct code (with WORKDIR)
```dockerfile theme={null}
FROM busybox
WORKDIR /usr/src/app
RUN git clone git@github.com:lukasmartinelli/hadolint.git
```
### Correct code (with absolute paths)
```dockerfile theme={null}
FROM busybox
RUN cp somedir/somefile /usr/src/app/someDirInUsrSrcApp/
```
## Auto-fix
Splits `RUN` with `cd` into `WORKDIR` + `RUN` instructions. Removes redundant `mkdir` commands before `cd` targets.
```dockerfile theme={null}
# Before
RUN cd /tmp && git clone ... && cd repo && make
# After (with --fix)
WORKDIR /tmp
RUN git clone ...
WORKDIR repo
RUN make
```
## Reference
* [hadolint/DL3003](https://github.com/hadolint/hadolint/wiki/DL3003)
# hadolint/DL3004
Source: https://tally.wharflab.com/rules/hadolint/DL3004
Do not use sudo.
Do not use sudo.
| Property | Value |
| -------- | -------- |
| Severity | Error |
| Category | Security |
| Default | Enabled |
## Description
Do not use `sudo` as it leads to unpredictable behavior. Use a tool like [gosu](https://github.com/tianon/gosu) to enforce root.
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
RUN sudo apt-get install
```
### Correct code
```dockerfile theme={null}
FROM busybox
RUN apt-get install
```
## Reference
* [hadolint/DL3004](https://github.com/hadolint/hadolint/wiki/DL3004)
# hadolint/DL3006
Source: https://tally.wharflab.com/rules/hadolint/DL3006
Always tag the version of an image explicitly.
Always tag the version of an image explicitly.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
## Description
You can never rely that the `latest` tag is a specific version. Always tag the version of an image explicitly to ensure reproducible builds.
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian
```
### Correct code
```dockerfile theme={null}
FROM debian:jessie
```
### Exception
When the image name refers to a previously defined alias, tagging is not required:
```dockerfile theme={null}
FROM debian:jessie as build
RUN build_script
FROM build as tests
RUN test_script
FROM debian:jessie
COPY --from=build foo .
```
## Reference
* [hadolint/DL3006](https://github.com/hadolint/hadolint/wiki/DL3006)
# hadolint/DL3007
Source: https://tally.wharflab.com/rules/hadolint/DL3007
Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag.
Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
## Description
You can never rely that the `latest` tag is a specific version. Pin the version explicitly to a release tag to ensure reproducible builds.
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian:latest
```
### Correct code
```dockerfile theme={null}
FROM debian:jessie
```
## Reference
* [hadolint/DL3007](https://github.com/hadolint/hadolint/wiki/DL3007)
# hadolint/DL3010
Source: https://tally.wharflab.com/rules/hadolint/DL3010
Use ADD for extracting archives into an image.
Use ADD for extracting archives into an image.
| Property | Value |
| -------- | ------------- |
| Severity | Info |
| Category | Best Practice |
| Default | Enabled |
## Description
Although `ADD` and `COPY` are functionally similar, generally speaking, `COPY` is preferred. `COPY` only supports the basic copying of local files
into the container, while `ADD` has some features (like local-only tar extraction and remote URL support). The best use for `ADD` is local tar file
auto-extraction into the image.
## Reference
* [hadolint/DL3010](https://github.com/hadolint/hadolint/wiki/DL3010)
# hadolint/DL3011
Source: https://tally.wharflab.com/rules/hadolint/DL3011
Valid UNIX ports range from 0 to 65535.
Valid UNIX ports range from 0 to 65535.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
Valid UNIX ports range from 0 to 65535. Exposing a port outside this range is invalid and will result in an error.
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
EXPOSE 80000
```
### Correct code
```dockerfile theme={null}
FROM busybox
EXPOSE 65535
```
## Reference
* [hadolint/DL3011](https://github.com/hadolint/hadolint/wiki/DL3011)
# hadolint/DL3012
Source: https://tally.wharflab.com/rules/hadolint/DL3012
Multiple `HEALTHCHECK` instructions.
Multiple `HEALTHCHECK` instructions.
> **Superseded by
> [`buildkit/MultipleInstructionsDisallowed`](../buildkit/MultipleInstructionsDisallowed)**,
> which covers duplicate `HEALTHCHECK`, `CMD`, and `ENTRYPOINT`
> instructions with auto-fix support.
## Description
Multiple `HEALTHCHECK` instructions should not be used in the same stage because only the last one will be used.
## Reference
* [hadolint/DL3012](https://github.com/hadolint/hadolint/wiki/DL3012)
# hadolint/DL3014
Source: https://tally.wharflab.com/rules/hadolint/DL3014
Use the `-y` switch.
Use the `-y` switch.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Without the `--assume-yes` (`-y`) option it might be possible that the build breaks without human intervention. Always use `-y` with `apt-get install`
to avoid interactive prompts during the build.
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian
RUN apt-get install python=2.7
```
### Correct code
```dockerfile theme={null}
FROM debian
RUN apt-get install -y python=2.7
```
## Auto-fix
Adds `-y` flag to `apt-get install` commands.
```dockerfile theme={null}
# Before
RUN apt-get install curl
# After (with --fix)
RUN apt-get install -y curl
```
## Reference
* [hadolint/DL3014](https://github.com/hadolint/hadolint/wiki/DL3014)
# hadolint/DL3020
Source: https://tally.wharflab.com/rules/hadolint/DL3020
Use `COPY` instead of `ADD` for files and folders.
Use `COPY` instead of `ADD` for files and folders.
| Property | Value |
| -------- | ------------- |
| Severity | Error |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
For files and directories that do not require `ADD`'s tar auto-extraction capability, you should always use `COPY`. `COPY` is more transparent and
predictable than `ADD`, since it only supports basic copying of files into the container.
**Exception:** `ADD` is appropriate for local tar file auto-extraction into the image.
## Examples
### Problematic code
```dockerfile theme={null}
FROM python:3.4
ADD requirements.txt /usr/src/app/
```
### Correct code
```dockerfile theme={null}
FROM python:3.4
COPY requirements.txt /usr/src/app/
```
## Auto-fix
Replaces the `ADD` keyword with `COPY`, preserving all flags, sources, and destination unchanged.
* **Safe fix** (`FixSafe`): Always correct for local file/directory sources since `COPY` and `ADD` behave identically for non-URL, non-tar sources.
```dockerfile theme={null}
# Before
ADD --chown=app:app src/ /app/
# After (with --fix)
COPY --chown=app:app src/ /app/
```
The fix preserves instruction casing (`ADD` → `COPY`, `add` → `copy`).
## Reference
* [hadolint/DL3020](https://github.com/hadolint/hadolint/wiki/DL3020)
# hadolint/DL3021
Source: https://tally.wharflab.com/rules/hadolint/DL3021
`COPY` with more than 2 arguments requires the last argument to end with `/`.
`COPY` with more than 2 arguments requires the last argument to end with `/`.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
If multiple source resources are specified in a `COPY` instruction, then the destination must be a directory and it must end with a slash `/`.
Omitting the trailing slash causes ambiguity and may lead to build errors or unexpected behavior.
## Examples
### Problematic code
```dockerfile theme={null}
FROM node:carbon
COPY package.json yarn.lock my_app
```
### Correct code
```dockerfile theme={null}
FROM node:carbon
COPY package.json yarn.lock my_app/
```
## Reference
* [hadolint/DL3021](https://github.com/hadolint/hadolint/wiki/DL3021)
# hadolint/DL3022
Source: https://tally.wharflab.com/rules/hadolint/DL3022
`COPY --from` should reference a previously defined `FROM` alias.
`COPY --from` should reference a previously defined `FROM` alias.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Off |
## Description
When using multi-stage builds, `COPY --from` should reference a stage alias that was previously defined with `FROM ... AS `. Trying to copy
from a missing image alias results in an error at build time.
### Why default Off
This rule is off by default because it does not account for
[`--build-context`](https://docs.docker.com/reference/cli/docker/buildx/build/#build-context)
sources. As per the official Dockerfile documentation:
> You can also copy files directly from named contexts
> (specified with `--build-context =`) or images.
Since named build contexts are supplied at build time (`docker buildx build --build-context name=path`), a static linter cannot verify whether a
`COPY --from=name` reference is valid. Flagging these as violations produces
false positives that cannot be resolved without running the actual build.
To enable this rule, set its severity explicitly in your `.tally.toml`:
```toml theme={null}
[rules.hadolint.DL3022]
severity = "warning"
```
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian:jesse
RUN stuff
FROM debian:jesse
COPY --from=build some stuff ./
```
### Correct code
```dockerfile theme={null}
FROM debian:jesse as build
RUN stuff
FROM debian:jesse
COPY --from=build some stuff ./
```
## References
* [hadolint/DL3022](https://github.com/hadolint/hadolint/wiki/DL3022)
* [`COPY --from`](https://docs.docker.com/reference/dockerfile/#copy---from)
# hadolint/DL3023
Source: https://tally.wharflab.com/rules/hadolint/DL3023
`COPY --from` cannot reference its own `FROM` alias.
`COPY --from` cannot reference its own `FROM` alias.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
A `COPY --from` instruction must not reference the same stage it is running in. Trying to copy from the current stage results in an error because the
stage has not yet been finalized.
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian:jesse as build
COPY --from=build some stuff ./
```
### Correct code
```dockerfile theme={null}
FROM debian:jesse as build
RUN stuff
FROM debian:jesse
COPY --from=build some stuff ./
```
## Reference
* [hadolint/DL3023](https://github.com/hadolint/hadolint/wiki/DL3023)
# hadolint/DL3024
Source: https://tally.wharflab.com/rules/hadolint/DL3024
Duplicate stage names are not allowed.
Duplicate stage names are not allowed.
> **Superseded by [`buildkit/DuplicateStageName`](../buildkit/DuplicateStageName)**, which provides the same check.
## Description
Defining multiple stages with the same name results in an error because the builder is unable to uniquely resolve the stage name reference.
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian:latest AS builder
RUN apt-get update
FROM golang:latest AS builder
```
### Correct code
```dockerfile theme={null}
FROM debian:latest AS deb-builder
RUN apt-get update
FROM golang:latest AS go-builder
```
## Reference
* [hadolint/DL3024](https://github.com/hadolint/hadolint/wiki/DL3024)
# hadolint/DL3025
Source: https://tally.wharflab.com/rules/hadolint/DL3025
Use arguments JSON notation for CMD and ENTRYPOINT arguments.
Use arguments JSON notation for CMD and ENTRYPOINT arguments.
> **Superseded by [`buildkit/JSONArgsRecommended`](../buildkit/JSONArgsRecommended)**, which provides the same check with auto-fix support.
## Description
`ENTRYPOINT` and `CMD` instructions should use the JSON (exec) form to ensure proper OS signal handling. Shell form runs commands as a child process
of `/bin/sh`, which doesn't pass signals like `SIGTERM`.
## Examples
### Problematic code
```dockerfile theme={null}
FROM alpine
CMD my-program start
```
### Correct code
```dockerfile theme={null}
FROM alpine
CMD ["my-program", "start"]
```
## Reference
* [hadolint/DL3025](https://github.com/hadolint/hadolint/wiki/DL3025)
# hadolint/DL3026
Source: https://tally.wharflab.com/rules/hadolint/DL3026
Use only an allowed registry in the `FROM` image.
Use only an allowed registry in the `FROM` image.
| Property | Value |
| -------- | ------------------------------- |
| Severity | Off |
| Category | Security |
| Default | Off (disabled until configured) |
## Description
Using the `FROM` instruction is a significant exercise in trust. Some organizations copy trusted images into their own repositories to prevent
malicious retagging. This rule enforces that only images from explicitly allowed registries are used.
This rule is disabled by default and must be configured with a list of trusted registries to take effect.
## Examples
### Problematic code
```dockerfile theme={null}
FROM randomguy/python:3.6
```
### Correct code
```dockerfile theme={null}
FROM my-registry.com/python:3.6
```
## tally enhancements
tally extends the original Hadolint rule with:
* **Wildcard support**: `*` matches any registry, `*.example.com` matches any subdomain (suffix match), `prefix*` matches registries starting with
prefix
* **Docker Hub normalization**: `docker.io`, `index.docker.io`, `registry-1.docker.io`, `registry.hub.docker.com`, and `hub.docker.com` are all
normalized to `docker.io`
* **Stage references**: Automatically skips stage-to-stage references (`FROM stagename`)
* **Scratch always allowed**: The special `scratch` base image is always permitted
### Configuration
```toml theme={null}
[rules.hadolint.DL3026]
severity = "warning"
trusted-registries = ["docker.io", "gcr.io", "*.example.com"]
```
## Reference
* [hadolint/DL3026](https://github.com/hadolint/hadolint/wiki/DL3026)
# hadolint/DL3027
Source: https://tally.wharflab.com/rules/hadolint/DL3027
Do not use `apt` as it is meant to be an end-user tool, use `apt-get` or `apt-cache` instead.
Do not use `apt` as it is meant to be an end-user tool, use `apt-get` or `apt-cache` instead.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
`apt` is discouraged by the Linux distributions as an unattended tool as its interface may change between versions. Use the more stable `apt-get` and
`apt-cache` commands in Dockerfiles instead.
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
RUN apt install curl=1.1.0
```
### Correct code
```dockerfile theme={null}
FROM busybox
RUN apt-get install curl=1.1.0
```
## Auto-fix
Replaces `apt` with `apt-get` or `apt-cache` depending on the subcommand.
* **Safe fix** (`FixSafe`): `install`, `remove`, `update`, `upgrade`, `autoremove`, `purge`, `clean`, `autoclean` are replaced with `apt-get`
* **Suggestion fix** (`FixSuggestion`): `search`, `show`, `policy` are replaced with `apt-cache`
```dockerfile theme={null}
# Before
RUN apt install -y curl && apt search python
# After (with --fix)
RUN apt-get install -y curl && apt-cache search python
```
## Reference
* [hadolint/DL3027](https://github.com/hadolint/hadolint/wiki/DL3027)
# hadolint/DL3029
Source: https://tally.wharflab.com/rules/hadolint/DL3029
Do not use --platform flag with constant value in FROM.
Do not use --platform flag with constant value in FROM.
> **Superseded by [`buildkit/FromPlatformFlagConstDisallowed`](../buildkit/FromPlatformFlagConstDisallowed)**, which provides the same check with
> improved diagnostics.
## Description
Using a hardcoded `--platform` value in `FROM` restricts image building to a single target platform. Use `$BUILDPLATFORM` or similar variables
instead.
## Examples
### Problematic code
```dockerfile theme={null}
FROM --platform=linux/amd64 alpine
```
### Correct code
```dockerfile theme={null}
FROM --platform=${BUILDPLATFORM} alpine
```
## Reference
* [hadolint/DL3029](https://github.com/hadolint/hadolint/wiki/DL3029)
# hadolint/DL3030
Source: https://tally.wharflab.com/rules/hadolint/DL3030
Use the `-y` switch to avoid manual input: `yum install -y `.
Use the `-y` switch to avoid manual input: `yum install -y `.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Without the `-y` flag or the equivalent `--assumeyes` flag, `yum` will not successfully install a package because human input is expected. In a
Dockerfile `RUN` instruction there is no interactive terminal, so the build will fail.
## Examples
### Problematic code
```dockerfile theme={null}
FROM centos
RUN yum install httpd-2.24.4 && yum clean all
```
### Correct code
```dockerfile theme={null}
FROM centos
RUN yum install -y httpd-2.24.4 && yum clean all
```
## Auto-fix
Adds `-y` flag to `yum install`, `groupinstall`, `localinstall`, and `reinstall` commands.
## Reference
* [hadolint/DL3030](https://github.com/hadolint/hadolint/wiki/DL3030)
# hadolint/DL3034
Source: https://tally.wharflab.com/rules/hadolint/DL3034
Non-interactive switch missing from `zypper` command: `zypper install -y`.
Non-interactive switch missing from `zypper` command: `zypper install -y`.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Omitting the non-interactive switch causes the command to fail during the build process because `zypper` would expect manual input. Use the `-y` or
the equivalent `--no-confirm` flag to ensure unattended operation inside a Dockerfile.
## Examples
### Problematic code
```dockerfile theme={null}
RUN zypper install httpd=2.4.46 && zypper clean
```
### Correct code
```dockerfile theme={null}
RUN zypper install -y httpd=2.4.46 && zypper clean
```
## Auto-fix
Adds `-n` (non-interactive) flag to `zypper install`, `in`, `remove`, `rm`, `patch`, `source-install`, and `si` commands.
## Reference
* [hadolint/DL3034](https://github.com/hadolint/hadolint/wiki/DL3034)
# hadolint/DL3038
Source: https://tally.wharflab.com/rules/hadolint/DL3038
Use the `-y` switch to avoid manual input: `dnf install -y `.
Use the `-y` switch to avoid manual input: `dnf install -y `.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Omitting the non-interactive switch causes the command to fail during the build process because `dnf` would expect manual input. Use the `-y` or the
equivalent `--assumeyes` flag to ensure unattended operation inside a Dockerfile.
## Examples
### Problematic code
```dockerfile theme={null}
FROM fedora:32
RUN dnf install httpd-2.4.46 && dnf clean all
```
### Correct code
```dockerfile theme={null}
FROM fedora:32
RUN dnf install -y httpd-2.4.46 && dnf clean all
```
## Auto-fix
Adds `-y` flag to `dnf` and `microdnf` `install`, `groupinstall`, and `localinstall` commands.
## Reference
* [hadolint/DL3038](https://github.com/hadolint/hadolint/wiki/DL3038)
# hadolint/DL3043
Source: https://tally.wharflab.com/rules/hadolint/DL3043
`ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction.
`ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
The `ONBUILD` instruction does not allow `ONBUILD`, `FROM`, or `MAINTAINER` as nested instructions. Using any of these within an `ONBUILD` trigger is
an error and will cause a build failure.
## Examples
### Problematic code
```dockerfile theme={null}
ONBUILD ONBUILD /bin/true
ONBUILD FROM debian
ONBUILD MAINTAINER Ron Weasley
```
## Reference
* [hadolint/DL3043](https://github.com/hadolint/hadolint/wiki/DL3043)
* [Dockerfile ONBUILD reference](https://docs.docker.com/engine/reference/builder/#onbuild)
# hadolint/DL3044
Source: https://tally.wharflab.com/rules/hadolint/DL3044
Do not refer to an undefined variable.
Do not refer to an undefined variable.
> **Superseded by [`buildkit/UndefinedVar`](../buildkit/UndefinedVar)**, which provides the same check with typo detection.
## Description
Ensures that environment variables and build arguments are declared before being used. Undeclared variables can lead to unexpected behavior or build
errors.
## Examples
### Problematic code
```dockerfile theme={null}
FROM alpine
COPY $foo .
```
### Correct code
```dockerfile theme={null}
FROM alpine
ARG foo
COPY $foo .
```
## Reference
* [hadolint/DL3044](https://github.com/hadolint/hadolint/wiki/DL3044)
# hadolint/DL3045
Source: https://tally.wharflab.com/rules/hadolint/DL3045
`COPY` to a relative destination without `WORKDIR` set.
`COPY` to a relative destination without `WORKDIR` set.
| Property | Value |
| -------- | ---------------------------------------------------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (suggestion `--fix-unsafe`; safe with `--slow-checks`) |
## Description
While copying to a relative path is not problematic per se, errors happen when changes are introduced to the `WORKDIR` without updating the
destination of the `COPY` command. It is assumed that when a `WORKDIR` is set, the programmer will make sure it works well together with the `COPY`
destinations.
## Examples
### Problematic code
```dockerfile theme={null}
FROM scratch
COPY foo bar
```
### Correct code
```dockerfile theme={null}
FROM scratch
COPY foo /bar
```
or:
```dockerfile theme={null}
FROM scratch
WORKDIR /
COPY foo bar
```
## Auto-fix
Inserts an explicit `WORKDIR` instruction before the first `COPY` with a relative destination.
* **Fast path** (no registry access): suggests `WORKDIR /app` as a conventional default (`FixSuggestion`, requires `--fix-unsafe`).
* **With `--slow-checks`**: resolves the base image's actual `WORKDIR` from the registry and uses that value (`FixSafe`, applied with `--fix`).
```dockerfile theme={null}
# Before
FROM python:3.12
COPY requirements.txt .
# After (with --fix-unsafe, fast path)
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
# After (with --fix --slow-checks, base image has WORKDIR /usr/src/app)
FROM python:3.12
WORKDIR /usr/src/app
COPY requirements.txt .
```
## Reference
* [hadolint/DL3045](https://github.com/hadolint/hadolint/wiki/DL3045)
# hadolint/DL3046
Source: https://tally.wharflab.com/rules/hadolint/DL3046
`useradd` without flag `-l` and high UID will result in excessively large image.
`useradd` without flag `-l` and high UID will result in excessively large image.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Performance |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Without the `-l` or `--no-log-init` flag, `useradd` will add the user to the lastlog and faillog databases. This can result in the creation of
logically large (sparse) files under `/var/log`, which inflates container image sizes due to the lack of support for sparse files in overlay
filesystems.
## Examples
### Problematic code
```dockerfile theme={null}
RUN useradd -u 123456 foobar
```
### Correct code
```dockerfile theme={null}
RUN useradd -l -u 123456 foobar
```
## Auto-fix
Inserts `-l` flag after `useradd` when UID is greater than 99999 and `-l`/`--no-log-init` is not already present.
```dockerfile theme={null}
# Before
RUN useradd -u 100001 appuser
# After (with --fix)
RUN useradd -u 100001 -l appuser
```
## Reference
* [hadolint/DL3046](https://github.com/hadolint/hadolint/wiki/DL3046)
# hadolint/DL3047
Source: https://tally.wharflab.com/rules/hadolint/DL3047
Use `wget --progress` to avoid excessively bloated build logs.
Use `wget --progress` to avoid excessively bloated build logs.
| Property | Value |
| -------- | ------------- |
| Severity | Info |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
`wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files because it outputs a line for each
fraction of a percentage point.
## Examples
### Problematic code
```dockerfile theme={null}
FROM ubuntu:20
RUN wget https://example.com/big_file.tar
```
### Correct code
```dockerfile theme={null}
FROM ubuntu:20
RUN wget --progress=dot:giga https://example.com/big_file.tar
```
or:
```dockerfile theme={null}
FROM ubuntu:20
RUN wget -nv https://example.com/big_file.tar
```
## Auto-fix
Adds `--progress=dot:giga` to wget commands. Skipped if `-q`, `--quiet`, `-nv`, `--no-verbose`, `-o`, `--output-file`, `-a`, `--append-output`, or
`--progress` is already present.
```dockerfile theme={null}
# Before
RUN wget http://example.com/file.tar.gz
# After (with --fix)
RUN wget --progress=dot:giga http://example.com/file.tar.gz
```
## Reference
* [hadolint/DL3047](https://github.com/hadolint/hadolint/wiki/DL3047)
# hadolint/DL3057
Source: https://tally.wharflab.com/rules/hadolint/DL3057
`HEALTHCHECK` instruction missing.
`HEALTHCHECK` instruction missing.
| Property | Value |
| -------- | ----------------------- |
| Severity | Ignore (off by default) |
| Category | Best Practice |
| Default | Off |
## Description
This is an optional rule. When it is required to define a health check (e.g. by company policy), it must not be omitted.
This rule is disabled by default because a `HEALTHCHECK` is not desirable in all circumstances. Images used with Kubernetes do not benefit from a
`HEALTHCHECK` instruction, as Kubernetes brings its own mechanisms.
## Examples
### Problematic code
```dockerfile theme={null}
FROM busybox
```
### Correct code
```dockerfile theme={null}
FROM busybox
HEALTHCHECK CMD /bin/health
```
or:
```dockerfile theme={null}
FROM busybox
HEALTHCHECK NONE
```
## tally enhancements
### Smart suppression
tally automatically suppresses this rule when the Dockerfile shows strong signals that a `HEALTHCHECK` would not be beneficial:
#### Serverless / FaaS base images
Containers built for serverless platforms have their lifecycle managed externally — the platform decides when to start, stop, and replace
function instances. A container-level `HEALTHCHECK` is ignored in these environments.
| Platform | Suppressed image patterns |
| ------------------- | ---------------------------------------------------------------------------------- |
| **AWS Lambda** | `public.ecr.aws/lambda/*`, `gallery.ecr.aws/lambda/*`, `amazon/aws-lambda-*` |
| **Azure Functions** | `mcr.microsoft.com/azure-functions/*` |
| **OpenFaaS** | `openfaas/of-watchdog`, `openfaas/classic-watchdog` (including `ghcr.io` variants) |
If **any** stage in the Dockerfile uses a recognized serverless base image, the violation is suppressed for the entire file. Multi-stage builds
that pull from a Lambda image in one stage and copy artifacts into another are still covered because the presence of the serverless image signals
the target runtime.
#### Serverless framework entrypoints
When the **final stage's** `CMD` or `ENTRYPOINT` invokes a known serverless function framework, the container is a short-lived function handler
managed by the platform — not a service that benefits from `HEALTHCHECK`.
| Framework | Example |
| -------------------------------------------------- | ----------------------------------------------- |
| **Google Cloud Functions** (`functions-framework`) | `CMD ["functions-framework", "--target=hello"]` |
The `exec` prefix commonly used in shell form is handled:
```dockerfile theme={null}
CMD exec functions-framework --target=hello --port=$PORT
```
#### Interactive / shell-only containers
When the **final stage's** `CMD` or `ENTRYPOINT` resolves to a bare interactive shell (`sh`, `bash`, `zsh`, `ash`, `dash`, `fish`, `csh`, `tcsh`,
`ksh`), the container is clearly not a long-running service — there is no endpoint to health-check.
Recognized patterns:
```dockerfile theme={null}
CMD ["bash"] # exec form
CMD bash # shell form
CMD ["bash", "-l"] # shell with flags (still interactive)
ENTRYPOINT ["/bin/sh"] # entrypoint shell
```
Not suppressed when the shell is used to execute a command:
```dockerfile theme={null}
CMD ["bash", "-c", "my-app"] # runs my-app, not interactive
```
If an `ENTRYPOINT` is present, it takes precedence over `CMD` (matching Docker runtime semantics).
#### No explicit CMD/ENTRYPOINT (external parent delegation)
When the **final stage** has no `CMD` or `ENTRYPOINT` instruction and its base is an **external image** (not another build stage), the image
delegates run orchestration to its parent. In these cases the parent likely also defines a `HEALTHCHECK`, so flagging the child produces false
positives. The violation is suppressed.
```dockerfile theme={null}
FROM nginx:latest
RUN echo "custom config" > /etc/nginx/conf.d/default.conf
EXPOSE 80
# No CMD — nginx base image provides CMD and likely HEALTHCHECK
```
This does **not** apply when the final stage inherits from a prior build stage (`FROM `), because CMD/ENTRYPOINT are inherited from
the prior stage and the image is not opaque:
```dockerfile theme={null}
FROM alpine AS base
CMD ["my-app"]
FROM base
RUN echo "setup"
# DL3057 still fires — CMD inherited from "base", image is not opaque
```
### Explicit opt-out with `HEALTHCHECK NONE`
`HEALTHCHECK NONE` is treated as a deliberate opt-out. When present in any stage, DL3057 is fully suppressed — no fast-path violation is
emitted and no async registry checks are planned. This matches Docker's semantics where `HEALTHCHECK NONE` explicitly disables health checking.
### Async registry resolution
tally extends this rule with **async registry resolution** (enabled with `--slow-checks`):
* **Base image inspection**: For each external base image, tally checks if it already defines a `HEALTHCHECK` in its image metadata. If so, the
violation is suppressed since the health check is inherited. This check is skipped when any explicit `HEALTHCHECK` instruction (CMD or NONE)
is already present.
* **Cross-rule awareness**: `buildkit/MultipleInstructionsDisallowed` may still flag duplicate `HEALTHCHECK` instructions even when DL3057 is
suppressed.
## Reference
* [hadolint/DL3057](https://github.com/hadolint/hadolint/wiki/DL3057)
# hadolint/DL3059
Source: https://tally.wharflab.com/rules/hadolint/DL3059
Multiple consecutive `RUN` instructions.
Multiple consecutive `RUN` instructions.
> **Superseded by
> [`tally/prefer-run-heredoc`](../tally/prefer-run-heredoc)**,
> which provides the same check with enhanced detection
> (consecutive RUNs and chained commands) and auto-fix support
> using heredoc syntax.
## Description
Multiple consecutive `RUN` instructions can be consolidated into a single instruction to reduce image layers.
## Examples
### Problematic code
```dockerfile theme={null}
FROM ubuntu
RUN apt-get update
RUN apt-get install -y curl
```
### Correct code
```dockerfile theme={null}
FROM ubuntu
RUN apt-get update && apt-get install -y curl
```
Or with heredoc syntax (tally auto-fix):
```dockerfile theme={null}
FROM ubuntu
RUN < **Superseded by [`buildkit/ReservedStageName`](../buildkit/ReservedStageName)**, which provides the same check.
## Description
Stage aliases in `FROM ... AS ` must not use reserved Dockerfile stage names such as `scratch` or `context`.
## Examples
### Problematic code
```dockerfile theme={null}
FROM alpine:3.21 AS scratch
```
### Correct code
```dockerfile theme={null}
FROM alpine:3.21 AS builder
```
## Reference
* [hadolint/DL3063](https://github.com/hadolint/hadolint/wiki/DL3063)
# hadolint/DL4000
Source: https://tally.wharflab.com/rules/hadolint/DL4000
MAINTAINER is deprecated.
MAINTAINER is deprecated.
> **Superseded by [`buildkit/MaintainerDeprecated`](../buildkit/MaintainerDeprecated)**, which provides the same check with auto-fix support.
## Description
The `MAINTAINER` instruction is deprecated. Use the `org.opencontainers.image.authors` label instead.
## Examples
### Problematic code
```dockerfile theme={null}
MAINTAINER moby@example.com
```
### Correct code
```dockerfile theme={null}
LABEL org.opencontainers.image.authors="moby@example.com"
```
## Reference
* [hadolint/DL4000](https://github.com/hadolint/hadolint/wiki/DL4000)
# hadolint/DL4001
Source: https://tally.wharflab.com/rules/hadolint/DL4001
Either use Wget or Curl but not both.
Either use Wget or Curl but not both.
| Property | Value |
| -------- | ----------------------------------------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`; AI AutoFix fallback) |
## Description
Don't install two tools that have the same effect. Using both `wget` and `curl` in a Dockerfile adds unnecessary cruft to the image. Pick one and use
it consistently.
The rule fires when both tools are present in the Dockerfile in any combination: both invoked, both installed, or one installed while the other is
invoked. Installing the second tool is itself the offense — even if you never call it, it still inflates the image.
## Examples
### Problematic code
```dockerfile theme={null}
FROM debian
RUN wget http://google.com
RUN curl http://bing.com
```
### Correct code
```dockerfile theme={null}
FROM debian
RUN curl http://google.com
RUN curl http://bing.com
```
## Auto-fix
The rule rewrites offending commands to the tool it picks as the winner for the Dockerfile. Rewriting is **bidirectional**: `curl → wget` or
`wget → curl`. All fixes are **unsafe** (`FixUnsafe`) and require `--fix --fix-unsafe`.
### How the winning tool is chosen (auto mode)
Auto mode uses usage-based heuristics, not tool-install flags. The winner — and therefore the direction of the rewrite — is decided in this order:
1. **Used without explicit install wins.** If exactly one of `curl` / `wget` is used without being explicitly installed by the Dockerfile (e.g.,
it comes from the base image), that tool wins and the other one is rewritten. Installing the tool you already get "for free" is the thing
DL4001 is pushing back on.
2. **Most invocations wins.** If both tools are in the same install category (both installed, or both used-without-install), the tool with more
total invocations across the Dockerfile wins.
3. **First seen wins.** If invocation counts tie, the tool whose first use appears earlier in the Dockerfile wins.
The fix runs in two phases:
* **Rewrite each invocation.** Tally doesn't try to translate `curl` flags into the equivalent `wget` flags argument-by-argument — that's
a fragile game with many edge cases. Instead, each offending command is read as a single HTTP action ("download this URL into this file",
"download this URL and pipe it into `tar`", etc.), and the replacement is produced from scratch using whichever target tool was chosen.
What matters for a Docker image build is the *outcome* — which bytes end up where, which errors fail the build — and that's what the
rewrite preserves. When a command is too elaborate to reinterpret this way (non-HTTP features, shell interpolation, custom scripting),
tally falls back to the AI AutoFix path, which additionally requires an ACP-capable agent configured in the top-level `[ai]` section.
* **Clean up what's left.** After all the invocation rewrites are applied, tally re-reads the resulting Dockerfile and removes anything
that exists only to serve the evicted tool:
* the tool's entry in install commands (dropped from the package list, or the whole install RUN if it only installed that one tool),
* COPY heredocs that write the tool's config file (`.curlrc` / `.wgetrc` / `/etc/wgetrc`),
* `ENV` bindings that point at those config paths (`CURL_HOME`, `WGETRC`, `WGETHOSTS`),
* any annotation comments tally itself added when it inserted the config.
Doing the cleanup after the main rewrite is what lets DL4001 play nicely with peer rules. If `tally/sort-packages` reorders the install
line or `tally/prefer-curl-config` inserts a `.curlrc` heredoc, those edits are already applied by the time cleanup runs, so the cleanup
sees the finished state and does the right thing instead of fighting anyone for the same source range.
The rule refuses to attach a sync fix when the preferred tool already appears in the same `RUN` (for example `curl ... || wget ...`).
On Windows stages the replacement is emitted with a `.exe` suffix (`curl.exe`, `wget.exe`); on Linux stages the bare tool name is used.
### Deterministic rewrite examples
```dockerfile theme={null}
# Before — curl is explicitly installed, wget comes from the base image,
# so wget wins (used-without-install) and the installed curl is rewritten.
RUN apt-get update && apt-get install -y curl
RUN curl -fsSL https://example.com/bootstrap.tgz
RUN wget https://example.com/file.tgz
# After (with --fix --fix-unsafe)
RUN apt-get update && apt-get install -y curl
RUN wget -nv -O- https://example.com/bootstrap.tgz
RUN wget https://example.com/file.tgz
```
```dockerfile theme={null}
# Before — both tools are installed and both are used. curl wins the first-seen
# tie-break, and wget is dropped from the install list so it isn't pulled at all.
RUN apt-get update && apt-get install -y curl wget
RUN curl https://example.com/bootstrap.tgz
RUN wget https://example.com/file.tgz
# After (with --fix --fix-unsafe)
RUN apt-get update && apt-get install -y curl
RUN curl https://example.com/bootstrap.tgz
RUN curl -fL -O https://example.com/file.tgz
```
```dockerfile theme={null}
# Before — neither tool is installed (both come from the base image) and
# curl is used twice vs wget once, so curl wins the count tie-break.
RUN wget https://example.com/file1
RUN curl https://example.com/file2
RUN curl https://example.com/file3
# After (with --fix --fix-unsafe)
RUN curl -fL -O https://example.com/file1
RUN curl https://example.com/file2
RUN curl https://example.com/file3
```
### When the AI AutoFix fallback is used
Examples where deterministic lowering cannot preserve Dockerfile-relevant behavior and the rule falls back to AI AutoFix:
```dockerfile theme={null}
# curl without -L: redirect-following semantics cannot be preserved deterministically.
RUN curl -fsS -o /tmp/file https://example.com/file
```
```dockerfile theme={null}
# curl without -f: fail-on-HTTP-status semantics cannot be preserved deterministically.
RUN curl -sSL https://example.com/app.tgz | tar -xz -C /opt
```
## Configuration
### `fix-preference`
* Type: `string`
* Allowed values: `"auto"`, `"curl"`, `"wget"`
* Default: `"auto"`
Controls which tool auto-fixes converge on, and therefore which of the two offending tools is reported as the violation.
* `"auto"` (default): pick the winner using the usage-based heuristics described in the Auto-fix section above (used-without-install → invocation
count → first seen).
* `"curl"`: always report and rewrite `wget` calls to `curl`, regardless of usage or install state.
* `"wget"`: always report and rewrite `curl` calls to `wget`, regardless of usage or install state.
Explicit preferences are useful when you want a project-wide convention and don't want auto mode's heuristics to decide per-file. An invalid value
falls back to `"auto"`.
```toml theme={null}
[rules.hadolint.DL4001]
fix-preference = "curl"
```
## Reference
* [hadolint/DL4001](https://github.com/hadolint/hadolint/wiki/DL4001)
# hadolint/DL4003
Source: https://tally.wharflab.com/rules/hadolint/DL4003
Multiple `CMD` instructions found.
Multiple `CMD` instructions found.
> **Superseded by
> [`buildkit/MultipleInstructionsDisallowed`](../buildkit/MultipleInstructionsDisallowed)**,
> which covers duplicate `CMD`, `ENTRYPOINT`, and `HEALTHCHECK`
> instructions with auto-fix support.
## Description
If more than one `CMD` instruction is listed, only the last one takes effect.
## Examples
### Problematic code
```dockerfile theme={null}
FROM alpine
CMD echo "first"
CMD echo "second"
```
### Correct code
```dockerfile theme={null}
FROM alpine
CMD echo "second"
```
## Reference
* [hadolint/DL4003](https://github.com/hadolint/hadolint/wiki/DL4003)
# hadolint/DL4004
Source: https://tally.wharflab.com/rules/hadolint/DL4004
Multiple `ENTRYPOINT` instructions found.
Multiple `ENTRYPOINT` instructions found.
> **Superseded by
> [`buildkit/MultipleInstructionsDisallowed`](../buildkit/MultipleInstructionsDisallowed)**,
> which covers duplicate `CMD`, `ENTRYPOINT`, and `HEALTHCHECK`
> instructions with auto-fix support.
## Description
If more than one `ENTRYPOINT` instruction is listed, only the last one takes effect.
## Examples
### Problematic code
```dockerfile theme={null}
FROM alpine
ENTRYPOINT ["echo", "first"]
ENTRYPOINT ["echo", "second"]
```
### Correct code
```dockerfile theme={null}
FROM alpine
ENTRYPOINT ["echo", "second"]
```
## Reference
* [hadolint/DL4004](https://github.com/hadolint/hadolint/wiki/DL4004)
# hadolint/DL4005
Source: https://tally.wharflab.com/rules/hadolint/DL4005
Use `SHELL` to change the default shell.
Use `SHELL` to change the default shell.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Docker provides a `SHELL` instruction which does not require overwriting `/bin/sh` in your container. Instead of using `ln -sf` to redirect `/bin/sh`,
use the `SHELL` instruction to set the desired shell.
## Examples
### Problematic code
```dockerfile theme={null}
RUN apk add --update-cache bash=4.3.42-r3
RUN ln -sfv /bin/bash /bin/sh
```
### Correct code
```dockerfile theme={null}
RUN apk add --update-cache bash=4.3.42-r3
SHELL ["/bin/bash", "-c"]
```
## Auto-fix
Replaces `ln -sf` targeting `/bin/sh` with a `SHELL` instruction. If the `ln` command is part of a larger `RUN`, the `ln` portion is removed and a
`SHELL` instruction is inserted after.
```dockerfile theme={null}
# Before
RUN ln -sf /bin/bash /bin/sh && apk add curl
# After (with --fix)
RUN apk add curl
SHELL ["/bin/bash", "-c"]
```
## Reference
* [hadolint/DL4005](https://github.com/hadolint/hadolint/wiki/DL4005)
# hadolint/DL4006
Source: https://tally.wharflab.com/rules/hadolint/DL4006
Set the `SHELL` option `-o pipefail` before `RUN` with a pipe in.
Set the `SHELL` option `-o pipefail` before `RUN` with a pipe in.
| Property | Value |
| -------- | ------------- |
| Severity | Warning |
| Category | Best Practice |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Some `RUN` commands depend on the ability to pipe the output of one
command into another using the pipe character (`|`). Docker executes
these commands using `/bin/sh -c`, which only evaluates the exit code
of the last operation in the pipe.
Since there are some shells that do not accept the `-o pipefail` option, it is not enough to add `set -o pipefail` inside the `RUN` instruction.
Therefore, we recommend always explicitly adding the `SHELL` instruction before using pipes in `RUN`.
## Examples
### Problematic code
```dockerfile theme={null}
RUN wget -O - https://some.site | wc -l > /number
```
### Correct code
```dockerfile theme={null}
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN wget -O - https://some.site | wc -l > /number
```
or for Alpine/busybox:
```dockerfile theme={null}
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN wget -O - https://some.site | wc -l > /number
```
## Auto-fix
Inserts a `SHELL ["/bin/bash", "-o", "pipefail", "-c"]` instruction before the first `RUN` with a pipe in each stage. Only generated once per stage
since `SHELL` persists.
```dockerfile theme={null}
# Before
RUN cmd1 | cmd2 | cmd3
# After (with --fix)
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN cmd1 | cmd2 | cmd3
```
## Reference
* [hadolint/DL4006](https://github.com/hadolint/hadolint/wiki/DL4006)
# Hadolint rules
Source: https://tally.wharflab.com/rules/hadolint/overview
tally implements Hadolint DL rules natively and covers additional ones via equivalent BuildKit and tally rules.
[Hadolint](https://github.com/hadolint/hadolint) is a widely-used Dockerfile linter. tally implements its rules natively — so you get
Hadolint-compatible checks without installing a separate tool — and adds enhancements like auto-fix and smart suppression.
* DL rules implemented natively by tally
* Additional rules covered by equivalent BuildKit or tally rules
* Full Hadolint rule documentation: [github.com/hadolint/hadolint/wiki](https://github.com/hadolint/hadolint/wiki)
tally supports the `# hadolint ignore=DLxxxx` directive format natively, so existing Hadolint suppressions work without any changes. The `# hadolint shell=powershell` directive is also supported.
***
## Implemented rules
Rules natively implemented by tally. Auto-fixable rules are marked with 🔧.
| Rule | Description | Severity | Notes |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `hadolint/DL3001` | For some bash commands it makes no sense running them in a Docker container like `ssh`, `vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`. | Info | |
| `hadolint/DL3002` | Last user should not be root. | Warning | |
| `hadolint/DL3003` 🔧 | Use WORKDIR to switch to a directory. | Warning | Auto-fixable |
| `hadolint/DL3004` | Do not use `sudo` as it leads to unpredictable behavior. Use a tool like gosu to enforce root. | Error | |
| `hadolint/DL3006` | Always tag the version of an image explicitly. | Warning | |
| `hadolint/DL3007` | Using `latest` is prone to errors if the image will ever update. Pin the version explicitly to a release tag. | Warning | |
| `hadolint/DL3010` | Use ADD for extracting archives into an image. | Info | |
| `hadolint/DL3011` | Valid UNIX ports range from 0 to 65535. | Error | |
| `hadolint/DL3014` 🔧 | Use the `-y` switch. | Warning | Auto-fixable |
| `hadolint/DL3020` 🔧 | Use `COPY` instead of `ADD` for files and folders. | Error | Auto-fixable |
| `hadolint/DL3021` | `COPY` with more than 2 arguments requires the last argument to end with `/`. | Error | |
| `hadolint/DL3022` | `COPY --from` should reference a previously defined `FROM` alias. | Off | Off by default — does not account for `--build-context` sources |
| `hadolint/DL3023` | `COPY --from` cannot reference its own `FROM` alias. | Error | |
| `hadolint/DL3026` | Use only an allowed registry in the FROM image. | Off | Off by default — requires `trusted-registries` configuration |
| `hadolint/DL3027` 🔧 | Do not use `apt` as it is meant to be an end-user tool; use `apt-get` or `apt-cache` instead. | Warning | Auto-fixable |
| `hadolint/DL3030` 🔧 | Use the `-y` switch to avoid manual input: `yum install -y `. | Warning | Auto-fixable |
| `hadolint/DL3034` 🔧 | Non-interactive switch missing from `zypper` command: `zypper install -y`. | Warning | Auto-fixable |
| `hadolint/DL3038` 🔧 | Use the `-y` switch to avoid manual input: `dnf install -y `. | Warning | Auto-fixable |
| `hadolint/DL3043` | `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction. | Error | |
| `hadolint/DL3045` 🔧 | `COPY` to a relative destination without `WORKDIR` set. | Warning | Auto-fixable |
| `hadolint/DL3046` 🔧 | `useradd` without flag `-l` and a high UID will result in an excessively large image. | Warning | Auto-fixable |
| `hadolint/DL3047` 🔧 | `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files. | Info | Auto-fixable |
| `hadolint/DL3057` | `HEALTHCHECK` instruction missing. | Info | Enhanced: smart suppression for serverless/FaaS and registry-backed check with `--slow-checks` |
| `hadolint/DL3061` | Invalid instruction order. Dockerfile must begin with `FROM`, `ARG`, or a comment. | Error | |
| `hadolint/DL4001` | Either use Wget or Curl but not both. | Warning | |
| `hadolint/DL4005` 🔧 | Use `SHELL` to change the default shell. | Warning | Auto-fixable |
| `hadolint/DL4006` 🔧 | Set the `SHELL` option `-o pipefail` before `RUN` with a pipe in it. | Warning | Auto-fixable |
### Enabling off-by-default rules
**DL3022** and **DL3026** are off by default and must be enabled in `.tally.toml`:
```toml theme={null}
# Enable DL3026 with trusted registry enforcement
[rules.hadolint.DL3026]
trusted-registries = ["docker.io", "ghcr.io"]
```
Providing `trusted-registries` automatically enables the rule with `severity = "warning"`. Set `severity` explicitly to use a different level.
***
## Covered by BuildKit and tally rules
These Hadolint rules are superseded by an equivalent BuildKit or tally rule. You do not need to enable both — tally's rule provides the same or better
coverage.
| Hadolint rule | Equivalent tally rule | Notes |
| ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| DL3000 | `buildkit/WorkdirRelativePath` 🔧 | Covers absolute WORKDIR enforcement |
| DL3012 | `buildkit/MultipleInstructionsDisallowed` 🔧 | Covers multiple HEALTHCHECK instructions |
| DL3024 | `buildkit/DuplicateStageName` | Covers duplicate FROM alias names |
| DL3025 | `buildkit/JSONArgsRecommended` 🔧 | Covers JSON notation for CMD and ENTRYPOINT |
| DL3029 | `buildkit/FromPlatformFlagConstDisallowed` + `tally/platform-mismatch` | `buildkit/FromPlatformFlagConstDisallowed` is off by default; `tally/platform-mismatch` provides a stricter registry-backed check |
| DL3044 | `buildkit/UndefinedVar` | Covers referencing an ENV variable in the same ENV statement |
| DL3059 | `tally/prefer-run-heredoc` 🔧 | Suggests heredoc syntax instead of consolidating consecutive RUN instructions |
| DL3063 | `buildkit/ReservedStageName` | Covers reserved `scratch` and `context` FROM aliases |
| DL4000 | `buildkit/MaintainerDeprecated` 🔧 | Covers deprecated MAINTAINER instruction |
| DL4003 | `buildkit/MultipleInstructionsDisallowed` 🔧 | Covers multiple CMD instructions |
| DL4004 | `buildkit/MultipleInstructionsDisallowed` 🔧 | Covers multiple ENTRYPOINT instructions |
***
## Not planned
The following rules are intentionally not implemented. tally promotes BuildKit cache mounts via `tally/prefer-package-cache-mounts` as the modern
alternative to manual cache-cleanup patterns.
| Rule | Description | Reason not planned |
| ------ | ------------------------------------------------------------------ | --------------------------------------------------------------- |
| DL3009 | Delete the apt-get lists after installing something. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3015 | Avoid additional packages by specifying `--no-install-recommends`. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3019 | Use the `--no-cache` switch with `apk add`. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3032 | `yum clean all` missing after yum command. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3036 | `zypper clean` missing after zypper use. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3040 | `dnf clean all` missing after dnf command. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3042 | Avoid cache directory with `pip install --no-cache-dir`. | `tally/prefer-package-cache-mounts` is the recommended approach |
| DL3060 | `yarn cache clean` missing after `yarn install`. | `tally/prefer-package-cache-mounts` is the recommended approach |
Cache-cleanup instructions add build-time overhead and produce smaller layers at the cost of slower rebuilds. BuildKit cache mounts
(`RUN --mount=type=cache`) solve the same problem more efficiently by keeping package caches on the host between builds. See
`tally/prefer-package-cache-mounts` for details.
***
## DL3057: HEALTHCHECK instruction missing (enhanced)
tally's implementation of DL3057 goes beyond Hadolint's static check with smart suppression and an optional registry-backed resolution path.
### Smart suppression (static)
The rule is automatically suppressed when a `HEALTHCHECK` would not be beneficial:
* **Serverless base images** — AWS Lambda (`public.ecr.aws/lambda/*`, `amazon/aws-lambda-*`), Azure Functions (`mcr.microsoft.com/azure-functions/*`),
and OpenFaaS watchdog images. These platforms manage function lifecycle externally.
* **Serverless framework entrypoints** — When the final stage's `CMD` or `ENTRYPOINT` invokes a known FaaS wrapper (e.g. `functions-framework` for
Google Cloud Functions), including the common `exec` prefix pattern.
* **Shell-only containers** — When the final stage's `CMD` or `ENTRYPOINT` is a bare interactive shell (`bash`, `sh`, etc.), the container is not a
long-running service.
### Registry-backed resolution (`--slow-checks`)
`HEALTHCHECK` is inherited from base images at runtime. If a base image defines `HEALTHCHECK CMD ...`, child images inherit it automatically. tally
can resolve this by inspecting the base image registry.
| Scenario | Fast path (static) | With `--slow-checks` |
| ------------------------------------------------------------- | ----------------------------- | ------------------------------------------ |
| No `HEALTHCHECK CMD` in Dockerfile, base has `HEALTHCHECK` | Violation (false positive) | Suppressed (inherited from base) |
| No `HEALTHCHECK CMD` in Dockerfile, base has no `HEALTHCHECK` | Violation | Violation confirmed |
| `HEALTHCHECK NONE` in Dockerfile, base has no `HEALTHCHECK` | Violation (generic "missing") | Specific: "HEALTHCHECK NONE has no effect" |
When `--slow-checks` is off, only the fast static check runs.
***
## Migrating from Hadolint
tally is a drop-in replacement for common Hadolint workflows. Existing inline suppression comments work without modification:
```dockerfile theme={null}
# hadolint ignore=DL3006
FROM ubuntu
# hadolint ignore=DL3004,DL3027
RUN apt install curl
```
Both `ignore=DL3006` and `ignore=hadolint/DL3006` are valid. You can also use tally's own directive format:
```dockerfile theme={null}
# tally ignore=hadolint/DL3006
FROM ubuntu
```
### Shell directive
When using base images with non-POSIX shells (e.g., Windows images with PowerShell), declare the shell to disable POSIX-specific rules:
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# hadolint shell=powershell
RUN Get-Process notepad | Stop-Process
```
Supported non-POSIX shells: `powershell`, `pwsh`, `cmd` / `cmd.exe`.
When a non-POSIX shell is declared, tally automatically disables shell command analysis rules (e.g., DL3004 sudo detection, DL4001 wget/curl
detection) and future ShellCheck-based rules.
Both `# hadolint shell=` and `# tally shell=` formats are supported.
***
## ShellCheck rules (SC rules)
ShellCheck rules analyze shell scripts within `RUN` commands. Implementation has started with native Go reimplementations that use tally's fix and
reporting infrastructure.
| Rule | Description | Status |
| ------------------------------ | -------------------------------------------------------- | -------------- |
| [SC1040](../shellcheck/SC1040) | `<<-` heredoc terminators may only be indented with tabs | Implemented 🔧 |
Additional SC1xxx (syntax/parsing) and SC2xxx (logic/correctness) rules are planned.
# Rules overview
Source: https://tally.wharflab.com/rules/overview
tally rules span four namespaces: tally/, buildkit/, hadolint/, and shellcheck/. Learn how to enable, disable, and suppress rules.
tally integrates rules from multiple sources. Each rule belongs to a namespace that indicates its origin, and all rules share a common configuration
and suppression model.
## Rule namespaces
| Namespace | Source | Description |
| ------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `tally/` | tally custom rules | Security, correctness, performance, style, GPU, PHP, PowerShell, and Windows |
| `buildkit/` | [Docker's BuildKit linter](https://docs.docker.com/reference/build-checks/) | Captured during parsing or reimplemented for static analysis |
| `hadolint/` | [Hadolint](https://github.com/hadolint/hadolint) | Hadolint-compatible rules implemented natively |
| `shellcheck/` | Embedded ShellCheck | Shell script analysis within `RUN` instructions |
## Severity levels
| Severity | Meaning |
| --------- | -------------------------------------------- |
| `error` | Critical issue; blocks CI by default |
| `warning` | Important issue that should be addressed |
| `info` | Informational suggestion |
| `style` | Style preference; auto-fixable in most cases |
| `off` | Rule disabled |
The default fail level is `style`, meaning any violation causes a non-zero exit. Use `--fail-level` to adjust this.
## Auto-fixable rules
Rules marked with 🔧 can be fixed automatically with `tally lint --fix`. Some fixes are classified as suggestions (unsafe) and require
`--fix --fix-unsafe` to apply. Auto-fixable rules cover formatting, style normalization, and many correctness improvements.
## Enabling and disabling rules
### In `.tally.toml`
Use `include` and `exclude` glob patterns to select which rules run:
```toml theme={null}
[rules]
# Enable entire namespaces
include = ["buildkit/*", "tally/*", "hadolint/*"]
# Disable specific rules
exclude = [
"buildkit/MaintainerDeprecated",
"hadolint/DL3008",
]
```
Configure individual rules with `[rules..]`:
```toml theme={null}
[rules.tally.max-lines]
severity = "warning"
max = 100
skip-blank-lines = true
skip-comments = true
[rules.buildkit.StageNameCasing]
severity = "info"
[rules.hadolint.DL3026]
severity = "warning"
trusted-registries = ["docker.io", "gcr.io", "ghcr.io"]
```
Rules that are off by default (such as `hadolint/DL3026`) are automatically enabled with `severity = "warning"` when you provide configuration options for them — no need to set `severity` explicitly unless you want a different level.
### With CLI flags
Use `--select` to enable rules and `--ignore` to disable them:
```bash theme={null}
# Enable only buildkit rules
tally lint --select "buildkit/*" Dockerfile
# Disable a specific rule
tally lint --ignore "buildkit/MaintainerDeprecated" Dockerfile
```
## Inline suppression directives
Suppress specific violations directly in your Dockerfile using comment directives.
### Next-line suppression
```dockerfile theme={null}
# tally ignore=StageNameCasing
FROM alpine AS Build
# tally ignore=DL3006,DL3007
FROM ubuntu:16.04
```
### File-wide suppression
```dockerfile theme={null}
# tally global ignore=max-lines;reason=Generated file, size is expected
FROM alpine
```
### Adding a reason
Use `;reason=` to document why a rule is suppressed. Required when `--require-reason` is set:
```dockerfile theme={null}
# tally ignore=DL3006;reason=Using older base image for compatibility
FROM ubuntu:16.04
```
### Suppress all rules on a line
```dockerfile theme={null}
# tally ignore=all
FROM Ubuntu AS Build
```
### Migration compatibility
tally also accepts directive formats from hadolint and Docker's `check=skip` syntax:
```dockerfile theme={null}
# hadolint ignore=DL3024
FROM alpine AS builder
# check=skip=StageNameCasing
FROM alpine AS Builder
```
Directives work with or without namespace prefixes. Both `ignore=DL3024` and `ignore=hadolint/DL3024` are valid.
### Shell directive for non-POSIX shells
When using a non-POSIX shell (PowerShell, cmd), use the `shell` directive to disable incompatible rules:
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# hadolint shell=powershell
RUN Get-Process notepad | Stop-Process
```
Supported values: `powershell`, `pwsh`, `cmd`, `cmd.exe`.
## Explore rules by category
Secret detection, VEX attestations, secret mounts, privilege rules, and telemetry opt-out.
Stage structure, signal handling, JSON exec-form, identity resolution, curl/wget config, and platform checks.
Multi-stage builds, cache mounts, heredocs, and archive extraction.
Formatting, sorting, indentation, and epilogue ordering — all auto-fixable.
Image metadata key validation, duplicate detection, Buildx overlap checks, base digest checks, and Docker namespace guardrails.
NVIDIA/CUDA-aware rules for build-time queries, driver capabilities, and image size.
Node and JavaScript container rules for native addon build caches.
Composer dependency hygiene and Xdebug detection.
Windows container-specific rules for mounts, signals, and ownership flags.
Docker's official BuildKit linter checks.
Hadolint DL rules implemented natively.
# powershell/PSAlignAssignmentStatement
Source: https://tally.wharflab.com/rules/powershell/PSAlignAssignmentStatement
Align assignment statement
`powershell/PSAlignAssignmentStatement` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Consecutive assignment statements are more readable when they're aligned. Assignments are considered
aligned when their `equals` signs line up vertically.
This rule looks at the key-value pairs in hashtables (including DSC configurations) as well as enum
definitions.
Consider the following example with a hashtable and enum that isn't aligned.
```powershell theme={null}
$hashtable = @{
property = 'value'
anotherProperty = 'another value'
}
enum Enum {
member = 1
anotherMember = 2
}
```
Alignment in this case would look like the following.
```powershell theme={null}
$hashtable = @{
property = 'value'
anotherProperty = 'another value'
}
enum Enum {
member = 1
anotherMember = 2
}
```
The rule ignores any assignments within hashtables and enums which are on the same line as others.
For example, the rule ignores `$h = @{a = 1; b = 2}`.
## Configuration
```powershell theme={null}
Rules = @{
PSAlignAssignmentStatement = @{
Enable = $true
CheckHashtable = $true
AlignHashtableKvpWithInterveningComment = $true
CheckEnum = $true
AlignEnumMemberWithInterveningComment = $true
IncludeValuelessEnumMembers = $true
}
}
```
### Parameters
#### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
#### CheckHashtable: bool (Default value is `$true`)
Enforce alignment of assignment statements in a hashtable and in a DSC Configuration. There is only
one setting for hashtable and DSC configuration because the property value pairs in a DSC
configuration are parsed as key-value pairs of a hashtable.
#### AlignHashtableKvpWithInterveningComment: bool (Default value is `$true`)
Include key-value pairs in the alignment that have an intervening comment - that is to say a comment
between the key name and the equals sign.
Consider the following:
```powershell theme={null}
$hashtable = @{
property = 'value'
anotherProperty <#A Comment#> = 'another value'
anotherDifferentProperty = 'yet another value'
}
```
With this setting disabled, the line with the comment is ignored, and it would be aligned like so:
```powershell theme={null}
$hashtable = @{
property = 'value'
anotherProperty <#A Comment#> = 'another value'
anotherDifferentProperty = 'yet another value'
}
```
With it enabled, the comment line is included in alignment:
```powershell theme={null}
$hashtable = @{
property = 'value'
anotherProperty <#A Comment#> = 'another value'
anotherDifferentProperty = 'yet another value'
}
```
#### CheckEnum: bool (Default value is `$true`)
Enforce alignment of assignment statements of an Enum definition.
#### AlignEnumMemberWithInterveningComment: bool (Default value is `$true`)
Include enum members in the alignment that have an intervening comment - that is to say a comment
between the member name and the equals sign.
Consider the following:
```powershell theme={null}
enum Enum {
member = 1
anotherMember <#A Comment#> = 2
anotherDifferentMember = 3
}
```
With this setting disabled, the line with the comment is ignored, and it would be aligned like so:
```powershell theme={null}
enum Enum {
member = 1
anotherMember <#A Comment#> = 2
anotherDifferentMember = 3
}
```
With it enabled, the comment line is included in alignment:
```powershell theme={null}
enum Enum {
member = 1
anotherMember <#A Comment#> = 2
anotherDifferentMember = 3
}
```
#### IncludeValuelessEnumMembers: bool (Default value is `$true`)
Include enum members in the alignment that don't have an explicitly assigned value. Enum's don't
need to be given a value when they're defined.
Consider the following:
```powershell theme={null}
enum Enum {
member = 1
anotherMember = 2
anotherDifferentMember
}
```
With this setting disabled, the third line, which has no value, isn't considered when choosing where
to align assignments. It would be aligned like so:
```powershell theme={null}
enum Enum {
member = 1
anotherMember = 2
anotherDifferentMember
}
```
With it enabled, the valueless member is included in alignment as if it had a value:
```powershell theme={null}
enum Enum {
member = 1
anotherMember = 2
anotherDifferentMember
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AlignAssignmentStatement](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AlignAssignmentStatement.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidAssignmentToAutomaticVariable
Source: https://tally.wharflab.com/rules/powershell/PSAvoidAssignmentToAutomaticVariable
Changing automatic variables might have undesired side effects
`powershell/PSAvoidAssignmentToAutomaticVariable` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
PowerShell has built-in variables known as automatic variables. Many of them are read-only and
PowerShell throws an error when trying to assign a value on those. Other automatic variables should
only be assigned in certain special cases to achieve a certain effect as a special technique.
To understand more about automatic variables, see `Get-Help about_Automatic_Variables`.
## How
Use variable names in functions or their parameters that do not conflict with automatic variables.
## Examples
### Problematic code
The variable `$Error` is an automatic variable that exists in the global scope and should therefore
never be used as a variable or parameter name.
```powershell theme={null}
function foo($Error){ }
```
```powershell theme={null}
function Get-CustomErrorMessage($ErrorMessage){ $Error = "Error occurred: $ErrorMessage" }
```
### Correct code
```powershell theme={null}
function Get-CustomErrorMessage($ErrorMessage){ $FinalErrorMessage = "Error occurred: $ErrorMessage" }
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidAssignmentToAutomaticVariable](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidAssignmentToAutomaticVariable.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidDefaultValueForMandatoryParameter
Source: https://tally.wharflab.com/rules/powershell/PSAvoidDefaultValueForMandatoryParameter
Avoid Default Value For Mandatory Parameter
`powershell/PSAvoidDefaultValueForMandatoryParameter` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidDefaultValueForMandatoryParameter"]` or by setting
`rules.powershell.PSAvoidDefaultValueForMandatoryParameter.severity = "warning"` in `.tally.toml`.
## Description
Mandatory parameters should not have a default value because there is no scenario where the default
can be used. PowerShell prompts for a value if the parameter value is not specified when calling the
function.
## Examples
### Problematic code
```powershell theme={null}
function Test
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true)]
$Parameter1 = 'default Value'
)
}
```
### Correct code
```powershell theme={null}
function Test
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true)]
$Parameter1
)
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidDefaultValueForMandatoryParameter](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidDefaultValueForMandatoryParameter.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidDefaultValueSwitchParameter
Source: https://tally.wharflab.com/rules/powershell/PSAvoidDefaultValueSwitchParameter
Switch Parameters Should Not Default To True
`powershell/PSAvoidDefaultValueSwitchParameter` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidDefaultValueSwitchParameter"]` or by setting
`rules.powershell.PSAvoidDefaultValueSwitchParameter.severity = "warning"` in `.tally.toml`.
## Description
If your parameter takes only `true` and `false`, define the parameter as type `[Switch]`. PowerShell
treats a switch parameter as `true` when it's used with a command. If the parameter isn't included
with the command, PowerShell considers the parameter to be false. Don't define `[Boolean]`
parameters.
You shouldn't define a switch parameter with a default value of `$true` because this isn't the
expected behavior of a switch parameter.
## How
Change the default value of the switch parameter to be `$false` or don't provide a default value.
Write the logic of the script to assume that the switch parameter default value is `$false` or not
provided.
## Examples
### Problematic code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[String]
$Param1,
[switch]
$Switch=$True
)
...
}
```
### Correct code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[String]
$Param1,
[switch]
$Switch
)
begin {
# Ensure that the $Switch is set to false if not provided
if (-not $PSBoundParameters.ContainsKey('Switch')) {
$Switch = $false
}
}
...
}
```
## More information
* [Strongly Encouraged Development Guidelines][01]
[01]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/strongly-encouraged-development-guidelines#parameters-that-take-true-and-false
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidDefaultValueSwitchParameter](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidDefaultValueSwitchParameter.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidExclaimOperator
Source: https://tally.wharflab.com/rules/powershell/PSAvoidExclaimOperator
Avoid exclaim operator
`powershell/PSAvoidExclaimOperator` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Avoid using the negation operator (`!`). Use `-not` for improved readability.
Upstream PSScriptAnalyzer does not enable this rule by default. Configure `rules.powershell.PSAvoidExclaimOperator.Enable = true` to forward the
upstream setting to PSScriptAnalyzer.
## How to Fix
Replace the `!` negation operator with the PowerShell `-not` operator.
## Examples
### Problematic code
```powershell theme={null}
$MyVar = !$true
```
### Correct code
```powershell theme={null}
$MyVar = -not $true
```
## Configuration
Upstream PSScriptAnalyzer supports the following rule setting. tally forwards matching `rules.powershell.PSAvoidExclaimOperator` options to
PSScriptAnalyzer.
```powershell theme={null}
Rules = @{
PSAvoidExclaimOperator = @{
Enable = $true
}
}
```
### Parameters
* `Enable`: **bool** (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidExclaimOperator](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidExclaimOperator.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidGlobalAliases
Source: https://tally.wharflab.com/rules/powershell/PSAvoidGlobalAliases
Avoid global aliases.
`powershell/PSAvoidGlobalAliases` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidGlobalAliases"]` or by setting
`rules.powershell.PSAvoidGlobalAliases.severity = "warning"` in `.tally.toml`.
## Description
Globally scoped aliases override existing aliases in the session with matching names, which can cause
difficult-to-debug issues for consumers of modules and scripts.
To understand more about scoping, see `Get-Help about_Scopes`.
**NOTE** This rule is not available in PowerShell version 3 or 4 because it uses the
`StaticParameterBinder.BindCommand` API.
## How
Use other scope modifiers for new aliases.
## Examples
### Problematic code
```powershell theme={null}
New-Alias -Name Name -Value Value -Scope Global
```
### Correct code
```powershell theme={null}
New-Alias -Name Name1 -Value Value
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidGlobalAliases](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalAliases.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidGlobalFunctions
Source: https://tally.wharflab.com/rules/powershell/PSAvoidGlobalFunctions
Avoid global functions and aliases
`powershell/PSAvoidGlobalFunctions` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidGlobalFunctions"]` or by setting
`rules.powershell.PSAvoidGlobalFunctions.severity = "warning"` in `.tally.toml`.
## Description
Globally scoped functions override existing functions within the sessions with matching names. This
name collision can cause difficult-to-debug issues for consumers of modules.
To understand more about scoping, see `Get-Help about_Scopes`.
## How
Use other scope modifiers for functions.
## Examples
### Problematic code
```powershell theme={null}
function global:functionName {}
```
### Correct code
```powershell theme={null}
function functionName {}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidGlobalFunctions](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalFunctions.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidGlobalVars
Source: https://tally.wharflab.com/rules/powershell/PSAvoidGlobalVars
No Global Variables
`powershell/PSAvoidGlobalVars` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidGlobalVars"]` or by setting
`rules.powershell.PSAvoidGlobalVars.severity = "warning"` in `.tally.toml`.
## Description
A variable is a unit of memory in which values are stored. PowerShell controls access to variables,
functions, aliases, and drives through a mechanism known as scoping. Variables and functions that
are present when PowerShell starts have been created in the global scope.
Globally scoped variables include:
* Automatic variables
* Preference variables
* Variables, aliases, and functions that are in your PowerShell profiles
To understand more about scoping, see `Get-Help about_Scopes`.
## How
Use other scope modifiers for variables.
## Examples
### Problematic code
```powershell theme={null}
$Global:var1 = $null
function Test-NotGlobal ($var)
{
$a = $var + $var1
}
```
### Correct code
```powershell theme={null}
$var1 = $null
function Test-NotGlobal ($var1, $var2)
{
$a = $var1 + $var2
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidGlobalVars](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidGlobalVars.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidInvokingEmptyMembers
Source: https://tally.wharflab.com/rules/powershell/PSAvoidInvokingEmptyMembers
Avoid Invoking Empty Members
`powershell/PSAvoidInvokingEmptyMembers` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Invoking non-constant members can cause potential bugs. Please double-check the syntax to make sure
that invoked members are constants.
## How
Provide the requested members for a given type or class.
## Examples
### Problematic code
```powershell theme={null}
$MyString = 'abc'
$MyString.('len'+'gth')
```
### Correct code
```powershell theme={null}
$MyString = 'abc'
$MyString.('length')
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidInvokingEmptyMembers](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidInvokingEmptyMembers.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidLongLines
Source: https://tally.wharflab.com/rules/powershell/PSAvoidLongLines
Avoid long lines
`powershell/PSAvoidLongLines` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
The length of lines, including leading spaces (indentation), should be less than the configured
number of characters. The default length is 120 characters.
This rule isn't enabled by default. The user needs to enable it through settings.
## Configuration
```powershell theme={null}
Rules = @{
PSAvoidLongLines = @{
Enable = $true
MaximumLineLength = 120
}
}
```
## Parameters
### `Enable`: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
### `MaximumLineLength`: int (Default value is 120)
Optional parameter to override the default maximum line length.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidLongLines](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidLongLines.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidMultipleTypeAttributes
Source: https://tally.wharflab.com/rules/powershell/PSAvoidMultipleTypeAttributes
Avoid multiple type specifiers on parameters.
`powershell/PSAvoidMultipleTypeAttributes` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Parameters should not have more than one type specifier. Multiple type specifiers on parameters
can cause runtime errors.
## How
Ensure each parameter has only 1 type specifier.
## Examples
### Problematic code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[switch]
[int]
$Switch
)
}
```
### Correct code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[switch]
$Switch
)
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidMultipleTypeAttributes](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidMultipleTypeAttributes.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidNullOrEmptyHelpMessageAttribute
Source: https://tally.wharflab.com/rules/powershell/PSAvoidNullOrEmptyHelpMessageAttribute
Avoid using null or empty HelpMessage parameter attribute.
`powershell/PSAvoidNullOrEmptyHelpMessageAttribute` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidNullOrEmptyHelpMessageAttribute"]` or by setting
`rules.powershell.PSAvoidNullOrEmptyHelpMessageAttribute.severity = "warning"` in `.tally.toml`.
## Description
The value of the `HelpMessage` attribute should not be an empty string or a null value as this
causes PowerShell's interpreter to throw an error when executing the function or cmdlet.
## How
Specify a value for the `HelpMessage` attribute.
## Examples
### Problematic code
```powershell theme={null}
Function BadFuncEmptyHelpMessageEmpty
{
Param(
[Parameter(HelpMessage='')]
[String]
$Param
)
$Param
}
Function BadFuncEmptyHelpMessageNull
{
Param(
[Parameter(HelpMessage=$null)]
[String]
$Param
)
$Param
}
Function BadFuncEmptyHelpMessageNoAssignment
{
Param(
[Parameter(HelpMessage)]
[String]
$Param
)
$Param
}
```
### Correct code
```powershell theme={null}
Function GoodFuncHelpMessage
{
Param(
[Parameter(HelpMessage='This is helpful')]
[String]
$Param
)
$Param
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidNullOrEmptyHelpMessageAttribute](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidNullOrEmptyHelpMessageAttribute.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidOverwritingBuiltInCmdlets
Source: https://tally.wharflab.com/rules/powershell/PSAvoidOverwritingBuiltInCmdlets
Avoid overwriting built in cmdlets
`powershell/PSAvoidOverwritingBuiltInCmdlets` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule flags cmdlets that are available in a given edition/version of PowerShell on a given
operating system which are overwritten by a function declaration. It works by comparing function
declarations against a set of allowlists that ship with PSScriptAnalyzer. These allowlist files are
used by other PSScriptAnalyzer rules. More information can be found in the documentation for the
[UseCompatibleCmdlets][01] rule.
## Configuration
To enable the rule to check if your script is compatible on PowerShell Core on Windows, put the
following in your settings file.
tally forwards matching `rules.powershell.PSAvoidOverwritingBuiltInCmdlets` options to PSScriptAnalyzer.
```powershell theme={null}
@{
'Rules' = @{
'PSAvoidOverwritingBuiltInCmdlets' = @{
'PowerShellVersion' = @('core-6.1.0-windows')
}
}
}
```
### Parameters
#### PowerShellVersion
The parameter `PowerShellVersion` is a list of allowlists that ship with PSScriptAnalyzer.
In upstream PSScriptAnalyzer, the default value for `PowerShellVersion` is `core-6.1.0-windows` if PowerShell 6 or later is installed, and
`desktop-5.1.14393.206-windows` if it is not. tally itself requires `pwsh` 7.x as the analyzer host and does not support `powershell.exe` or Windows
PowerShell 5.1 as the host process.
Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major
and minor versions of PowerShell are supplied. One can also create a custom settings file as well
with the [New-CommandDataFile.ps1][02] script and use it by placing the created `JSON` into the
`Settings` folder of the `PSScriptAnalyzer` module installation folder, then the `PowerShellVersion`
parameter is just its filename (that can also be changed if desired). Note that the `core-6.0.2-*`
files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0 reached end of life.
[01]: /rules/powershell/PSUseCompatibleCmdlets
[02]: https://github.com/PowerShell/PSScriptAnalyzer/blob/main/Utils/New-CommandDataFile.ps1
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidOverwritingBuiltInCmdlets](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidOverwritingBuiltInCmdlets.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidReservedWordsAsFunctionNames
Source: https://tally.wharflab.com/rules/powershell/PSAvoidReservedWordsAsFunctionNames
Avoid reserved words as function names
`powershell/PSAvoidReservedWordsAsFunctionNames` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Avoid using reserved words as function names. Using reserved words as function names can cause
errors or unexpected behavior in scripts.
## How to Fix
Avoid using any of the reserved words as function names. Choose a different name that's not a
reserved word.
See [about\_Reserved\_Words][01] for a list of reserved words in PowerShell.
## Examples
### Problematic code
```powershell theme={null}
# Function is a reserved word
function function {
Write-Host "Hello, World!"
}
```
### Correct code
```powershell theme={null}
# myFunction is not a reserved word
function myFunction {
Write-Host "Hello, World!"
}
```
[01]: /powershell/module/microsoft.powershell.core/about/about_reserved_words
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidReservedWordsAsFunctionNames](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidReservedWordsAsFunctionNames.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidSemicolonsAsLineTerminators
Source: https://tally.wharflab.com/rules/powershell/PSAvoidSemicolonsAsLineTerminators
Avoid semicolons as line terminators
`powershell/PSAvoidSemicolonsAsLineTerminators` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Lines should not end with a semicolon.
This rule is not enabled by default. The user needs to enable it through settings.
## Examples
### Problematic code
```powershell theme={null}
Install-Module -Name PSScriptAnalyzer; $a = 1 + $b;
```
```powershell theme={null}
Install-Module -Name PSScriptAnalyzer;
$a = 1 + $b
```
### Correct code
```powershell theme={null}
Install-Module -Name PSScriptAnalyzer; $a = 1 + $b
```
```powershell theme={null}
Install-Module -Name PSScriptAnalyzer
$a = 1 + $b
```
## Configuration
```powershell theme={null}
Rules = @{
PSAvoidSemicolonsAsLineTerminators = @{
Enable = $true
}
}
```
### Parameters
#### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidSemicolonsAsLineTerminators](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidSemicolonsAsLineTerminators.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidShouldContinueWithoutForce
Source: https://tally.wharflab.com/rules/powershell/PSAvoidShouldContinueWithoutForce
Avoid Using ShouldContinue Without Boolean Force Parameter
`powershell/PSAvoidShouldContinueWithoutForce` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSAvoidShouldContinueWithoutForce"]` or by setting
`rules.powershell.PSAvoidShouldContinueWithoutForce.severity = "warning"` in `.tally.toml`.
## Description
Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it.
You can get more details by running `Get-Help about_Functions_CmdletBindingAttribute` and
`Get-Help about_Functions_Advanced_Methods` command in PowerShell.
## How
Call the `ShouldContinue` method in advanced functions when `ShouldProcess` method returns `$true`.
## Examples
### Problematic code
```powershell theme={null}
Function Test-ShouldContinue
{
[CmdletBinding(SupportsShouldProcess=$true)]
Param
(
$MyString = 'blah'
)
if ($PsCmdlet.ShouldContinue('ShouldContinue Query', 'ShouldContinue Caption'))
{
...
}
}
```
### Correct code
```powershell theme={null}
Function Test-ShouldContinue
{
[CmdletBinding(SupportsShouldProcess=$true)]
Param
(
$MyString = 'blah',
[Switch]$Force
)
if ($Force -or $PsCmdlet.ShouldContinue('ShouldContinue Query', 'ShouldContinue Caption'))
{
...
}
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidShouldContinueWithoutForce](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidShouldContinueWithoutForce.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidTrailingWhitespace
Source: https://tally.wharflab.com/rules/powershell/PSAvoidTrailingWhitespace
Avoid trailing whitespace
`powershell/PSAvoidTrailingWhitespace` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Lines should not end with whitespace characters. This can cause problems with the line-continuation
backtick, and also clutters up future commits to source control.
This diagnostic can coexist with [`tally/no-trailing-spaces`](/rules/tally/no-trailing-spaces):
PSScriptAnalyzer reports trailing whitespace in PowerShell snippets, while tally checks the
Dockerfile as a whole. If both rules suggest the same edit, tally applies it once.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidTrailingWhitespace](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidTrailingWhitespace.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingAllowUnencryptedAuthentication
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingAllowUnencryptedAuthentication
Avoid sending credentials and secrets over unencrypted connections
`powershell/PSAvoidUsingAllowUnencryptedAuthentication` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Avoid using the **AllowUnencryptedAuthentication** parameter of `Invoke-WebRequest` and
`Invoke-RestMethod`. When using this parameter, the cmdlets send credentials and secrets over
unencrypted connections. This should be avoided except for compatibility with legacy systems.
For more details, see [Invoke-RestMethod](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/invoke-restmethod).
## How
Avoid using the **AllowUnencryptedAuthentication** parameter.
## Example 1
### Problematic code
```powershell theme={null}
Invoke-WebRequest foo -AllowUnencryptedAuthentication
```
### Correct code
```powershell theme={null}
Invoke-WebRequest foo
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingAllowUnencryptedAuthentication](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingAllowUnencryptedAuthentication.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingBrokenHashAlgorithms
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingBrokenHashAlgorithms
Avoid using broken hash algorithms
`powershell/PSAvoidUsingBrokenHashAlgorithms` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Avoid using the broken algorithms MD5 or SHA-1.
## How
Replace broken algorithms with secure alternatives. MD5 and SHA-1 should be replaced with SHA256,
SHA384, SHA512, or other safer algorithms when possible, with MD5 and SHA-1 only being utilized by
necessity for backwards compatibility.
## Example 1
### Problematic code
```powershell theme={null}
Get-FileHash foo.txt -Algorithm MD5
```
### Correct code
```powershell theme={null}
Get-FileHash foo.txt -Algorithm SHA256
```
## Example 2
### Problematic code
```powershell theme={null}
Get-FileHash foo.txt -Algorithm SHA1
```
### Correct code
```powershell theme={null}
Get-FileHash foo.txt
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingBrokenHashAlgorithms](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingBrokenHashAlgorithms.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingCmdletAliases
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingCmdletAliases
Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix.
`powershell/PSAvoidUsingCmdletAliases` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function,
script, file, or executable file. You can use the alias instead of the command name in any
PowerShell commands.
There are also implicit aliases. When PowerShell cannot find the cmdlet name, it will try to append
`Get-` to the command as a last resort. Therefore using the command `verb` will execute `Get-Verb`.
Every PowerShell author learns the actual command names, but different authors learn and use
different aliases. Aliases can make code difficult to read, understand and impact availability.
Using the full command name makes it easier to maintain your scripts in the future.
Using the full command names also allows for syntax highlighting in sites and applications like
GitHub and Visual Studio Code.
## How to Fix
Use the full cmdlet name and not an alias.
## Alias Allowlist
To prevent `PSScriptAnalyzer` from flagging your preferred aliases, create an allowlist of the
aliases in your settings file and point `PSScriptAnalyzer` to use the settings file. For example, to
disable `PSScriptAnalyzer` from flagging `cd`, which is an alias of `Set-Location`, set the settings
file content to the following.
```powershell theme={null}
# PSScriptAnalyzerSettings.psd1
@{
'Rules' = @{
'PSAvoidUsingCmdletAliases' = @{
'allowlist' = @('cd')
}
}
}
```
## Examples
### Problematic code
```powershell theme={null}
gps | Where-Object {$_.WorkingSet -gt 20000000}
```
### Correct code
```powershell theme={null}
Get-Process | Where-Object {$_.WorkingSet -gt 20000000}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingCmdletAliases](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingCmdletAliases.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingComputerNameHardcoded
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingComputerNameHardcoded
Avoid Using ComputerName Hardcoded
`powershell/PSAvoidUsingComputerNameHardcoded` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
The names of computers should never be hard coded as this will expose sensitive information. The
`ComputerName` parameter should never have a hard coded value.
## How
Remove hard coded computer names.
## Example 1
### Problematic code
```powershell theme={null}
Function Invoke-MyRemoteCommand ()
{
Invoke-Command -Port 343 -ComputerName hardcoderemotehostname
}
```
### Correct code
```powershell theme={null}
Function Invoke-MyCommand ($ComputerName)
{
Invoke-Command -Port 343 -ComputerName $ComputerName
}
```
## Example 2
### Problematic code
```powershell theme={null}
Function Invoke-MyLocalCommand ()
{
Invoke-Command -Port 343 -ComputerName hardcodelocalhostname
}
```
### Correct code
```powershell theme={null}
Function Invoke-MyLocalCommand ()
{
Invoke-Command -Port 343 -ComputerName $env:COMPUTERNAME
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingComputerNameHardcoded](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingComputerNameHardcoded.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingConvertToSecureStringWithPlainText
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingConvertToSecureStringWithPlainText
Avoid Using SecureString With Plain Text
`powershell/PSAvoidUsingConvertToSecureStringWithPlainText` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
The use of the `AsPlainText` parameter with the `ConvertTo-SecureString` command can expose secure
information.
## How
Use a standard encrypted variable to perform any SecureString conversions.
## Recommendations
If you do need an ability to retrieve the password from somewhere without prompting the user,
consider using the
[SecretStore](https://www.powershellgallery.com/packages/Microsoft.PowerShell.SecretStore)
module from the PowerShell Gallery.
## Examples
### Problematic code
```powershell theme={null}
$UserInput = Read-Host 'Please enter your secure code'
$EncryptedInput = ConvertTo-SecureString -String $UserInput -AsPlainText -Force
```
### Correct code
```powershell theme={null}
$SecureUserInput = Read-Host 'Please enter your secure code' -AsSecureString
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingConvertToSecureStringWithPlainText](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingConvertToSecureStringWithPlainText.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingDeprecatedManifestFields
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingDeprecatedManifestFields
Avoid Using Deprecated Manifest Fields
`powershell/PSAvoidUsingDeprecatedManifestFields` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Dockerfiles do not ship PowerShell module
manifests (`.psd1` files) — this rule targets module-authoring artifacts that are out of
scope for container builds. Re-enable it with
`include = ["powershell/PSAvoidUsingDeprecatedManifestFields"]` or by setting
`rules.powershell.PSAvoidUsingDeprecatedManifestFields.severity = "warning"` in `.tally.toml`.
## Description
In PowerShell 5.0, a number of fields in module manifest files (`.psd1`) have been changed.
The field `ModuleToProcess` has been replaced with the `RootModule` field.
## How
Replace `ModuleToProcess` with `RootModule` in the module manifest.
## Examples
### Problematic code
```powershell theme={null}
ModuleToProcess ='psscriptanalyzer'
ModuleVersion = '1.0'
```
### Correct code
```powershell theme={null}
RootModule ='psscriptanalyzer'
ModuleVersion = '1.0'
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingDeprecatedManifestFields](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingDeprecatedManifestFields.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingDoubleQuotesForConstantString
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingDoubleQuotesForConstantString
Avoid using double quotes if the string is constant.
`powershell/PSAvoidUsingDoubleQuotesForConstantString` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Single quotes should be used when the value of a string is constant. A constant string doesn't
contain variables or expressions intended to insert values into the string, such as
`"$PID-$(hostname)"`).
This makes the intent clearer that the string is a constant and makes it easier to use some special
characters such as `$` within that string expression without needing to escape them.
There are exceptions to that when double quoted strings are more readable. For example, when the
string value itself must contain a single quote or other special characters, such as newline
(``"`n"``), are already being escaped. The rule does not warn in these cases.
## Examples
### Problematic code
```powershell theme={null}
$constantValue = "I Love PowerShell"
```
### Correct code
```powershell theme={null}
$constantValue = 'I Love PowerShell'
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingDoubleQuotesForConstantString](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingDoubleQuotesForConstantString.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingEmptyCatchBlock
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingEmptyCatchBlock
Avoid Using Empty Catch Block
`powershell/PSAvoidUsingEmptyCatchBlock` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Empty catch blocks are considered a poor design choice because any errors occurring in a
`try` block cannot be handled.
## How
Use `Write-Error` or `throw` statements within the catch block.
## Examples
### Problematic code
```powershell theme={null}
try
{
1/0
}
catch [DivideByZeroException]
{
}
```
### Correct code
```powershell theme={null}
try
{
1/0
}
catch [DivideByZeroException]
{
Write-Error 'DivideByZeroException'
}
try
{
1/0
}
catch [DivideByZeroException]
{
throw 'DivideByZeroException'
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingEmptyCatchBlock](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingEmptyCatchBlock.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingInvokeExpression
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingInvokeExpression
Avoid Using Invoke-Expression
`powershell/PSAvoidUsingInvokeExpression` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Care must be taken when using the `Invoke-Expression` command. The `Invoke-Expression` executes the
specified string and returns the results.
Code injection into your application or script can occur if the expression passed as a string
includes any data provided from the user.
## How
Remove the use of `Invoke-Expression`.
## Examples
### Problematic code
```powershell theme={null}
Invoke-Expression 'Get-Process'
```
### Correct code
```powershell theme={null}
Get-Process
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingInvokeExpression](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingInvokeExpression.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingPlainTextForPassword
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingPlainTextForPassword
Avoid Using Plain Text For Password Parameter
`powershell/PSAvoidUsingPlainTextForPassword` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Password parameters that take in plaintext will expose passwords and compromise the security of your
system. Passwords should be stored in the **SecureString** type.
The following parameters are considered password parameters (this is not case sensitive):
* Password
* Pass
* Passwords
* Passphrase
* Passphrases
* PasswordParam
If a parameter is defined with a name in the above list, it should be declared with type
**SecureString**.
## How
Change the type to **SecureString**.
## Examples
### Problematic code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[string]
$Password
)
...
}
```
### Correct code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[SecureString]
$Password
)
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingPlainTextForPassword](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingPlainTextForPassword.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingPositionalParameters
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingPositionalParameters
Avoid Using Positional Parameters
`powershell/PSAvoidUsingPositionalParameters` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because there is no
downstream pipeline or caller in a one-shot container build, so the script-reusability
concerns this rule targets do not apply. Re-enable it with
`include = ["powershell/PSAvoidUsingPositionalParameters"]` or by setting
`rules.powershell.PSAvoidUsingPositionalParameters.severity = "warning"` in `.tally.toml`.
## Description
Using positional parameters reduces the readability of code and can introduce errors. It is possible
that a future version of the cmdlet could change in a way that would break existing scripts if calls
to the cmdlet rely on the position of the parameters.
For simple cmdlets with only a few positional parameters, the risk is much smaller. To prevent this
rule from being too noisy, this rule gets only triggered when there are 3 or more parameters
supplied. A simple example where the risk of using positional parameters is negligible, is
`Test-Path $Path`.
## Configuration
```powershell theme={null}
Rules = @{
PSAvoidUsingPositionalParameters = @{
CommandAllowList = 'Join-Path', 'MyCmdletOrScript'
Enable = $true
}
}
```
### Parameters
#### CommandAllowList: string\[] (Default value is `@()`)
Commands or scripts to be excluded from this rule.
#### Enable: bool (Default value is `$true`)
Enable or disable the rule during ScriptAnalyzer invocation.
## How
Use full parameter names when calling commands.
## Examples
### Problematic code
```powershell theme={null}
Get-Command ChildItem Microsoft.PowerShell.Management
```
### Correct code
```powershell theme={null}
Get-Command -Noun ChildItem -Module Microsoft.PowerShell.Management
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingPositionalParameters](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingPositionalParameters.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingUsernameAndPasswordParams
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingUsernameAndPasswordParams
Avoid Using Username and Password Parameters
`powershell/PSAvoidUsingUsernameAndPasswordParams` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
To standardize command parameters, credentials should be accepted as objects of type
**PSCredential**. Functions should not make use of username or password parameters.
## How
Change the parameter to type **PSCredential**.
## Examples
### Problematic code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[String]
$Username,
[SecureString]
$Password
)
...
}
```
### Correct code
```powershell theme={null}
function Test-Script
{
[CmdletBinding()]
Param
(
[PSCredential]
$Credential
)
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingUsernameAndPasswordParams](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingUsernameAndPasswordParams.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingWMICmdlet
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingWMICmdlet
Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance
`powershell/PSAvoidUsingWMICmdlet` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
As of PowerShell 3.0, the CIM cmdlets should be used over the WMI cmdlets.
The following cmdlets should not be used:
* `Get-WmiObject`
* `Remove-WmiObject`
* `Invoke-WmiMethod`
* `Register-WmiEvent`
* `Set-WmiInstance`
Use the following cmdlets instead:
* `Get-CimInstance`
* `Remove-CimInstance`
* `Invoke-CimMethod`
* `Register-CimIndicationEvent`
* `Set-CimInstance`
The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model
(CIM) standard, allowing for the management of Windows and non-Windows operating systems.
## How
Change to the equivalent CIM-based cmdlet.
* `Get-WmiObject` -> `Get-CimInstance`
* `Remove-WmiObject` -> `Remove-CimInstance`
* `Invoke-WmiMethod` -> `Invoke-CimMethod`
* `Register-WmiEvent` -> `Register-CimIndicationEvent`
* `Set-WmiInstance` -> `Set-CimInstance`
## Examples
### Problematic code
```powershell theme={null}
Get-WmiObject -Query 'Select * from Win32_Process where name LIKE "myprocess%"' | Remove-WmiObject
Invoke-WmiMethod -Class Win32_Process -Name 'Create' -ArgumentList @{ CommandLine = 'notepad.exe' }
```
### Correct code
```powershell theme={null}
Get-CimInstance -Query 'Select * from Win32_Process where name LIKE "myprocess%"' | Remove-CIMInstance
Invoke-CimMethod -ClassName Win32_Process -MethodName 'Create' -Arguments @{ CommandLine = 'notepad.exe' }
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingWMICmdlet](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingWMICmdlet.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSAvoidUsingWriteHost
Source: https://tally.wharflab.com/rules/powershell/PSAvoidUsingWriteHost
Avoid Using Write-Host
`powershell/PSAvoidUsingWriteHost` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because there is no downstream
pipeline or interactive host inside a one-shot container build — the script-reusability concerns
this rule targets do not apply. Re-enable it with
`include = ["powershell/PSAvoidUsingWriteHost"]` or by setting
`rules.powershell.PSAvoidUsingWriteHost.severity = "warning"` in `.tally.toml`.
## Description
The primary purpose of the `Write-Host` cmdlet is to produce display-only output in the host. For
example: printing colored text or prompting the user for input when combined with `Read-Host`.
`Write-Host` uses the `ToString()` method to write the output. The particular result depends on the
program that's hosting PowerShell. The output from `Write-Host` isn't sent to the pipeline. To
output data to the pipeline, use `Write-Output` or implicit output.
The use of `Write-Host` in a function is discouraged unless the function uses the `Show` verb. The
`Show` verb explicitly means *display information to the user*. This rule doesn't apply to functions
with the `Show` verb.
## How
Replace `Write-Host` with `Write-Output` or `Write-Verbose` depending on whether the intention is
logging or returning one or more objects.
## Examples
### Problematic code
```powershell theme={null}
function Get-MeaningOfLife
{
Write-Host 'Computing the answer to the ultimate question of life, the universe and everything'
Write-Host 42
}
```
### Correct code
Use `Write-Verbose` for informational messages. The user can decide whether to see the message by
providing the **Verbose** parameter.
```powershell theme={null}
function Get-MeaningOfLife
{
[CmdletBinding()]Param() # makes it possible to support Verbose output
Write-Verbose 'Computing the answer to the ultimate question of life, the universe and everything'
Write-Output 42
}
function Show-Something
{
Write-Host 'show something on screen'
}
```
## More information
[Write-Host](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/write-host)
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[AvoidUsingWriteHost](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/AvoidUsingWriteHost.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCDscExamplesPresent
Source: https://tally.wharflab.com/rules/powershell/PSDSCDscExamplesPresent
DSC examples are present
`powershell/PSDSCDscExamplesPresent` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCDscExamplesPresent"]` or by setting
`rules.powershell.PSDSCDscExamplesPresent.severity = "warning"` in `.tally.toml`.
## Description
Checks that DSC examples for given resource are present.
## How
To fix a violation of this rule, please make sure `Examples` directory is present:
* For non-class based resources it should exist at the same folder level as `DSCResources` folder.
* For class based resources it should be present at the same folder level as resource `.psm1` file.
The `Examples` folder should contain a sample configuration for given resource. The filename should
contain the resource's name.
## Examples
### Non-class based resource
Let's assume we have non-class based resource with a following file structure:
* xAzure
* DSCResources
* MSFT\_xAzureSubscription
* MSFT\_xAzureSubscription.psm1
* MSFT\_xAzureSubscription.schema.mof
In this case, to fix this warning, we should add examples in a following way:
* xAzure
* DSCResources
* MSFT\_xAzureSubscription
* MSFT\_xAzureSubscription.psm1
* MSFT\_xAzureSubscription.schema.mof
* Examples
* MSFT\_xAzureSubscription\_AddSubscriptionExample.ps1
* MSFT\_xAzureSubscription\_RemoveSubscriptionExample.ps1
### Class based resource
Let's assume we have class based resource with a following file structure:
* MyDscResource
* MyDscResource.psm1
* MyDscResource.psd1
In this case, to fix this warning, we should add examples in a following way:
* MyDscResource
* MyDscResource.psm1
* MyDscResource.psd1
* Examples
* MyDscResource\_Example1.ps1
* MyDscResource\_Example2.ps1
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCDscExamplesPresent](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCDscExamplesPresent.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCDscTestsPresent
Source: https://tally.wharflab.com/rules/powershell/PSDSCDscTestsPresent
Dsc tests are present
`powershell/PSDSCDscTestsPresent` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCDscTestsPresent"]` or by setting
`rules.powershell.PSDSCDscTestsPresent.severity = "warning"` in `.tally.toml`.
## Description
Checks that DSC tests for given resource are present.
## How
To fix a violation of this rule, please make sure `Tests` directory is present:
* For non-class-based resources, it should exist at the same folder level as the `DSCResources` folder.
* For class-based resources, it should be present at the same folder level as the resource `.psm1` file.
The `Tests` folder should contain a test script for a given resource. The filename should contain the
resource's name.
## Examples
### Non-class-based resource
Let's assume we have a non-class-based resource with the following file structure:
* xAzure
* DSCResources
* MSFT\_xAzureSubscription
* MSFT\_xAzureSubscription.psm1
* MSFT\_xAzureSubscription.schema.mof
In this case, to fix this warning, we should add tests in a following way:
* xAzure
* DSCResources
* MSFT\_xAzureSubscription
* MSFT\_xAzureSubscription.psm1
* MSFT\_xAzureSubscription.schema.mof
* Tests
* MSFT\_xAzureSubscription\_Tests.ps1
### Class-based resource
Let's assume we have a class-based resource with the following file structure:
* MyDscResource
* MyDscResource.psm1
* MyDscResource.psd1
In this case, to fix this warning, we should add tests in a following way:
* MyDscResource
* MyDscResource.psm1
* MyDscResource.psd1
* Tests
* MyDscResource\_Tests.ps1
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCDscTestsPresent](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCDscTestsPresent.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCReturnCorrectTypesForDSCFunctions
Source: https://tally.wharflab.com/rules/powershell/PSDSCReturnCorrectTypesForDSCFunctions
Return Correct Types For DSC Functions
`powershell/PSDSCReturnCorrectTypesForDSCFunctions` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCReturnCorrectTypesForDSCFunctions"]` or by setting
`rules.powershell.PSDSCReturnCorrectTypesForDSCFunctions.severity = "warning"` in `.tally.toml`.
## Description
The functions in DSC resources have specific return objects.
For non-class based resources:
* `Set-TargetResource` must not return any value.
* `Test-TargetResource` must return a boolean.
* `Get-TargetResource` must return a hash table.
For class based resources:
* `Set` must not return any value.
* `Test` must return a boolean.
* `Get` must return an instance of the DSC class.
## How
Ensure that each function returns the correct type.
## Example 1
### Problematic code
```powershell theme={null}
function Get-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Set-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Test-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
```
### Correct code
```powershell theme={null}
function Get-TargetResource
{
[OutputType([Hashtable])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Set-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Test-TargetResource
{
[OutputType([System.Boolean])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
```
## Example 2
### Problematic code
```powershell theme={null}
[DscResource()]
class MyDSCResource
{
[DscProperty(Key)]
[string] $Name
[String] Get()
{
...
}
[String] Set()
{
...
}
[bool] Test()
{
...
}
}
```
### Correct code
```powershell theme={null}
[DscResource()]
class MyDSCResource
{
[DscProperty(Key)]
[string] $Name
[MyDSCResource] Get()
{
...
}
[void] Set()
{
...
}
[bool] Test()
{
...
}
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCReturnCorrectTypesForDSCFunctions](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCReturnCorrectTypesForDSCFunctions.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCStandardDSCFunctionsInResource
Source: https://tally.wharflab.com/rules/powershell/PSDSCStandardDSCFunctionsInResource
Use Standard Get/Set/Test TargetResource functions in DSC Resource
`powershell/PSDSCStandardDSCFunctionsInResource` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCStandardDSCFunctionsInResource"]` or by setting
`rules.powershell.PSDSCStandardDSCFunctionsInResource.severity = "warning"` in `.tally.toml`.
## Description
All DSC resources are required to implement the correct functions.
For non-class-based resources:
* `Set-TargetResource`
* `Test-TargetResource`
* `Get-TargetResource`
For class-based resources:
* `Set`
* `Test`
* `Get`
## How
Add the missing functions to the resource.
## Example 1
### Problematic code
```powershell theme={null}
function Get-TargetResource
{
[OutputType([Hashtable])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Set-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
```
### Correct code
```powershell theme={null}
function Get-TargetResource
{
[OutputType([Hashtable])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Set-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Test-TargetResource
{
[OutputType([System.Boolean])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
```
## Example 2
### Problematic code
```powershell theme={null}
[DscResource()]
class MyDSCResource
{
[DscProperty(Key)]
[string] $Name
[void] Set()
{
...
}
[bool] Test()
{
...
}
}
```
### Correct code
```powershell theme={null}
[DscResource()]
class MyDSCResource
{
[DscProperty(Key)]
[string] $Name
[MyDSCResource] Get()
{
...
}
[void] Set()
{
...
}
[bool] Test()
{
...
}
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCStandardDSCFunctionsInResource](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCStandardDSCFunctionsInResource.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCUseIdenticalMandatoryParametersForDSC
Source: https://tally.wharflab.com/rules/powershell/PSDSCUseIdenticalMandatoryParametersForDSC
Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions
`powershell/PSDSCUseIdenticalMandatoryParametersForDSC` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCUseIdenticalMandatoryParametersForDSC"]` or by setting
`rules.powershell.PSDSCUseIdenticalMandatoryParametersForDSC.severity = "warning"` in `.tally.toml`.
## Description
For script-based DSC resources, if a property is declared with attributes `Key` or `Required` in a
MOF file, then it should be present as a mandatory parameter in the corresponding
`Get-TargetResource`, `Set-TargetResource` and `Test-TargetResource` functions.
## How
Make sure all the properties with `Key` or `Required` attributes have equivalent mandatory
parameters in the `Get/Set/Test` functions.
## Examples
Consider the following `mof` file.
```powershell theme={null}
class WaitForAny : OMI_BaseResource
{
[key, Description("Name of Resource on remote machine")]
string Name;
[required, Description("List of remote machines")]
string NodeName[];
};
```
### Problematic code
```powershell theme={null}
function Get-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message
)
}
function Set-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
}
function Test-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
}
```
### Correct code
```powershell theme={null}
function Get-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
}
function Set-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
}
function Test-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Name
)
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCUseIdenticalMandatoryParametersForDSC](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCUseIdenticalMandatoryParametersForDSC.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCUseIdenticalParametersForDSC
Source: https://tally.wharflab.com/rules/powershell/PSDSCUseIdenticalParametersForDSC
Use Identical Parameters For DSC Test and Set Functions
`powershell/PSDSCUseIdenticalParametersForDSC` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCUseIdenticalParametersForDSC"]` or by setting
`rules.powershell.PSDSCUseIdenticalParametersForDSC.severity = "warning"` in `.tally.toml`.
## Description
The `Get-TargetResource`, `Test-TargetResource` and `Set-TargetResource` functions of DSC Resource
must have the same parameters.
## How
Correct the parameters for the functions in DSC resource.
## Examples
### Problematic code
```powershell theme={null}
function Get-TargetResource
{
[OutputType([Hashtable])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name,
[String]
$TargetResource
)
...
}
function Set-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
function Test-TargetResource
{
[OutputType([System.Boolean])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name
)
...
}
```
### Correct code
```powershell theme={null}
function Get-TargetResource
{
[OutputType([Hashtable])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name,
[String]
$TargetResource
)
...
}
function Set-TargetResource
{
param
(
[parameter(Mandatory = $true)]
[String]
$Name,
[String]
$TargetResource
)
...
}
function Test-TargetResource
{
[OutputType([System.Boolean])]
param
(
[parameter(Mandatory = $true)]
[String]
$Name,
[String]
$TargetResource
)
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCUseIdenticalParametersForDSC](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCUseIdenticalParametersForDSC.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSDSCUseVerboseMessageInDSCResource
Source: https://tally.wharflab.com/rules/powershell/PSDSCUseVerboseMessageInDSCResource
Use verbose message in DSC resource
`powershell/PSDSCUseVerboseMessageInDSCResource` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Desired State Configuration resources are out
of scope for Dockerfile `RUN` analysis. Re-enable it with
`include = ["powershell/PSDSCUseVerboseMessageInDSCResource"]` or by setting
`rules.powershell.PSDSCUseVerboseMessageInDSCResource.severity = "warning"` in `.tally.toml`.
## Description
Best practice recommends that additional user information is provided within commands, functions and
scripts using `Write-Verbose`.
## How
Make use of the `Write-Verbose` command.
## Examples
### Problematic code
```powershell theme={null}
Function Test-Function
{
[CmdletBinding()]
Param()
...
}
```
### Correct code
```powershell theme={null}
Function Test-Function
{
[CmdletBinding()]
Param()
Write-Verbose 'Verbose output'
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[DSCUseVerboseMessageInDSCResource](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/DSCUseVerboseMessageInDSCResource.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSMisleadingBacktick
Source: https://tally.wharflab.com/rules/powershell/PSMisleadingBacktick
Misleading Backtick
`powershell/PSMisleadingBacktick` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Checks that lines don't end with a backtick followed by whitespace.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[MisleadingBacktick](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/MisleadingBacktick.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSMissingModuleManifestField
Source: https://tally.wharflab.com/rules/powershell/PSMissingModuleManifestField
Module Manifest Fields
`powershell/PSMissingModuleManifestField` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Dockerfiles do not ship PowerShell module
manifests (`.psd1` files) — this rule targets module-authoring artifacts that are out of
scope for container builds. Re-enable it with
`include = ["powershell/PSMissingModuleManifestField"]` or by setting
`rules.powershell.PSMissingModuleManifestField.severity = "warning"` in `.tally.toml`.
## Description
A module manifest is a `.psd1` file that contains a hash table. The keys and values in the hash
table describe the contents and attributes of the module, define the prerequisites, and determine
how the components are processed.
Module manifests must contain the following keys (and a corresponding value) to be considered valid:
* `ModuleVersion`
All other keys are optional. The order of the entries is not important.
## How
Please consider adding the missing fields to the manifest.
## Examples
### Problematic code
```powershell theme={null}
@{
Author = 'PowerShell Author'
NestedModules = @('.\mymodule.psm1')
FunctionsToExport = '*'
CmdletsToExport = '*'
VariablesToExport = '*'
}
```
### Correct code
```powershell theme={null}
@{
ModuleVersion = '1.0'
Author = 'PowerShell Author'
NestedModules = @('.\mymodule.psm1')
FunctionsToExport = '*'
CmdletsToExport = '*'
VariablesToExport = '*'
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[MissingModuleManifestField](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/MissingModuleManifestField.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSPlaceCloseBrace
Source: https://tally.wharflab.com/rules/powershell/PSPlaceCloseBrace
Place close braces
`powershell/PSPlaceCloseBrace` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Close brace placement should follow a consistent style. It should be on a new line by itself and
should not be followed by an empty line.
**Note**: This rule is not enabled by default. The user needs to enable it through settings.
## Configuration
```powershell theme={null}
Rules = @{
PSPlaceCloseBrace = @{
Enable = $true
NoEmptyLineBefore = $false
IgnoreOneLineBlock = $true
NewLineAfter = $true
}
}
```
### Parameters
#### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
#### NoEmptyLineBefore: bool (Default value is `$false`)
Create violation if there is an empty line before a close brace.
#### IgnoreOneLineBlock: bool (Default value is `$true`)
Indicates if closed brace pairs in a one line block should be ignored or not. For example,
`$x = if ($true) { 'blah' } else { 'blah blah' }`, if the property is set to true then the rule
doesn't fire a violation.
#### NewLineAfter: bool (Default value is `$true`)
Indicates if a new line should follow a close brace. If set to true a close brace should be followed
by a new line.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[PlaceCloseBrace](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/PlaceCloseBrace.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSPlaceOpenBrace
Source: https://tally.wharflab.com/rules/powershell/PSPlaceOpenBrace
Place open braces consistently
`powershell/PSPlaceOpenBrace` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Open brace placement should follow a consistent style. It can either follow K\&R style (on same line)
or the Allman style (not on same line).
**Note**: This rule is not enabled by default. The user needs to enable it through settings.
## Configuration
```powershell theme={null}
Rules = @{
PSPlaceOpenBrace = @{
Enable = $true
OnSameLine = $true
NewLineAfter = $true
IgnoreOneLineBlock = $true
}
}
```
### Parameters
#### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
#### OnSameLine: bool (Default value is `$true`)
Enforce open brace to be on the same line as that of its preceding keyword.
#### NewLineAfter: bool (Default value is `$true`)
Enforce a new line character after an open brace. The default value is true.
#### IgnoreOneLineBlock: bool (Default value is `$true`)
Indicates if open braces in a one line block should be ignored or not. For example,
`$x = if ($true) { 'blah' } else { 'blah blah' }`, if the property is set to true then the rule
doesn't fire a violation.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[PlaceOpenBrace](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/PlaceOpenBrace.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSPossibleIncorrectComparisonWithNull
Source: https://tally.wharflab.com/rules/powershell/PSPossibleIncorrectComparisonWithNull
Null Comparison
`powershell/PSPossibleIncorrectComparisonWithNull` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
To ensure that PowerShell performs comparisons correctly, the `$null` element should be on the left
side of the operator.
There are multiple reasons why this occurs:
* `$null` is a scalar value. When the value on the left side of an operator is a scalar, comparison
operators return a **Boolean** value. When the value is a collection, the comparison operators
return any matching values or an empty array if there are no matches in the collection.
* PowerShell performs type casting on the right-hand operand, resulting in incorrect comparisons
when `$null` is cast to other scalar types.
The only way to reliably check if a value is `$null` is to place `$null` on the left side of the
operator so that a scalar comparison is performed.
## How
Move `$null` to the left side of the comparison.
## Examples
### Problematic code
```powershell theme={null}
function Test-CompareWithNull
{
if ($DebugPreference -eq $null)
{
}
}
```
### Correct code
```powershell theme={null}
function Test-CompareWithNull
{
if ($null -eq $DebugPreference)
{
}
}
```
## Try it Yourself
```powershell theme={null}
# This example returns 'false' because the comparison does not return any objects from the array
if (@() -eq $null) { 'true' } else { 'false' }
# This example returns 'true' because the array is empty
if ($null -ne @()) { 'true' } else { 'false' }
```
This is how the comparison operator works by-design. But, as demonstrated, this can lead
to non-intuitive behavior, especially when the intent is a simple test for null.
The following example demonstrates the designed behavior of the comparison operator when the
left-hand side is a collection. Each element in the collection is compared to the right-hand side
value. When true, that element of the collection is returned.
```powershell theme={null}
PS> 1,2,3,1,2 -eq $null
PS> 1,2,3,1,2 -eq 1
1
1
PS> (1,2,3,1,2 -eq $null).count
0
PS> (1,2,$null,3,$null,1,2 -eq $null).count
2
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[PossibleIncorrectComparisonWithNull](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/PossibleIncorrectComparisonWithNull.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSPossibleIncorrectUsageOfAssignmentOperator
Source: https://tally.wharflab.com/rules/powershell/PSPossibleIncorrectUsageOfAssignmentOperator
Equal sign is not an assignment operator. Did you mean the equality operator \'-eq\'?
`powershell/PSPossibleIncorrectUsageOfAssignmentOperator` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
In many programming languages, the equality operator is denoted as `==` or `=`, but `PowerShell`
uses `-eq`. Therefore, it can easily happen that the wrong operator is used unintentionally. This
rule catches a few special cases where the likelihood of that is quite high.
The rule looks for usages of `==` and `=` operators inside `if`, `else if`, `while` and `do-while`
statements but it does not warn if any kind of command or expression is used at the right hand side
as this is probably by design.
## Examples
### Problematic code
```powershell theme={null}
if ($a = $b)
{
...
}
```
```powershell theme={null}
if ($a == $b)
{
}
```
### Correct code
```powershell theme={null}
if ($a -eq $b) # Compare $a with $b
{
...
}
```
```powershell theme={null}
if ($a = Get-Something) # Only execute action if command returns something and assign result to variable
{
Do-SomethingWith $a
}
```
## Implicit suppression using Clang style
There are some rare cases where assignment of variable inside an `if` statement is by design.
Instead of suppressing the rule, one can also signal that assignment was intentional by wrapping the
expression in extra parenthesis. An exception for this is when `$null` is used on the LHS because
there is no use case for this.
```powershell theme={null}
if (($shortVariableName = $SuperLongVariableName['SpecialItem']['AnotherItem']))
{
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[PossibleIncorrectUsageOfAssignmentOperator](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/PossibleIncorrectUsageOfAssignmentOperator.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSPossibleIncorrectUsageOfRedirectionOperator
Source: https://tally.wharflab.com/rules/powershell/PSPossibleIncorrectUsageOfRedirectionOperator
\'>\' is not a comparison operator. Use \'-gt\' (greater than) or \'-ge\' (greater or equal).
`powershell/PSPossibleIncorrectUsageOfRedirectionOperator` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
In many programming languages, the comparison operator for 'greater than' is `>` but `PowerShell`
uses `-gt` for it and `-ge` (greater or equal) for `>=`. Therefore, it can easily happen that the
wrong operator is used unintentionally. This rule catches a few special cases where the likelihood
of that is quite high.
The rule looks for usages of `>` or `>=` operators inside `if`, `elseif`, `while` and `do-while`
statements because this is likely going to be unintentional usage.
## Examples
### Problematic code
```powershell theme={null}
if ($a > $b)
{
...
}
```
### Correct code
```powershell theme={null}
if ($a -gt $b)
{
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[PossibleIncorrectUsageOfRedirectionOperator](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/PossibleIncorrectUsageOfRedirectionOperator.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSProvideCommentHelp
Source: https://tally.wharflab.com/rules/powershell/PSProvideCommentHelp
Basic Comment Help
`powershell/PSProvideCommentHelp` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Info |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSProvideCommentHelp"]` or by setting
`rules.powershell.PSProvideCommentHelp.severity = "warning"` in `.tally.toml`.
## Description
Comment based help should be provided for all PowerShell commands. This test only checks for the
presence of comment based help and not on the validity or format.
For assistance on comment based help, use the command `Get-Help about_comment_based_help` or the
following articles:
* [Writing Comment-based Help][01]
* [Writing Help for PowerShell Cmdlets][02]
* [Create XML-based help using PlatyPS][03]
## Configuration
```powershell theme={null}
Rules = @{
PSProvideCommentHelp = @{
Enable = $true
ExportedOnly = $false
BlockComment = $true
VSCodeSnippetCorrection = $false
Placement = 'before'
}
}
```
### Parameters
* `Enable`: **bool** (Default value is `$true`)
Enable or disable the rule during ScriptAnalyzer invocation.
* `ExportedOnly`: **bool** (Default value is `$true`)
If enabled, throw violation only on functions/cmdlets that are exported using the
`Export-ModuleMember` cmdlet.
* `BlockComment`: **bool** (Default value is `$true`)
If enabled, returns comment help in block comment style (`<#...#>`). Otherwise returns
comment help in line comment style where each comment line starts with `#`.
* `VSCodeSnippetCorrection`: **bool** (Default value is `$false`)
If enabled, returns comment help in vscode snippet format.
* `Placement`: **string** (Default value is `before`)
Represents the position of comment help with respect to the function definition.
Possible values are:
* `before`: means the help is placed before the function definition
* `begin` means the help is placed at the beginning of the function definition body
* `end` means the help is places the end of the function definition body
If any invalid value is given, the property defaults to `before`.
## Examples
### Problematic code
```powershell theme={null}
function Get-File
{
[CmdletBinding()]
Param
(
...
)
}
```
### Correct code
```powershell theme={null}
<#
.Synopsis
Short description
.DESCRIPTION
Long description
.EXAMPLE
Example of how to use this cmdlet
.EXAMPLE
Another example of how to use this cmdlet
.INPUTS
Inputs to this cmdlet (if any)
.OUTPUTS
Output from this cmdlet (if any)
.NOTES
General notes
.COMPONENT
The component this cmdlet belongs to
.ROLE
The role this cmdlet belongs to
.FUNCTIONALITY
The functionality that best describes this cmdlet
#>
function Get-File
{
[CmdletBinding()]
Param
(
...
)
}
```
[01]: /powershell/scripting/developer/help/writing-comment-based-help-topics
[02]: /powershell/scripting/developer/help/writing-help-for-windows-powershell-cmdlets
[03]: /powershell/utility-modules/platyps/create-help-using-platyps
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[ProvideCommentHelp](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/ProvideCommentHelp.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSReservedCmdletChar
Source: https://tally.wharflab.com/rules/powershell/PSReservedCmdletChar
Reserved Cmdlet Chars
`powershell/PSReservedCmdletChar` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSReservedCmdletChar"]` or by setting
`rules.powershell.PSReservedCmdletChar.severity = "warning"` in `.tally.toml`.
## Description
You cannot use following reserved characters in a function or cmdlet name as these can cause parsing
or runtime errors.
Reserved Characters include: ``#,(){}[]&/\\$^;:\"'<>|?@`*%+=~``
## How
Remove reserved characters from names.
## Examples
### Problematic code
```powershell theme={null}
function MyFunction[1]
{...}
```
### Correct code
```powershell theme={null}
function MyFunction
{...}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[ReservedCmdletChar](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/ReservedCmdletChar.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSReservedParams
Source: https://tally.wharflab.com/rules/powershell/PSReservedParams
Reserved Parameters
`powershell/PSReservedParams` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Error |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSReservedParams"]` or by setting
`rules.powershell.PSReservedParams.severity = "warning"` in `.tally.toml`.
## Description
You can't redefine [common parameters][01] in an advanced function. Using the `CmdletBinding` or
`Parameter` attributes creates an advanced function. The common parameters are are automatically
available in advanced functions, so you can't redefine them.
## How
Change the name of the parameter.
## Examples
### Problematic code
```powershell theme={null}
function Test
{
[CmdletBinding()]
Param
(
$ErrorVariable,
$Parameter2
)
}
```
### Correct code
```powershell theme={null}
function Test
{
[CmdletBinding()]
Param
(
$Err,
$Parameter2
)
}
```
[01]: /powershell/module/microsoft.powershell.core/about/about_commonparameters
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[ReservedParams](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/ReservedParams.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSReviewUnusedParameter
Source: https://tally.wharflab.com/rules/powershell/PSReviewUnusedParameter
ReviewUnusedParameter
`powershell/PSReviewUnusedParameter` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSReviewUnusedParameter"]` or by setting
`rules.powershell.PSReviewUnusedParameter.severity = "warning"` in `.tally.toml`.
## Description
This rule identifies parameters declared in a script, scriptblock, or function scope that have not
been used in that scope.
## Configuration settings
By default, this rule doesn't consider child scopes other than scriptblocks provided to
`Where-Object` or `ForEach-Object`. The `CommandsToTraverse` setting is a string array that allows you
to add additional commands that accept scriptblocks that this rule should examine.
```powershell theme={null}
@{
Rules = @{
PSReviewUnusedParameter = @{
CommandsToTraverse = @(
'Invoke-PSFProtectedCommand'
)
}
}
}
```
## How
Consider removing the unused parameter.
## Examples
### Problematic code
```powershell theme={null}
function Test-Parameter
{
Param (
$Parameter1,
# this parameter is never called in the function
$Parameter2
)
Get-Something $Parameter1
}
```
### Correct code
```powershell theme={null}
function Test-Parameter
{
Param (
$Parameter1,
# now this parameter is being called in the same scope
$Parameter2
)
Get-Something $Parameter1 $Parameter2
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[ReviewUnusedParameter](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/ReviewUnusedParameter.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSShouldProcess
Source: https://tally.wharflab.com/rules/powershell/PSShouldProcess
Should Process
`powershell/PSShouldProcess` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSShouldProcess"]` or by setting
`rules.powershell.PSShouldProcess.severity = "warning"` in `.tally.toml`.
## Description
If a cmdlet declares the `SupportsShouldProcess` attribute, then it should also call
`ShouldProcess`. A violation is any function which either declares `SupportsShouldProcess` attribute
but makes no calls to `ShouldProcess` or it calls `ShouldProcess` but does not declare
`SupportsShouldProcess`.
For more information, see the following articles:
* [about\_Functions\_Advanced\_Methods][01]
* [about\_Functions\_CmdletBindingAttribute][02]
* [Everything you wanted to know about ShouldProcess][03]
## How
To fix a violation of this rule, please call `ShouldProcess` method when a cmdlet declares
`SupportsShouldProcess` attribute. Or please add `SupportsShouldProcess` attribute argument when
calling `ShouldProcess`.
## Examples
### Problematic code
```powershell theme={null}
function Set-File
{
[CmdletBinding(SupportsShouldProcess=$true)]
Param
(
# Path to file
[Parameter(Mandatory=$true)]
$Path
)
'String' | Out-File -FilePath $Path
}
```
### Correct code
```powershell theme={null}
function Set-File
{
[CmdletBinding(SupportsShouldProcess=$true)]
Param
(
# Path to file
[Parameter(Mandatory=$true)]
$Path,
[Parameter(Mandatory=$true)]
[string]$Content
)
if ($PSCmdlet.ShouldProcess($Path, ("Setting content to '{0}'" -f $Content)))
{
$Content | Out-File -FilePath $Path
}
else
{
# Code that should be processed if doing a WhatIf operation
# Must NOT change anything outside of the function / script
}
}
```
[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_advanced_methods
[02]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute
[03]: https://learn.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-shouldprocess
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[ShouldProcess](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/ShouldProcess.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseApprovedVerbs
Source: https://tally.wharflab.com/rules/powershell/PSUseApprovedVerbs
Cmdlet Verbs
`powershell/PSUseApprovedVerbs` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSUseApprovedVerbs"]` or by setting
`rules.powershell.PSUseApprovedVerbs.severity = "warning"` in `.tally.toml`.
## Description
All cmdlets must use approved verbs.
Approved verbs can be found by running the command `Get-Verb`.
For more information about approved verbs, see [Approved Verbs for PowerShell Commands][01]. Some
unapproved verbs are documented on the approved verbs page and point to approved alternatives. Try
searching for the verb you used to find its approved form. For example, searching for `Read`,
`Open`, or `Search` leads you to `Get`.
## How
Change the verb in the cmdlet's name to an approved verb.
## Examples
### Problematic code
```powershell theme={null}
function Change-Item
{
...
}
```
### Correct code
```powershell theme={null}
function Update-Item
{
...
}
```
[01]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseApprovedVerbs](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseApprovedVerbs.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseBOMForUnicodeEncodedFile
Source: https://tally.wharflab.com/rules/powershell/PSUseBOMForUnicodeEncodedFile
Use BOM encoding for non-ASCII files
`powershell/PSUseBOMForUnicodeEncodedFile` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Dockerfile `RUN` bodies are inline strings, not
files on disk with encoding metadata. Re-enable it with
`include = ["powershell/PSUseBOMForUnicodeEncodedFile"]` or by setting
`rules.powershell.PSUseBOMForUnicodeEncodedFile.severity = "warning"` in `.tally.toml`.
## Description
For a file encoded with a format other than ASCII, ensure Byte Order Mark (BOM) is present to ensure
that any application consuming this file can interpret it correctly.
You can use this rule to test any arbitrary text file, but the intent is to ensure that PowerShell
scripts are saved with a BOM when using a Unicode encoding.
## How
For PowerShell commands that write to files, ensure that you set the encoding parameter to a value
that produces a BOM. In PowerShell 7 and higher, the following values of the **Encoding** parameter
produce a BOM:
* `bigendianunicode`
* `bigendianutf32`
* `oem`
* `unicode`
* `utf32`
* `utf8BOM`
When you create a script file using a text editor, ensure that the editor is configured to save the
file with a BOM. Consult the documentation for your text editor for instructions on how to save
files with a BOM.
## Further reading
For more information, see the following articles:
* [about\_Character\_Encoding](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_character_encoding)
* [Set-Content](https://learn.microsoft.com/powershell/module/microsoft.powershell.management/set-content)
* [Understanding file encoding in VS Code and PowerShell](https://learn.microsoft.com/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding)
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseBOMForUnicodeEncodedFile](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseBOMForUnicodeEncodedFile.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseCmdletCorrectly
Source: https://tally.wharflab.com/rules/powershell/PSUseCmdletCorrectly
Use Cmdlet Correctly
`powershell/PSUseCmdletCorrectly` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Whenever we call a command, care should be taken that it is invoked with the correct syntax and
parameters.
## How
Specify all mandatory parameters when calling commands.
## Examples
### Problematic code
```powershell theme={null}
Function Set-TodaysDate ()
{
Set-Date
...
}
```
### Correct code
```powershell theme={null}
Function Set-TodaysDate ()
{
$date = Get-Date
Set-Date -Date $date
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseCmdletCorrectly](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseCmdletCorrectly.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseCompatibleCmdlets
Source: https://tally.wharflab.com/rules/powershell/PSUseCompatibleCmdlets
Use compatible cmdlets
`powershell/PSUseCompatibleCmdlets` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule flags cmdlets that aren't available in a given Edition and Version of PowerShell on a
given Operating System. It works by comparing a cmdlet against a set of allowlists which ship with
PSScriptAnalyzer. They can be found at `/path/to/PSScriptAnalyzerModule/Settings`. These files use
the `--.json` form, where `` can be either `Core` or `Desktop`,
`` can be either `Windows`, `Linux` or `macOS`, and `` is the PowerShell version. To
enable the rule to check if your script is compatible on PowerShell Core on Windows, put the
following in your settings file:
tally forwards matching `rules.powershell.PSUseCompatibleCmdlets` options, such as `compatibility`, to PSScriptAnalyzer.
```powershell theme={null}
@{
'Rules' = @{
'PSUseCompatibleCmdlets' = @{
'compatibility' = @('core-6.1.0-windows')
}
}
}
```
The parameter `compatibility` is a list that contains any of the following:
* desktop-2.0-windows
* desktop-3.0-windows
* desktop-4.0-windows (taken from Windows Server 2012R2)
* desktop-5.1.14393.206-windows
* core-6.1.0-windows (taken from Windows 10 - 1803)
* core-6.1.0-linux (taken from Ubuntu 18.04)
* core-6.1.0-linux-arm (taken from Raspbian)
* core-6.1.0-macos
The `desktop-*` values are upstream PSScriptAnalyzer compatibility targets, not tally analyzer host support. tally runs the analyzer through
PowerShell 7 (`pwsh`); Windows PowerShell 5.1 (`powershell.exe`) is out of scope as a sidecar host.
Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major
and minor versions of PowerShell are supplied. You can also create a custom settings file with the
[New-CommandDataFile.ps1][01] script. Place the created `.json` file in the `Settings` folder of the
`PSScriptAnalyzer` module folder. Then the `compatibility` parameter value is just the filename.
Note that the `core-6.0.2-*` files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0
reached its end of life.
[01]: https://github.com/PowerShell/PSScriptAnalyzer/blob/main/Utils/New-CommandDataFile.ps1
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseCompatibleCmdlets](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseCompatibleCmdlets.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseCompatibleCommands
Source: https://tally.wharflab.com/rules/powershell/PSUseCompatibleCommands
Use compatible commands
`powershell/PSUseCompatibleCommands` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule identifies commands that are not available on a targeted PowerShell platform.
This page describes upstream PSScriptAnalyzer compatibility profiles. tally runs the analyzer through PowerShell 7 (`pwsh`); Windows PowerShell 5.1
(`powershell.exe`) is out of scope as a sidecar host.
A PowerShell platform is identified by a name in the following format:
```text theme={null}
``_``_``_``_``_``_``
```
Where:
* ``: The name of the operating system PowerShell is running on.
On Windows, this includes the SKU number.
On Linux, this is the name of the distribution.
* ``: The machine architecture the operating system is running on (this is usually `x64`).
* ``: The self-reported version of the operating system (on Linux, this is the
distribution version).
* ``: The PowerShell version (from `$PSVersionTable.PSVersion`).
* ``: The machine architecture of the PowerShell process.
* ``: The reported version of the .NET runtime PowerShell is running on (from
`System.Environment.Version`).
* ``: The .NET runtime flavor PowerShell is running on (currently `framework` or
`core`).
For example:
* `win-4_x64_10.0.18312.0_5.1.18312.1000_x64_4.0.30319.42000_framework` is PowerShell 5.1 running on
Windows 10 Enterprise (build 18312) for x64.
* `win-4_x64_10.0.18312.0_6.1.2_x64_4.0.30319.42000_core` is PowerShell 6.1.2 running on the same
operating system.
* `ubuntu_x64_18.04_6.2.0_x64_4.0.30319.42000_core` is PowerShell 6.2.0 running on Ubuntu 18.04.
Some platforms come bundled with PSScriptAnalyzer as JSON files, named in this way for targeting in
your configuration.
Platforms bundled by default are:
| PowerShell Version | Operating System | ID |
| :----------------: | ---------------------- | --------------------------------------------------------------------- |
| 3.0 | Windows Server 2012 | `win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework` |
| 4.0 | Windows Server 2012 R2 | `win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework` |
| 5.1 | Windows Server 2016 | `win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework` |
| 5.1 | Windows Server 2019 | `win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` |
| 5.1 | Windows 10 Pro | `win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` |
| 6.2 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_4.0.30319.42000_core` |
| 6.2 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_6.2.4_x64_4.0.30319.42000_core` |
| 6.2 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` |
| 6.2 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_6.2.4_x64_4.0.30319.42000_core` |
| 7.0 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_7.0.0_x64_3.1.2_core` |
| 7.0 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_7.0.0_x64_3.1.2_core` |
| 7.0 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core` |
| 7.0 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_7.0.0_x64_3.1.2_core` |
Other profiles can be found in the [GitHub repo][02].
You can also generate your own platform profile using the [PSCompatibilityCollector module][01].
The compatibility profile settings takes a list of platforms to target under `TargetProfiles`. A
platform can be specified as:
* A platform name (like `ubuntu_x64_18.04_6.1.1_x64_4.0.30319.42000_core`), which will have `.json`
added to the end and is searched for in the default profile directory.
* A filename (like `my_custom_platform.json`), which will be searched for in the default profile
directory.
* An absolute path to a file (like `D:\PowerShellProfiles\TargetMachine.json`).
The default profile directory is under the PSScriptAnalyzer module at
`$PSScriptRoot/compatibility_profiles` (where `$PSScriptRoot` here refers to the directory
containing `PSScriptAnalyzer.psd1`).
The compatibility analysis compares a command used to both a target profile and a 'union' profile
(containing all commands available in *any* profile in the profile dir). If a command is not present
in the union profile, it is assumed to be locally created and ignored. Otherwise, if a command is
present in the union profile but not present in a target, it is deemed to be incompatible with that
target.
## Configuration settings
| Configuration key | Meaning | Accepted values | Mandatory | Example |
| ----------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `Enable` | Activates the rule | bool (`$true`/`$false`) | No (default: `$false`) | `$true` |
| `TargetProfiles` | The list of PowerShell profiles to target | string\[]: absolute paths to profile files or names of profiles in the profile directory | No (default: `@()`) | `@('ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core', 'win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework')` |
| `ProfileDirPath` | The location to search for profiles by name and use for union profile generation | string: absolute path to new profile dir | No (defaults to `compatibility_profiles` directory in PSScriptAnalyzer module) | `C:\Users\me\Documents\pssaCompatProfiles` |
| `IgnoreCommands` | Commands to ignore compatibility of in scripts | string\[]: names of commands to ignore | No (default: `@()`) | `@('Get-ChildItem','Import-Module')` |
An example configuration might look like:
```powershell theme={null}
@{
Rules = @{
PSUseCompatibleCommands = @{
Enable = $true
TargetProfiles = @(
'ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core'
'win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
'MyProfile'
'another_custom_profile_in_the_profiles_directory.json'
'D:\My Profiles\profile1.json'
)
# You can specify commands to not check like this, which also will ignore its parameters:
IgnoreCommands = @(
'Install-Module'
)
}
}
}
```
## Suppression
Command compatibility diagnostics can be suppressed with an attribute on the `param` block of a
scriptblock as with other rules.
```powershell theme={null}
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', '')]
```
The rule can also be suppressed only for particular commands:
```powershell theme={null}
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands',
'Start-Service')]
```
And also suppressed only for parameters:
```powershell theme={null}
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands',
'Import-Module/FullyQualifiedName')]
```
[01]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector
[02]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector/optional_profiles
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseCompatibleCommands](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseCompatibleCommands.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseCompatibleSyntax
Source: https://tally.wharflab.com/rules/powershell/PSUseCompatibleSyntax
Use compatible syntax
`powershell/PSUseCompatibleSyntax` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule identifies syntax elements that are incompatible with targeted PowerShell versions.
It cannot identify syntax elements incompatible with PowerShell 3 or 4 when run from those
PowerShell versions because they aren't able to parse the incompatible syntaxes.
```powershell theme={null}
@{
Rules = @{
PSUseCompatibleSyntax = @{
Enable = $true
TargetVersions = @(
'6.0',
'5.1',
'4.0'
)
}
}
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseCompatibleSyntax](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseCompatibleSyntax.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseCompatibleTypes
Source: https://tally.wharflab.com/rules/powershell/PSUseCompatibleTypes
Use compatible types
`powershell/PSUseCompatibleTypes` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule identifies types that are not available (loaded by default) in targeted PowerShell
platforms.
A PowerShell platform is identified by a name in the following format:
```text theme={null}
``_``_``_``_``_``_``
```
Where:
* ``: The name of the operating system PowerShell is running on.
On Windows, this includes the SKU number.
On Linux, this is the name of the distribution.
* ``: The machine architecture the operating system is running on (this is usually `x64`).
* ``: The self-reported version of the operating system (on Linux, this is the
distribution version).
* ``: The PowerShell version (from `$PSVersionTable.PSVersion`).
* ``: The machine architecture of the PowerShell process.
* ``: The reported version of the .NET runtime PowerShell is running on (from
`System.Environment.Version`).
* ``: The .NET runtime flavor PowerShell is running on (currently `framework` or
`core`).
For example:
* `win-4_x64_10.0.18312.0_5.1.18312.1000_x64_4.0.30319.42000_framework` is PowerShell 5.1 running on
Windows 10 Enterprise (build 18312) for x64.
* `win-4_x64_10.0.18312.0_6.1.2_x64_4.0.30319.42000_core` is PowerShell 6.1.2 running on the same
operating system.
* `ubuntu_x64_18.04_6.2.0_x64_4.0.30319.42000_core` is PowerShell 6.2.0 running on Ubuntu 18.04.
Some platforms come bundled with PSScriptAnalyzer as JSON files, named in this way for targeting in
your configuration.
Platforms bundled by default are:
| PowerShell Version | Operating System | ID |
| :----------------: | ---------------------- | --------------------------------------------------------------------- |
| 3.0 | Windows Server 2012 | `win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework` |
| 4.0 | Windows Server 2012 R2 | `win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework` |
| 5.1 | Windows Server 2016 | `win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework` |
| 5.1 | Windows Server 2019 | `win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` |
| 5.1 | Windows 10 Pro | `win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` |
| 6.2 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_4.0.30319.42000_core` |
| 6.2 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_6.2.4_x64_4.0.30319.42000_core` |
| 6.2 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` |
| 6.2 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_6.2.4_x64_4.0.30319.42000_core` |
| 7.0 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_7.0.0_x64_3.1.2_core` |
| 7.0 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_7.0.0_x64_3.1.2_core` |
| 7.0 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core` |
| 7.0 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_7.0.0_x64_3.1.2_core` |
Other profiles can be found in the [GitHub repo][02].
You can also generate your own platform profile using the [PSCompatibilityCollector module][01].
The compatibility profile settings takes a list of platforms to target under `TargetProfiles`. A
platform can be specified as:
* A platform name (like `ubuntu_x64_18.04_6.1.1_x64_4.0.30319.42000_core`), which will have `.json`
added to the end and is searched for in the default profile directory.
* A filename (like `my_custom_platform.json`), which will be searched for in the default profile
directory.
* An absolute path to a file (like `D:\PowerShellProfiles\TargetMachine.json`).
The default profile directory is under the PSScriptAnalyzer module at
`$PSScriptRoot/PSCompatibilityCollector/profiles` (where `$PSScriptRoot` here refers to the
directory containing `PSScriptAnalyzer.psd1`).
The compatibility analysis compares a type used to both a target profile and a 'union' profile
(containing all types available in *any* profile in the profile dir). If a type is not present in
the union profile, it is assumed to be locally created and ignored. Otherwise, if a type is present
in the union profile but not present in a target, it is deemed to be incompatible with that target.
## Configuration settings
| Configuration key | Meaning | Accepted values | Mandatory | Example |
| ----------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `Enable` | Activates the rule | bool (`$true`/`$false`) | No (default: `$false`) | `$true` |
| `TargetProfiles` | The list of PowerShell profiles to target | string\[]: absolute paths to profile files or names of profiles in the profile directory | No (default: `@()`) | `@('ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core', 'win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework')` |
| `ProfileDirPath` | The location to search for profiles by name and use for union profile generation | string: absolute path to new profile dir | No (defaults to `compatibility_profiles` directory in PSScriptAnalyzer module) | `C:\Users\me\Documents\pssaCompatProfiles` |
| `IgnoreTypes` | Full names of types or type accelerators to ignore compatibility of in scripts | string\[]: names of types to ignore | No (default: `@()`) | `@('System.Collections.ArrayList','string')` |
An example configuration might look like:
```powershell theme={null}
@{
Rules = @{
PSUseCompatibleTypes = @{
Enable = $true
TargetProfiles = @(
'ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core'
'win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
'MyProfile'
'another_custom_profile_in_the_profiles_directory.json'
'D:\My Profiles\profile1.json'
)
# You can specify types to not check like this, which will also ignore methods and members on it:
IgnoreTypes = @(
'System.IO.Compression.ZipFile'
)
}
}
}
```
Alternatively, you could provide a settings object as follows:
```powershell theme={null}
PS> $settings = @{
Rules = @{
PSUseCompatibleTypes = @{
Enable = $true
TargetProfiles = @('win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework')
}
}
}
PS> Invoke-ScriptAnalyzer -Settings $settings -ScriptDefinition "[System.Management.Automation.SemanticVersion]'1.18.0-rc1'"
RuleName Severity ScriptName Line Message
-------- -------- ---------- ---- -------
PSUseCompatibleTypes Warning 1 The type 'System.Management.Automation.SemanticVersion' is
not available by default in PowerShell version
'5.1.17763.316' on platform 'Microsoft Windows 10 Pro'
```
## Suppression
Command compatibility diagnostics can be suppressed with an attribute on the `param` block of a
scriptblock as with other rules.
```powershell theme={null}
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes', '')]
```
The rule can also be suppressed only for particular types:
```powershell theme={null}
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes',
'System.Management.Automation.Security.SystemPolicy')]
```
And also suppressed only for type members:
```powershell theme={null}
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes',
'System.Management.Automation.LanguagePrimitives/ConvertTypeNameToPSTypeName')]
```
[01]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector
[02]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector/optional_profiles
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseCompatibleTypes](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseCompatibleTypes.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseConsistentIndentation
Source: https://tally.wharflab.com/rules/powershell/PSUseConsistentIndentation
Use consistent indentation
`powershell/PSUseConsistentIndentation` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Indentation should be consistent throughout the source file.
**Note**: This rule is not enabled by default. The user needs to enable it through settings.
## Configuration
```powershell theme={null}
Rules = @{
PSUseConsistentIndentation = @{
Enable = $true
IndentationSize = 4
PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
Kind = 'space'
}
}
```
### Parameters
#### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
#### IndentationSize: int (Default value is `4`)
Indentation size in the number of space characters.
#### PipelineIndentation: string (Default value is `IncreaseIndentationForFirstPipeline`)
Whether to increase indentation after a pipeline for multi-line statements. The settings are:
* IncreaseIndentationForFirstPipeline (default): Indent once after the first pipeline and keep this
indentation. Example:
```powershell theme={null}
foo |
bar |
baz
```
* IncreaseIndentationAfterEveryPipeline: Indent more after the first pipeline and keep this
indentation. Example:
```powershell theme={null}
foo |
bar |
baz
```
* NoIndentation: Do not increase indentation. Example:
```powershell theme={null}
foo |
bar |
baz
```
* None: Do not change any existing pipeline indentation.
#### Kind: string (Default value is `space`)
Represents the kind of indentation to be used. Possible values are: `space`, `tab`. If any invalid
value is given, the property defaults to `space`.
`space` means `IndentationSize` number of `space` characters are used to provide one level of
indentation. `tab` means a tab character, `\t`.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseConsistentIndentation](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseConsistentIndentation.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseConsistentParameterSetName
Source: https://tally.wharflab.com/rules/powershell/PSUseConsistentParameterSetName
Use consistent parameter set names and proper parameter set configuration.
`powershell/PSUseConsistentParameterSetName` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Parameter set names in PowerShell are case-sensitive, unlike most other PowerShell elements. This
rule ensures consistent casing and proper configuration of parameter sets to avoid runtime errors
and improve code clarity.
The rule performs five different checks:
1. **Missing DefaultParameterSetName** - Warns when parameter sets are used but no default is
specified
2. **Multiple parameter declarations** - Detects when a parameter is declared multiple times in the
same parameter set. This is ultimately a runtime exception - this check helps catch it sooner.
3. **Case mismatch between DefaultParameterSetName and ParameterSetName** - Ensures consistent
casing
4. **Case mismatch between different ParameterSetName values** - Ensures all references to the same
parameter set use identical casing
5. **Parameter set names containing newlines** - Warns against using newline characters in parameter
set names
This rule isn't enabled by default. The user needs to enable it through settings.
## How
* Use a `DefaultParameterSetName` when defining multiple parameter sets
* Ensure consistent casing between `DefaultParameterSetName` and `ParameterSetName` values
* Use identical casing for all references to the same parameter set name
* Avoid declaring the same parameter multiple times in a single parameter set
* Do not use newline characters in parameter set names
## Examples
### Problematic code
```powershell theme={null}
# Missing DefaultParameterSetName
function Get-Data {
[CmdletBinding()]
param(
[Parameter(ParameterSetName='ByName')]
[string]$Name,
[Parameter(ParameterSetName='ByID')]
[int]$ID
)
}
# Case mismatch between DefaultParameterSetName and ParameterSetName
function Get-Data {
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='byname')]
[string]$Name,
[Parameter(ParameterSetName='ByID')]
[int]$ID
)
}
# Inconsistent casing between ParameterSetName values
function Get-Data {
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName')]
[string]$Name,
[Parameter(ParameterSetName='byname')]
[string]$DisplayName
)
}
# Multiple parameter declarations in same set
function Get-Data {
param(
[Parameter(ParameterSetName='ByName')]
[Parameter(ParameterSetName='ByName')]
[string]$Name
)
}
# Parameter set name with newline
function Get-Data {
param(
[Parameter(ParameterSetName="Set`nOne")]
[string]$Name
)
}
```
### Correct code
```powershell theme={null}
# Proper parameter set configuration
function Get-Data {
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName', Mandatory)]
[string]$Name,
[Parameter(ParameterSetName='ByName')]
[Parameter(ParameterSetName='ByID')]
[string]$ComputerName,
[Parameter(ParameterSetName='ByID', Mandatory)]
[int]$ID
)
}
```
## Configuration
```powershell theme={null}
Rules = @{
PSUseConsistentParameterSetName = @{
Enable = $true
}
}
```
### Parameters
* `Enable`: **bool** (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
## Notes
* Parameter set names are case-sensitive in PowerShell, making this different from most other
PowerShell elements
* The first occurrence of a parameter set name in your code is treated as the canonical casing
* Parameters without `[Parameter()]` attributes are automatically part of all parameter sets
* It's a PowerShell best practice to always specify a `DefaultParameterSetName` when using parameter
sets
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseConsistentParameterSetName](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseConsistentParameterSetName.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseConsistentParametersKind
Source: https://tally.wharflab.com/rules/powershell/PSUseConsistentParametersKind
Use the same pattern when defining parameters.
`powershell/PSUseConsistentParametersKind` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
All functions should use the same pattern when defining parameters. Possible pattern types are:
1. `Inline`
```powershell theme={null}
function f([Parameter()]$FirstParam) {
return
}
```
2. `ParamBlock`
```powershell theme={null}
function f {
param([Parameter()]$FirstParam)
return
}
```
In simple scenarios, both function definitions shown are considered to be equal. The purpose of this
rule is to enforce consistent code style across the codebase.
## How to Fix
Rewrite function so it defines parameters as specified in the rule
## Examples
When the rule sets parameters definition kind to `Inline`:
```powershell theme={null}
# Correct
function f([Parameter()]$FirstParam) {
return
}
# Incorrect
function g {
param([Parameter()]$FirstParam)
return
}
```
When the rule sets parameters definition kind to `ParamBlock`:
```powershell theme={null}
# Incorrect
function f([Parameter()]$FirstParam) {
return
}
# Correct
function g {
param([Parameter()]$FirstParam)
return
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseConsistentParametersKind](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseConsistentParametersKind.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseConsistentWhitespace
Source: https://tally.wharflab.com/rules/powershell/PSUseConsistentWhitespace
Use whitespaces
`powershell/PSUseConsistentWhitespace` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule is not enabled by default. The user needs to enable it through settings.
## Configuration
```powershell theme={null}
Rules = @{
PSUseConsistentWhitespace = @{
Enable = $true
CheckInnerBrace = $true
CheckOpenBrace = $true
CheckOpenParen = $true
CheckOperator = $true
CheckPipe = $true
CheckPipeForRedundantWhitespace = $false
CheckSeparator = $true
CheckParameter = $false
IgnoreAssignmentOperatorInsideHashTable = $false
}
}
```
### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
### CheckInnerBrace: bool (Default value is `$true`)
Checks if there is a space after the opening brace and a space before the closing brace. E.g.
`if ($true) { foo }` instead of `if ($true) {bar}`.
### CheckOpenBrace: bool (Default value is `$true`)
Checks if there is a space between a keyword and its corresponding open brace. E.g. `foo { }`
instead of `foo{ }`. If an open brace is preceded by an open parenthesis, then no space is required.
### CheckOpenParen: bool (Default value is `$true`)
Checks if there is space between a keyword and its corresponding open parenthesis. E.g. `if (true)`
instead of `if(true)`.
### CheckOperator: bool (Default value is `$true`)
Checks if a binary or unary operator is surrounded on both sides by a space. E.g. `$x = 1` instead
of `$x=1`.
### CheckSeparator: bool (Default value is `$true`)
Checks if a comma or a semicolon is followed by a space. E.g. `@(1, 2, 3)` or `@{a = 1; b = 2}`
instead of `@(1,2,3)` or `@{a = 1;b = 2}`.
### CheckPipe: bool (Default value is `$true`)
Checks if a pipe is surrounded on both sides by a space but ignores redundant whitespace. E.g.
`foo | bar` instead of `foo|bar`.
### CheckPipeForRedundantWhitespace : bool (Default value is `$false`)
Checks if a pipe is surrounded by redundant whitespace (i.e. more than 1 whitespace). E.g.
`foo | bar` instead of `foo | bar`.
### CheckParameter: bool (Default value is `$false` at the moment due to the setting being new)
Checks if there is more than one space between parameters and values. E.g. `foo -bar $baz -bat`
instead of `foo -bar $baz -bat`. This eliminates redundant whitespace that was probably added
unintentionally. The rule does not check for whitespace between parameter and value when the colon
syntax `-ParameterName:$ParameterValue` is used as some users prefer either 0 or 1 whitespace in
this case.
### IgnoreAssignmentOperatorInsideHashTable: bool (Default value is `$false`)
When `CheckOperator` is set, ignore whitespace around assignment operators within multi-line hash
tables. Set this option to use the `AlignAssignmentStatement` rule and still check whitespace around
operators everywhere else.
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseConsistentWhitespace](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseConsistentWhitespace.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseConstrainedLanguageMode
Source: https://tally.wharflab.com/rules/powershell/PSUseConstrainedLanguageMode
Use patterns compatible with Constrained Language Mode
`powershell/PSUseConstrainedLanguageMode` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This rule identifies PowerShell patterns that are restricted or not permitted in Constrained
Language Mode (CLM).
Constrained Language Mode is a PowerShell security feature that restricts:
* .NET types that can be used
* COM objects that can be instantiated
* Commands that can be executed
* Language features that can be used
CLM is commonly used in:
* Application Control environments (Application Control for Business, AppLocker)
* Just Enough Administration (JEA) endpoints
* Secure environments requiring additional PowerShell restrictions
Digitally signed scripts from trusted publishers execute in Full Language Mode (FLM) even in CLM
environments. The rule detects signature blocks (`# SIG # Begin signature block`) and adjusts checks
accordingly. Most restrictions don't apply to signed scripts, but certain checks (dot-sourcing,
parameter types, manifest best practices) are always enforced.
> \[!IMPORTANT]
> The rule performs a simple text check for signature blocks and does NOT validate signature
> authenticity or certificate trust. Actual signature validation is performed by PowerShell at
> runtime.
## Constrained Language Mode Restrictions
### Unsigned Scripts (Full CLM Checking)
The following are flagged for unsigned scripts:
1. **Add-Type** - Code compilation not permitted
2. **Disallowed COM Objects** - Only Scripting.Dictionary, Scripting.FileSystemObject,
VBScript.RegExp allowed
3. **Disallowed .NET Types** - Only \~70 allowed types (string, int, hashtable, pscredential, etc.)
4. **Type Constraints** - On parameters and variables
5. **Type Expressions** - Static type references like `[Type]::Method()`
6. **Type Casts** - Converting to disallowed types
7. **Member Invocations** - Methods/properties on disallowed types
8. **PowerShell Classes** - `class` keyword not permitted
9. **XAML/WPF** - Not permitted
10. **Invoke-Expression** - Restricted
11. **Dot-Sourcing** - May be restricted depending on the file being sourced
12. **Module Manifest Wildcards** - Wildcard exports not recommended
13. **Module Manifest .ps1 Files** - Script modules ending with .ps1 not allowed
Always enforced, even for signed scripts
### Signed Scripts (Selective Checking)
For scripts with signature blocks, only these are checked:
* Dot-sourcing
* Parameter type constraints
* Module manifest wildcards (.psd1 files)
* Module manifest script modules (.psd1 files)
## Configuration
### Basic Configuration
```powershell theme={null}
@{
Rules = @{
PSUseConstrainedLanguageMode = @{
Enable = $true
}
}
}
```
### Parameters
#### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation. This rule is disabled by default
because not all scripts need CLM compatibility.
#### IgnoreSignatures: bool (Default value is `$false`)
Control signature detection behavior:
* `$false` (default): Automatically detect signatures. Signed scripts get selective checking,
unsigned get full checking.
* `$true`: Bypass signature detection. ALL scripts get full CLM checking regardless of signature
status.
```powershell theme={null}
@{
Rules = @{
PSUseConstrainedLanguageMode = @{
Enable = $true
IgnoreSignatures = $true # Enforce full CLM compliance for all scripts
}
}
}
```
Use `IgnoreSignatures = $true` when:
* Auditing signed scripts for complete CLM compatibility
* Preparing scripts for untrusted environments
* Enforcing strict CLM compliance organization-wide
* Development/testing to see all potential issues
## How to Fix
### Replace Add-Type
Use allowed cmdlets or pre-compile assemblies.
### Replace Disallowed COM Objects
Use only allowed COM objects (Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp) or
PowerShell cmdlets.
### Replace Disallowed Types
Use allowed type accelerators (`[string]`, `[int]`, `[hashtable]`, etc.) or allowed cmdlets instead
of disallowed .NET types.
### Replace PowerShell Classes
Use `New-Object PSObject` with `Add-Member` or hashtables instead of classes.
> \[!IMPORTANT]
> `[PSCustomObject]@{}` syntax is NOT allowed in CLM because it uses type casting.
### Avoid XAML
Don't use WPF/XAML in CLM-compatible scripts.
### Replace Invoke-Expression
Use direct execution (`&`) or safer alternatives.
### Replace Dot-Sourcing
Use modules with Import-Module instead of dot-sourcing when possible.
### Fix Module Manifests
* Replace wildcard exports (`*`) with explicit lists.
* Use `.psm1` or `.dll` instead of `.ps1` for RootModule/NestedModules.
* Don't use `ScriptsToProcess`. These scripts are loaded in the caller's scope and are blocked.
## Examples
### Example 1: Add-Type
#### Wrong
```powershell theme={null}
Add-Type -TypeDefinition @"
public class Helper {
public static string DoWork() { return "Done"; }
}
"@
```
#### Correct
```powershell theme={null}
# Code sign your scripts/modules using proper signing tools
# (for example, Set-AuthenticodeSignature or external signing processes)
# Use allowed cmdlets instead of Add-Type-defined types where possible
# Or pre-compile, sign, and load the assembly (for example, via Add-Type -Path)
```
### Example 2: COM Objects
#### Wrong
```powershell theme={null}
$excel = New-Object -ComObject Excel.Application
```
#### Correct
```powershell theme={null}
# Use allowed COM object
$dict = New-Object -ComObject Scripting.Dictionary
# Or use PowerShell cmdlets
Import-Excel -Path $file # From ImportExcel module
```
### Example 3: Disallowed Types
#### Wrong
```powershell theme={null}
# Type constraint and member invocation flagged
function Download-File {
param([System.Net.WebClient]$Client)
$Client.DownloadString($url)
}
# Type cast and method call flagged
[System.Net.WebClient]$client = New-Object System.Net.WebClient
$data = $client.DownloadData($url)
```
#### Correct
```powershell theme={null}
# Use allowed cmdlets
function Download-File {
param([string]$Url)
Invoke-WebRequest -Uri $Url
}
# Use allowed types
function Process-Text {
param([string]$Text)
$upper = $Text.ToUpper() # String methods are allowed
}
```
### Example 4: PowerShell Classes
#### Wrong
```powershell theme={null}
class MyClass {
[string]$Name
[string]GetInfo() {
return $this.Name
}
}
# Also wrong - uses type cast
$obj = [PSCustomObject]@{
Name = "Test"
}
```
#### Correct
```powershell theme={null}
# Option 1: New-Object PSObject with Add-Member
$obj = New-Object PSObject -Property @{
Name = "Test"
}
$obj | Add-Member -MemberType ScriptMethod -Name GetInfo -Value {
return $this.Name
}
Add-Member -InputObject $obj -NotePropertyMembers @{"Number" = 42}
# Option 2: Hashtable
$obj = @{
Name = "Test"
Number = 42
}
```
### Example 5: Module Manifests
#### Wrong
```powershell theme={null}
@{
ModuleVersion = '1.0.0'
RootModule = 'MyModule.ps1' # .ps1 not recommended
FunctionsToExport = '*' # Wildcard not recommended
CmdletsToExport = '*'
}
```
#### Correct
```powershell theme={null}
@{
ModuleVersion = '1.0.0'
RootModule = 'MyModule.psm1' # Use .psm1 or .dll
FunctionsToExport = @( # Explicit list
'Get-MyFunction'
'Set-MyFunction'
)
CmdletsToExport = @()
}
```
### Example 6: Array Types
#### Wrong
```powershell theme={null}
# Disallowed type in array
param([System.Net.WebClient[]]$Clients)
```
#### Correct
```powershell theme={null}
# Allowed types in arrays are fine
param([string[]]$Names)
param([int[]]$Numbers)
param([hashtable[]]$Configuration)
```
## Detailed Restrictions
### 1. Add-Type
`Add-Type` allows compiling arbitrary C# code and isn't permitted in CLM.
**Enforced For**: Unsigned scripts only
### 2. COM Objects
Only three COM objects are allowed:
* `Scripting.Dictionary`
* `Scripting.FileSystemObject`
* `VBScript.RegExp`
All others (Excel.Application, WScript.Shell, etc.) are flagged.
**Enforced For**: Unsigned scripts only
### 3. .NET Types
Only \~70 allowed types including:
* Primitives: `string`, `int`, `bool`, `byte`, `char`, `datetime`, `decimal`, `double`, etc.
* Collections: `hashtable`, `array`, `arraylist`
* PowerShell: `pscredential`, `psobject`, `securestring`
* Utilities: `regex`, `guid`, `version`, `uri`, `xml`
* Arrays: `string[]`, `int[][]`, etc. (array of any allowed type)
The rule checks type usage in:
* Parameter type constraints (**always enforced, even for signed scripts**)
* Variable type constraints
* New-Object -TypeName
* Type expressions (`[Type]::Method()`)
* Type casts (`[Type]$variable`)
* Member invocations on typed variables
**Enforced For**: Parameter constraints always; others unsigned only
### 4. PowerShell Classes
The `class` keyword is not permitted. Use `New-Object PSObject` with `Add-Member` or hashtables.
**Note**: `[PSCustomObject]@{}` is also not allowed because it uses type casting.
**Enforced For**: Unsigned scripts only
### 5. XAML/WPF
XAML and WPF are not permitted in CLM.
**Enforced For**: Unsigned scripts only
### 6. Invoke-Expression
`Invoke-Expression` is restricted in CLM.
**Enforced For**: Unsigned scripts only
### 7. Dot-Sourcing
Dot-sourcing (`. $PSScriptRoot\script.ps1`) may be restricted depending on source location.
**Enforced For**: ALL scripts (unsigned and signed)
### 8. Module Manifest Best Practices
#### Wildcard Exports
Don't use `*` in: `FunctionsToExport`, `CmdletsToExport`, `AliasesToExport`, `VariablesToExport`
Use explicit lists for security and clarity.
**Enforced For**: ALL .psd1 files (unsigned and signed)
#### Script Module Files
Don't use `.ps1` files in: `RootModule`, `ModuleToProcess`, `NestedModules`
Use `.psm1` (script modules) or `.dll` (binary modules) for better performance and compatibility.
**Enforced For**: ALL .psd1 files (unsigned and signed)
## More Information
* [About Language Modes][01]
* [PowerShell Constrained Language Mode and the Dot-Source Operator][02]
* [PowerShell Constrained Language Mode][03]
* [PowerShell Module Function Export in Constrained Language][04]
[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes
[02]: https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode-and-the-dot-source-operator/
[03]: https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode/
[04]: https://devblogs.microsoft.com/powershell/powershell-module-function-export-in-constrained-language/
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseConstrainedLanguageMode](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseConstrainedLanguageMode.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseCorrectCasing
Source: https://tally.wharflab.com/rules/powershell/PSUseCorrectCasing
Use exact casing of cmdlet/function/parameter name.
`powershell/PSUseCorrectCasing` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
This is a style/formatting rule. PowerShell is case insensitive wherever possible, so the casing of
cmdlet names, parameters, keywords and operators doesn't matter. This rule nonetheless ensures
consistent casing for clarity and readability. Using lowercase keywords helps distinguish them from
commands. Using lowercase operators helps distinguish them from parameters.
## How
* Use exact casing for type names.
* Use exact casing of the cmdlet and its parameters.
* Use lowercase for language keywords and operators.
## Configuration
```powershell theme={null}
Rules = @{
PSUseCorrectCasing = @{
Enable = $true
CheckCommands = $true
CheckKeyword = $true
CheckOperator = $true
}
}
```
## Parameters
### Enable: bool (Default value is `$false`)
Enable or disable the rule during ScriptAnalyzer invocation.
### CheckCommands: bool (Default value is `$true`)
If true, require the case of all command and parameter names to match their canonical casing.
### CheckKeyword: bool (Default value is `$true`)
If true, require the case of all keywords to be lowercase.
### CheckOperator: bool (Default value is `$true`)
If true, require the case of all operators to be lowercase. For example: `-eq`, `-ne`, `-gt`
## Examples
### Wrong way
```powershell theme={null}
ForEach ($file in Get-childitem -Recurse) {
$file.Extension -EQ '.txt'
}
invoke-command { 'foo' } -runasadministrator
```
### Correct way
```powershell theme={null}
foreach ($file in Get-ChildItem -Recurse) {
$file.Extension -eq '.txt'
}
Invoke-Command { 'foo' } -RunAsAdministrator
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseCorrectCasing](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseCorrectCasing.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseDeclaredVarsMoreThanAssignments
Source: https://tally.wharflab.com/rules/powershell/PSUseDeclaredVarsMoreThanAssignments
Extra Variables
`powershell/PSUseDeclaredVarsMoreThanAssignments` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Variables that are assigned but not used are not needed.
For this rule, the variable must be used within the same scriptblock that it was declared or it
won't be considered to be 'used'.
## How
Remove the variables that are declared but not used.
## Examples
### Problematic code
```powershell theme={null}
function Test
{
$declaredVar = 'Declared just for fun'
$declaredVar2 = 'Not used'
Write-Output $declaredVar
}
```
### Correct code
```powershell theme={null}
function Test
{
$declaredVar = 'Declared just for fun'
Write-Output $declaredVar
}
```
### Special cases
The following examples trigger the **PSUseDeclaredVarsMoreThanAssignments** warning. This behavior
is a limitation of the rule. There is no way to avoid these false positive warnings.
In this case, the warning is triggered because `$bar` is not used within the scriptblock where it
was defined.
```powershell theme={null}
$foo | ForEach-Object {
if ($_ -eq $false) {
$bar = $true
}
}
if($bar){
Write-Host 'Collection contained a false case.'
}
```
In the next example, the warning is triggered because `$errResult` isn't recognized as being used in
the `Write-Host` command.
```powershell theme={null}
$errResult = $null
Write-Host 'Ugh:' -ErrorVariable errResult
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseDeclaredVarsMoreThanAssignments](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseDeclaredVarsMoreThanAssignments.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseLiteralInitializerForHashtable
Source: https://tally.wharflab.com/rules/powershell/PSUseLiteralInitializerForHashtable
Create hashtables with literal initializers
`powershell/PSUseLiteralInitializerForHashtable` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Creating a hashtable using `[hashtable]::new()` or `New-Object -TypeName hashtable` without passing
a `IEqualityComparer` object to the constructor creates a hashtable where the keys are looked-up in
a case-sensitive manner. However, PowerShell is case-insensitive in nature and it is best to create
hashtables with case-insensitive key look-up.
This rule is intended to warn the author of the case-sensitive nature of the hashtable when created
using the `new` method or the `New-Object` cmdlet.
## How to Fix
Create the hashtable using a literal hashtable expression.
## Examples
### Problematic code (`[hashtable]::new()`)
```powershell theme={null}
$hashtable = [hashtable]::new()
```
### Problematic code (`New-Object`)
```powershell theme={null}
$hashtable = New-Object -TypeName hashtable
```
### Correct code
```powershell theme={null}
$hashtable = @{}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseLiteralInitializerForHashtable](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseLiteralInitializerForHashtable.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseOutputTypeCorrectly
Source: https://tally.wharflab.com/rules/powershell/PSUseOutputTypeCorrectly
Use OutputType Correctly
`powershell/PSUseOutputTypeCorrectly` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Information |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSUseOutputTypeCorrectly"]` or by setting
`rules.powershell.PSUseOutputTypeCorrectly.severity = "warning"` in `.tally.toml`.
## Description
A command should return the same type as declared in `OutputType`.
You can get more details by running `Get-Help about_Functions_OutputTypeAttribute` command in
PowerShell.
## How
Specify that the OutputType attribute lists and the types returned in the cmdlet match.
## Examples
### Problematic code
```powershell theme={null}
function Get-Foo
{
[CmdletBinding()]
[OutputType([String])]
Param(
)
return 4
}
```
### Correct code
```powershell theme={null}
function Get-Foo
{
[CmdletBinding()]
[OutputType([String])]
Param(
)
return 'four'
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseOutputTypeCorrectly](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseOutputTypeCorrectly.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUsePSCredentialType
Source: https://tally.wharflab.com/rules/powershell/PSUsePSCredentialType
Use PSCredential type.
`powershell/PSUsePSCredentialType` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
If the cmdlet or function has a **Credential** parameter, the parameter must accept the
**PSCredential** type.
## How
Change the **Credential** parameter's type to be **PSCredential**.
## Examples
### Problematic code
```powershell theme={null}
function Credential([String]$Credential)
{
...
}
```
### Correct code
```powershell theme={null}
function Credential([PSCredential]$Credential)
{
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UsePSCredentialType](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UsePSCredentialType.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseProcessBlockForPipelineCommand
Source: https://tally.wharflab.com/rules/powershell/PSUseProcessBlockForPipelineCommand
Use process block for command that accepts input from pipeline.
`powershell/PSUseProcessBlockForPipelineCommand` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSUseProcessBlockForPipelineCommand"]` or by setting
`rules.powershell.PSUseProcessBlockForPipelineCommand.severity = "warning"` in `.tally.toml`.
## Description
Functions that support pipeline input should always handle parameter input in a process block.
Unexpected behavior can result if input is handled directly in the body of a function where
parameters declare pipeline support.
## Examples
### Problematic code
```powershell theme={null}
Function Get-Number
{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline)]
[int]
$Number
)
$Number
}
```
#### Result
```text theme={null}
PS C:\> 1..5 | Get-Number
5
```
### Correct code
```powershell theme={null}
Function Get-Number
{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline)]
[int]
$Number
)
process
{
$Number
}
}
```
#### Result
```text theme={null}
PS C:\> 1..5 | Get-Number
1
2
3
4
5
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseProcessBlockForPipelineCommand](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseProcessBlockForPipelineCommand.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseShouldProcessForStateChangingFunctions
Source: https://tally.wharflab.com/rules/powershell/PSUseShouldProcessForStateChangingFunctions
Use ShouldProcess For State Changing Functions
`powershell/PSUseShouldProcessForStateChangingFunctions` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in
Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSUseShouldProcessForStateChangingFunctions"]` or by setting
`rules.powershell.PSUseShouldProcessForStateChangingFunctions.severity = "warning"` in `.tally.toml`.
## Description
Functions whose verbs change system state should support `ShouldProcess`. To enable the
`ShouldProcess` feature, set the `SupportsShouldProcess` argument in the `CmdletBinding` attribute.
The `SupportsShouldProcess` argument adds **Confirm** and **WhatIf** parameters to the function. The
**Confirm** parameter prompts the user before it runs the command on each object in the pipeline.
The **WhatIf** parameter lists the changes that the command would make, instead of running the
command.
Verbs that should support `ShouldProcess`:
* `New`
* `Set`
* `Remove`
* `Start`
* `Stop`
* `Restart`
* `Reset`
* `Update`
## How
Include the `SupportsShouldProcess` argument in the `CmdletBinding` attribute.
## Examples
### Problematic code
```powershell theme={null}
function Set-ServiceObject
{
[CmdletBinding()]
param
(
[string]
$Parameter1
)
...
}
```
### Correct code
```powershell theme={null}
function Set-ServiceObject
{
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[string]
$Parameter1
)
...
}
```
## More information
* [about\_Functions\_CmdletBindingAttribute][01]
* [Everything you wanted to know about ShouldProcess][04]
* [Required Development Guidelines][03]
* [Requesting Confirmation from Cmdlets][02]
[01]: /powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute
[02]: /powershell/scripting/developer/cmdlet/requesting-confirmation-from-cmdlets
[03]: /powershell/scripting/developer/cmdlet/required-development-guidelines#support-confirmation-requests-rd04
[04]: /powershell/scripting/learn/deep-dives/everything-about-shouldprocess
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseShouldProcessForStateChangingFunctions](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseShouldProcessForStateChangingFunctions.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseSingleValueFromPipelineParameter
Source: https://tally.wharflab.com/rules/powershell/PSUseSingleValueFromPipelineParameter
Use at most a single ValueFromPipeline parameter per parameter set.
`powershell/PSUseSingleValueFromPipelineParameter` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
Parameter sets should have at most one parameter marked as `ValueFromPipeline = true`.
This rule identifies functions where multiple parameters within the same parameter set have
`ValueFromPipeline` set to `true` (either explicitly or implicitly).
## How
Ensure that only one parameter per parameter set accepts pipeline input by value. If you need
multiple parameters to accept different types of pipeline input, use separate parameter sets.
## Examples
### Problematic code
```powershell theme={null}
function Process-Data {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string] $InputData,
[Parameter(ValueFromPipeline)]
[string] $ProcessingMode
)
process {
Write-Output "$ProcessingMode`: $InputData"
}
}
```
### Correct code
```powershell theme={null}
function Process-Data {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string] $InputData,
[Parameter(Mandatory)]
[string] $ProcessingMode
)
process {
Write-Output "$ProcessingMode`: $InputData"
}
}
```
## Suppression
To suppress this rule for a specific parameter set, use the `SuppressMessage` attribute with the
parameter set name:
```powershell theme={null}
function Process-Data {
[Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'MyParameterSet')]
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')]
[string] $InputData,
[Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')]
[string] $ProcessingMode
)
process {
Write-Output "$ProcessingMode`: $InputData"
}
}
```
For the default parameter set, use `'default'` as the suppression target:
```powershell theme={null}
[Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'default')]
```
## Notes
* This rule applies to both explicit `ValueFromPipeline = $true` and implicit `ValueFromPipeline`
(which is the same as using `= $true`)
* Parameters with `ValueFromPipeline=$false` are not flagged by this rule
* The rule correctly handles the default parameter set (`__AllParameterSets`) and named parameter
sets
* Different parameter sets can each have their own single `ValueFromPipeline` parameter without
triggering this rule
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseSingleValueFromPipelineParameter](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseSingleValueFromPipelineParameter.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseSingularNouns
Source: https://tally.wharflab.com/rules/powershell/PSUseSingularNouns
Cmdlet Singular Noun
`powershell/PSUseSingularNouns` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSUseSingularNouns"]` or by setting
`rules.powershell.PSUseSingularNouns.severity = "warning"` in `.tally.toml`.
## Description
PowerShell team best practices state cmdlets should use singular nouns and not plurals. Suppression
allows you to suppress the rule for specific function names. For example:
```text theme={null}
function Get-Elements {
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Get-Elements')]
Param()
}
```
## Configuration
```powershell theme={null}
Rules = @{
PSUseSingularNouns = @{
Enable = $true
NounAllowList = 'Data', 'Windows', 'Foos'
}
}
```
### Parameters
* `Enable`: `bool` (Default value is `$true`)
Enable or disable the rule during ScriptAnalyzer invocation.
* `NounAllowList`: `string[]` (Default value is `{'Data', 'Windows'}`)
Commands to be excluded from this rule. `Data` and `Windows` are common false positives and are
excluded by default.
## How
Change plurals to singular.
## Examples
### Problematic code
```powershell theme={null}
function Get-Files
{
...
}
```
### Correct code
```powershell theme={null}
function Get-File
{
...
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseSingularNouns](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseSingularNouns.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseSupportsShouldProcess
Source: https://tally.wharflab.com/rules/powershell/PSUseSupportsShouldProcess
Use SupportsShouldProcess
`powershell/PSUseSupportsShouldProcess` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default for Dockerfile `RUN` snippets because a `RUN` executes
statements inline rather than defining cmdlets, parameters, or modules with external callers. Re-enable it with
`include = ["powershell/PSUseSupportsShouldProcess"]` or by setting
`rules.powershell.PSUseSupportsShouldProcess.severity = "warning"` in `.tally.toml`.
## Description
This rule discourages manual declaration of `WhatIf` and `Confirm` parameters in a function/cmdlet.
These parameters are, however, provided automatically when a function declares a `CmdletBinding`
attribute with `SupportsShouldProcess` as its named argument. Using `SupportsShouldProcess` not only
provides these parameters but also some generic functionality that allows the function/cmdlet
authors to provide the desired interactive experience while using the cmdlet.
## Examples
### Problematic code
```powershell theme={null}
function foo {
param(
$param1,
$Confirm,
$WhatIf
)
}
```
### Correct code
```powershell theme={null}
function foo {
[CmdletBinding(SupportsShouldProcess)]
param(
$param1
)
}
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseSupportsShouldProcess](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseSupportsShouldProcess.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseToExportFieldsInManifest
Source: https://tally.wharflab.com/rules/powershell/PSUseToExportFieldsInManifest
Use the *ToExport module manifest fields.
`powershell/PSUseToExportFieldsInManifest` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Dockerfiles do not ship PowerShell module
manifests (`.psd1` files) — this rule targets module-authoring artifacts that are out of
scope for container builds. Re-enable it with
`include = ["powershell/PSUseToExportFieldsInManifest"]` or by setting
`rules.powershell.PSUseToExportFieldsInManifest.severity = "warning"` in `.tally.toml`.
## Description
To improve the performance of module auto-discovery, module manifests should not use wildcards
(`'*'`) or null (`$null`) in the following entries:
* `AliasesToExport`
* `CmdletsToExport`
* `FunctionsToExport`
* `VariablesToExport`
Using wildcards or null causes PowerShell to perform expensive work to analyze a module during
module auto-discovery.
## How
Use an explicit list in the entries.
## Example 1
Suppose there are no functions in your module to export. Then,
### Problematic code
```powershell theme={null}
FunctionsToExport = $null
```
### Correct code
```powershell theme={null}
FunctionsToExport = @()
```
## Example 2
Suppose there are only two functions in your module, `Get-Foo` and `Set-Foo` that you want to
export. Then,
### Problematic code
```powershell theme={null}
FunctionsToExport = '*'
```
### Correct code
```powershell theme={null}
FunctionsToExport = @(Get-Foo, Set-Foo)
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseToExportFieldsInManifest](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseToExportFieldsInManifest.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseUTF8EncodingForHelpFile
Source: https://tally.wharflab.com/rules/powershell/PSUseUTF8EncodingForHelpFile
Use UTF8 Encoding For Help File
`powershell/PSUseUTF8EncodingForHelpFile` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ----------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Default | Disabled in tally |
| Auto-fix | No |
tally disables this rule by default because Dockerfile `RUN` bodies are inline strings, not
files on disk with encoding metadata. Re-enable it with
`include = ["powershell/PSUseUTF8EncodingForHelpFile"]` or by setting
`rules.powershell.PSUseUTF8EncodingForHelpFile.severity = "warning"` in `.tally.toml`.
## Description
Check that an `about_` help file uses UTF-8 encoding. The filename must start with `about_` and end
with `.help.txt`. The rule uses the **CurrentEncoding** property of the **StreamReader** class to
determine the encoding of the file.
## How
For PowerShell commands that write to files, ensure that you set the encoding parameter to `utf8`,
`utf8BOM`, or `utf8NoBOM`.
When you create a help file using a text editor, ensure that the editor is configured to save the
file in a UTF8 format. Consult the documentation for your text editor for instructions on how to
save files with a specific encoding.
## Further reading
For more information, see the following articles:
* `System.IO.StreamReader`
* [about\_Character\_Encoding](/powershell/module/microsoft.powershell.core/about/about_character_encoding)
* [Set-Content](https://learn.microsoft.com/powershell/module/microsoft.powershell.management/set-content)
* [Understanding file encoding in VS Code and PowerShell](/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding)
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseUTF8EncodingForHelpFile](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseUTF8EncodingForHelpFile.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PSUseUsingScopeModifierInNewRunspaces
Source: https://tally.wharflab.com/rules/powershell/PSUseUsingScopeModifierInNewRunspaces
Use 'Using:' scope modifier in RunSpace ScriptBlocks
`powershell/PSUseUsingScopeModifierInNewRunspaces` is a PSScriptAnalyzer diagnostic emitted by tally for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | PSScriptAnalyzer |
| Auto-fix | No |
## Description
If a scriptblock is intended to be run in a new runspace, variables inside it should use the
`$using:` scope modifier, or be initialized within the scriptblock. This applies to:
* `Invoke-Command`- Only with the **ComputerName** or **Session** parameter.
* `Workflow { InlineScript {} }`
* `Foreach-Object` - Only with the **Parallel** parameter
* `Start-Job`
* `Start-ThreadJob`
* The `Script` resource in DSC configurations, specifically for the `GetScript`, `TestScript` and
`SetScript` properties.
## How to Fix
Within the ScriptBlock, instead of just using a variable from the parent scope, you have to add the
`using:` scope modifier to it.
## Examples
### Problematic code
```powershell theme={null}
$var = 'foo'
1..2 | ForEach-Object -Parallel { $var }
```
### Correct code
```powershell theme={null}
$var = 'foo'
1..2 | ForEach-Object -Parallel { $using:var }
```
## More correct examples
```powershell theme={null}
$bar = 'bar'
Invoke-Command -ComputerName 'foo' -ScriptBlock { $using:bar }
```
```powershell theme={null}
$bar = 'bar'
$s = New-PSSession -ComputerName 'foo'
Invoke-Command -Session $s -ScriptBlock { $using:bar }
```
```powershell theme={null}
# Remark: Workflow is supported on Windows PowerShell only
Workflow {
$foo = 'foo'
InlineScript { $using:foo }
}
```
```powershell theme={null}
$foo = 'foo'
Start-ThreadJob -ScriptBlock { $using:foo }
Start-Job -ScriptBlock {$using:foo }
```
## Source
This rule documentation is adapted from Microsoft's PSScriptAnalyzer documentation for
[UseUsingScopeModifierInNewRunspaces](https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/blob/main/reference/docs-conceptual/PSScriptAnalyzer/Rules/UseUsingScopeModifierInNewRunspaces.md),
licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
# powershell/PowerShell
Source: https://tally.wharflab.com/rules/powershell/PowerShell
Runs PowerShell script diagnostics for PowerShell snippets embedded in Dockerfiles.
Runs PowerShell script diagnostics for PowerShell snippets embedded in Dockerfiles.
| Property | Value |
| -------- | ------------------------------------------------------ |
| Severity | Warning |
| Category | Best Practices |
| Default | Enabled, gated by slow checks |
| Auto-fix | Suggestions when PSScriptAnalyzer provides corrections |
## Description
tally analyzes Dockerfile `RUN` instructions that execute PowerShell, including stages with `SHELL ["pwsh", "-Command"]`, explicit `RUN pwsh -Command
...` wrappers, and PowerShell heredoc bodies.
Diagnostics are reported in the `powershell/*` namespace using the upstream PowerShell rule name, such as `powershell/PSAvoidUsingWriteHost`. Each
concrete diagnostic links to its own tally documentation page, for example
[`powershell/PSAvoidUsingWriteHost`](/rules/powershell/PSAvoidUsingWriteHost).
The PowerShell analyzer is enabled by default, but it is gated by slow checks. With the default `slow-checks.mode = "auto"`, tally runs it locally and
skips it in CI; use `--slow-checks=on` when CI should run PowerShell analysis, or `--slow-checks=off` to skip it explicitly.
The sidecar still starts lazily. Dockerfiles without PowerShell snippets do not start `pwsh`, even when slow checks are enabled. Selecting
`powershell/PowerShell`, `powershell/*`, or a specific rule such as `powershell/PSAvoidUsingWriteHost` controls rule filtering, but it does not
bypass the slow-check gate.
Rule-specific options under `rules.powershell.` are forwarded to PSScriptAnalyzer as `Settings.Rules` entries. This supports upstream
options such as `Enable`, `TargetProfiles`, and compatibility profile settings documented on the concrete `powershell/*` rule pages.
When an upstream PSScriptAnalyzer diagnostic includes suggested corrections, tally exposes them as normal fix suggestions. These fixes are only
attached when the PowerShell snippet can be mapped back to precise Dockerfile source ranges.
## Default-disabled PSScriptAnalyzer rules
A subset of PSScriptAnalyzer rules is targeted at long-lived script reusability, function or module authoring, manifest authoring, or Desired State
Configuration — concerns that do not apply to a one-shot Dockerfile `RUN`. tally ships with these rules disabled by default so the diagnostics you see
are correctness, security, and compatibility findings. Each affected rule's documentation page has a `Default: Disabled in tally` row and a note
explaining why.
To re-enable a default-disabled rule for your project, opt in explicitly:
```toml theme={null}
# .tally.toml
[rules]
include = ["powershell/PSAvoidUsingWriteHost"]
# Or per-rule:
[rules.powershell.PSAvoidUsingWriteHost]
severity = "warning"
```
To re-enable the entire default-disabled set, use the namespace wildcard: `include = ["powershell/*"]`. The engine selector
`include = ["powershell/PowerShell"]` only toggles the analyzer engine and does **not** override per-rule defaults.
## Requirements
* PowerShell 7 (`pwsh`) must be available on `PATH`.
* Windows PowerShell 5.1 (`powershell.exe`) is not supported as an analyzer host; use PowerShell 7+ (`pwsh`).
* The first PowerShell analyzer run installs tally's pinned `PSScriptAnalyzer` release automatically for that `pwsh` environment when the tested
version is missing.
Use `TALLY_POWERSHELL=/path/to/pwsh` when `pwsh` is not on `PATH` or when tally should use a specific PowerShell installation.
The automatic install uses the selected `pwsh` environment's PowerShell package tooling (`Install-PSResource` when available, otherwise
`Install-Module`) and installs tally's pinned `PSScriptAnalyzer` release to `CurrentUser` scope. If the host has no network access or blocks
PowerShell Gallery, preinstall the version pinned by your tally release and rerun tally.
Cold installation downloads the PSScriptAnalyzer module and can take more than a few seconds depending on network speed. If the bootstrap is still
running after 3 seconds, tally writes a progress note to stderr and repeats it periodically until the sidecar is ready. Set
`TALLY_POWERSHELL_PROGRESS=0` to silence these notes.
## Examples
### Problematic code
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command"]
RUN Write-Host hi
```
### Reported diagnostic
```text theme={null}
powershell/PSAvoidUsingWriteHost
```
### Disable a specific PowerShell rule
```bash theme={null}
tally lint --ignore powershell/PSAvoidUsingWriteHost Dockerfile
```
### Disable PowerShell script analysis
```bash theme={null}
tally lint --ignore powershell/* Dockerfile
```
## Reference
* [PSScriptAnalyzer rules](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/readme?view=ps-modules)
# shellcheck/SC1040
Source: https://tally.wharflab.com/rules/shellcheck/SC1040
When using `<<-`, the heredoc terminator may only be indented with tabs.
When using `<<-`, the heredoc terminator may only be indented with tabs.
| Property | Value |
| -------- | ---------------------------- |
| Severity | Error |
| Category | Best Practices |
| Default | Enabled (via `shellcheck/*`) |
| Auto-fix | Yes (`--fix`, safe) |
## Description
The `<<-` heredoc form strips leading **tabs** from heredoc lines. Leading spaces before the terminator are invalid and can prevent correct heredoc
termination.
tally reports this as `shellcheck/SC1040` and provides an auto-fix that removes only the extra leading spaces from the terminator indentation.
## Examples
### Problematic code
```dockerfile theme={null}
RUN <<'SCRIPT'
cat <<-EOF
hello
EOF
EOF
SCRIPT
```
### Correct code
```dockerfile theme={null}
RUN <<'SCRIPT'
cat <<-EOF
hello
EOF
EOF
SCRIPT
```
## Auto-fix
The fix is minimal:
* removes only leading space runs in the offending terminator line
* preserves tab indentation
* emits narrow edits so other fixes can still apply to the same script
```bash theme={null}
tally lint --fix --select shellcheck/SC1040 Dockerfile
```
## Reference
* [ShellCheck SC1040](https://www.shellcheck.net/wiki/SC1040)
# tally/circular-stage-deps
Source: https://tally.wharflab.com/rules/tally/circular-stage-deps
Detects circular dependencies between build stages.
Detects circular dependencies between build stages.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
Detects cycles in the stage dependency graph. A cycle occurs when stages form mutual dependencies
through `COPY --from=`, `RUN --mount from=`, or `FROM ` references.
Circular dependencies always cause build failures because no stage in the cycle can finish
building before the others — each waits for output from another.
Common causes:
* A refactoring accidentally swaps stage references
* An AI-generated patch links stages in both directions
* A copy-paste introduces a forward `COPY --from` that mirrors an existing backward reference
## Examples
### Bad
```dockerfile theme={null}
# builder copies from runtime, runtime copies from builder → cycle
FROM golang:1.22-alpine AS builder
COPY --from=runtime /usr/local/bin/tini /usr/local/bin/tini
RUN go build -o /app ./cmd/server
FROM alpine:3.19 AS runtime
COPY --from=builder /app /usr/local/bin/app
RUN apk add --no-cache ca-certificates
FROM runtime
ENTRYPOINT ["/usr/local/bin/app"]
```
### Good
```dockerfile theme={null}
# Dependencies flow in one direction: deps → builder → runtime
FROM golang:1.22-alpine AS builder
RUN go build -o /app ./cmd/server
FROM alpine:3.19 AS runtime
RUN apk add --no-cache ca-certificates
COPY --from=builder /app /usr/local/bin/app
FROM runtime
ENTRYPOINT ["/usr/local/bin/app"]
```
## Configuration
```toml theme={null}
[rules.tally.circular-stage-deps]
severity = "error" # Options: "off", "error", "warning", "info", "style"
```
# tally/consistent-indentation
Source: https://tally.wharflab.com/rules/tally/consistent-indentation
Enforces consistent indentation for Dockerfile build stages.
Enforces consistent indentation for Dockerfile build stages.
| Property | Value |
| -------- | ------------------ |
| Severity | Style |
| Category | Style |
| Default | Off (experimental) |
| Auto-fix | Yes (safe) |
## Description
Enforces consistent indentation to visually separate build stages in multi-stage Dockerfiles. This rule always uses **tabs** for indentation.
**Behavior depends on the number of stages:**
* **Multi-stage** (2+ FROM instructions): Commands within each stage must be indented with 1 tab. FROM lines remain at column 0.
* **Single-stage** (1 FROM instruction): All indentation is removed — tabs, spaces, or any mix. Since there is no stage structure to communicate,
indenting commands adds noise. The auto-fix strips all leading whitespace from every instruction.
### Why tabs only?
Docker heredoc syntax (`<<-`) strips **leading tabs** from body lines. Spaces have no equivalent shell whitespace treatment — using them for
indentation would corrupt heredoc content when `<<-` is applied. Because this rule must convert `<<` to `<<-` when adding indentation to heredoc
instructions, only tabs produce correct results.
```dockerfile theme={null}
FROM alpine:3.20
COPY <<-EOF /etc/config
key=value
other=setting
EOF
```
With spaces, `<<-` cannot strip indentation, so the content would retain unwanted leading whitespace.
## Companion editor configuration
Use [EditorConfig integration](/integrations/editorconfig) to configure IDEs and editors with the same tab indentation style recommended for
Dockerfiles and Containerfiles.
### Multi-stage (indentation required)
```dockerfile theme={null}
FROM golang:1.23 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app
FROM alpine:3.20
COPY --from=builder /app /usr/local/bin/app
ENTRYPOINT ["app"]
```
### Single-stage (no indentation)
```dockerfile theme={null}
FROM alpine:3.20
RUN apk add --no-cache curl
COPY . /app
CMD ["./app"]
```
## Examples
### Bad (multi-stage without indentation)
```dockerfile theme={null}
FROM golang:1.23 AS builder
WORKDIR /src
RUN go build -o /app
FROM alpine:3.20
COPY --from=builder /app /app
```
### Good (multi-stage with tab indentation)
```dockerfile theme={null}
FROM golang:1.23 AS builder
WORKDIR /src
RUN go build -o /app
FROM alpine:3.20
COPY --from=builder /app /app
```
### Bad (single-stage with indentation)
In a single-stage Dockerfile, indentation is unnecessary and will be removed by `--fix`:
```dockerfile theme={null}
# Before (violation: unexpected indentation)
FROM alpine:3.20
RUN apk add curl
COPY . /app
# After --fix (indentation removed)
FROM alpine:3.20
RUN apk add curl
COPY . /app
```
## Configuration
Enable the rule (no configurable options — tabs are always used):
```toml theme={null}
[rules.tally.consistent-indentation]
severity = "style"
```
## Auto-fix
This rule provides safe auto-fixes that adjust indentation:
* **Multi-stage**: Adds 1 tab indentation to commands within stages
* **Single-stage**: Removes all leading whitespace (tabs and spaces) from commands
* **Style correction**: Replaces wrong indent characters (e.g., spaces to tabs)
* **Heredoc `<<-` conversion**: When tab indentation is applied to a heredoc instruction (`RUN <` ancestry chain. If a parent stage sets a
non-root `USER` that flows into a child stage via `FROM`, `COPY`/`ADD` in the
child stage without `--chown` will trigger the rule.
## Relationship to other rules
| | `tally/copy-after-user-without-chown` | `tally/prefer-copy-chmod` |
| ---------- | -------------------------------------------- | -------------------------- |
| Fires when | COPY/ADD after non-root USER without --chown | COPY followed by RUN chmod |
| Fix | Adds `--chown=` or moves USER | Merges into `COPY --chmod` |
| Scope | All stages | All stages |
Both rules can fire on the same `COPY` instruction. Their fixes compose
correctly: the result is `COPY --chown=user --chmod=mode file /dest`.
## Windows stages
On Windows containers `--chown` is silently ignored (see
[`tally/windows/no-chown-flag`](./windows/no-chown-flag)), so the "add
`--chown`" fix is suppressed. The rule still fires — the ownership confusion is
real — but only the "move `USER`" rearrangement fix is offered.
## Auto-fix
Two fix alternatives are offered:
1. **Add `--chown=`** (preferred, safe): inserts `--chown=` to
match the active `USER`, fixing the ownership mismatch directly.
2. **Move `USER` after COPY/ADD** (safe): relocates the `USER` instruction to
just before the first `RUN` or `WORKDIR` that follows. This is a semantic
no-op because `COPY`/`ADD` ownership is always `root:root` regardless of
`USER`. It clarifies that `USER` only affects `RUN`, `WORKDIR`, and runtime
identity. This alternative is only offered when no `RUN` or `WORKDIR` exists
between the `USER` and the `COPY`/`ADD`.
## References
* [Dockerfile reference -- USER](https://docs.docker.com/reference/dockerfile/#user)
* [Dockerfile reference -- COPY --chown](https://docs.docker.com/reference/dockerfile/#copy---chown)
* [Dockerfile reference -- ADD --chown](https://docs.docker.com/reference/dockerfile/#add---chown)
* [Docker Blog -- Understanding the Docker USER Instruction](https://www.docker.com/blog/understanding-the-docker-user-instruction/)
## Examples
### Bad
```dockerfile theme={null}
# COPY after USER without --chown: files are root-owned
FROM ubuntu:22.04
RUN useradd -r appuser
USER appuser
COPY app /app
CMD ["/app"]
```
```dockerfile theme={null}
# ADD after USER without --chown
FROM ubuntu:22.04
USER 1000:1000
ADD config.tar.gz /etc/app/
RUN setup.sh
```
### Good
```dockerfile theme={null}
# Explicit --chown matches the active USER
FROM ubuntu:22.04
RUN useradd -r appuser
USER appuser
COPY --chown=appuser app /app
CMD ["/app"]
```
```dockerfile theme={null}
# USER placed after COPY, before RUN (clarifies intent)
FROM ubuntu:22.04
RUN useradd -r appuser
COPY app /app
USER appuser
RUN setup.sh
CMD ["/app"]
```
```dockerfile theme={null}
# Ownership managed via RUN chown (suppressed)
FROM ubuntu:22.04
RUN useradd -r appuser
USER appuser
COPY app /app
RUN chown -R appuser:appuser /app
CMD ["/app"]
```
```dockerfile theme={null}
# Numeric UID with --chown
FROM ubuntu:22.04
USER 1000:1000
COPY --chown=1000:1000 app /app
CMD ["/app"]
```
## Configuration
```toml theme={null}
[rules.tally.copy-after-user-without-chown]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
# tally/copy-from-empty-scratch-stage
Source: https://tally.wharflab.com/rules/tally/copy-from-empty-scratch-stage
Detects COPY --from referencing a scratch stage with no file-producing instructions.
Detects COPY --from referencing a scratch stage with no file-producing instructions.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
## Description
Detects `COPY --from=` instructions where the source stage is `FROM scratch` and contains
no `ADD`, `COPY`, or `RUN` instructions. Since scratch stages start with an empty filesystem,
any `COPY --from` referencing such a stage is guaranteed to fail at build time.
Common causes:
* A stage was renamed or deleted during a refactor, leaving an empty placeholder
* An AI patch accidentally removed the instructions that populated the stage
* A `COPY`/`RUN` was moved to a different stage but the `COPY --from` reference wasn't updated
Instructions like `ENV`, `LABEL`, `EXPOSE`, `WORKDIR`, and `USER` do not produce filesystem
content and are not considered file-producing for this check.
## Examples
### Bad
```dockerfile theme={null}
# "artifacts" stage is scratch with no file-producing instructions
FROM scratch AS artifacts
FROM alpine:3.19
COPY --from=artifacts /out/app /usr/local/bin/app
```
### Good
```dockerfile theme={null}
FROM golang:1.22 AS builder
RUN go build -o /out/app ./...
# "artifacts" stage has a COPY that populates it
FROM scratch AS artifacts
COPY --from=builder /out/app /out/app
FROM alpine:3.19
COPY --from=artifacts /out/app /usr/local/bin/app
```
## Related rules
* [`tally/shell-run-in-scratch`](./shell-run-in-scratch) — a scratch stage with only a shell-form
`RUN` is not considered empty by this rule (any `RUN` counts as file-producing). The
`shell-run-in-scratch` rule warns about the failing `RUN` instead. If the user removes that `RUN`,
this rule will then fire on downstream `COPY --from` references.
## Configuration
```toml theme={null}
[rules.tally.copy-from-empty-scratch-stage]
severity = "error" # Options: "off", "error", "warning", "info", "style"
```
# tally/curl-should-follow-redirects
Source: https://tally.wharflab.com/rules/tally/curl-should-follow-redirects
curl commands should include `--location` (or `--follow`) to follow HTTP redirects.
curl commands should include `--location` (or `--follow`) to follow HTTP redirects.
| Property | Value |
| -------- | -------------------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
Flags `curl` commands in `RUN` instructions that are missing a redirect-following flag.
Without such a flag, curl will not follow HTTP redirects (301, 302, 307, 308), which can
cause downloads to silently fail when URLs are relocated.
Other Dockerfile download mechanisms follow redirects by default:
* **`ADD `** follows up to 10 redirects (Go `net/http` default behavior)
* **`wget`** follows up to 20 redirects by default
### `--location` vs `--follow`
The fix depends on how curl is invoked:
* **`--location`** (`-L`) — suggested for standard downloads (GET, POST, PUT, or no `-X`).
This is the classic redirect flag available in all curl versions.
* **`--follow`** — suggested when `-X`/`--request` specifies a method other than GET, POST,
or PUT (e.g., DELETE, PATCH, QUERY). `--location` changes non-GET methods to GET on
301/302 redirects, which breaks these methods. `--follow` (curl 8.16.0+) preserves the
HTTP method across redirects.
## Examples
### Before (violation)
```dockerfile theme={null}
FROM ubuntu:22.04
RUN curl -fsSo /tmp/file.tar.gz https://example.com/file.tar.gz
RUN curl -X DELETE https://example.com/api/item/123
```
### After (fixed with --fix --fix-unsafe)
```dockerfile theme={null}
FROM ubuntu:22.04
RUN curl --location -fsSo /tmp/file.tar.gz https://example.com/file.tar.gz
RUN curl --follow -X DELETE https://example.com/api/item/123
```
## Exceptions
The rule does **not** trigger when:
* `-L` or `--location` is already present (including combined flags like `-fsSL`)
* `--location-trusted` is present (implies redirect following)
* `--follow` is already present (curl 8.16.0+)
* All URL arguments point to IP addresses (e.g., `http://127.0.0.1:8080/health`,
`http://10.0.0.1/api`), since local/internal services typically don't redirect
* The curl command is a non-transfer invocation (`--help`, `--version`, `--manual`)
where redirect flags have no effect
## Limitations
* Only detects `curl` commands directly visible to the shell parser; commands inside
variables or dynamically constructed strings are not analyzed
* Skips non-POSIX shells (e.g., PowerShell stages)
## References
* [curl `--location` documentation](https://curl.se/docs/manpage.html#-L)
* [Follow redirects, but differently](https://daniel.haxx.se/blog/2025/08/06/follow-redirects-but-differently/) — curl 8.16.0 `--follow` flag
* [Dockerfile `ADD` reference](https://docs.docker.com/reference/dockerfile/#add)
# tally/eol-last
Source: https://tally.wharflab.com/rules/tally/eol-last
Enforces a newline at the end of non-empty files.
Enforces a newline at the end of non-empty files.
| Property | Value |
| -------- | ---------- |
| Severity | Style |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (safe) |
## Description
POSIX defines a line as a sequence of characters ending with a newline. Files that lack a trailing newline can cause issues with file concatenation,
produce messy diffs in version control, and interfere with shell prompts when printed with `cat`. This rule enforces (or prohibits) a final newline in
Dockerfiles, mirroring ESLint's [`eol-last`](https://eslint.style/rules/eol-last) rule.
In the default `"always"` mode, the rule reports a violation when a non-empty file does not end with `\n`. In `"never"` mode, it reports when a file
does end with `\n`.
Empty files (zero bytes) are always ignored.
## Companion editor configuration
Use [EditorConfig integration](/integrations/editorconfig) to configure IDEs and editors to insert the final newline before tally reports it.
## Examples
### Bad (mode: "always")
```dockerfile theme={null}
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
ENTRYPOINT ["/app"]⌁
```
(where `⌁` marks the end of file with no trailing newline)
### Good (mode: "always")
```dockerfile theme={null}
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
ENTRYPOINT ["/app"]
```
## Configuration
Default (no config needed):
```toml theme={null}
# Enabled by default with mode = "always"
```
Disable the rule:
```toml theme={null}
[rules.tally.eol-last]
severity = "off"
```
Require files to **not** end with a newline:
```toml theme={null}
[rules.tally.eol-last]
mode = "never"
```
## Options
| Option | Type | Default | Description |
| ------ | ------ | ---------- | ----------------------------------------------------------- |
| `mode` | string | `"always"` | `"always"` requires a final newline; `"never"` prohibits it |
## Auto-fix
This rule provides a safe auto-fix:
```bash theme={null}
tally lint --fix Dockerfile
```
* In `"always"` mode, a missing final newline is appended.
* In `"never"` mode, the trailing newline is removed.
## Related Rules
* [`tally/no-multiple-empty-lines`](./no-multiple-empty-lines) — controls excess blank lines at the end (and beginning) of files
* [`tally/no-trailing-spaces`](./no-trailing-spaces) — removes trailing whitespace on individual lines
# tally/epilogue-order
Source: https://tally.wharflab.com/rules/tally/epilogue-order
Runtime-configuration instructions should appear at the end of each output stage in canonical order.
Runtime-configuration instructions should appear at the end of each output stage in canonical order.
| Property | Value |
| -------- | ---------- |
| Severity | Style |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (safe) |
## Description
Dockerfiles should end each output stage with runtime-configuration instructions in a canonical order:
**STOPSIGNAL, HEALTHCHECK, ENTRYPOINT, CMD**. These epilogue instructions configure how the container
runs rather than how the image is built, and placing them at the end of the stage makes the Dockerfile
easier to read and maintain.
This rule checks two conditions for each applicable stage:
1. **Position**: All epilogue instructions must appear at the end of the stage (no build instructions like RUN, COPY, ENV after them)
2. **Order**: Among the epilogue instructions, they must appear in canonical order
**Applicable stages**: The final stage and any stage with no dependents (not referenced by `COPY --from`, `FROM`, or `RUN --mount=from`). Intermediate
builder stages are skipped since they typically don't use epilogue instructions.
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
CMD ["/app", "serve"]
RUN apk add --no-cache ca-certificates
ENTRYPOINT ["/app"]
```
CMD appears before RUN (position violation), and CMD comes before ENTRYPOINT (order violation).
### Good
```dockerfile theme={null}
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
ENTRYPOINT ["/app"]
CMD ["serve"]
```
All build instructions come first, then epilogue instructions in canonical order.
### Multi-stage (builder skipped)
```dockerfile theme={null}
FROM golang:1.21 AS builder
RUN go build -o /app
# No violation here - builder stage is skipped
FROM alpine:3.20
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
CMD ["serve"]
```
## Auto-fix
This rule provides a safe auto-fix that moves epilogue instructions to the end of the stage in canonical order:
```bash theme={null}
tally lint --fix Dockerfile
```
The fix:
* Removes each epilogue instruction from its current position
* Inserts all epilogue instructions at the end of the stage in canonical order
* Preserves preceding comments and continuation lines
When duplicate epilogue instructions of the same type exist (e.g., two CMD instructions), the fix is skipped for safety. The
`MultipleInstructionsDisallowed` rule handles duplicate removal.
## Cross-rule interactions
| Rule | Interaction |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tally/newline-between-instructions` | Runs after this rule's fix (priority 200 vs 175). Normalizes blank lines between the reordered epilogue instructions. The combined result is stable. |
| `buildkit/MultipleInstructionsDisallowed` | Runs before (sync fix). Removes duplicate CMD/ENTRYPOINT/HEALTHCHECK. If duplicates remain (rule disabled), this rule skips the fix for safety. |
# tally/gpu/cuda-version-mismatch
Source: https://tally.wharflab.com/rules/tally/gpu/cuda-version-mismatch
CUDA-specific pip/conda wheel version does not match the base image's CUDA toolkit.
CUDA-specific pip/conda wheel version does not match the base image's CUDA toolkit.
| Property | Value |
| -------- | ---------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (suggestion) |
## Description
Detects `RUN` instructions where pip, uv, or conda installs reference a CUDA version
that does not match the base image's CUDA toolkit version. A mismatch can cause:
* Silent fallback to CPU execution
* Runtime CUDA errors or build failures
* Subtle performance degradation
The rule supports four detection paths:
1. **pip/pip3 package suffixes** -- `torch==2.0.0+cu118`
2. **pip/pip3/uv index URLs** -- `--index-url https://download.pytorch.org/whl/cu118`
3. **uv `--torch-backend`** -- `--torch-backend cu118`
4. **conda/mamba/micromamba** -- `pytorch-cuda=11.8` or `cudatoolkit=11.8`
## Why this matters
* **Silent CPU fallback** -- PyTorch may load but silently use CPU instead of GPU
when the CUDA wheel version doesn't match the available CUDA runtime
* **Runtime crashes** -- mismatched CUDA versions can produce cryptic `CUDA error`
messages at runtime
* **Copy-paste bugs** -- the most common real-world pattern is upgrading the base
image CUDA version but forgetting to update the pip `--index-url` suffix
## Examples
### Violation
```dockerfile theme={null}
# Base provides CUDA 12.1, but pip installs CUDA 11.8 wheels
FROM nvidia/cuda:12.1.0-devel-ubuntu20.04
RUN pip install --index-url https://download.pytorch.org/whl/cu118 torch
```
```dockerfile theme={null}
# Base provides CUDA 12.4, but --extra-index-url uses CUDA 11.8
FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
RUN pip install --extra-index-url https://download.pytorch.org/whl/cu118 xformers
```
```dockerfile theme={null}
# Base provides CUDA 12.2, but conda installs CUDA 11.8 pytorch
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN conda install -y pytorch pytorch-cuda=11.8 -c pytorch -c nvidia
```
```dockerfile theme={null}
# Base provides CUDA 12.4, but uv uses CUDA 11.8 backend
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04
RUN uv pip install --torch-backend cu118 torch
```
### No violation
```dockerfile theme={null}
# Exact match: CUDA 11.8 base with cu118 wheels
FROM nvidia/cuda:11.8.0-base-ubuntu22.04
RUN pip install --index-url https://download.pytorch.org/whl/cu118 torch
```
```dockerfile theme={null}
# Forward-compatible: cu121 wheel on CUDA 12.6 base (older minor is fine)
FROM nvidia/cuda:12.6.0-runtime-ubuntu22.04
RUN pip install --index-url https://download.pytorch.org/whl/cu121 torch
```
```dockerfile theme={null}
# Non-CUDA base: rule does not apply
FROM ubuntu:22.04
RUN pip install --index-url https://download.pytorch.org/whl/cu121 torch
```
## Version compatibility
The rule uses NVIDIA's forward compatibility guarantee:
* **Same major, wheel minor at or below base minor** -- OK (forward-compatible)
* **Same major, wheel minor above base minor** -- Mismatch (wheel needs newer CUDA)
* **Different major** -- Always a mismatch
### CUDA suffix mapping
| Suffix | CUDA version |
| ------- | ------------ |
| `cu118` | 11.8 |
| `cu121` | 12.1 |
| `cu124` | 12.4 |
| `cu126` | 12.6 |
| `cu128` | 12.8 |
## Fix suggestions
The rule offers two fix alternatives:
1. **Update the wheel/index to match the base image** -- preferred when the base image
has a higher CUDA version (the common case: base was upgraded but pip URL was not)
2. **Update the base image to match the wheel** -- preferred when the wheel targets a
newer CUDA version than the base
Both fixes use `FixSuggestion` safety -- verify the target wheel or image tag exists
before applying.
## Applicability
This rule fires when:
* The base image is `nvidia/cuda:*` (or `docker.io/nvidia/cuda:*`)
* The CUDA version can be parsed from the image tag
* A CUDA version reference is found in a `RUN` instruction
* In multi-stage builds, stages that inherit from a CUDA-based parent stage
(`FROM builder` where `builder` uses `nvidia/cuda:*`) also trigger the rule.
In this case, only the "update wheel/index" fix is offered -- the "update base
image" fix is skipped since the `FROM` line references a stage name, not an
image tag.
It does **not** fire on:
* Non-NVIDIA base images
* Digest-only or ARG-based image tags (version cannot be determined)
* pip installs without CUDA suffixes or index URLs
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.cuda-version-mismatch]
severity = "warning"
```
## References
* [PyTorch Previous Versions](https://pytorch.org/get-started/previous-versions/)
* [NVIDIA CUDA Docker Hub](https://hub.docker.com/r/nvidia/cuda/)
* [uv PyTorch integration](https://docs.astral.sh/uv/guides/integration/pytorch/)
* [NVIDIA CUDA Compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/)
# tally/gpu/no-buildtime-gpu-queries
Source: https://tally.wharflab.com/rules/tally/gpu/no-buildtime-gpu-queries
GPU hardware is not available during `docker build`; runtime GPU queries in `RUN` will fail or return misleading results.
GPU hardware is not available during `docker build`; runtime GPU queries in `RUN` will fail or return misleading results.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | No |
## Description
Detects `RUN` instructions that query GPU hardware at build time. GPU devices are not attached during a normal
`docker build`, so commands like `nvidia-smi` and runtime framework checks like `torch.cuda.is_available()` will
either fail outright or return misleading results (e.g., zero devices, `False`).
This rule does **not** fire on `CMD` or `ENTRYPOINT`, where GPU queries are expected to run at container startup.
## Why this matters
* **Build failures** -- `nvidia-smi` will exit non-zero when no GPU is present, breaking the build
* **Silent wrong results** -- `torch.cuda.is_available()` returns `False` at build time, which can cause downstream
logic to skip GPU code paths or produce incorrect configuration
* **Misleading smoke tests** -- a passing build does not mean GPU support works; the check must happen at runtime
* **Official guidance** -- Hugging Face explicitly warns that GPU hardware is not available during `docker build`
## Detected patterns
### GPU query commands
| Command | Description |
| --------------------- | --------------------------------------------------------- |
| `nvidia-smi` | NVIDIA System Management Interface (queries GPU hardware) |
| `nvidia-debugdump` | NVIDIA debug information dump |
| `nvidia-persistenced` | NVIDIA persistence daemon (requires GPU) |
### Python/ML framework runtime checks
| Pattern | Description |
| ----------------------------------- | ---------------------------------- |
| `torch.cuda.is_available()` | PyTorch CUDA availability check |
| `torch.cuda.device_count()` | PyTorch CUDA device count |
| `torch.cuda.get_device_name()` | PyTorch CUDA device name query |
| `torch.cuda.current_device()` | PyTorch current CUDA device |
| `tf.test.is_gpu_available()` | TensorFlow GPU availability check |
| `tf.config.list_physical_devices()` | TensorFlow physical device listing |
## Examples
### Violation
```dockerfile theme={null}
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN nvidia-smi
```
```dockerfile theme={null}
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN python3 -c "import torch; print(torch.cuda.is_available())"
```
```dockerfile theme={null}
FROM tensorflow/tensorflow:2.14.0-gpu
RUN python -c "import tensorflow as tf; print(tf.test.is_gpu_available())"
```
### No violation
```dockerfile theme={null}
# GPU query in CMD runs at container startup — correct
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
CMD ["nvidia-smi", "--loop=10"]
```
```dockerfile theme={null}
# GPU query in ENTRYPOINT runs at container startup — correct
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENTRYPOINT ["python3", "-c", "import torch; print(torch.cuda.is_available())"]
```
```dockerfile theme={null}
# Normal package installation — no GPU query
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y python3 python3-pip
```
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.no-buildtime-gpu-queries]
severity = "error"
```
## References
* [Hugging Face Docker Spaces GPU docs](https://huggingface.co/docs/hub/main/en/spaces-sdks-docker) -- explicitly warns GPU is unavailable during
build
* [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html)
# tally/gpu/no-container-runtime-in-image
Source: https://tally.wharflab.com/rules/tally/gpu/no-container-runtime-in-image
NVIDIA container runtime packages belong on the host, not inside the image.
NVIDIA container runtime packages belong on the host, not inside the image.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | No |
## Description
Detects `RUN` instructions that install NVIDIA Container Toolkit host-side packages
(`nvidia-container-toolkit`, `nvidia-docker2`, `libnvidia-container*`) inside the
container image via a package manager (`apt`, `apt-get`, `yum`, `dnf`, `microdnf`, `apk`).
These packages are part of the
[NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html), which hooks into the
container runtime on the **host** to expose GPUs to containers. Installing them inside the image does not make the image GPU-enabled, wastes image
layers, and can mask the real requirement: that the host or cluster runtime must have the toolkit configured.
## Why this matters
* **Wrong layer** -- the toolkit runs on the host/node, not in the container
* **Does not enable GPU access** -- GPU device injection is handled by the container runtime (e.g., `nvidia-container-runtime`, CDI), not by packages
inside the image
* **Bloats the image** -- the toolkit pulls in host-specific libraries that serve no purpose in the container filesystem
* **Hides requirements** -- a working GPU setup depends on the host runtime configuration, not on image contents
## Examples
### Violation
```dockerfile theme={null}
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nvidia-container-toolkit
```
```dockerfile theme={null}
FROM centos:7
RUN yum install -y nvidia-docker2
```
```dockerfile theme={null}
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y libnvidia-container1
```
### No violation
```dockerfile theme={null}
# GPU base image with application packages only
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y python3 python3-pip
```
```dockerfile theme={null}
# Non-GPU image with unrelated packages
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl wget git
```
## Matched packages
| Package | Description |
| -------------------------- | ------------------------------------------------------------------------------------------- |
| `nvidia-container-toolkit` | Main toolkit meta-package (CLI, hook, CDI generator) |
| `nvidia-docker2` | Legacy wrapper for Docker runtime integration |
| `libnvidia-container*` | Low-level container GPU library (`libnvidia-container1`, `libnvidia-container-tools`, etc.) |
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.no-container-runtime-in-image]
severity = "warning"
```
## References
* [NVIDIA Container Toolkit Install Guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
* [NVIDIA Container Toolkit Architecture](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/arch-overview.html)
# tally/gpu/no-hardcoded-visible-devices
Source: https://tally.wharflab.com/rules/tally/gpu/no-hardcoded-visible-devices
GPU visibility is deployment policy; hardcoding it in the image reduces portability.
GPU visibility is deployment policy; hardcoding it in the image reduces portability.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Partial |
## Description
Detects `ENV` instructions that hardcode GPU device visibility variables (`NVIDIA_VISIBLE_DEVICES`,
`CUDA_VISIBLE_DEVICES`) inside the image. GPU visibility is deployment policy that should be set at
runtime via `docker run --gpus`, `NVIDIA_VISIBLE_DEVICES` in the orchestrator, or similar mechanisms
\-- not baked into the container image.
## Why this matters
* **Portability** -- images with hardcoded device indices or UUIDs cannot run on hosts with different GPU topologies
without rebuilding
* **Orchestrator conflict** -- Kubernetes device plugins, Slurm, and other schedulers set GPU visibility externally;
image-level settings can conflict with or override orchestrator intent
* **Redundancy** -- official `nvidia/cuda` base images already set `NVIDIA_VISIBLE_DEVICES=all` via image labels;
re-declaring it in the Dockerfile is pure noise
## What is flagged
| Pattern | Flagged? | Fix safety |
| --------------------------------------------------------- | --------------------------------------- | --------------------------- |
| `ENV NVIDIA_VISIBLE_DEVICES=all` on `nvidia/cuda:*` base | Yes (redundant) | `FixSafe` -- safe to delete |
| `ENV NVIDIA_VISIBLE_DEVICES=0` or `=0,1` (device indices) | Yes | `FixSuggestion` |
| `ENV NVIDIA_VISIBLE_DEVICES=GPU-` or `MIG-` | Yes | `FixSuggestion` |
| `ENV CUDA_VISIBLE_DEVICES=` | Yes | `FixSuggestion` |
| `ENV NVIDIA_VISIBLE_DEVICES=all` on non-CUDA base | No -- intentional for custom GPU images | -- |
| `ENV NVIDIA_VISIBLE_DEVICES=none` / `void` / empty | No -- intentional disable signal | -- |
| `ENV NVIDIA_VISIBLE_DEVICES=${VAR}` (variable reference) | No -- parameterized, not hardcoded | -- |
| `ENV CUDA_VISIBLE_DEVICES=none` / `NoDevFiles` / empty | No -- intentional disable | -- |
## Examples
### Violation
```dockerfile theme={null}
# Redundant: nvidia/cuda already sets NVIDIA_VISIBLE_DEVICES=all
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENV NVIDIA_VISIBLE_DEVICES=all
```
```dockerfile theme={null}
# Hardcoded device indices make the image non-portable
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENV NVIDIA_VISIBLE_DEVICES=0,1
```
```dockerfile theme={null}
# CUDA_VISIBLE_DEVICES bakes deployment policy into the image
FROM ubuntu:22.04
ENV CUDA_VISIBLE_DEVICES=0
```
```dockerfile theme={null}
# GPU UUIDs are host-specific
FROM ubuntu:22.04
ENV NVIDIA_VISIBLE_DEVICES=GPU-aaaa-bbbb-cccc-dddd-eeee-ffffffffffff
```
### No violation
```dockerfile theme={null}
# NVIDIA_VISIBLE_DEVICES=all on a non-CUDA base is intentional
FROM ubuntu:22.04
ENV NVIDIA_VISIBLE_DEVICES=all
```
```dockerfile theme={null}
# Disable signals are intentional
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENV NVIDIA_VISIBLE_DEVICES=none
```
```dockerfile theme={null}
# Variable references are parameterized, not hardcoded
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ARG GPU_DEVICES=all
ENV NVIDIA_VISIBLE_DEVICES=${GPU_DEVICES}
```
## Auto-fix behavior
The rule offers two fix safety levels:
* **`FixSafe`** (applied with `--fix`): removes the redundant `NVIDIA_VISIBLE_DEVICES=all` on `nvidia/cuda` base
images. This is 100% behavior-preserving because the base image already sets this value.
* **`FixSuggestion`** (applied with `--fix --fix-unsafe`): removes hardcoded device indices, UUIDs, or
`CUDA_VISIBLE_DEVICES` values. This improves portability but changes deployment semantics -- the user must
ensure GPU visibility is provided at runtime.
For multi-key `ENV` instructions, only the flagged key is removed; other keys are preserved.
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.no-hardcoded-visible-devices]
severity = "warning"
```
## References
* [NVIDIA Container Toolkit: Environment Variables](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html)
* [CUDA Environment Variables](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#env-vars)
# tally/gpu/no-redundant-cuda-install
Source: https://tally.wharflab.com/rules/tally/gpu/no-redundant-cuda-install
CUDA packages are already provided by the nvidia/cuda base image.
CUDA packages are already provided by the nvidia/cuda base image.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | No |
## Description
Detects `RUN` instructions that install CUDA userspace packages via a package manager
(`apt`, `apt-get`, `yum`, `dnf`, `microdnf`, `apk`) in stages that already inherit from
`nvidia/cuda:*`.
The rule is **flavor-aware**: it parses the image tag to determine the variant (`base`,
`runtime`, or `devel`) and only flags packages that the variant already includes. For
example, installing `cuda-toolkit` on a `runtime` image is legitimate (runtime does not
include the toolkit), but installing `cuda-runtime` on a `runtime` image is redundant.
## Why this matters
* **Redundant work** -- the base image already provides the CUDA stack for the selected variant
* **Version drift** -- the package manager may install a different CUDA version than the one
baked into the base image, causing subtle incompatibilities
* **Image bloat** -- duplicate CUDA libraries waste space in the image layers
* **Maintenance burden** -- two sources of truth for the CUDA version make upgrades harder
## Examples
### Violation
```dockerfile theme={null}
# devel includes the full toolkit -- reinstalling is redundant
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN apt-get update && apt-get install -y cuda-toolkit
```
```dockerfile theme={null}
# cudnn tag already includes cuDNN -- reinstalling is redundant
FROM nvidia/cuda:12.2.0-cudnn-devel-ubuntu22.04
RUN apt-get update && apt-get install -y libcudnn8
```
```dockerfile theme={null}
# runtime includes cuda-runtime -- reinstalling is redundant
FROM nvidia/cuda:12.2.0-runtime-centos7
RUN yum install -y cuda-runtime-12-2
```
### No violation
```dockerfile theme={null}
# runtime does NOT include the toolkit -- this install is legitimate
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y cuda-toolkit
```
```dockerfile theme={null}
# nvidia/cuda base with application packages only
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y python3 python3-pip
```
```dockerfile theme={null}
# Non-nvidia/cuda base -- intentional CUDA install is not flagged
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nvidia-cuda-toolkit
```
## Flavor-aware matching
The rule maps packages to the nvidia/cuda image variant that includes them:
| Package | Included in | Match type |
| -------------------------------------------------- | -------------------- | ------------ |
| `cuda`, `cuda-runtime` | base, runtime, devel | Exact |
| `cuda-runtime-*`, `cuda-compat-*` | base, runtime, devel | Prefix |
| `cuda-libraries`, `cuda-libraries-*` | runtime, devel | Exact/Prefix |
| `nvidia-cuda-toolkit`, `cuda-toolkit`, `cuda-nvcc` | devel | Exact |
| `cuda-toolkit-*`, `cuda-nvcc-*` | devel | Prefix |
| `libcudnn*` | cudnn tags only | Prefix |
TensorRT packages (`tensorrt*`) are never flagged because standard `nvidia/cuda` tags do
not include TensorRT.
When the tag cannot be parsed (e.g., digest-only or ARG-based), the rule defaults to
`devel` to avoid false positives.
## Applicability
This rule only fires on stages where the base image is `nvidia/cuda:*` (or `docker.io/nvidia/cuda:*`).
It does **not** fire on:
* Stages with a non-NVIDIA base image (e.g., `ubuntu:22.04`)
* Stages using other NVIDIA images (e.g., `nvcr.io/nvidia/pytorch:*`, `nvidia/cudagl:*`)
* Stages that reference another build stage (`FROM builder`)
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.no-redundant-cuda-install]
severity = "warning"
```
## References
* [NVIDIA CUDA Docker Hub](https://hub.docker.com/r/nvidia/cuda/)
* [NVIDIA CUDA image variants](https://gitlab.com/nvidia/container-images/cuda/blob/master/doc/supported-tags.md)
# tally/gpu/prefer-minimal-driver-capabilities
Source: https://tally.wharflab.com/rules/tally/gpu/prefer-minimal-driver-capabilities
`NVIDIA_DRIVER_CAPABILITIES=all` exposes more driver surface than most workloads need; prefer a minimal capability set.
`NVIDIA_DRIVER_CAPABILITIES=all` exposes more driver surface than most workloads need; prefer a minimal capability set.
| Property | Value |
| -------- | --------------- |
| Severity | Info |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Suggestion only |
## Description
Detects `ENV NVIDIA_DRIVER_CAPABILITIES=all` in Dockerfiles. The `all` capability set mounts every
NVIDIA driver library and binary into the container, but most ML and CUDA workloads only need
`compute,utility` (NVIDIA's documented default). A smaller set follows the principle of least
privilege and avoids potential compatibility issues.
## Why this matters
* **Least privilege** -- `all` exposes driver capabilities (`graphics`, `video`, `display`, `compat32`)
that most inference and training workloads never use
* **Compatibility** -- mounting unnecessary driver components can surface driver/library version
conflicts in environments where the host driver differs from what the image expects
* **Clarity** -- explicitly listing needed capabilities documents the workload's actual requirements
## What is flagged
| Pattern | Flagged? | Fix safety |
| ------------------------------------------------------------ | --------------------- | --------------- |
| `ENV NVIDIA_DRIVER_CAPABILITIES=all` | Yes | `FixSuggestion` |
| `ENV NVIDIA_DRIVER_CAPABILITIES=ALL` (case-insensitive) | Yes | `FixSuggestion` |
| `ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility` | No -- already minimal | -- |
| `ENV NVIDIA_DRIVER_CAPABILITIES=graphics,compute,utility` | No -- intentional | -- |
| `ENV NVIDIA_DRIVER_CAPABILITIES=` (empty) | No | -- |
| `ENV NVIDIA_DRIVER_CAPABILITIES=${VAR}` (variable reference) | No -- parameterized | -- |
## Examples
### Violation
```dockerfile theme={null}
# Exposes all driver capabilities unnecessarily
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENV NVIDIA_DRIVER_CAPABILITIES=all
```
```dockerfile theme={null}
# Same issue on a custom GPU base image
FROM ubuntu:22.04
ENV NVIDIA_DRIVER_CAPABILITIES=all
```
### No violation
```dockerfile theme={null}
# Explicit minimal set -- preferred
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
```
```dockerfile theme={null}
# Workload that genuinely needs graphics
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ENV NVIDIA_DRIVER_CAPABILITIES=graphics,compute,utility
```
```dockerfile theme={null}
# Parameterized -- not hardcoded
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
ARG CAPS=all
ENV NVIDIA_DRIVER_CAPABILITIES=${CAPS}
```
## Auto-fix behavior
The rule offers a **`FixSuggestion`** (applied with `--fix --fix-unsafe`): replaces `all` with
`compute,utility`. This is safe for most ML/CUDA workloads but may break workloads that genuinely
need `graphics`, `video`, or `display` capabilities -- review before accepting.
For multi-key `ENV` instructions, only the `NVIDIA_DRIVER_CAPABILITIES` value is replaced; other
keys are preserved.
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.prefer-minimal-driver-capabilities]
severity = "info"
```
## References
* [NVIDIA Container Toolkit: Environment Variables](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html)
# tally/gpu/prefer-runtime-final-stage
Source: https://tally.wharflab.com/rules/tally/gpu/prefer-runtime-final-stage
Final stage uses an NVIDIA devel image without clear build-time needs; prefer a runtime or base variant for the shipped stage.
Final stage uses an NVIDIA devel image without clear build-time needs; prefer a runtime or base variant for the shipped stage.
| Property | Value |
| -------- | -------------- |
| Severity | Warning |
| Category | Best Practices |
| Default | Enabled |
| Auto-fix | Not available |
## Description
Detects when the final stage of a Dockerfile uses `nvidia/cuda:*devel*` as its base image without
obvious compile-time needs such as `nvcc`, `gcc`, `make`, or `cmake`. The `devel` variant includes
the full CUDA compiler toolchain, development headers, and static libraries, which can add several
gigabytes to the final image.
## Why this matters
* **Image size** -- `devel` images are typically 2--4 GB larger than the corresponding `runtime`
variant due to `nvcc`, development headers, and static libraries
* **Attack surface** -- shipping compiler toolchains and development headers in production images
exposes unnecessary binaries that could be leveraged in a container escape or supply-chain attack
* **Build cache efficiency** -- larger images take longer to pull, push, and layer-cache, slowing
down CI/CD pipelines
* **Best practice alignment** -- NVIDIA, Hugging Face, and major ML projects recommend using
`devel` only in builder stages and switching to `runtime` or `base` for the shipped image
## What is flagged
| Pattern | Flagged? |
| ---------------------------------------------------------------------------- | ---------------------------------------------- |
| Final stage `FROM nvidia/cuda:12.x-devel-*` with no compile signal | Yes |
| Final stage `FROM nvidia/cuda:12.x-cudnn-devel-*` with no compile signal | Yes |
| Final stage `FROM nvidia/cuda:12.x-devel-*` with `nvcc`, `gcc`, `make`, etc. | No -- legitimate build stage |
| Final stage `FROM nvidia/cuda:12.x-devel-*` with `build-essential` installed | No -- build tools present |
| Final stage `FROM nvidia/cuda:12.x-runtime-*` | No -- already a runtime variant |
| Final stage `FROM nvidia/cuda:12.x-base-*` | No -- already a minimal variant |
| Non-final stage using `devel` | No -- builder stages legitimately need `devel` |
## Examples
### Violation
```dockerfile theme={null}
# Single-stage devel with no compilation
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN pip install torch
CMD ["python", "app.py"]
```
```dockerfile theme={null}
# Multi-stage: final stage uses devel but only runs pre-built binaries
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 AS builder
RUN nvcc -o /app main.cu
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
COPY --from=builder /app /app
CMD ["/app"]
```
### No violation
```dockerfile theme={null}
# Final stage uses runtime variant
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 AS builder
RUN nvcc -o /app main.cu
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
COPY --from=builder /app /app
CMD ["/app"]
```
```dockerfile theme={null}
# Final stage uses devel but has compile signal
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
RUN nvcc -o /app main.cu
CMD ["/app"]
```
```dockerfile theme={null}
# Devel only in builder stage
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 AS builder
RUN cmake . && make
FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
COPY --from=builder /app /app
CMD ["/app"]
```
## Detection details
The rule fires only when all of the following are true:
1. The final stage base image is `nvidia/cuda:*devel*`
2. No compile signal is detected in the final stage
Compile signals that suppress the rule:
* **Commands**: `nvcc`, `gcc`, `g++`, `make`, `cmake`, `ninja`
* **Packages**: `build-essential`, `gcc`, `g++`, `make`, `cmake`, `ninja-build`
When a `COPY --from=...` instruction is present in the final stage, the violation detail notes this
as additional evidence that the stage serves as a runtime image.
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.prefer-runtime-final-stage]
severity = "warning"
```
## References
* [NVIDIA CUDA Docker images](https://hub.docker.com/r/nvidia/cuda/)
* [Hugging Face Docker Spaces GPU docs](https://huggingface.co/docs/hub/main/en/spaces-sdks-docker)
# tally/gpu/prefer-uv-over-conda
Source: https://tally.wharflab.com/rules/tally/gpu/prefer-uv-over-conda
Narrow GPU Python Dockerfiles can often be migrated from conda to uv for faster, lock-friendly installs.
Narrow GPU Python Dockerfiles can often be migrated from conda to uv for faster, lock-friendly installs.
| Property | Value |
| -------- | ------------------------ |
| Severity | Info |
| Category | Best-practices |
| Default | Enabled (experimental) |
| Auto-fix | Yes (AI AutoFix, unsafe) |
## Description
This rule fires on Dockerfiles that:
* Use a **GPU/PyTorch-oriented base image** (`nvidia/cuda:*`, `nvcr.io/nvidia/*`, `pytorch/pytorch:*cuda*`, or a stage that inherits CUDA from one of
these).
* Install Python/ML packages (e.g. `torch`, `torchvision`, `transformers`, `flash-attn`, `xformers`) via `conda`, `mamba`, or `micromamba`.
* Do **not** rely on a heavy conda environment-management workflow (no `conda env create`, no `environment.yml` / `conda-lock.yml` copied into the
image).
For that narrow, migratable slice the rule suggests an AI-assisted conversion to [uv](https://docs.astral.sh/uv/), which offers faster resolution,
explicit CUDA wheel index support, and lock-friendly reproducibility.
## Why this matters
* Conda resolution on GPU images is slow and can pull in unused Anaconda channels.
* Many GPU images use conda only as a Python package installer — uv is a closer fit with
`pip install uv && uv pip install --index-url https://download.pytorch.org/whl/cuXYZ ...`.
* uv supports explicit CUDA wheel indexes, which makes the CUDA alignment story (see also
[`tally/gpu/cuda-version-mismatch`](/rules/tally/gpu/cuda-version-mismatch)) simpler and more auditable.
## Examples
### Violation
```dockerfile theme={null}
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
RUN conda install -y pytorch pytorch-cuda=12.1 -c pytorch -c nvidia
CMD ["python"]
```
```dockerfile theme={null}
FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-devel
RUN mamba install -y numpy transformers
```
```dockerfile theme={null}
FROM nvidia/cuda:12.4.0-devel-ubuntu22.04
RUN micromamba install -y flash-attn xformers
```
### No violation
```dockerfile theme={null}
# Already uses uv.
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
RUN pip install uv && uv pip install --system --index-url https://download.pytorch.org/whl/cu121 torch
```
```dockerfile theme={null}
# Heavy conda environment workflow; migration is out of scope.
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
COPY environment.yml /app/
RUN conda env create -f /app/environment.yml
```
```dockerfile theme={null}
# CPU base image; rule does not fire.
FROM ubuntu:22.04
RUN conda install -y numpy
```
```dockerfile theme={null}
# conda installs only system packages; not a Python package workflow.
FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
RUN conda install -y gcc cmake
```
## Auto-fix
When the rule fires, it attaches an **unsafe**, async `SuggestedFix` backed by the AI AutoFix resolver. The AI agent is asked to:
* Replace conda/mamba/micromamba Python/ML installs with `uv pip install ...`.
* Install uv before its first use (via `pip install uv` or the official installer).
* Preserve the base image and OS package installs.
* Preserve runtime invariants in the final stage (`CMD`, `ENTRYPOINT`, `USER`, `WORKDIR`, `ENV`, `LABEL`, `EXPOSE`, `HEALTHCHECK`).
* Output `NO_CHANGE` if the file is better left alone (e.g. an `environment.yml`-driven workflow snuck past detection).
Applying the fix requires:
* `--fix --fix-unsafe`
* A configured ACP-capable agent in the config file (see the top-level `[ai]` section)
The resolver returns a single edit that replaces the entire Dockerfile content.
## Applicability
The rule fires when **all** of the following hold for one stage:
1. The stage is GPU-oriented:
* base image resolves to `nvidia/cuda:*` (or `nvcr.io/nvidia/*`, `nvidia/cudagl:*`), or
* base image resolves to `pytorch/pytorch:*` (or `nvcr.io/nvidia/pytorch:*`), or
* `StageFacts.CUDAMajor > 0` (inherited via a stage reference).
2. A `RUN` invokes `conda`, `mamba`, or `micromamba` with an `install` subcommand that lists at least one known Python/ML package.
3. The Dockerfile as a whole does not show signs of a heavier conda workflow:
* no `conda env create` / `mamba env create` / `micromamba env create` in any `RUN`
* no `environment.yml`, `environment.yaml`, `conda-lock.yml`, or `conda-lock.yaml` in the build context
Violations are emitted at most once per stage; the AI AutoFix rewrite is file-scoped.
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.gpu.prefer-uv-over-conda]
severity = "info"
fix = "explicit"
```
## References
* [uv: PyTorch integration](https://docs.astral.sh/uv/guides/integration/pytorch/)
* [uv: Docker integration](https://docs.astral.sh/uv/guides/integration/docker/)
* [NVIDIA CUDA image tags](https://hub.docker.com/r/nvidia/cuda/)
* [tally/gpu/cuda-version-mismatch](/rules/tally/gpu/cuda-version-mismatch)
# tally/invalid-json-form
Source: https://tally.wharflab.com/rules/tally/invalid-json-form
Arguments appear to use JSON exec-form but contain invalid JSON.
Arguments appear to use JSON exec-form but contain invalid JSON.
| Property | Value |
| -------- | ----------------------------------------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (suggestion, requires `--fix-unsafe`) |
## Description
Several Dockerfile instructions (`CMD`, `ENTRYPOINT`, `RUN`, `SHELL`, `COPY`, `ADD`,
`VOLUME`, `HEALTHCHECK CMD`, `ONBUILD `) accept JSON exec-form syntax:
```dockerfile theme={null}
CMD ["executable", "param1", "param2"]
```
When the arguments start with `[` but contain invalid JSON (unquoted strings, single
quotes, trailing commas), BuildKit's parser silently treats them as shell form. This
produces unexpected behavior:
* `CMD [bash, -lc, "echo hi"]` is treated as the shell command
`[bash, -lc, "echo hi"]` rather than exec-form `["bash", "-lc", "echo hi"]`.
* `SHELL [/bin/bash, -c]` causes a build error because `SHELL` requires valid JSON.
The auto-fix rewrites the arguments as valid JSON. It is classified as `suggestion`
because intent cannot be guaranteed -- review it before applying.
## Related Rules
* [`buildkit/JSONArgsRecommended`](../buildkit/JSONArgsRecommended) -- recommends JSON
exec-form for `CMD` and `ENTRYPOINT`. Because BuildKit falls back to shell-form when
JSON is invalid, `JSONArgsRecommended` (info severity) also fires on the same instruction.
tally's supersession processor automatically suppresses the lower-severity
`JSONArgsRecommended` violation when this rule (error severity) is present at the same
line — so users see only the more actionable `invalid-json-form` error.
## References
* [Dockerfile reference -- CMD](https://docs.docker.com/reference/dockerfile/#cmd)
* [Dockerfile reference -- ENTRYPOINT](https://docs.docker.com/reference/dockerfile/#entrypoint)
* [Dockerfile reference -- SHELL](https://docs.docker.com/reference/dockerfile/#shell)
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
# Unquoted strings -- treated as shell form
CMD [bash, -lc, "echo hello"]
# Single quotes -- not valid JSON
ENTRYPOINT ['/usr/bin/app', '--serve']
# Trailing comma -- invalid JSON
RUN ["echo", "hello",]
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
CMD ["bash", "-lc", "echo hello"]
ENTRYPOINT ["/usr/bin/app", "--serve"]
RUN ["echo", "hello"]
```
## Configuration
```toml theme={null}
[rules.tally.invalid-json-form]
severity = "error" # Options: "off", "error", "warning", "info", "style"
```
# tally/invalid-onbuild-trigger
Source: https://tally.wharflab.com/rules/tally/invalid-onbuild-trigger
ONBUILD trigger instruction is not a valid Dockerfile instruction.
ONBUILD trigger instruction is not a valid Dockerfile instruction.
| Property | Value |
| -------- | ----------------------------------------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (suggestion, requires `--fix-unsafe`) |
## Description
`ONBUILD` allows a base image to embed instructions that run automatically when the
image is used as a base for another build. However, the trigger instruction must be
a valid Dockerfile keyword.
Typo trigger keywords (e.g. `ONBUILD COPPY . /app`) successfully parse at the AST
level but fail at build time. The outer `ONBUILD` keyword is valid, so the
top-level unknown-instruction check does not catch these. This rule checks the
trigger keyword itself.
When the unknown trigger closely resembles a valid instruction (Levenshtein distance ≤ 2),
tally proposes a correction. The fix is classified as `suggestion` because it is
based on edit-distance inference — review it before applying.
Forbidden triggers (`FROM`, `ONBUILD`, `MAINTAINER`) are excluded from this rule;
they are already caught by [`hadolint/DL3043`](../hadolint/DL3043).
## References
* [Dockerfile reference — ONBUILD limitations](https://docs.docker.com/reference/dockerfile/#onbuild-limitations)
* [`hadolint/DL3043`](../hadolint/DL3043) — forbidden instructions as ONBUILD triggers
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.19
# COPPY is not a valid instruction — should be COPY
ONBUILD COPPY . /app
# RUNN is not a valid instruction — should be RUN
ONBUILD RUNN apk add --no-cache ca-certificates
```
### Good
```dockerfile theme={null}
FROM alpine:3.19
ONBUILD COPY . /app
ONBUILD RUN apk add --no-cache ca-certificates
```
## Configuration
```toml theme={null}
[rules.tally.invalid-onbuild-trigger]
severity = "error" # Options: "off", "error", "warning", "info", "style"
```
# tally/js/node-gyp-cache-mounts
Source: https://tally.wharflab.com/rules/tally/js/node-gyp-cache-mounts
Native Node addon installs should cache node-gyp header downloads with BuildKit cache mounts.
Native Node addon installs should cache node-gyp header downloads with BuildKit cache mounts.
| Property | Value |
| -------- | ----------------------------------------- |
| Severity | Info |
| Category | Performance |
| Default | Enabled |
| Auto-fix | Yes (suggestion, requires `--fix-unsafe`) |
## Description
Flags JavaScript package install or rebuild `RUN` instructions in stages that look likely to compile native Node addons, but do not cache
node-gyp's header download directory.
The rule looks for native build signals such as:
* OS packages: `python3`, `make`, `gcc`, `g++`, `build-base`, or `build-essential`
* native addon helpers: `node-gyp`, `node-pre-gyp`, or `prebuild-install`
* rebuild commands: `npm rebuild`, `pnpm rebuild`, or `yarn rebuild`
* observable `package.json` dependencies such as `sharp`, `canvas`, `bcrypt`, `sqlite3`, `better-sqlite3`, `node-rdkafka`, `grpc`, or `isolated-vm`
* dev-only native dependencies, but only when the install command includes dev packages
## What The Fix Adds
The suggested fix adds a cache mount for node-gyp's devdir:
```dockerfile theme={null}
--mount=type=cache,target=/root/.cache/node-gyp,id=node-gyp,sharing=locked
```
When `tally/prefer-package-cache-mounts` is not enabled for the same run, the fix also adds the matching package-manager cache mount for `npm`,
`pnpm`, or `yarn`. When the generic cache-mount rule is enabled, this rule leaves package-manager caches to that rule to avoid duplicate suggestions.
For shell-form `RUN` instructions, the fix also inserts:
```dockerfile theme={null}
NPM_CONFIG_DEVDIR="/root/.cache/node-gyp"
```
If the stage already sets `NPM_CONFIG_DEVDIR`, `npm_config_devdir`, or `npm_package_config_node_gyp_devdir`, the rule uses that path for the cache
mount instead of adding another environment assignment.
## Examples
### Before
```dockerfile theme={null}
FROM node:22
RUN apt-get update && apt-get install -y python3 make g++
RUN npm ci --omit=dev
```
### After
```dockerfile theme={null}
FROM node:22
RUN apt-get update && apt-get install -y python3 make g++
RUN --mount=type=cache,target=/root/.npm,id=npm \
--mount=type=cache,target=/root/.cache/node-gyp,id=node-gyp,sharing=locked \
--mount=type=tmpfs,target=/tmp \
NPM_CONFIG_DEVDIR="/root/.cache/node-gyp" npm ci --omit=dev
```
### Existing devdir
```dockerfile theme={null}
FROM node:22
ENV npm_package_config_node_gyp_devdir=/cache/node-gyp
RUN --mount=type=cache,target=/cache/node-gyp pnpm install --frozen-lockfile
```
No violation is reported because the install already uses an explicit node-gyp devdir cache.
## Guardrails
* Windows container stages are skipped because BuildKit `RUN --mount` is not supported there.
* Stages with explicit native build caches such as `ccache` or prebuild artifact cache mounts are skipped.
* The rule does not suggest tmpfs for `node_modules` or package `build/` directories. Compiled `.node` artifacts must remain in the image layer.
## References
* [node-gyp README](https://github.com/nodejs/node-gyp#readme)
* [Dockerfile `RUN --mount` reference](https://docs.docker.com/reference/dockerfile/#run---mount)
* [Docker cache optimization: Use cache mounts](https://docs.docker.com/build/cache/optimize/#use-cache-mounts)
# tally/labels/no-buildx-git-overlap
Source: https://tally.wharflab.com/rules/tally/labels/no-buildx-git-overlap
Dockerfile labels should not duplicate git provenance labels generated by Buildx.
Dockerfile labels should not duplicate git provenance labels generated by
Buildx.
| Property | Value |
| -------- | ---------------------------------------------------------------------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled, models `BUILDX_GIT_LABELS=full` |
| Auto-fix | Suggestion, when `org.opencontainers.image.revision` can be mapped precisely |
## Description
Buildx can generate image labels from the current git checkout when
`BUILDX_GIT_LABELS` is enabled. Manually maintaining the same keys in the
Dockerfile can leave stale source, revision, or Dockerfile-path metadata on the
image.
`org.opencontainers.image.revision` means the source control revision of the
packaged content. A Dockerfile cannot know that value reliably unless the build
system injects it for the exact checkout being built. When Buildx is already
configured to generate the revision label, the Dockerfile label is redundant at
best and stale at worst.
This rule checks labels that affect the exported image. Labels in throwaway
builder stages are ignored unless the final image inherits from that stage.
## Buildx modes
| Mode | Generated labels checked |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `off`, `none`, or `strconv.ParseBool` false values such as `false`, `0`, `f`, `F` | Disabled |
| `strconv.ParseBool` true values such as `true`, `1`, `t`, `T` | `org.opencontainers.image.revision`, `com.docker.image.source.entrypoint` |
| `full` | The `true` labels plus `org.opencontainers.image.source` |
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.revision="${VCS_REF}"
LABEL org.opencontainers.image.source="https://github.com/example/app" \
com.docker.image.source.entrypoint="Dockerfile"
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="app" \
org.opencontainers.image.description="Example application"
```
Let Buildx attach git-derived labels at build time:
```bash theme={null}
BUILDX_GIT_LABELS=full docker buildx build .
```
## Fixes
For `LABEL org.opencontainers.image.revision=...`, Tally offers two
suggestion-level fixes when the source pair can be mapped precisely:
* comment out the instruction, preserving the original text for review
* delete the instruction or pair
When `org.opencontainers.image.revision` appears inside a grouped `LABEL`,
Tally removes only that key/value pair and preserves the unrelated labels. The
comment-out fix inserts a commented standalone `LABEL` before the grouped
instruction, then removes the active pair from the group.
Fixes are scoped to the exported image's stage chain. If a final stage shadows
an inherited `org.opencontainers.image.revision`, the suggested fix also removes
the inherited copy so applying the fix once does not reveal the same issue
again. Builder-only stages outside the exported chain are still ignored.
`org.opencontainers.image.source` and
`com.docker.image.source.entrypoint` are reported but not auto-fixed by this
rule; they may be intentional static project metadata in some build workflows.
## Configuration
Tally does not infer builder configuration from its own process environment.
By default, this rule models `BUILDX_GIT_LABELS=full` because the diagnostics
are informational and help keep git-derived metadata owned by the build system.
Configure the mode explicitly when the repository uses a narrower Buildx git
label policy. Boolean string values are parsed like Docker/Buildx documents
`BUILDX_GIT_LABELS` parsing: `true`, `1`, `t`, and `T` all select the narrower
non-`full` generated-label set.
```toml theme={null}
[rules.tally.labels.no-buildx-git-overlap]
buildx-git-labels = "full"
```
Disable the rule when Dockerfile labels intentionally own these keys:
```toml theme={null}
[rules.tally.labels.no-buildx-git-overlap]
buildx-git-labels = "off"
```
## References
* [Docker Build variables: `BUILDX_GIT_LABELS`](https://docs.docker.com/build/building/variables/#buildx_git_labels)
* [Docker Buildx 0.10.0 release notes](https://github.com/docker/buildx/releases/tag/v0.10.0)
* [Docker object labels](https://docs.docker.com/engine/manage-resources/labels/)
* [OCI image annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
## Related Rules
* [`tally/labels/no-stale-base-digest`](/rules/tally/labels/no-stale-base-digest)
* [`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys)
* [`tally/labels/valid-key`](/rules/tally/labels/valid-key)
# tally/labels/no-duplicate-keys
Source: https://tally.wharflab.com/rules/tally/labels/no-duplicate-keys
LABEL keys should be set at most once per build stage.
LABEL keys should be set at most once per build stage.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes |
## Description
Docker image labels are a key/value map. If the same key is written more than once
in one build stage, Docker keeps the last value and the earlier value becomes
review noise.
This rule reports duplicate keys within the same stage. It does not compare labels
across stages, because intermediate stages often describe different build artifacts
and only the selected final stage's labels are published in the resulting image.
Dynamic keys such as `LABEL "$PREFIX.name"=value` are skipped by duplicate
detection because their final key cannot be proven statically.
## Auto-fix
For redundant standalone `LABEL` instructions, the preferred fix comments out
the earlier instruction. A second fix option removes it. The fix targets earlier
labels because Docker keeps the last value for a key.
When the duplicate key appears inside a multi-pair `LABEL` instruction, Tally
still reports the duplicate but does not rewrite the instruction unless the
obsolete pair can be removed without dropping unrelated labels.
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.source="https://github.com/example/app"
LABEL org.opencontainers.image.source="https://github.com/example/app-v2"
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="app" \
org.opencontainers.image.source="https://github.com/example/app"
```
## Configuration
No custom configuration options. The rule is enabled by default with severity
`warning`.
```toml theme={null}
[rules.tally.labels.no-duplicate-keys]
severity = "off"
```
## Related Rules
* [`tally/labels/no-buildx-git-overlap`](/rules/tally/labels/no-buildx-git-overlap)
* [`tally/labels/no-stale-base-digest`](/rules/tally/labels/no-stale-base-digest)
* [`tally/labels/valid-key`](/rules/tally/labels/valid-key)
# tally/labels/no-stale-base-digest
Source: https://tally.wharflab.com/rules/tally/labels/no-stale-base-digest
OCI base digest labels must be backed by a digest-pinned base image.
OCI base digest labels must be backed by a digest-pinned base image.
| Property | Value |
| -------- | ------------------------------------------------------------------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Suggestion, for standalone `LABEL` instructions and grouped `LABEL` pairs |
## Description
`org.opencontainers.image.base.digest` is the digest of the image this image is
based on. It is not the digest of the image produced by the current Dockerfile.
For a Dockerfile-owned label to be trustworthy, the exported image's stage chain
must include a digest-pinned external base image, such as
`FROM alpine:3.20@sha256:...`, and the label value must match that digest. If
the exported image is built from `FROM alpine:3.20`, `FROM scratch`, or a stage
chain whose external base is not pinned by digest, a checked-in
`org.opencontainers.image.base.digest` label can only drift from the actual base
image selected during the build.
This rule checks labels that affect the exported image. Labels in throwaway
builder stages are ignored unless the final image inherits from that stage.
## Examples
### Bad: Unpinned base
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.base.digest="sha256:1111111111111111111111111111111111111111111111111111111111111111"
```
The `FROM` line is tag-based, so the Dockerfile does not prove the base image
digest.
### Bad: Mismatched digest
```dockerfile theme={null}
FROM alpine:3.20@sha256:1111111111111111111111111111111111111111111111111111111111111111
LABEL org.opencontainers.image.base.digest="sha256:2222222222222222222222222222222222222222222222222222222222222222"
```
The `FROM` line is digest-pinned, but the label disagrees with it.
### Good: Matching digest
```dockerfile theme={null}
FROM alpine:3.20@sha256:1111111111111111111111111111111111111111111111111111111111111111
LABEL org.opencontainers.image.base.digest="sha256:1111111111111111111111111111111111111111111111111111111111111111"
```
### Good: Omitted without pinned base
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="app"
```
When the base image is not digest-pinned, omit `org.opencontainers.image.base.digest`.
## Multi-stage Builds
When the final image inherits from another Dockerfile stage, Tally follows that
`FROM ` chain to the first external base image:
```dockerfile theme={null}
FROM alpine:3.20@sha256:1111111111111111111111111111111111111111111111111111111111111111 AS base
FROM base
LABEL org.opencontainers.image.base.digest="sha256:1111111111111111111111111111111111111111111111111111111111111111"
```
This is valid because the exported image still traces back to a digest-pinned
external base. A label in a builder-only stage is ignored when the exported
image does not inherit from that stage.
## Fixes
For `LABEL org.opencontainers.image.base.digest=...`, Tally offers two
suggestion-level fixes when the source pair can be mapped precisely:
* comment out the instruction, preserving the original text for review
* delete the instruction or pair
When `org.opencontainers.image.base.digest` appears inside a grouped `LABEL`,
Tally removes only that key/value pair and preserves the unrelated labels. The
comment-out fix inserts a commented standalone `LABEL` before the grouped
instruction, then removes the active pair from the group.
Fixes are scoped to the exported image's stage chain. If a final stage shadows
an inherited stale base digest label, the suggested fix also removes the
inherited copy so applying the fix once does not reveal the same issue again.
Builder-only stages outside the exported chain are still ignored.
## Related Rules
* [`tally/labels/no-buildx-git-overlap`](/rules/tally/labels/no-buildx-git-overlap)
* [`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys)
* [`tally/labels/valid-key`](/rules/tally/labels/valid-key)
# tally/labels/prefer-grouped
Source: https://tally.wharflab.com/rules/tally/labels/prefer-grouped
Combine adjacent LABEL instructions in the same stage into one multi-line LABEL block.
Adjacent `LABEL` instructions in the same stage are easier to review when they
are written as a single multi-line `LABEL` block.
| Property | Value |
| -------- | ------- |
| Severity | Info |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes |
## Description
Modern Docker no longer needs separate `LABEL` instructions to keep image
layers small, so scattered LABELs serve no build purpose. Grouping them into
one multi-line block keeps related image metadata together, makes diffs easier
to read, and reduces the chance that two unrelated edits land on different
lines of the same logical metadata block.
The rule reports when a stage has at least `min-labels` label key/value pairs
spread across two or more adjacent `LABEL` instructions. Two `LABEL`
instructions are considered adjacent only when no other Dockerfile
instruction (such as `ARG`, `ENV`, or `RUN`) and no comment line appears
between them. Comments are treated as deliberate section breaks and never
crossed by the auto-fix.
This rule does not reorder labels. Stable ordering is left to a separate rule.
## Auto-fix
The fix replaces the first `LABEL` instruction in the run with one multi-line
`LABEL` containing every pair from the run, and deletes the remaining `LABEL`
instructions in the run. Pairs are emitted in source order so that Docker's
"last value wins" rule still produces the same effective image label map.
The fix is suppressed when:
* a key in the run is dynamic (for example `LABEL "$PREFIX.name"=...`)
* a pair uses the legacy `LABEL key value` form
* the run contains a duplicate key (let
[`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys) rewrite the
duplicate first)
The fix output uses the same shape that
[`tally/newline-per-chained-call`](/rules/tally/newline-per-chained-call) produces
for split LABELs, so the two rules agree on a single canonical form and a
re-lint after fixing is clean.
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="demo"
LABEL org.opencontainers.image.description="example image"
LABEL org.opencontainers.image.source="https://github.com/example/demo"
LABEL org.opencontainers.image.licenses="Apache-2.0"
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="demo" \
org.opencontainers.image.description="example image" \
org.opencontainers.image.source="https://github.com/example/demo" \
org.opencontainers.image.licenses="Apache-2.0"
```
## Configuration
```toml theme={null}
[rules.tally.labels.prefer-grouped]
severity = "info"
min-labels = 3
```
| Option | Type | Default | Description |
| ------------ | -------------- | ------- | ----------------------------------------------------------------------------------- |
| `min-labels` | integer (>= 2) | `3` | Minimum total label key/value pairs across an adjacent run before the rule reports. |
## Related Rules
* [`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys) — runs first when a duplicate key would otherwise be merged.
* [`tally/newline-per-chained-call`](/rules/tally/newline-per-chained-call) —
splits multi-pair `LABEL` instructions onto separate continuation lines;
together with this rule it produces a canonical multi-line `LABEL` shape.
* [`tally/labels/no-buildx-git-overlap`](/rules/tally/labels/no-buildx-git-overlap)
* [`tally/labels/no-stale-base-digest`](/rules/tally/labels/no-stale-base-digest)
* [`tally/labels/valid-key`](/rules/tally/labels/valid-key)
# tally/labels/prefer-stable-order
Source: https://tally.wharflab.com/rules/tally/labels/prefer-stable-order
Reorder LABEL key/value pairs into a deterministic, human-readable order.
A multi-pair `LABEL` block reads better when keys appear in a stable, logical
order. This rule reorders pairs inside a single `LABEL` instruction so that
related image metadata stays grouped and diffs stay small.
| Property | Value |
| -------- | -------------- |
| Severity | Info |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (in-block) |
## Description
Image metadata reads best when keys cluster by purpose. The default
`oci-logical` order follows how reviewers usually scan a `LABEL` block:
| Group | Keys |
| --------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1. Identity | `org.opencontainers.image.title`, `org.opencontainers.image.description` |
| 2. Source / refs | `org.opencontainers.image.source`, `org.opencontainers.image.url`, `org.opencontainers.image.documentation` |
| 3. Ownership / legal | `org.opencontainers.image.authors`, `org.opencontainers.image.vendor`, `org.opencontainers.image.licenses` |
| 4. Release / provenance | `org.opencontainers.image.version`, `org.opencontainers.image.revision`, `org.opencontainers.image.created`, `org.opencontainers.image.ref.name` |
| 5. Base image | `org.opencontainers.image.base.name`, `org.opencontainers.image.base.digest` |
| 6. OpenShift / Kubernetes catalog | `io.k8s.display-name`, `io.k8s.description`, `io.openshift.tags`, `io.openshift.expose-services`, `io.openshift.s2i.scripts-url` |
| 7. Docker ecosystem | `com.docker.image.source.entrypoint`, `com.docker.extension.*` |
| 8. Legacy | `org.label-schema.*`, `maintainer` |
| 9. Unknown reverse-DNS | preserved or namespace-clustered (see `sort-unknown`) |
| 10. Unknown unqualified | preserved |
The `lexical` order is a flat alphabetical comparator across all keys.
The rule reports when a single multi-pair `LABEL` instruction has at least two
pairs and the configured comparator says they are not in stable order.
## Auto-fix
The fix swaps the source text of individual `key=value` spans inside the
existing `LABEL`, leaving continuation backslashes, indentation, and any
surrounding whitespace untouched. Per-pair edits are narrow on purpose so they
can co-run with [`tally/labels/prefer-grouped`](/rules/tally/labels/prefer-grouped)
and [`tally/newline-per-chained-call`](/rules/tally/newline-per-chained-call)
without conflicts.
The fix is suppressed when:
* The `LABEL` is single-line multi-pair. Let
[`tally/newline-per-chained-call`](/rules/tally/newline-per-chained-call) split
it onto continuation lines first; the next lint pass then reorders.
* A comment line splits the pairs. Comments mark intentional sections, and the
v1 fixer never crosses them.
* The block contains a duplicate key. Defer to
[`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys);
reordering duplicates would change the effective image metadata.
* A pair has a dynamic key, an empty key, an expansion error, or uses the
legacy `LABEL key value` form.
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.description="example image" \
org.opencontainers.image.source="https://github.com/example/demo" \
org.opencontainers.image.title="demo"
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="demo" \
org.opencontainers.image.description="example image" \
org.opencontainers.image.source="https://github.com/example/demo"
```
## Configuration
```toml theme={null}
[rules.tally.labels.prefer-stable-order]
severity = "info"
order = "oci-logical"
sort-unknown = false
```
| Option | Type | Default | Description |
| -------------- | ------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `order` | `"oci-logical"` \| `"lexical"` | `"oci-logical"` | Comparator. `"oci-logical"` clusters keys by purpose. `"lexical"` sorts purely alphabetically. |
| `sort-unknown` | boolean | `false` | When `true`, group 9 (unknown reverse-DNS keys) is clustered by namespace and sorted lexically within each namespace. When `false`, those keys keep their relative source order. Groups 1–8 are unaffected because their ordering is already fully specified, and group 10 (unqualified unknown keys) always preserves source order. |
## Related Rules
* [`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys) —
must run first when a duplicate would otherwise change the effective image
metadata after reordering.
* [`tally/labels/prefer-grouped`](/rules/tally/labels/prefer-grouped) —
cooperative: combines scattered `LABEL` instructions into one multi-line
block; this rule then reorders the pairs inside that block.
* [`tally/newline-per-chained-call`](/rules/tally/newline-per-chained-call) —
splits single-line multi-pair `LABEL` instructions onto continuation lines so
this rule can reorder them on the next lint pass.
* [`tally/labels/valid-key`](/rules/tally/labels/valid-key)
* [`tally/labels/no-buildx-git-overlap`](/rules/tally/labels/no-buildx-git-overlap)
* [`tally/labels/no-stale-base-digest`](/rules/tally/labels/no-stale-base-digest)
# tally/labels/valid-key
Source: https://tally.wharflab.com/rules/tally/labels/valid-key
LABEL keys should follow Docker's documented key format and avoid reserved Docker namespaces.
LABEL keys should follow Docker's documented key format and avoid reserved Docker
namespaces.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | No |
## Description
Docker recommends label keys use lower-case alphanumeric characters, periods, and
hyphens. Labels intended for wider reuse should use reverse-DNS prefixes such as
`org.opencontainers.image.source` or `com.example.team.owner` so independent tools
do not collide.
This rule reports keys with whitespace, uppercase characters, unsupported
punctuation, repeated separators, missing alphanumeric boundaries, or Docker
reserved namespaces such as `com.docker.*`, `io.docker.*`, and
`org.dockerproject.*`.
The rule allows known Docker-owned keys that appear in normal Docker workflows,
including `com.docker.image.source.entrypoint` and Docker extension namespaces.
Dynamic keys are reported at `info` severity because they prevent static checks
from validating keys and finding duplicates. The old `LABEL key value` form is
left to BuildKit's `LegacyKeyValueFormat` rule.
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
LABEL "bad key"=value
LABEL Bad.Key=value
LABEL bad/key=value
LABEL com.docker.compose.project=demo
LABEL "$LABEL_PREFIX.name"=demo
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
LABEL org.opencontainers.image.title="demo" \
org.opencontainers.image.source="https://github.com/example/demo" \
com.example.team.owner="platform"
```
## Configuration
No custom configuration options. The rule is enabled by default with severity
`warning`.
```toml theme={null}
[rules.tally.labels.valid-key]
severity = "off"
```
## References
* [Docker object labels](https://docs.docker.com/engine/manage-resources/labels/)
* [OCI image annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
## Related Rules
* [`buildkit/LegacyKeyValueFormat`](/rules/buildkit/LegacyKeyValueFormat)
* [`tally/labels/no-buildx-git-overlap`](/rules/tally/labels/no-buildx-git-overlap)
* [`tally/labels/no-duplicate-keys`](/rules/tally/labels/no-duplicate-keys)
* [`tally/labels/no-stale-base-digest`](/rules/tally/labels/no-stale-base-digest)
# tally/max-lines
Source: https://tally.wharflab.com/rules/tally/max-lines
Enforces maximum number of lines in a Dockerfile.
Enforces maximum number of lines in a Dockerfile.
| Property | Value |
| -------- | ------------------ |
| Severity | Error |
| Category | Maintainability |
| Default | Enabled (50 lines) |
## Description
Limits Dockerfile size to encourage modular builds. Enabled by default with a 50-line limit (P90 of analyzed public Dockerfiles).
Large Dockerfiles are harder to maintain, review, and debug. This rule encourages:
* Breaking complex builds into multi-stage patterns
* Using base images for common dependencies
* Keeping build logic modular
## Options
| Option | Type | Default | Description |
| ------------------ | ------- | ------- | -------------------------------- |
| `max` | integer | 50 | Maximum lines allowed |
| `skip-blank-lines` | boolean | true | Exclude blank lines from count |
| `skip-comments` | boolean | true | Exclude comment lines from count |
## Examples
### Bad
```dockerfile theme={null}
# A 100+ line Dockerfile with everything in one file
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
build-essential \
curl \
# ... 50 more packages ...
vim
# ... 80 more lines of setup ...
```
### Good
```dockerfile theme={null}
# Base image with common dependencies
FROM myorg/base:1.0
# Application-specific setup only
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
```
## Configuration
```toml theme={null}
[rules.tally.max-lines]
severity = "warning"
max = 100
skip-blank-lines = true
skip-comments = true
```
## CLI Flags
```bash theme={null}
tally lint --max-lines 100 --skip-blank-lines --skip-comments Dockerfile
```
# tally/named-identity-in-passwdless-stage
Source: https://tally.wharflab.com/rules/tally/named-identity-in-passwdless-stage
Named user/group in USER or --chown requires /etc/passwd which passwd-less stages lack.
Named user/group in USER or --chown requires /etc/passwd which passwd-less stages lack.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
## Description
Detects named (non-numeric) user or group references in `USER` instructions or `COPY`/`ADD --chown` flags
within stages that lack `/etc/passwd` or `/etc/group`. Named identity resolution requires these database
files; without them, the build will fail at `RUN` time or the runtime will reject the container.
This is a common pitfall in `scratch` and multi-stage builds that inherit from scratch without copying
the passwd/group databases from a builder stage.
Numeric UIDs/GIDs (e.g., `65532`, `1000:1000`) work without any passwd database and are the recommended
approach for minimal images.
The rule suppresses after a `SHELL` instruction, since the user may have bootstrapped tools that handle
identity resolution.
## Examples
### Bad
```dockerfile theme={null}
FROM scratch
COPY --from=builder /myapp /myapp
USER appuser
```
```dockerfile theme={null}
FROM scratch
COPY --chown=appuser:appgroup --from=builder /myapp /myapp
```
### Good (numeric IDs)
```dockerfile theme={null}
FROM scratch
COPY --from=builder /myapp /myapp
USER 65532:65532
```
```dockerfile theme={null}
FROM scratch
COPY --chown=1000:1000 --from=builder /myapp /myapp
```
### Good (passwd copied from builder)
```dockerfile theme={null}
FROM golang:1.22 AS builder
RUN useradd -r appuser
FROM scratch
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder /myapp /myapp
USER appuser
```
### Good (non-scratch base image)
```dockerfile theme={null}
FROM alpine:3.19
RUN adduser -D appuser
USER appuser
```
## Suggested fix
The rule suggests replacing named identities with the numeric UID/GID `65532` (the conventional
non-root ID used by distroless and Chainguard images). This fix uses `FixSuggestion` safety because
the numeric ID may not match the author's intended user.
Alternatively, copy `/etc/passwd` and `/etc/group` from a builder stage that has the desired user.
## Related rules
* [`tally/shell-run-in-scratch`](./shell-run-in-scratch) -- detects shell-form RUN in scratch
stages (different concern: shell availability vs identity resolution)
* [`tally/copy-after-user-without-chown`](./copy-after-user-without-chown) -- detects missing
`--chown` after `USER` (complementary: different condition)
* [`tally/user-created-but-never-used`](./user-created-but-never-used) -- detects created
users that are never switched to (complementary: different condition)
## Configuration
```toml theme={null}
[rules.tally.named-identity-in-passwdless-stage]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
# tally/newline-between-instructions
Source: https://tally.wharflab.com/rules/tally/newline-between-instructions
Controls blank lines between Dockerfile instructions.
Controls blank lines between Dockerfile instructions.
| Property | Value |
| -------- | ----------------- |
| Severity | Style |
| Category | Style |
| Default | Enabled (grouped) |
| Auto-fix | Yes (safe) |
## Description
Enforces consistent blank-line spacing between Dockerfile instructions. Three modes are available:
* **`grouped`** (default): Same instruction types are grouped together with no blank lines between them; different types are separated by exactly one
blank line.
* **`always`**: Every instruction is followed by at least one blank line.
* **`never`**: All blank lines between instructions are removed.
### Grouped mode (default)
```dockerfile theme={null}
FROM alpine:3.20
RUN apk add --no-cache curl
ENV FOO=bar
ENV BAZ=qux
COPY . /app
```
Same-type instructions (both `ENV`) have no blank line between them. Different types (`FROM` to `RUN`, `RUN` to `ENV`, `ENV` to `COPY`) are separated
by a blank line.
### Always mode
```dockerfile theme={null}
FROM alpine:3.20
RUN apk add --no-cache curl
ENV FOO=bar
ENV BAZ=qux
COPY . /app
```
### Never mode
```dockerfile theme={null}
FROM alpine:3.20
RUN apk add --no-cache curl
ENV FOO=bar
ENV BAZ=qux
COPY . /app
```
## Examples
### Bad (grouped mode, missing blank line)
```dockerfile theme={null}
FROM alpine:3.20
RUN echo hello
```
### Good (grouped mode)
```dockerfile theme={null}
FROM alpine:3.20
RUN echo hello
```
### Bad (grouped mode, unwanted blank between same types)
```dockerfile theme={null}
ENV FOO=bar
ENV BAZ=qux
```
### Good (grouped mode, same types adjacent)
```dockerfile theme={null}
ENV FOO=bar
ENV BAZ=qux
```
## Configuration
Default (grouped mode, no config needed):
```toml theme={null}
# Grouped mode is the default when the rule is enabled
```
Always mode:
```toml theme={null}
[rules.tally.newline-between-instructions]
mode = "always"
```
Never mode:
```toml theme={null}
[rules.tally.newline-between-instructions]
mode = "never"
```
String shorthand:
```toml theme={null}
[rules.tally]
newline-between-instructions = "always"
```
## Auto-fix
This rule provides safe auto-fixes:
* **Insert blank line**: Adds a single blank line between instructions that need separation.
* **Remove blank lines**: Removes excess blank lines between instructions that should be adjacent.
```bash theme={null}
tally lint --fix Dockerfile
```
## Interaction with buildkit/InvalidDefinitionDescription
When both rules are enabled, `buildkit/InvalidDefinitionDescription` may insert blank lines
between comments and instructions (to indicate a comment is not a description). These blank
lines are **not** affected by `newline-between-instructions` because the newline rule only
measures gaps between instruction nodes, not between comments and their associated instructions.
For example, with `never` mode and `InvalidDefinitionDescription` enabled:
```dockerfile theme={null}
# This comment doesn't describe the ARG
ARG foo=bar
FROM scratch AS base
RUN echo hello
```
After auto-fix, `InvalidDefinitionDescription` inserts a blank line between the comment and
`ARG`, while `newline-between-instructions` keeps instructions adjacent:
```dockerfile theme={null}
# This comment doesn't describe the ARG
ARG foo=bar
FROM scratch AS base
RUN echo hello
```
# tally/newline-per-chained-call
Source: https://tally.wharflab.com/rules/tally/newline-per-chained-call
Each chained element within an instruction should be on its own line.
Each chained element within an instruction should be on its own line.
| Property | Value |
| -------- | ---------- |
| Severity | Style |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (safe) |
## Description
Enforces that chained elements within Dockerfile instructions are placed on separate continuation lines using `\`. This improves readability and
produces cleaner diffs.
Applies to three instruction types:
* **RUN** -- splits `&&`/`||` chain boundaries AND splits multiple `--mount=` flags
* **LABEL** -- splits multiple `key=value` pairs
* **HEALTHCHECK CMD** -- splits `&&`/`||` chain boundaries (shell form only)
## Examples
### Bad
```dockerfile theme={null}
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,target=/var/cache/apt --mount=type=bind,source=go.sum,target=go.sum apt-get update
LABEL org.opencontainers.image.title=myapp org.opencontainers.image.version=1.0 org.opencontainers.image.vendor=acme
HEALTHCHECK CMD curl -f http://localhost/ && wget -qO- http://localhost/health || exit 1
```
### Good
```dockerfile theme={null}
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=bind,source=go.sum,target=go.sum \
apt-get update
LABEL org.opencontainers.image.title=myapp \
org.opencontainers.image.version=1.0 \
org.opencontainers.image.vendor=acme
HEALTHCHECK CMD curl -f http://localhost/ \
&& wget -qO- http://localhost/health \
|| exit 1
```
## Configuration
Default (no config needed):
```toml theme={null}
# Enabled by default with min-commands = 2
```
Require 3+ chained commands before splitting:
```toml theme={null}
[rules.tally.newline-per-chained-call]
min-commands = 3
```
## Options
| Option | Type | Default | Description |
| -------------- | ------- | ------- | ----------------------------------------------------------------------------------------------- |
| `min-commands` | integer | `2` | Minimum chained commands to trigger chain splitting (>= 2). Applies to RUN and HEALTHCHECK CMD. |
LABEL pair splitting and mount splitting always trigger when 2+ elements share a line (not affected by `min-commands`).
## Skipped Cases
The rule skips the following:
* **Exec form** (`RUN ["cmd"]`) -- no shell to parse
* **Heredoc RUN** (`RUN <`
* Is not the target of a `--target` build argument
Unreachable stages add complexity and confusion without providing value. They may be:
* Leftover from refactoring
* Copy-paste artifacts
* Forgotten experimental code
## Examples
### Bad
```dockerfile theme={null}
# This stage is never used
FROM golang:1.21 AS unused-builder
RUN go build -o /app .
# This is the actual build
FROM golang:1.21 AS builder
COPY . .
RUN go build -o /app .
FROM alpine:3.18
COPY --from=builder /app /app
CMD ["/app"]
```
### Good
```dockerfile theme={null}
# All stages contribute to the final image
FROM golang:1.21 AS builder
COPY . .
RUN go build -o /app .
FROM alpine:3.18
COPY --from=builder /app /app
CMD ["/app"]
```
### Also Good (targeted builds)
```dockerfile theme={null}
# Both stages are valid targets
FROM golang:1.21 AS dev
RUN go install github.com/cosmtrek/air@latest
CMD ["air"]
FROM golang:1.21 AS builder
RUN go build -o /app .
FROM alpine:3.18 AS prod
COPY --from=builder /app /app
CMD ["/app"]
```
```bash theme={null}
# Build for development
docker build --target dev -t myapp:dev .
# Build for production
docker build --target prod -t myapp:prod .
```
## Configuration
```toml theme={null}
[rules.tally.no-unreachable-stages]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
# tally/php/composer-no-dev-in-production
Source: https://tally.wharflab.com/rules/tally/php/composer-no-dev-in-production
Production Composer install commands should include `--no-dev`.
Production Composer install commands should include `--no-dev`.
| Property | Value |
| -------- | ----------------------------------------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | Yes (suggestion, requires `--fix-unsafe`) |
## Description
Flags `composer install` in production-like stages when dev dependencies are still included.
Shipping `require-dev` packages in a production image increases the dependency graph, image size, and attack surface. The rule accepts either:
* `composer install --no-dev`
* `ENV COMPOSER_NO_DEV=1` earlier in the same stage
Stages explicitly named `dev`, `development`, `test`, `testing`, `ci`, or `debug` are skipped.
## Examples
### Before
```dockerfile theme={null}
FROM php:8.4-cli AS app
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-interaction
```
### After
```dockerfile theme={null}
FROM php:8.4-cli AS app
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction
```
### Stage-level env equivalent
```dockerfile theme={null}
FROM php:8.4-cli AS app
ENV COMPOSER_NO_DEV=1
RUN composer install
```
## Why this rule is a suggestion fix
The edit is narrow, but it changes which dependencies are installed. That is the right default for production images, but it is still a behavior
change, so the fix is classified as `FixSuggestion`.
## References
* [Composer CLI: install](https://getcomposer.org/doc/03-cli.md#install-i)
* [Docker guide for PHP: develop](https://docs.docker.com/guides/php/develop/)
# tally/php/enable-opcache-in-production
Source: https://tally.wharflab.com/rules/tally/php/enable-opcache-in-production
Production PHP web runtime images should install and enable OPcache.
Production PHP web runtime images should install and enable OPcache.
| Property | Value |
| -------- | --------------------------------------------------------------------- |
| Severity | Info |
| Category | Performance |
| Default | Enabled |
| Auto-fix | Yes (heuristic: official `php:*` images or single `php*-fpm` package) |
## What is OPcache?
OPcache is a PHP extension that stores precompiled script bytecode in shared memory, removing the need for PHP to re-read and re-parse scripts on
every request. Because only the low-level bytecode needs to run — no lexing, parsing, or compilation — it has a large, measurable impact on the
request throughput of any real-world PHP application.
When a request comes in:
1. PHP checks whether OPcache is enabled.
2. If the script's bytecode is already in the cache, OPcache returns it straight from shared memory and the Zend Engine executes it. The tokenize →
parse → compile pipeline is skipped entirely.
3. If the bytecode is not cached, PHP processes the script as usual (tokenize, parse, compile to opcodes), stores the resulting bytecode in shared
memory, and then executes it. Subsequent requests hit the cache.
OPcache ships with PHP core and is the canonical production cache for PHP web runtimes. Enabling it is a one-line change with no application-code
impact.
## Description
Flags long-running PHP web runtime stages that do not install, enable, or configure OPcache. OPcache stores precompiled PHP bytecode in shared memory,
avoiding per-request script compilation and delivering a substantial throughput improvement in production.
The rule only triggers on the **final** build stage. A stage is treated as a PHP web runtime when any of the following hold:
* the base image is an official `php:*` tag whose tag contains `fpm` or `apache`
* the base image is a known PHP web runtime derivative (`serversideup/php`, `dunglas/frankenphp`, `bitnami/php-fpm`, `trafex/php-nginx`,
`webdevops/php-nginx`, `webdevops/php-apache`)
* the effective `ENTRYPOINT` or `CMD` starts a known PHP web server wrapper (`php-fpm`, `php-fpm7.4`, `php-fpm8.3`, `apache2-foreground`,
`httpd-foreground`, `frankenphp`, `rr`)
Stages explicitly named `dev`, `development`, `test`, `testing`, `ci`, or `debug` are skipped, as are non-final stages. CLI-only PHP images
(`php:*-cli` with no PHP web server wrapper) are also skipped.
### Signals treated as compliant
Any of the following in the final stage suppress the violation:
* `docker-php-ext-install opcache`, `docker-php-ext-enable opcache`, or `docker-php-ext-configure opcache`
* `pecl install opcache`
* Package-manager installs referencing OPcache (`apt-get install php-opcache`, `apk add php83-opcache`, `dnf install php-opcache`, etc.), with
version/architecture specifiers stripped (`php-opcache=8.3+1ubuntu1`, `php-opcache:amd64`)
* An image file that looks like an OPcache ini (`*opcache*.ini`) or whose content enables OPcache via `opcache.*` keys (e.g., `opcache.enable=1`) or
`zend_extension=opcache`
* A stage-level environment variable prefixed with `PHP_OPCACHE_`
## Fix behavior
The rule offers two heuristic `FixSuggestion` fixes:
### 1. Official `php:*fpm*` / `*apache*` base
When the base image is an official `php:*fpm*` or `php:*apache*` Linux tag, the rule inserts a new `RUN docker-php-ext-install opcache` line
immediately after the final `FROM`. The edit is narrow, zero-width at the insertion column, and does not modify any existing lines.
### 2. Generic base that installs PHP via a package manager
When the final stage is detected as PHP only via `CMD`/`ENTRYPOINT` (e.g., `debian:12-slim` + `CMD ["php-fpm8.3"]`) **and** the stage's install
commands include exactly one `php*-fpm` package, the rule appends the matching `php*-opcache` package to that same install command. The matching
package name is derived directly from the existing PHP package the user chose:
| Package in `RUN` | Derived OPcache package |
| ---------------------------- | ----------------------- |
| `php8.3-fpm` (Debian/Ubuntu) | `php8.3-opcache` |
| `php-fpm` (unversioned) | `php-opcache` |
| `php83-fpm` (Alpine) | `php83-opcache` |
| `php83-php-fpm` (Remi SCL) | `php83-php-opcache` |
Example:
```dockerfile theme={null}
# Before
RUN apt-get install -y php8.3-fpm
# After
RUN apt-get install -y php8.3-fpm php8.3-opcache
```
### No fix is offered
* the base image is a non-official derivative (`serversideup/php`, `dunglas/frankenphp`, `bitnami/php-fpm`, ...) — they usually bundle OPcache or
expect different ini paths, and the right remediation is image-specific
* the stage is detected via `CMD`/`ENTRYPOINT` but no `php*-fpm` package is visible in an install command in the same Dockerfile
* the stage installs **multiple different** `php*-fpm` packages (ambiguous target version)
The package-manager fix coordinates with `tally/sort-packages`: when both rules are enabled, the inserted opcache package is appended at the end of
the install command so that sort-packages produces a correctly ordered list in the same `--fix` pass.
## Examples
### Before
```dockerfile theme={null}
FROM php:8.4-fpm
WORKDIR /app
COPY . .
```
### After
```dockerfile theme={null}
FROM php:8.4-fpm
RUN docker-php-ext-install opcache
WORKDIR /app
COPY . .
```
## References
* [PHP OPcache manual](https://www.php.net/manual/en/book.opcache.php)
* [Docker guide for PHP: develop](https://docs.docker.com/guides/php/develop/)
* [Composer autoloader optimization](https://getcomposer.org/doc/articles/autoloader-optimization.md)
# tally/php/no-xdebug-in-final-image
Source: https://tally.wharflab.com/rules/tally/php/no-xdebug-in-final-image
Final image installs or enables Xdebug, a development-only tool.
Final image installs or enables Xdebug, a development-only tool.
| Property | Value |
| -------- | ------------------------------------------------- |
| Severity | Warning |
| Category | Best Practices |
| Default | Enabled |
| Auto-fix | Yes (comment-out as suggestion, delete as unsafe) |
## Description
Flags Xdebug installations in the final image stage. Xdebug is a PHP debugging and profiling tool designed for development workflows. Shipping it in
production images degrades performance, increases image size, and widens the attack surface.
The rule detects:
* `docker-php-ext-install xdebug`
* `docker-php-ext-enable xdebug`
* `pecl install xdebug` (including versioned forms like `xdebug-3.4.0`)
* Package manager installs containing xdebug (`apt-get install php-xdebug`, `apk add php-pecl-xdebug`, etc.)
* Observable scripts (COPY heredoc, build context) that install Xdebug
Stages explicitly named `dev`, `development`, `test`, `testing`, `ci`, or `debug` are skipped. Only the final stage is checked — intermediate builder
stages are expected to have development tooling.
## Examples
### Before
```dockerfile theme={null}
FROM php:8.4-fpm AS app
WORKDIR /app
COPY . .
RUN docker-php-ext-install gd intl
RUN pecl install xdebug && docker-php-ext-enable xdebug
```
### After
Move Xdebug into a dedicated development stage:
```dockerfile theme={null}
FROM php:8.4-fpm AS app
WORKDIR /app
COPY . .
RUN docker-php-ext-install gd intl
FROM app AS dev
RUN pecl install xdebug && docker-php-ext-enable xdebug
```
## Fix behavior
When the entire RUN instruction only installs or enables Xdebug, two alternative fixes are offered:
1. **Comment out** (suggestion, preferred): Prefixes each line with `#`.
2. **Delete** (unsafe): Removes the instruction entirely.
When Xdebug is mixed with other extensions in the same command (e.g., `docker-php-ext-install gd xdebug intl`), no auto-fix is offered — the rule
reports the violation for manual resolution.
## References
* [Docker guide for PHP: develop](https://docs.docker.com/guides/php/develop/)
* [Laravel Sail: Xdebug](https://laravel.com/docs/12.x/sail)
* [PHP OPcache manual](https://www.php.net/manual/en/book.opcache.php)
# tally/platform-mismatch
Source: https://tally.wharflab.com/rules/tally/platform-mismatch
Explicit `--platform` on FROM does not match what the registry provides.
Explicit `--platform` on FROM does not match what the registry provides.
| Property | Value |
| -------- | ------------------------------------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Requires | `--slow-checks=on` (registry queries) |
## Description
When a `FROM` instruction uses an explicit `--platform` flag, this rule queries the
container registry to verify that the requested platform is actually available for the
specified image. This catches provable mismatches before they fail at build time.
Unlike `buildkit/InvalidBaseImagePlatform`, this rule:
* **Only fires when `--platform` is explicitly set** on the `FROM` instruction
* **Never compares against the host platform**, so results are deterministic across machines
* **Skips automatic build args** (`$BUILDPLATFORM`, `$TARGETPLATFORM`, etc.) which are dynamic
## When it fires
| Scenario | Result |
| -------------------------------------------------------------------------------- | ---------------------- |
| `FROM --platform=linux/arm64 image:tag` and registry has `linux/arm64` | No violation |
| `FROM --platform=linux/arm64 image:tag` and registry does NOT have `linux/arm64` | **Violation** |
| `FROM image:tag` (no `--platform`) | No violation |
| `FROM --platform=$BUILDPLATFORM image:tag` | No violation (dynamic) |
| `FROM --platform=$TARGETPLATFORM image:tag` | No violation (dynamic) |
## Examples
### Bad
```dockerfile theme={null}
# python:3.12 is only published for linux/arm64 but linux/amd64 is requested
FROM --platform=linux/amd64 python:3.12
RUN pip install flask
```
### Good
```dockerfile theme={null}
# No --platform: builder picks the right platform at build time
FROM python:3.12
RUN pip install flask
```
```dockerfile theme={null}
# Correct platform that the image actually provides
FROM --platform=linux/arm64 python:3.12
RUN pip install flask
```
```dockerfile theme={null}
# Dynamic platform via build arg
FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
RUN go build -o /app
```
## Relationship to other rules
* **`buildkit/InvalidBaseImagePlatform`** (default: Off) — the BuildKit rule compares
against the host platform even without `--platform`, producing non-deterministic results.
This rule supersedes it with a stricter, deterministic approach.
* **`buildkit/FromPlatformFlagConstDisallowed`** (default: Off) — the BuildKit rule warns
on any constant `--platform` value. This is too strict: hardcoded `--platform` is
legitimate for ARM-only services, Windows containers, and cross-compilation. The new
rule validates the platform against the registry instead of discouraging it.
# tally/powershell/error-action-preference
Source: https://tally.wharflab.com/rules/tally/powershell/error-action-preference
Require `$ErrorActionPreference = 'Stop'` and `$PSNativeCommandUseErrorActionPreference = $true` in PowerShell RUN instructions.
Require `$ErrorActionPreference = 'Stop'` and `$PSNativeCommandUseErrorActionPreference = $true` in PowerShell `RUN` instructions.
| Property | Value |
| -------- | -------------------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
This rule detects PowerShell `RUN` instructions that lack fail-fast error handling. It checks for two related preferences:
1. **`$ErrorActionPreference = 'Stop'`** -- catches non-terminating PowerShell cmdlet errors that would otherwise be silently swallowed.
2. **`$PSNativeCommandUseErrorActionPreference = $true`** -- extends error handling to native command exit codes (e.g., `git`, `dotnet`, `curl`) in
PowerShell 7.3+.
The rule fires on both Linux and Windows stages whenever PowerShell is the effective shell (via `SHELL` instruction or explicit `powershell -Command`
/ `pwsh -Command` wrappers).
## Why this matters
PowerShell does not fail-fast by default. Without `$ErrorActionPreference = 'Stop'`, an intermediate `Invoke-WebRequest` can fail silently, then
`Start-Process` runs on a missing installer, then `Remove-Item` succeeds because nothing was there. The cascading silent failures are the real danger
in multi-statement Docker build steps.
The `$PSNativeCommandUseErrorActionPreference` variable was added in PowerShell 7.3 to close a gap: even with `$ErrorActionPreference = 'Stop'`,
non-zero exit codes from native executables were ignored. Setting it to `$true` extends the fail-fast behavior to all commands.
## Examples
### Before (violation)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command"]
RUN Install-Module PSReadLine -Force; Write-Host "done"
```
### After (fixed with `--fix --fix-unsafe`)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true;"]
RUN Install-Module PSReadLine -Force; Write-Host "done"
```
### Already clean (no violation)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true; $ProgressPreference = 'SilentlyContinue';"]
RUN Install-Module PSReadLine -Force; Write-Host "done"
```
## Configuration
### `min-statements`
Minimum number of PowerShell statements in a `RUN` to trigger the rule.
* **Type:** integer
* **Default:** `2`
* **Minimum:** `1`
Set to `1` to also catch non-terminating error swallowing on single-command `RUN` instructions.
```toml theme={null}
[rules.tally.powershell.error-action-preference]
min-statements = 1
```
## Fix behavior
The auto-fix injects whichever preferences are missing. The strategy depends on the shell context:
* **Existing `SHELL` instruction**: appends the missing preferences to the last argument.
* **No `SHELL` instruction**: inserts a new `SHELL` instruction after the `FROM`.
* **Explicit wrapper** (`RUN powershell -Command ...`): inserts the missing preferences at the
start of the inner script, right before the first command.
The fix uses `FixSuggestion` safety, requiring `--fix-unsafe` to apply.
## Interaction with other rules
* **`tally/powershell/prefer-shell-instruction`** (priority 95): runs first. If it inserts a SHELL with the full prelude, the error-action-preference
fix is skipped as overlapping.
* **`tally/prefer-run-heredoc`** (priority 100): when converting multi-statement RUNs to heredocs, the heredoc formatter automatically injects both
preferences if missing.
## References
* [`$ErrorActionPreference`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables#erroractionpreference)
\-- Microsoft Learn: controls how PowerShell responds to non-terminating errors. Default is `Continue` (silently
swallow); `Stop` converts them to terminating errors.
* [`$PSNativeCommandUseErrorActionPreference`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables#psnativecommanduserroractionpreference)
\-- Microsoft Learn: when `$true`, non-zero exit codes from native commands are treated as errors according to
`$ErrorActionPreference`. Added in PowerShell 7.3; default is `$false`.
# tally/powershell/prefer-shell-instruction
Source: https://tally.wharflab.com/rules/tally/powershell/prefer-shell-instruction
Prefer a `SHELL` instruction over repeating `pwsh` or `powershell` wrappers in `RUN`.
Prefer a `SHELL` instruction over repeating `pwsh` or `powershell` wrappers in `RUN`.
| Property | Value |
| -------- | -------------------------- |
| Severity | Style |
| Category | Style |
| Default | Enabled (experimental) |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
This rule detects repeated shell-form `RUN` instructions that invoke PowerShell explicitly, for example:
* `RUN pwsh -Command ...`
* `RUN powershell -Command ...`
* `RUN @powershell -Command ...`
When PowerShell is already the effective shell via a preceding `SHELL [...]` instruction, the rule does nothing.
The recommendation applies on both Windows and Linux. A Linux image such as `mcr.microsoft.com/powershell:ubuntu-22.04` still benefits from
switching to a PowerShell `SHELL` once multiple PowerShell `RUN` commands appear.
## Why this matters
Repeating the full wrapper on every `RUN` line adds noise and makes PowerShell-specific defaults easy to forget. A dedicated `SHELL` instruction:
* makes repeated PowerShell build steps easier to read
* centralizes the shell choice instead of duplicating it across `RUN`s
* lets tally inject sane build defaults once:
* `$ErrorActionPreference = 'Stop'`
* `$PSNativeCommandUseErrorActionPreference = $true`
* `$ProgressPreference = 'SilentlyContinue'`
## Examples
### Before (violation)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
RUN pwsh -NoLogo -NoProfile -Command Install-Module PSReadLine -Force
ENV POWERSHELL_TELEMETRY_OPTOUT=1
RUN pwsh -NoLogo -NoProfile -Command Invoke-WebRequest https://example.com/tools.zip -OutFile /tmp/tools.zip
```
### After (fixed with `--fix --fix-unsafe`)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-NoLogo", "-NoProfile", "-Command", "$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true; $ProgressPreference = 'SilentlyContinue';"]
RUN Install-Module PSReadLine -Force
ENV POWERSHELL_TELEMETRY_OPTOUT=1
RUN Invoke-WebRequest https://example.com/tools.zip -OutFile /tmp/tools.zip
```
### Windows example
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN powershell -Command Invoke-WebRequest https://example.com/file.zip -OutFile C:\temp\file.zip
RUN powershell -Command Expand-Archive C:\temp\file.zip -DestinationPath C:\tools
```
becomes:
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true; $ProgressPreference = 'SilentlyContinue';"]
RUN Invoke-WebRequest https://example.com/file.zip -OutFile C:\temp\file.zip
RUN Expand-Archive C:\temp\file.zip -DestinationPath C:\tools
```
## Configuration
This rule has no rule-specific options today.
```toml theme={null}
[rules.tally.powershell.prefer-shell-instruction]
severity = "style"
```
## Fix behavior
The fixer is intentionally conservative. It only rewrites repeated PowerShell wrappers when the repeated `RUN` instructions share the same executable
and the same arguments before `-Command` (for example, repeated `pwsh -NoProfile -Command ...`).
On Windows container stages, the fixer also collaborates with `tally/prefer-run-heredoc`:
* it can qualify bare `RUN powershell ...` chains, not only explicit `-Command` wrappers
* when a `cmd` stage is converted to a PowerShell `SHELL`, later PowerShell-safe `RUN` instructions can stay under that shell instead of forcing an
immediate restore to `cmd`
* after that rewrite, `tally/prefer-run-heredoc` can merge the resulting PowerShell `RUN` sequence into a PowerShell heredoc in the same fix pass
## References
* [PowerShell Docker image](https://mcr.microsoft.com/en-us/product/powershell/about)
# tally/powershell/progress-preference
Source: https://tally.wharflab.com/rules/tally/powershell/progress-preference
Suppress PowerShell progress bars before `Invoke-WebRequest` by setting `$ProgressPreference = 'SilentlyContinue'`.
Suppress PowerShell progress bars before `Invoke-WebRequest` (or its alias `iwr`)
by setting `$ProgressPreference = 'SilentlyContinue'`.
| Property | Value |
| -------- | -------------------------- |
| Severity | Style |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
PowerShell's `Invoke-WebRequest` renders a per-response progress bar by default.
On Windows containers this collapses download throughput by an order of magnitude
because the host cannot render the bar and the cmdlet stalls waiting for terminal
updates. Microsoft's official Windows container guidance is to set
`$ProgressPreference = 'SilentlyContinue'` before any `Invoke-WebRequest` call.
The rule fires whenever a `RUN` invokes `Invoke-WebRequest` or `iwr` without a
preceding `$ProgressPreference = 'SilentlyContinue'` assignment. Detection
does not depend on the stage's default shell — `Invoke-WebRequest` is a
PowerShell-unique cmdlet, so finding it is itself the shell signal. That way the
rule catches both SHELL-based PowerShell stages and one-off `powershell -Command`
wrappers embedded inside bash stages.
## Why this matters
On Windows Server Core images specifically, users have reported
`Invoke-WebRequest` downloads taking minutes where a `curl` or the same call
with `$ProgressPreference = 'SilentlyContinue'` takes seconds. The progress bar
is pure noise in a non-interactive build step.
## Examples
### Before (violation)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command"]
RUN Invoke-WebRequest https://example.com/file.zip -OutFile /tmp/file.zip
```
### After (fixed with `--fix --fix-unsafe`)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command", "$ProgressPreference = 'SilentlyContinue';"]
RUN Invoke-WebRequest https://example.com/file.zip -OutFile /tmp/file.zip
```
### Already clean (no violation)
```dockerfile theme={null}
FROM mcr.microsoft.com/powershell:ubuntu-22.04
SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true; $ProgressPreference = 'SilentlyContinue';"]
RUN Invoke-WebRequest https://example.com/file.zip -OutFile /tmp/file.zip
```
### Explicit wrapper (violation)
```dockerfile theme={null}
FROM ubuntu:22.04
RUN pwsh -Command "Invoke-WebRequest https://example.com/file.zip -OutFile /tmp/file.zip"
```
The fix prepends the assignment to the inner script:
```dockerfile theme={null}
FROM ubuntu:22.04
RUN pwsh -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest https://example.com/file.zip -OutFile /tmp/file.zip"
```
## Fix behavior
The fix adapts to where the PowerShell script lives:
* **`SHELL` with bare `-Command`**: appends a new array element
`"$ProgressPreference = 'SilentlyContinue';"` to the `SHELL` instruction.
* **`SHELL` with an existing prelude**: appends the assignment to the existing
prelude string, inserting a `;` separator if missing.
* **No `SHELL` in a PowerShell-by-default stage**: inserts a new `SHELL`
instruction after the `FROM`.
* **Explicit `powershell -Command` / `pwsh -Command` wrapper**: zero-width
insertion at the start of the inner script.
* **`RUN <` over `git clone` inside `RUN`.
Prefer `ADD ` over `git clone` inside `RUN`.
| Property | Value |
| -------- | -------------------------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
Flags `RUN` instructions that fetch source code with `git clone`, recommending BuildKit git sources such as
[`ADD --link https://github.com/user/repo.git /src/repo`](https://docs.docker.com/reference/dockerfile/#add).
Moving repository acquisition out of `RUN` makes the fetch explicit in the Dockerfile dependency graph, reduces mutable network behavior inside
shell steps, and improves hermeticity for supply-chain-sensitive builds.
## Detected Patterns
The rule reports remote `git clone` usage in shell-form `RUN` instructions, including:
1. Plain clones: `RUN git clone https://github.com/NVIDIA/apex`
2. Branch or tag selection: `RUN git clone https://github.com/aws/aws-ofi-nccl.git -b v${BRANCH_OFI}`
3. Clone flows in chained commands: `RUN echo foo && git clone ... && cd repo && git checkout && make`
4. GitLab HTTP remotes that need the generic selector form:
`RUN git clone https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta.git -b ${GHC_WASM_META_COMMIT}`
## Examples
### Before (violation)
```dockerfile theme={null}
FROM alpine:3.20
RUN git clone https://github.com/NVIDIA/apex
RUN echo before && git clone https://github.com/NVIDIA/apex && cd apex && git checkout 0123456789abcdef0123456789abcdef01234567 && echo after
```
### After (fixed with --fix --fix-unsafe)
```dockerfile theme={null}
FROM alpine:3.20
ADD --link https://github.com/NVIDIA/apex.git /apex
RUN echo before
ADD --link --checksum=0123456789abcdef0123456789abcdef01234567 https://github.com/NVIDIA/apex.git?ref=0123456789abcdef0123456789abcdef01234567 /apex
RUN cd /apex && echo after
```
## Auto-fix Conditions
The rule emits a sync `FixSuggestion` when it can safely isolate the clone flow into:
* optional leading `RUN` commands that stay before the fetch
* one `ADD `
* optional trailing `RUN` commands that continue after the fetch
Current auto-fix coverage supports:
* simple POSIX shell-form `RUN` instructions
* `&&` chains where the clone flow can be isolated cleanly
* optional `-b` / `--branch`
* optional explicit destination directory
* optional `cd ` followed by `git checkout `
* optional recursive clone flags, mapped to `submodules=true`
* GitLab HTTP remotes via the generic `?ref=` selector form
* `ADD --link` for better cache reuse on extracted git-source layers
* `ADD --keep-git-dir=true` when later commands in the rewritten flow still run `git`
* `ADD --checksum=` when the selected ref is a full commit ID
## Report-Only Cases
The rule still reports, but does not auto-fix, when the clone appears in a shape that currently cannot be rewritten without dropping execution
context, such as:
* `RUN` instructions with non-mount BuildKit flags like `--network=...`
* `RUN` instructions using mounts
* complex shell constructs outside a simple `&&` chain
* abbreviated hex `git checkout` values like `aa756ce`, because BuildKit git URLs safely encode full commit IDs, not abbreviated checkout SHAs
* clone flows with unsupported git flags or unresolved destination paths
## Limitations
* Current auto-fix targets POSIX shell parsing; non-POSIX shells are report-only
* The generated fix uses BuildKit git-source URLs, so it requires BuildKit-enabled builds
* The fixer emits `ref=` as the git-source selector. It does not guess between `branch=` and `tag=` because `git clone -b ` can refer to either
one.
* When the selected ref is a full commit ID, the fixer emits `--checksum=` as a verifier.
* The fixer only rewrites the first clone flow in a matching `RUN`; additional clone flows can be fixed on a later run
## References
* [Dockerfile `ADD` reference](https://docs.docker.com/reference/dockerfile/#add)
* [Docker build context git URL queries (`branch`, `ref`, `commit`, `submodules`)](https://docs.docker.com/build/concepts/context/#url-queries)
* [GitLab remote URL format using `ref=` selectors](https://docs.gitlab.com/editor_extensions/visual_studio_code/remote_urls/)
# tally/prefer-add-unpack
Source: https://tally.wharflab.com/rules/tally/prefer-add-unpack
Prefer `ADD --unpack` for downloading and extracting remote archives.
Prefer `ADD --unpack` for downloading and extracting remote archives.
| Property | Value |
| -------- | -------------------------- |
| Severity | Info |
| Category | Performance |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
Flags `RUN` instructions that download a remote tar archive with `curl` / `wget`, Windows `curl.exe` / `wget.exe`, or PowerShell
`Invoke-WebRequest` / `iwr`, and extract it with `tar`, suggesting
[`ADD --unpack `](https://docs.docker.com/reference/dockerfile/#add---unpack) instead.
`ADD --unpack` is a [BuildKit feature](https://docs.docker.com/build/buildkit/) that downloads and extracts a remote tar archive in a single layer,
reducing image size and build complexity. It is implemented directly in BuildKit's Go codepath, so it works on Windows containers too and avoids
spawning download and extraction processes inside the build container.
## Detected Patterns
1. **Pipe pattern**: `curl -fsSL | tar -xz -C /dest`
2. **Download-then-extract**: `curl -o /tmp/app.tar.gz && tar -xf /tmp/app.tar.gz -C /dest`
3. **wget variants**: Same patterns with `wget` instead of `curl`
4. **Windows cmd variants**: `curl.exe ... -o C:\tmp\app.tar.gz && tar.exe -xf C:\tmp\app.tar.gz -C C:\tools`
5. **PowerShell variants**: `Invoke-WebRequest ... -OutFile C:\tmp\app.tar.gz; tar.exe -xf C:\tmp\app.tar.gz -C C:\tools`
The rule checks that the URL has a recognized archive extension and that a `tar` extraction command is present in the same `RUN` instruction.
## Examples
### Before (violation)
```dockerfile theme={null}
FROM ubuntu:22.04
RUN curl -fsSL https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -xz -C /usr/local
RUN wget -O /tmp/node.tar.xz https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz && \
tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1
FROM mcr.microsoft.com/windows/servercore:ltsc2022
SHELL ["powershell", "-Command"]
RUN Invoke-WebRequest https://example.com/app.tar.gz -OutFile C:\tmp\app.tar.gz; tar.exe -xf C:\tmp\app.tar.gz -C C:\tools
```
### After (fixed with --fix --fix-unsafe)
```dockerfile theme={null}
FROM ubuntu:22.04
ADD --unpack https://go.dev/dl/go1.22.0.linux-amd64.tar.gz /usr/local
ADD --unpack https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz /usr/local
FROM mcr.microsoft.com/windows/servercore:ltsc2022
SHELL ["powershell", "-Command"]
ADD --unpack https://example.com/app.tar.gz C:\tools
```
## Auto-fix Conditions
The auto-fix is only emitted when:
* The `RUN` instruction contains **only** download and extraction commands (`curl` / `wget` / `curl.exe` / `wget.exe` / `Invoke-WebRequest` / `iwr` +
`tar`)
* A `tar` extraction command is present (`ADD --unpack` only handles tar archives)
If additional commands are present (e.g. `chmod`, `rm`, `mv`), the violation is still reported but no fix is suggested, since those commands would be
lost.
The tar destination is extracted from `-C`, `--directory=`, or `--directory` flags. If no destination is specified, the effective `WORKDIR` is used.
## Limitations
* PowerShell and Windows support is limited to download-then-extract patterns; POSIX-style pipe detection remains POSIX-shell-only
* Only detects `tar` extraction (`ADD --unpack` does not handle single-file decompressors)
* Does not match ZIP-oriented flows such as `Expand-Archive`
* URL must have a recognized archive file extension
## Options
| Option | Type | Default | Description |
| --------- | ------- | ------- | -------------------------- |
| `enabled` | boolean | true | Enable or disable the rule |
## Configuration
```toml theme={null}
[rules.tally.prefer-add-unpack]
enabled = true
```
## References
* [Dockerfile `ADD` reference](https://docs.docker.com/reference/dockerfile/#add)
* [`ADD --unpack` flag](https://docs.docker.com/reference/dockerfile/#add---unpack)
* [BuildKit overview](https://docs.docker.com/build/buildkit/)
# tally/prefer-canonical-stopsignal
Source: https://tally.wharflab.com/rules/tally/prefer-canonical-stopsignal
STOPSIGNAL should use canonical signal names for clarity and consistency.
STOPSIGNAL should use canonical signal names for clarity and consistency.
| Property | Value |
| -------- | ---------- |
| Severity | Info |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (safe) |
## Description
`STOPSIGNAL` accepts signal names in many formats: numeric values (`9`, `15`),
names without the `SIG` prefix (`TERM`, `QUIT`), quoted strings (`"SIGINT"`),
mixed case (`sigterm`), and non-standard real-time signal names (`RTMIN+3`).
While Docker accepts all of these, canonical signal names are easier to read and
easier to connect to upstream daemon documentation. This rule suggests the
canonical form:
* Ordinary signals: `SIGTERM`, `SIGINT`, `SIGQUIT`, `SIGKILL`, etc.
* Real-time signals: `SIGRTMIN+3`
Environment variable references (e.g. `STOPSIGNAL $MY_SIGNAL`) are skipped because
the signal value cannot be determined statically.
Windows stages are skipped because `STOPSIGNAL` has no effect on Windows
containers — POSIX signals are not delivered to Windows processes.
## References
* [Dockerfile reference -- STOPSIGNAL](https://docs.docker.com/reference/dockerfile/#stopsignal)
* [signal(7) -- Linux manual page](https://man7.org/linux/man-pages/man7/signal.7.html)
## Examples
### Bad
```dockerfile theme={null}
FROM alpine:3.20
# Numeric signal value
STOPSIGNAL 15
CMD ["/app"]
```
```dockerfile theme={null}
FROM nginx:1.27
# Missing SIG prefix
STOPSIGNAL QUIT
CMD ["nginx", "-g", "daemon off;"]
```
```dockerfile theme={null}
FROM postgres:16
# Quoted signal name
STOPSIGNAL "SIGINT"
CMD ["postgres"]
```
```dockerfile theme={null}
FROM fedora:40
# Non-canonical real-time signal
STOPSIGNAL RTMIN+3
CMD ["/sbin/init"]
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
STOPSIGNAL SIGTERM
CMD ["/app"]
```
```dockerfile theme={null}
FROM nginx:1.27
STOPSIGNAL SIGQUIT
CMD ["nginx", "-g", "daemon off;"]
```
```dockerfile theme={null}
FROM postgres:16
STOPSIGNAL SIGINT
CMD ["postgres"]
```
```dockerfile theme={null}
FROM fedora:40
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]
```
## Auto-fix
The fix replaces the non-canonical signal token with its canonical form:
```bash theme={null}
tally lint --fix Dockerfile
```
The fix uses `FixSafe` safety because the canonical form is semantically identical
to the original — Docker normalizes signal names internally, so no runtime behavior
changes.
## Configuration
```toml theme={null}
[rules.tally.prefer-canonical-stopsignal]
severity = "info" # Options: "off", "error", "warning", "info", "style"
```
# tally/prefer-copy-chmod
Source: https://tally.wharflab.com/rules/tally/prefer-copy-chmod
Prefer `COPY --chmod` over a separate `COPY` followed by `RUN chmod`.
Prefer `COPY --chmod` over a separate `COPY` followed by `RUN chmod`.
| Property | Value |
| -------- | ------------- |
| Severity | Info |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (`--fix`) |
## Description
Detects a `COPY` instruction immediately followed by a `RUN chmod` that targets the same file, and suggests merging them into a single `COPY --chmod`
instruction.
The [`--chmod` flag](https://docs.docker.com/reference/dockerfile/#copy---chmod) sets file permissions at copy time, eliminating an extra layer and
the overhead of running a shell container just to change permissions.
## Why use COPY --chmod?
* **Fewer layers**: Merging two instructions into one reduces image layer count
* **Performance**: `COPY --chmod` sets permissions without spawning a shell container
* **Readability**: A single instruction is cleaner and easier to understand
## Detected Patterns
The rule flags consecutive `COPY` + `RUN chmod` pairs where:
1. The COPY has a single source file, heredoc, or single-dest content (not a glob or multiple sources)
2. The `RUN` is a standalone `chmod` command (shell-form or exec-form, not chained with other commands)
3. The chmod target matches the COPY effective destination (resolved against `WORKDIR` for relative paths)
Both octal (`755`, `0755`) and symbolic (`+x`, `u+rwx`, `-x`) chmod modes are supported.
### Merging with existing `--chmod`
When the COPY already has `--chmod`, the rule still fires if a `RUN chmod` follows:
* **Symbolic overlay**: `COPY --chmod=644` + `RUN chmod +x` merges to `COPY --chmod=0755`
* **Octal override**: `COPY --chmod=644` + `RUN chmod 755` merges to `COPY --chmod=755`
* **Redundant chmod**: `COPY --chmod=777` + `RUN chmod +x` flags the useless RUN (777 already includes execute)
## Examples
### Before (violation)
```dockerfile theme={null}
FROM python:3.12-slim
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
COPY --chown=appuser:appuser start.sh /usr/local/bin/start.sh
RUN chmod 755 /usr/local/bin/start.sh
COPY --chmod=644 config.sh /app/config.sh
RUN chmod +x /app/config.sh
```
### After (fixed with --fix)
```dockerfile theme={null}
FROM python:3.12-slim
COPY --chmod=+x entrypoint.sh /app/entrypoint.sh
COPY --chmod=755 --chown=appuser:appuser start.sh /usr/local/bin/start.sh
COPY --chmod=0755 config.sh /app/config.sh
```
## Auto-fix Conditions
The fix is emitted when:
* The COPY has a single source file or heredoc content
* The `RUN` is a standalone chmod (single command, not recursive, shell-form or exec-form)
* The chmod target matches the COPY destination (absolute path or resolved via `WORKDIR`)
The fix preserves the original chmod notation (symbolic or octal) in the `--chmod` flag value.
When merging with an existing `--chmod`, the result is formatted as octal.
## Cross-Rule Interactions
* **`tally/prefer-copy-heredoc`**: Converts `RUN echo > file` to `COPY < file` patterns with `COPY < /path` is **not** observable —
tally can only see the shell source, not the final file — so downstream
rules give up, and the rule set catches fewer real issues in your image.
3. **Predictable cache keys.** `COPY < /path/to/file`
2. **printf with escape sequences**: `printf 'line1\nline2\n' > /path/to/file`
3. **File creation with chmod**: `echo "x" > /file && chmod 0755 /file`
4. **BuildKit heredoc piped to cat**: `RUN < /path/to/file`
5. **BuildKit heredoc piped to tee**: `RUN < /etc/nginx/nginx.conf <<'EOF'
worker_processes auto;
events { worker_connections 1024; }
http {
server {
listen 8080;
location /healthz { return 200 "ok"; }
}
}
EOF
RUN printf '#!/bin/sh\nexec nginx -g "daemon off;"\n' > /usr/local/bin/start-nginx && \
chmod 0755 /usr/local/bin/start-nginx
RUN apt-get update && \
echo "APP_ENV=production" > /etc/myapp.env && \
echo "LOG_FORMAT=json" >> /etc/myapp.env && \
apt-get clean
```
### After (fixed with --fix --fix-unsafe)
```dockerfile theme={null}
COPY <'; \
} | tee /usr/local/php/php/auto_prepends/default_prepend.php \
&& { \
echo 'FromLineOverride=YES'; \
echo 'UseTLS=NO'; \
} | tee /etc/ssmtp/ssmtp.conf \
&& { \
echo '[PHP]'; \
echo 'log_errors = On'; \
} | tee /usr/local/etc/php/conf.d/php.ini
```
#### After (fixed with `--fix --fix-unsafe`)
```dockerfile theme={null}
COPY <
EOF
COPY <>`) since COPY would change semantics
* Skips relative paths (only absolute paths like `/etc/file`)
* Skips commands with shell variables not defined as ARG/ENV
## Mount Handling
Since `COPY` doesn't support `--mount` flags, the rule handles RUN mounts carefully:
| Mount Type | Behavior |
| ---------- | ------------------------------------------ |
| `bind` | Skip - content might depend on bound files |
| `cache` | Safe if file target is outside cache path |
| `tmpfs` | Safe if file target is outside tmpfs path |
| `secret` | Safe if file target is outside secret path |
| `ssh` | Safe - no content dependency |
When extracting file creation from mixed commands, mounts are preserved on the remaining RUN instructions.
## Chmod Support
Preserves the original mode notation on `COPY --chmod`. `COPY --chmod`
accepts both octal and symbolic modes (Dockerfile frontend 1.14+), so the
fixer emits whichever form the source wrote:
* Octal: `chmod 755` → `--chmod=755`, `chmod 0755` → `--chmod=0755`
* Symbolic: `chmod +x` → `--chmod=+x`, `chmod u+x` → `--chmod=u+x`
Symbolic modes are copied verbatim — the fixer does not convert them to
octal. That keeps the diff minimal and preserves the author's intent.
## Options
| Option | Type | Default | Description |
| ------------------------ | ------- | ------- | ---------------------------------------------------- |
| `check-single-run` | boolean | true | Check for single RUN instructions with file creation |
| `check-consecutive-runs` | boolean | true | Check for consecutive RUN instructions to same file |
## Configuration
```toml theme={null}
[rules.tally.prefer-copy-heredoc]
severity = "style"
check-single-run = true
check-consecutive-runs = true
```
## Rule Coordination
This rule takes priority over `prefer-run-heredoc` for pure file creation patterns. When both rules detect a pattern, `prefer-copy-heredoc` handles
it.
## References
* [Dockerfile here-documents](https://docs.docker.com/reference/dockerfile/#here-documents)
* [Introduction to heredocs in Dockerfiles](https://www.docker.com/blog/introduction-to-heredocs-in-dockerfiles/)
* [Bazel: hermeticity](https://bazel.build/basics/hermeticity) — the build-system principle behind motivation #1
# tally/prefer-curl-config
Source: https://tally.wharflab.com/rules/tally/prefer-curl-config
Stages using curl should include a retry config to handle transient failures.
Stages using curl should include a retry config to handle transient failures.
| Property | Value |
| -------- | -------------------------- |
| Severity | Info |
| Category | Reliability |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
Detects Dockerfile stages that use `curl` (either invoked directly in a `RUN` command or
installed as a package) without a retry configuration file. Transient download failures are
common during image builds — network timeouts, temporary server errors, and DNS hiccups can
cause builds to fail unpredictably. A small `.curlrc` file with retry settings makes builds
significantly more robust.
The rule emits at most **one violation per stage** and triggers when:
* A `RUN` instruction invokes `curl` directly (e.g., `curl -fsSL https://...`)
* A `RUN` instruction installs the `curl` package (e.g., `apt-get install -y curl`)
* On Windows: `curl.exe` invocation or `choco install curl` / `winget install curl`
## Auto-fix
The fix inserts a short documentation comment plus two instructions before the first relevant `RUN`:
* **Install trigger** (`apt-get install curl`): inserts right before the install `RUN`
* **Invocation trigger** (`curl https://...`): inserts before the first `RUN` in the stage
(curl is already available from the base image)
### Linux
```dockerfile theme={null}
# [tally] curl configuration for improved robustness
ENV CURL_HOME=/etc/curl
COPY --chmod=0644 <`, such as
`Dockerfile.heredoc.sh`, `Dockerfile.heredoc.bash`, or `Dockerfile.heredoc.zsh`.
PowerShell heredocs use PSScriptAnalyzer's code formatter with command-casing rewrites disabled, so formatting is stable across Linux, macOS, and
Windows hosts. They are skipped when slow checks are disabled and do not use EditorConfig today.
The formatter also runs as a final auto-fix pass. This means heredocs emitted by other rules, such as `tally/prefer-copy-heredoc`, are formatted in
the same `--fix` run when this rule is enabled.
## Examples
### Bad: COPY
```dockerfile theme={null}
FROM alpine:3.20
COPY </target` (`id=cargo-target`), `/usr/local/cargo/git/db` (`id=cargo-git`), `/usr/local/cargo/registry` (`id=cargo-registry`) |
| `dotnet restore` | `/root/.nuget/packages` (`id=nuget`) |
| `composer install` | `/root/.cache/composer` (`id=composer`) |
| `uv sync`, `uv pip install`, `uv tool install`, `uv python install` | `/root/.cache/uv` (`id=uv`) |
| `bun install` | `$BUN_INSTALL_CACHE_DIR` or `/root/.bun/install/cache` (`id=bun`) |
### Cache path resolution from environment variables
The rule resolves custom cache paths from `ENV` instructions in the Dockerfile:
| ENV variable | Mount ID | Resolution |
| ------------------------------------- | -------- | --------------------------------------------------------- |
| `npm_config_cache` (case insensitive) | `npm` | Uses value directly (default: `/root/.npm`) |
| `PNPM_HOME` | `pnpm` | Appends `/store` to value (default: `/root/.pnpm-store`) |
| `BUN_INSTALL_CACHE_DIR` | `bun` | Uses value directly (default: `/root/.bun/install/cache`) |
If the variable value contains `$` (unresolved shell reference), the override is skipped.
## Examples
### Before (violation)
```dockerfile theme={null}
FROM ubuntu:24.04
RUN --mount=type=secret,id=aptcfg,target=/etc/apt/auth.conf \
apt-get update && apt-get install -y gcc && apt-get clean
```
### After (fixed with --fix --fix-unsafe)
```dockerfile theme={null}
FROM ubuntu:24.04
RUN --mount=type=secret,id=aptcfg,target=/etc/apt/auth.conf \
--mount=type=cache,target=/var/cache/apt,id=apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,id=aptlib,sharing=locked \
apt-get update && apt-get install -y gcc
```
### pnpm with PNPM\_HOME
```dockerfile theme={null}
FROM node:20-slim
ENV PNPM_HOME="/pnpm"
RUN pnpm install --frozen-lockfile && pnpm store prune
```
becomes:
```dockerfile theme={null}
FROM node:20-slim
ENV PNPM_HOME="/pnpm"
RUN --mount=type=cache,target=/pnpm/store,id=pnpm pnpm install --frozen-lockfile
```
### Heredoc RUN support
```dockerfile theme={null}
RUN <`
* `COPY .vex.json `
## Examples
### Violation
```dockerfile theme={null}
FROM alpine:3.20
COPY *.vex.json /usr/share/vex/
```
### Recommended direction (conceptual)
Attach VEX as an OCI attestation post-build (tooling varies):
* Docker Scout: `docker scout attestation add --file app.vex.json --predicate-type https://openvex.dev/ns/v0.2.0 `
* cosign: `cosign attest --predicate app.vex.json --type https://openvex.dev/ns/v0.2.0 `
## References
* OpenVEX specification: [https://github.com/openvex/spec](https://github.com/openvex/spec)
* Docker Scout VEX exceptions:
[https://docs.docker.com/scout/how-tos/create-exceptions-vex/](https://docs.docker.com/scout/how-tos/create-exceptions-vex/)
* Docker Scout CLI docs: [https://docs.docker.com/scout/](https://docs.docker.com/scout/)
# tally/prefer-wget-config
Source: https://tally.wharflab.com/rules/tally/prefer-wget-config
Stages using wget should include a retry config to handle transient failures.
Stages using wget should include a retry config to handle transient failures.
| Property | Value |
| -------- | -------------------------- |
| Severity | Info |
| Category | Reliability |
| Default | Enabled |
| Auto-fix | Yes (`--fix --fix-unsafe`) |
## Description
Detects Dockerfile stages that use `wget` without a retry configuration file. This applies
both when `wget` is invoked directly in a `RUN` command and when the stage installs the
`wget` package first. Transient download failures are common during image builds, so a small
`wgetrc` file makes those stages more resilient.
The rule emits at most **one violation per stage** and triggers when:
* A `RUN` instruction invokes `wget` directly (for example `wget https://...`)
* A `RUN` instruction installs the `wget` package (for example `apt-get install -y wget`)
* On Windows: `wget.exe` invocation or package installs that resolve to `wget`
## Auto-fix
The fix inserts a short documentation comment plus two instructions before the first relevant
`RUN`:
* **Install trigger** (`apt-get install wget`): inserts right before the install `RUN`
* **Invocation trigger** (`wget https://...`): inserts before the first `RUN` in the stage
when `wget` is already available from the base image
### Linux
```dockerfile theme={null}
# [tally] wget configuration for improved robustness
ENV WGETRC=/etc/wgetrc
COPY --chmod=0644 <-linux-gnu/`. The legacy `libjemalloc1` package ships `libjemalloc.so.1`
instead, so the canonical `.so.2` target does not exist there — the violation still fires but no auto-fix is
emitted. Migrate to `libjemalloc2` for production images.
For Alpine, RHEL/Fedora, openSUSE, and other distros the canonical path differs, so no auto-fix is offered;
the violation still fires. Add the equivalent symlink + `ENV LD_PRELOAD=…` (or `MALLOC_CONF`) for your
distro.
## References
* [Rails 7.1 Dockerfile generator template](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/Dockerfile.tt)
* [Mastodon Dockerfile](https://github.com/mastodon/mastodon/blob/main/Dockerfile) — `MALLOC_CONF` tuning
* [jemalloc tuning options](https://jemalloc.net/jemalloc.3.html#tuning)
* [Nate Berkopec — Halve Your Memory Usage With These 12 Weird Tricks](https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html)
# tally/secrets-in-code
Source: https://tally.wharflab.com/rules/tally/secrets-in-code
Detects hardcoded secrets, API keys, and credentials using [gitleaks](https://github.com/gitleaks/gitleaks) patterns.
Detects hardcoded secrets, API keys, and credentials using [gitleaks](https://github.com/gitleaks/gitleaks) patterns.
| Property | Value |
| -------- | -------- |
| Severity | Error |
| Category | Security |
| Default | Enabled |
## Description
Scans Dockerfile content for actual secret values (not just variable names):
* RUN commands and heredocs
* COPY/ADD heredocs
* ENV values
* ARG default values
* LABEL values
Uses gitleaks' curated database of 222+ secret patterns including AWS keys, GitHub tokens, private keys, and more.
## Complements BuildKit
**Complements `buildkit/SecretsUsedInArgOrEnv`**: BuildKit's rule checks variable *names* (e.g., `GITHUB_TOKEN`), while this rule detects actual
secret *values*.
## Examples
### Bad
```dockerfile theme={null}
# Hardcoded AWS credentials
ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Hardcoded API token in RUN
RUN curl -H "Authorization: Bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" https://api.github.com/user
```
### Good
```dockerfile theme={null}
# Use build secrets
RUN --mount=type=secret,id=aws_key \
AWS_ACCESS_KEY_ID=$(cat /run/secrets/aws_key) \
aws s3 cp ...
# Or use ARG without default value (passed at build time)
ARG GITHUB_TOKEN
RUN curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user
```
## Configuration
```toml theme={null}
[rules.tally.secrets-in-code]
severity = "error" # Options: "off", "error", "warning", "info", "style"
```
# tally/shell-run-in-scratch
Source: https://tally.wharflab.com/rules/tally/shell-run-in-scratch
Detects shell-form RUN instructions in scratch stages where no shell exists.
Detects shell-form RUN instructions in scratch stages where no shell exists.
| Property | Value |
| -------- | ----------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
## Description
Detects shell-form `RUN` instructions (e.g., `RUN echo "hello"`) in `FROM scratch` stages.
Shell-form RUN requires a shell (`/bin/sh` by default) in the container's root filesystem.
Since `scratch` is an empty image with no shell, these instructions will always fail at build time.
If you explicitly set a `SHELL` instruction in the scratch stage, this rule is suppressed because
it assumes you have bootstrapped a shell binary into the stage (e.g., via `COPY --from` or `ADD`).
Common causes:
* Changing `FROM alpine` to `FROM scratch` to shrink the image without reworking `RUN` instructions
* An AI patch replacing the base image without adjusting command forms
## Examples
### Bad
```dockerfile theme={null}
FROM scratch
RUN echo "hello"
```
### Good (exec-form)
```dockerfile theme={null}
FROM scratch
RUN ["/myapp", "--init"]
```
### Good (explicit SHELL after bootstrapping)
```dockerfile theme={null}
FROM scratch
COPY --from=builder /bin/sh /bin/sh
SHELL ["/bin/sh", "-c"]
RUN echo "hello"
```
### Good (different base image)
```dockerfile theme={null}
FROM alpine:3.19
RUN echo "hello"
```
## Related rules
* [`tally/copy-from-empty-scratch-stage`](./copy-from-empty-scratch-stage) — if a scratch stage
contains only a shell-form `RUN`, this rule fires but `copy-from-empty-scratch-stage` does not
(because any `RUN` counts as file-producing). If you remove the failing `RUN` in response to this
warning, the stage becomes truly empty and `copy-from-empty-scratch-stage` will then fire on any
downstream `COPY --from`.
## Configuration
```toml theme={null}
[rules.tally.shell-run-in-scratch]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
# tally/sort-packages
Source: https://tally.wharflab.com/rules/tally/sort-packages
Package lists in install commands should be sorted alphabetically.
Package lists in install commands should be sorted alphabetically.
| Property | Value |
| -------- | ---------- |
| Severity | Style |
| Category | Style |
| Default | Enabled |
| Auto-fix | Yes (safe) |
## Description
Whenever possible, multi-line arguments should be sorted alphanumerically to make maintenance easier. This helps to avoid duplication of packages and
makes the list much easier to update. This also makes PRs a lot easier to read and review.
This rule enforces the [official Docker best practice](https://docs.docker.com/build/building/best-practices/#sort-multi-line-arguments) for sorting
package lists across common package manager install commands.
### Supported Package Managers
| Manager | Install subcommands |
| ------------ | --------------------- |
| apt-get, apt | `install` |
| apk | `add` |
| dnf, yum | `install` |
| zypper | `install`, `in` |
| npm | `install`, `i`, `add` |
| yarn | `add` |
| pnpm | `add`, `install`, `i` |
| pip, pip3 | `install` |
| bun | `add`, `install`, `i` |
| composer | `require` |
| uv | `add`, `pip install` |
| choco | `install` |
### Sort key extraction
Version specifiers are stripped for comparison:
* `flask==2.0` sorts as `flask`
* `curl=7.88.1-10+deb12u5` sorts as `curl`
* `@eslint/js@8.0.0` sorts as `@eslint/js` (npm scoped package)
Sorting is case-insensitive.
### Variable arguments
When install commands mix literal packages and variable references (`$PKG`, `${PKG}`), only the literal packages are sorted. Variables are kept at the
end in their original relative order. Variable tokens are never touched by edits, avoiding conflicts with other rules like ShellCheck quoting.
### Skipped cases
No violation is emitted when:
* Fewer than 2 literal packages (nothing to sort)
* File-based install: `pip install -r requirements.txt`, `pip install -e .`
* All arguments are variables
* Exec-form RUN: `RUN ["apt-get", "install", "curl"]`
* Packages are already sorted
## Examples
### Bad
```dockerfile theme={null}
RUN apt-get update && apt-get install -y \
wget \
curl \
git \
mercurial \
subversion
RUN npm install express axios
```
### Good
```dockerfile theme={null}
RUN apt-get update && apt-get install -y \
curl \
git \
mercurial \
subversion \
wget
RUN npm install axios express
```
## Auto-fix
This rule provides a safe auto-fix that sorts packages in-place. Only the package name text is replaced; whitespace, continuation backslashes, and
newlines are preserved.
```bash theme={null}
tally lint --fix Dockerfile
```
## Configuration
No custom configuration options. The rule is enabled by default with severity "style".
```toml theme={null}
# Disable the rule
[rules.tally.sort-packages]
severity = "off"
```
## References
* [Docker official best practices: Sort multi-line arguments](https://docs.docker.com/build/building/best-practices/#sort-multi-line-arguments)
# tally/stateful-root-runtime
Source: https://tally.wharflab.com/rules/tally/stateful-root-runtime
Final stage runs as root and signals mutable/persistent state.
Final stage runs as root and signals mutable/persistent state.
| Property | Value |
| -------- | -------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | No |
## Description
This rule detects Dockerfiles where the final stage runs as root (either
explicitly via `USER root`/`USER 0` or implicitly by having no `USER`
instruction) **and** the stage positively signals mutable or persistent
state through `VOLUME` instructions or data/state directory patterns.
Running as root in a container that manages persistent state is a
higher-risk combination than running root without state. Root access over
writable volumes, mounted sockets, log directories, and database storage
increases the blast radius of a compromise: an attacker can corrupt
persistent data, tamper with host-mounted files, or escalate via
root-owned resources that outlive the container.
This rule is more targeted than `hadolint/DL3002` ("last USER should not be
root"). DL3002 warns whenever `USER root` appears, regardless of what the
container does. This rule only fires when root **intersects** with mutable
state.
## Stateful signals detected
* **`VOLUME`** instructions (highest confidence)
* **`WORKDIR`** paths matching data/state directories
* **`COPY`/`ADD`** destinations to data/state directories
* **`RUN mkdir`** creating data/state directories
Data/state directory patterns: `/data`, `/srv`, `/var/lib/*`, `/var/log/*`,
`/var/cache/*`, `/var/run/*`, `/var/spool/*`.
## Suppression
The rule is automatically suppressed when:
* A **privilege-drop tool** is referenced in ENTRYPOINT or CMD (`gosu`,
`su-exec`, `suexec`, `setpriv`). These are unambiguous privilege-drop
executables. Generic script names like `docker-entrypoint.sh` or
`entrypoint.sh` are **not** treated as suppression signals because they
could do anything without inspecting their content.
* The **base image is known to default to non-root**: Distroless `:nonroot`
tags, Chainguard/cgr.dev images.
* The **effective final `USER`** is non-root.
* The final stage **inherits from a local stage** whose last `USER` is
non-root.
## Relationship to hadolint/DL3002
| | `hadolint/DL3002` | `tally/stateful-root-runtime` |
| -------------------- | -------------------------------- | ---------------------------------------------------------------------------- |
| Fires when | Last `USER` is explicitly `root` | Effective user is root (explicit or implicit) **and** stateful signal exists |
| Scope | Any root USER in final stage | Root + state combination only |
| Privilege-drop aware | No | Yes (suppresses for gosu/su-exec patterns) |
| Non-root base aware | N/A (only checks explicit USER) | Yes (suppresses for distroless:nonroot, chainguard) |
The two rules are complementary and both may fire on the same Dockerfile (e.g.,
`USER root` + `VOLUME /data` triggers both). This is intentional:
* **DL3002** gives a broad "consider non-root" nudge.
* **This rule** highlights the specific elevated-risk combination of root + state.
Neither rule suppresses the other via `EnabledRules` coordination because they
serve different purposes and neither has fixes that could overlap. If you want
only the targeted warning, disable DL3002 and keep this rule.
## References
* [Dockerfile reference -- USER](https://docs.docker.com/reference/dockerfile/#user)
* [Dockerfile reference -- VOLUME](https://docs.docker.com/reference/dockerfile/#volume)
* [Docker Blog -- Understanding the Docker USER Instruction](https://www.docker.com/blog/understanding-the-docker-user-instruction/)
* [Chainguard Best Practices](https://github.com/chainguard-images/images/blob/main/BEST_PRACTICES.md)
## Examples
### Bad
```dockerfile theme={null}
# Implicit root + VOLUME: high risk
FROM ubuntu:22.04
VOLUME /var/lib/data
CMD ["app"]
```
```dockerfile theme={null}
# Explicit root + data directory
FROM ubuntu:22.04
USER root
WORKDIR /var/lib/mysql
CMD ["mysqld"]
```
### Good
```dockerfile theme={null}
# Non-root user with VOLUME
FROM ubuntu:22.04
RUN useradd -r -u 1000 appuser
USER appuser
VOLUME /data
CMD ["app"]
```
```dockerfile theme={null}
# Privilege-drop entrypoint (official image pattern)
FROM ubuntu:22.04
VOLUME /var/lib/postgresql
ENTRYPOINT ["gosu", "postgres", "docker-entrypoint.sh"]
CMD ["postgres"]
```
```dockerfile theme={null}
# Distroless nonroot base
FROM gcr.io/distroless/static:nonroot
VOLUME /data
CMD ["/app"]
```
```dockerfile theme={null}
# Numeric non-root UID
FROM ubuntu:22.04
USER 65532:65532
VOLUME /data
CMD ["/app"]
```
## Configuration
```toml theme={null}
[rules.tally.stateful-root-runtime]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
# tally/syntax-directive-typo
Source: https://tally.wharflab.com/rules/tally/syntax-directive-typo
Detects typos in `# syntax=` parser directives.
Detects typos in `# syntax=` parser directives.
| Property | Value |
| -------- | ------------------------------------ |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Type | Fail-fast syntax check (exit code 4) |
## Description
The `# syntax=` directive at the top of a Dockerfile selects the BuildKit frontend
image used to parse and build the file. A typo in this directive (e.g.,
`docker/dokcerfile` instead of `docker/dockerfile`) causes the build to fail
immediately because the frontend image cannot be resolved.
This check validates the directive value against well-known frontends
(`docker/dockerfile`, `docker.io/docker/dockerfile`) and suggests corrections
when the value is within a small edit distance. It also flags directives that
contain whitespace, which is never valid.
This is a fail-fast check: if a typo is detected, linting aborts with exit code 4.
## Examples
### Bad
```dockerfile theme={null}
# syntax=docker/dokcerfile:1.7
FROM alpine:3.20
RUN echo "hello"
```
Output:
```text theme={null}
Error: Dockerfile:1: syntax directive "docker/dokcerfile:1.7" looks misspelled (did you mean "docker/dockerfile:1.7"?)
```
### Good
```dockerfile theme={null}
# syntax=docker/dockerfile:1.7
FROM alpine:3.20
RUN echo "hello"
```
# tally/unknown-instruction
Source: https://tally.wharflab.com/rules/tally/unknown-instruction
Detects misspelled or invalid Dockerfile instruction keywords.
Detects misspelled or invalid Dockerfile instruction keywords.
| Property | Value |
| -------- | ------------------------------------ |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Type | Fail-fast syntax check (exit code 4) |
## Description
Dockerfile instructions must be one of the keywords recognized by BuildKit
(`FROM`, `RUN`, `COPY`, `ADD`, `WORKDIR`, `ENV`, `ARG`, `EXPOSE`, `LABEL`,
`CMD`, `ENTRYPOINT`, `VOLUME`, `USER`, `SHELL`, `HEALTHCHECK`, `ONBUILD`,
`STOPSIGNAL`, `MAINTAINER`).
A misspelled keyword (e.g., `FORM`, `COPPY`, `WROKDIR`) is silently treated as
a comment by the parser, making the resulting image incorrect without any
obvious error. This check catches typos early using Levenshtein distance and
suggests the closest valid instruction when within edit distance 2.
This is a fail-fast check: if any unknown instruction is found, linting aborts
with exit code 4.
## Examples
### Bad
```dockerfile theme={null}
FORM alpine:3.20
RUN echo "hello"
```
Output:
```text theme={null}
Error: Dockerfile:1: unknown instruction "FORM" (did you mean "FROM"?)
```
### Good
```dockerfile theme={null}
FROM alpine:3.20
RUN echo "hello"
```
# tally/user-created-but-never-used
Source: https://tally.wharflab.com/rules/tally/user-created-but-never-used
Final stage creates a user but never switches to it.
Final stage creates a user but never switches to it.
| Property | Value |
| -------- | ------------ |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | Yes (unsafe) |
## Description
This rule detects Dockerfiles where the final stage (or its `FROM` ancestry
chain) creates a dedicated user via `useradd` or `adduser`, but the effective
runtime identity stays root and no privilege-drop entrypoint pattern is
detected. This is a high-signal indicator of an incomplete hardening attempt
or cargo-culted user setup.
On Windows containers, the rule also detects `net user /add` and
`New-LocalUser` commands.
The `USER` instruction sets the default process identity for the container at
runtime. Creating a user without switching to it means the container runs as
root despite the preparation work.
## Suppression
The rule is automatically suppressed when:
* The **effective `USER`** in the final stage is non-root.
* A **privilege-drop tool** is referenced in ENTRYPOINT or CMD (`gosu`,
`su-exec`, `suexec`, `setpriv`).
* The **base image is known to default to non-root**: Distroless `:nonroot`
tags, Chainguard/cgr.dev images, or local stage refs whose parent stage
sets a non-root `USER`.
* The created user is **referenced in an ownership or permissions context**:
* Linux: `COPY --chown`, `ADD --chown`, or `RUN chown`
* Windows: `icacls /grant `, `icacls /setowner `,
`New-Object ...AccessRule("", ...)`
This indicates deliberate permissions orchestration rather than a forgotten
step.
## Cross-stage inheritance
The rule walks the `FROM ` ancestry chain. If a parent stage creates a
user that flows into the final image (via `FROM`), the rule detects it. User
creation in stages referenced only by `COPY --from` does not trigger the rule,
since `COPY` does not inherit `/etc/passwd`.
## Relationship to other rules
| | `hadolint/DL3002` | `tally/stateful-root-runtime` | `tally/user-created-but-never-used` |
| --------------------- | -------------------------------- | ------------------------------------------ | ----------------------------------- |
| Fires when | Last `USER` is explicitly `root` | Root + stateful signal (VOLUME, data dirs) | User created but never switched to |
| Scope | Explicit root USER only | Root + state combination | User creation without USER switch |
| Privilege-drop aware | No | Yes | Yes |
| Ownership suppression | N/A | N/A | Yes (--chown, chown, icacls) |
The rules are complementary and may fire on the same Dockerfile. Neither
suppresses the other.
## Auto-fix
The rule offers an **unsafe** auto-fix (requires `--fix-unsafe`) that inserts
`USER ` before the first `ENTRYPOINT` or `CMD` in the final
stage. The fix is marked unsafe because:
* Subsequent instructions might require root.
* A privilege-drop pattern (gosu) might be more appropriate.
* The inserted `USER` might not be the correct resolution in all cases.
## References
* [Dockerfile reference -- USER](https://docs.docker.com/reference/dockerfile/#user)
* [Dockerfile reference -- COPY --chown](https://docs.docker.com/reference/dockerfile/#copy---chown)
* [Docker Blog -- Understanding the Docker USER Instruction](https://www.docker.com/blog/understanding-the-docker-user-instruction/)
* [Chainguard Best Practices](https://github.com/chainguard-images/images/blob/main/BEST_PRACTICES.md)
## Examples
### Bad
```dockerfile theme={null}
# User created but never switched to
FROM ubuntu:22.04
RUN useradd -r appuser
CMD ["app"]
```
```dockerfile theme={null}
# User created in parent stage, never activated
FROM ubuntu:22.04 AS base
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
FROM base
CMD ["app"]
```
```dockerfile theme={null}
# Windows: user created but never switched to
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN net user appuser P@ssw0rd /add
CMD ["cmd", "/C", "app.exe"]
```
### Good
```dockerfile theme={null}
# User created and activated
FROM ubuntu:22.04
RUN useradd -r -u 1000 appuser
USER appuser
CMD ["app"]
```
```dockerfile theme={null}
# Privilege-drop entrypoint (official image pattern)
FROM ubuntu:22.04
RUN useradd -r appuser
ENTRYPOINT ["gosu", "appuser", "docker-entrypoint.sh"]
CMD ["postgres"]
```
```dockerfile theme={null}
# User created and used for file ownership (suppressed)
FROM ubuntu:22.04
RUN useradd -r appuser
COPY --chown=appuser:appuser app /app
CMD ["app"]
```
```dockerfile theme={null}
# Numeric non-root UID (no useradd needed)
FROM gcr.io/distroless/static:nonroot
COPY app /app
CMD ["/app"]
```
```dockerfile theme={null}
# Windows: user created and used for ACL permissions (suppressed)
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN net user appuser P@ssw0rd /add
RUN icacls C:\app /grant appuser:(OI)(CI)F
CMD ["cmd", "/C", "app.exe"]
```
## Configuration
```toml theme={null}
[rules.tally.user-created-but-never-used]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
# tally/user-explicit-group-drops-supplementary-groups
Source: https://tally.wharflab.com/rules/tally/user-explicit-group-drops-supplementary-groups
USER name:group silently drops supplementary groups established earlier in the Dockerfile.
USER name:group silently drops supplementary groups established earlier in the Dockerfile.
| Property | Value |
| --------- | ---------------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | Yes (suggestion) |
| Platforms | Linux + Windows |
## Description
Docker's [`USER`](https://docs.docker.com/reference/dockerfile/#user) reference
is explicit about an easy-to-miss behavior:
> Note that when specifying a group for the user, the user will have only the
> specified group membership. Any other configured group memberships will be
> ignored.
This applies to both Linux and Windows containers. Any supplementary group
the Dockerfile adds the user to via one of these commands is silently dropped
the moment `USER name:group` takes effect:
| Platform | Commands |
| ----------- | --------------------------------------------------------------------------------------------- |
| Linux | `useradd -G`, `usermod -aG` / `-G`, `gpasswd -a`, `adduser USER GROUP`, `addgroup USER GROUP` |
| Windows cmd | `net localgroup /add` |
| Windows PS | `Add-LocalGroupMember -Group -Member ` |
The most common real-world symptom on Linux is a user added to the `docker`
group then locked out of `/var/run/docker.sock` at runtime. On Windows, the
corresponding symptom is a user added to a local group (for example a
custom `app-writers` group) then unable to access files whose ACL only grants
that group.
## Examples
### Bad — Linux
```dockerfile theme={null}
FROM ubuntu:22.04
RUN groupadd -r docker && \
useradd -r -g app -G docker,wheel app
USER app:app
```
The `docker` and `wheel` supplementary groups are dropped at runtime.
### Bad — Windows cmd
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN net user app password /add && net localgroup docker app /add
USER app:docker
```
`app` is added to `docker` via `net localgroup /add`, but `USER app:docker`
restricts the process to only the `docker` token — any other local-group
membership is dropped.
### Bad — Windows PowerShell
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
SHELL ["pwsh", "-Command"]
RUN New-LocalUser -Name app -NoPassword -AccountNeverExpires ; \
Add-LocalGroupMember -Group docker -Member app
USER app:docker
```
### Good — drop the explicit group
```dockerfile theme={null}
FROM ubuntu:22.04
RUN useradd -r -g app -G docker app
USER app
```
Docker uses the user's primary group from `/etc/passwd` (or the Windows
token) plus every supplementary group that was added.
## Suppression
The rule does not fire when:
* The `USER` instruction uses no explicit group (`USER app` rather than `USER app:group`).
* The user is root or UID 0.
* The user specifier is numeric (`USER 1000:1000`). We do not correlate UIDs
to `useradd`-created accounts; the rule targets named identities.
* The stage is passwd-less (scratch-rooted without a copied `/etc/passwd`).
That case belongs to `tally/named-identity-in-passwdless-stage`.
## Suggested fix
The rule proposes a `FixSuggestion` that removes the `:group` portion so
the user's supplementary groups survive. Run with `--fix --fix-unsafe` to
apply it.
```dockerfile theme={null}
# Before
USER app:app
# After
USER app
```
If the explicit group was an intentional primary-group override, keep the
current form and resolve the rule via configuration:
```toml theme={null}
[rules.tally.user-explicit-group-drops-supplementary-groups]
severity = "off"
```
## Related rules
* [`tally/named-identity-in-passwdless-stage`](./named-identity-in-passwdless-stage) —
fires in scratch-rooted stages where `/etc/passwd` is missing. Passwd-less
stages are explicitly skipped by this rule to avoid overlapping edits on
the same operand.
* [`tally/user-created-but-never-used`](./user-created-but-never-used) —
fires when the final stage never switches to a non-root user.
Complementary; our rule requires an explicit non-root USER.
* [`tally/copy-after-user-without-chown`](./copy-after-user-without-chown) —
targets COPY/ADD ownership, not USER. Complementary.
## Configuration
```toml theme={null}
[rules.tally.user-explicit-group-drops-supplementary-groups]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```
## References
* Docker Dockerfile reference: [USER](https://docs.docker.com/reference/dockerfile/#user)
* `setgroups(2)` (Linux) — the syscall whose effects are described above
* [Microsoft Add-LocalGroupMember cmdlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.localaccounts/add-localgroupmember)
* [Microsoft `net localgroup` reference](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/cc754051\(v=ws.11\))
# tally/windows/no-chown-flag
Source: https://tally.wharflab.com/rules/tally/windows/no-chown-flag
`COPY --chown` and `ADD --chown` are silently ignored on Windows containers.
`COPY --chown` and `ADD --chown` are silently ignored on Windows containers.
| Property | Value |
| -------- | -------------------------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (removes the `--chown` flag) |
## Description
Windows containers do not use POSIX file ownership (uid:gid). The `--chown` flag on `COPY` and
`ADD` instructions is silently ignored when building for Windows — BuildKit accepts the flag
without error, but the resulting files have no ownership change applied.
Users who add `--chown=user:group` on a Windows stage expect ownership to be set, but the flag
has no effect. This rule catches the dead flag at lint time so authors can remove it or understand
that it is a no-op.
## Why this matters
* **Silent no-op** — the build succeeds but `--chown` does nothing on Windows
* **Misleading intent** — other maintainers may assume file ownership is being managed
* **Cross-platform confusion** — multi-stage Dockerfiles with both Linux and Windows stages may
copy patterns from Linux stages where `--chown` is meaningful
## Examples
### Violation
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# --chown is silently ignored on Windows
COPY --chown=ContainerUser app/ C:/app/
ADD --chown=1000:1000 config.tar.gz C:/config/
```
### After fix (`--fix`)
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# --chown removed — it had no effect
COPY app/ C:/app/
ADD config.tar.gz C:/config/
```
### No violation
```dockerfile theme={null}
# Linux stages can use --chown normally
FROM alpine:3.20
COPY --chown=app:app . /app/
# Windows stages without --chown are fine
FROM mcr.microsoft.com/windows/servercore:ltsc2022
COPY app/ C:/app/
```
## Related rules
* [`tally/copy-after-user-without-chown`](../copy-after-user-without-chown) — suggests adding
`--chown` on Linux stages after a non-root `USER` (complementary; fires on opposite condition)
* [`tally/windows/no-stopsignal`](./no-stopsignal) — another Windows-specific correctness rule
for silently ignored instructions
* [`tally/windows/no-run-mounts`](./no-run-mounts) — Windows-specific correctness rule for
unsupported `RUN --mount` flags
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.windows.no-chown-flag]
severity = "warning"
```
## References
* [Optimize Windows Dockerfiles](https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/optimize-windows-dockerfile)
# tally/windows/no-run-mounts
Source: https://tally.wharflab.com/rules/tally/windows/no-run-mounts
`RUN --mount` flags are not supported on Windows containers and will fail at runtime.
`RUN --mount` flags are not supported on Windows containers and will fail at runtime.
| Property | Value |
| -------- | ----------- |
| Severity | Error |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | No |
## Description
All `RUN --mount` types (`cache`, `secret`, `ssh`, `bind`, `tmpfs`) fail at runtime on Windows containers.
BuildKit's Dockerfile frontend has no platform guard for mount flags — `dispatchRunMounts()` processes every
mount type identically regardless of OS. The build starts successfully, pulls the base image, and begins
executing layers, only to fail at the containerd/HCS runtime layer when the mount is set up.
On large Windows images (5+ GB base layers), this means the user may wait minutes before hitting the error.
This rule catches the problem immediately at lint time.
## Why this matters
* **Guaranteed build failure** — this is not a style issue; the build *will* break
* **Late failure** — BuildKit validates mounts without error; the failure only surfaces at container runtime
* **Expensive retry** — Windows base image pulls are large (ServerCore \~5 GB); catching early saves minutes
## Examples
### Violation
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# cache mount: fails at HCS runtime (moby/buildkit#5678)
RUN --mount=type=cache,target=C:\Users\ContainerUser\.nuget\packages dotnet restore
# secret mount: tmpfs-based secrets unsupported (moby/buildkit#5273)
RUN --mount=type=secret,id=nuget_token cmd /C type C:\run\secrets\nuget_token
# ssh mount: Unix socket forwarding unavailable (moby/buildkit#4837)
RUN --mount=type=ssh git clone git@github.com:org/repo.git
```
### No violation
```dockerfile theme={null}
# Linux stages can use mounts normally
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
RUN --mount=type=cache,target=/root/.nuget/packages dotnet restore
# Windows stages without mounts are fine
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN powershell -Command Invoke-WebRequest https://example.com/file.zip -OutFile C:\temp\file.zip
```
## Affected mount types
| Mount type | BuildKit issue | Runtime behavior |
| --------------------- | ------------------ | ---------------------------------- |
| `--mount=type=cache` | moby/buildkit#5678 | HCS error setting up cache volume |
| `--mount=type=secret` | moby/buildkit#5273 | tmpfs-based secrets not supported |
| `--mount=type=ssh` | moby/buildkit#4837 | Unix socket forwarding unavailable |
| `--mount=type=bind` | — | Bind mount semantics differ on HCS |
| `--mount=type=tmpfs` | — | tmpfs not a Windows concept |
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.windows.no-run-mounts]
severity = "error"
```
## References
* [Optimize Windows Dockerfiles](https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/optimize-windows-dockerfile)
# tally/windows/no-stopsignal
Source: https://tally.wharflab.com/rules/tally/windows/no-stopsignal
`STOPSIGNAL` has no effect on Windows containers because they do not support POSIX signals.
`STOPSIGNAL` has no effect on Windows containers because they do not support POSIX signals.
| Property | Value |
| -------- | ---------------------------------- |
| Severity | Warning |
| Category | Correctness |
| Default | Enabled |
| Auto-fix | Yes (comments out the instruction) |
## Description
Windows containers do not support POSIX signals. BuildKit defines a `CheckPlatform()` method on
`StopSignalCommand` that rejects it on Windows, but this check is **never called** in the dispatch
path (dead code). The `STOPSIGNAL` instruction is silently accepted and written to the image config,
but has no effect at runtime.
This rule catches the useless instruction at lint time so authors can remove it or understand that
it will be ignored.
## Why this matters
* **Silent no-op** — the build succeeds but the instruction does nothing on Windows
* **Misleading config** — other maintainers may assume the signal is in effect
* **Dead code in BuildKit** — the platform check exists but is never called, so there is no
build-time warning from Docker itself
## Examples
### Violation
```dockerfile theme={null}
FROM mcr.microsoft.com/windows/servercore:ltsc2022
STOPSIGNAL SIGTERM
CMD ["myapp.exe"]
```
### No violation
```dockerfile theme={null}
# Linux stages can use STOPSIGNAL normally
FROM alpine:3.20
STOPSIGNAL SIGTERM
CMD ["myapp"]
# Windows stages without STOPSIGNAL are fine
FROM mcr.microsoft.com/windows/servercore:ltsc2022
CMD ["myapp.exe"]
```
## Auto-fix
The auto-fix comments out the instruction using the standard tally comment-out pattern:
```dockerfile theme={null}
# Before:
STOPSIGNAL SIGTERM
# After:
# [commented out by tally - STOPSIGNAL has no effect on Windows containers]: STOPSIGNAL SIGTERM
```
## Related rules
* [`tally/no-ungraceful-stopsignal`](../no-ungraceful-stopsignal) — checks the signal value on
Linux stages (skips Windows stages since this rule handles them)
* [`tally/windows/no-run-mounts`](./no-run-mounts) — another Windows-specific correctness rule
## Configuration
This rule has no rule-specific options.
```toml theme={null}
[rules.tally.windows.no-stopsignal]
severity = "warning"
```
## References
* [Optimize Windows Dockerfiles](https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/optimize-windows-dockerfile)
# tally/world-writable-state-path-workaround
Source: https://tally.wharflab.com/rules/tally/world-writable-state-path-workaround
chmod 777/a+rwx sets world-writable permissions, a common ownership confusion workaround.
chmod 777/a+rwx sets world-writable permissions, a common ownership confusion workaround.
| Property | Value |
| -------- | ----------------------------- |
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | Suggestion (octal modes only) |
## Description
This rule detects `RUN` instructions that use `chmod 777`, `chmod a+rwx`,
`mkdir -m 777`, or similarly broad world-writable permissions on any path.
Setting world-writable permissions is almost always a workaround for
ownership confusion rather than an intentional security decision. Common
causes:
* The author does not know which user/group will run the process, so they
open permissions to everyone.
* A `WORKDIR` was created as root, but the app runs as a non-root user.
* Files were `COPY`'d without `--chown` and the author used `chmod 777`
instead of fixing ownership.
World-writable paths inside a container allow any process (including a
compromised one) to modify files, inject content, or corrupt data. This
matters especially for state directories (`/data`, `/var/lib/*`,
`/var/log/*`, `/var/cache/*`, `/var/run/*`, `/srv`) that may back
persistent volumes or host mounts.
The fix is usually one of:
* Set proper ownership with `USER`, `COPY --chown`, or `RUN chown`
* Use group permissions (`chmod g+w`, `chgrp 0 && chmod g=u`) for
OpenShift-style arbitrary-UID containers
* Use tighter modes (`755`, `775`) that don't grant write to others
## Patterns detected
### Octal modes with others-write bit
Any octal mode where the last digit includes write (2, 3, 6, 7):
* `chmod 777 /path` (read+write+execute for all)
* `chmod 666 /path` (read+write for all)
* `chmod 776 /path` (others read+write)
* `mkdir -m 777 /path`
* `mkdir -pm 777 /path`
* `mkdir --mode=777 /path`
### Symbolic modes granting others-write
* `chmod a+rwx /path` (all: read+write+execute)
* `chmod o+w /path` (others: write)
* `chmod +w /path` (no who = all: write)
* `chmod a=rwx /path` (assign all rwx)
## Patterns NOT flagged
* `chmod 755`, `chmod 644`, `chmod 775`, `chmod 770` (no others-write)
* `chmod g+w`, `chmod g+rwx`, `chmod g=rwx` (group only, not others)
* `chmod g=u` (copy user permissions to group, an OpenShift pattern)
* `chmod u+x`, `chmod +x` (execute only, no write)
* `chmod o+r`, `chmod o+x` (read/execute only, no write)
## OpenShift and arbitrary-UID containers
Valid OpenShift patterns use group-only permission changes (`chgrp 0 && chmod g=u`,
`chmod g+rwx`, `chmod 775`) which do **not** set the others-write bit and therefore
do not trigger this rule. `chmod 777` is still flagged even when paired with `chgrp`,
because it grants write to all users, not just the intended group.
For OpenShift-compatible containers, prefer `chgrp 0 /path && chmod g=u /path`
over `chmod 777 /path`.
## Relationship to related rules
| Rule | Relationship |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tally/stateful-root-runtime` | Complementary. That rule flags root + state paths; this rule flags world-writable permissions on any path. Both can fire on the same Dockerfile. |
| `tally/prefer-copy-chmod` | Complementary. That rule suggests merging COPY + RUN chmod into COPY --chmod; this rule flags the permission mode itself. Different concerns (structure vs security). |
| `tally/copy-after-user-without-chown` | Same ownership confusion family. That rule detects missing --chown on COPY after USER; this rule detects chmod workarounds. |
## Examples
### Bad
```dockerfile theme={null}
# World-writable state directory
FROM ubuntu:22.04
RUN mkdir -p /data && chmod 777 /data
CMD ["app"]
```
```dockerfile theme={null}
# World-writable app directory
FROM ubuntu:22.04
COPY app /app
RUN chmod a+rwx /app
USER appuser
CMD ["/app/server"]
```
```dockerfile theme={null}
# World-writable mkdir
FROM ubuntu:22.04
RUN mkdir -pm 777 /var/lib/myapp/logs
```
### Good
```dockerfile theme={null}
# Proper ownership with USER and chown
FROM ubuntu:22.04
RUN useradd -r -u 1000 appuser
COPY --chown=appuser:appuser app /app
RUN chmod 755 /app
USER appuser
CMD ["/app/server"]
```
```dockerfile theme={null}
# OpenShift-style group permissions (suppressed by this rule)
FROM ubuntu:22.04
RUN mkdir -p /data && \
chgrp 0 /data && \
chmod g=u /data
USER 1001
CMD ["app"]
```
```dockerfile theme={null}
# Tight permissions without world-write
FROM ubuntu:22.04
RUN mkdir -p /data && chmod 775 /data
USER appuser
CMD ["app"]
```
## References
* [Dockerfile reference -- USER](https://docs.docker.com/reference/dockerfile/#user)
* [Dockerfile reference -- COPY --chown](https://docs.docker.com/reference/dockerfile/#copy---chown)
* [Red Hat -- A Guide to OpenShift and UIDs](https://www.redhat.com/en/blog/a-guide-to-openshift-and-uids)
* [Docker Blog -- Understanding the Docker USER Instruction](https://www.docker.com/blog/understanding-the-docker-user-instruction/)
## Configuration
```toml theme={null}
[rules.tally.world-writable-state-path-workaround]
severity = "warning" # Options: "off", "error", "warning", "info", "style"
```