One Docker image, many linters, a single command.
1.1K
lintorama is a single Docker image that bundles nine widely-used linters
and validators — yamllint,
ShellCheck,
hadolint,
markdownlint (mdl),
luacheck,
Ruff,
actionlint,
check-jsonschema, and
editorconfig-checker
— behind one entrypoint, lint-extras. Point it at a Git repository and it runs
the right check over every tracked YAML, shell, Lua, Dockerfile, Markdown, and
GitHub Actions workflow file, syntax-checks JSON, JSONC, and TOML, lints the
Python in supporting-script directories (scripts/, build/, .claude/hooks/),
schema-validates common config files (CI pipelines, Docker Compose, GitHub issue
forms and actions, Dependabot, Renovate, and more), and — when an
.editorconfig is present — verifies every tracked file against it, then exits
non-zero if any check fails. Dependency directories such as node_modules/ and
vendor/ are never linted. It is built for CI pipelines: no per-project linter
installs, no juggling tool versions, no bespoke setup — just docker run.
In short: one container image for polyglot static analysis and code-quality checks in continuous integration.
.editorconfig conformance are all covered by a single pull.lint-extras command lints an entire repository and
returns a meaningful exit code for your pipeline.python:3-alpine3.24.| Tool | Version | Checks |
|---|---|---|
| yamllint | 1.38.0 | YAML (*.yml, *.yaml) |
| ShellCheck | 0.11.0 | Shell scripts (*.sh, *.bash, and extensionless scripts with a shell shebang) |
| hadolint | 2.15.1 | Dockerfile, Containerfile, and variants such as Dockerfile.dev or app.Dockerfile, at any depth |
| markdownlint (mdl) | 0.18.1 | Markdown (*.md, *.markdown) |
| luacheck | 1.2.0 | Lua (*.lua) |
| Ruff | 0.16.10 | Python in scripts/, build/, .claude/hooks/ (*.py and Python-shebang scripts) |
| actionlint | 1.7.12 | GitHub Actions workflows (.github/workflows/*.yml) |
| check-jsonschema | 0.38.0 | Schema validation: CI pipelines, Docker Compose, GitHub issue forms and actions, Dependabot, Renovate, and more |
| editorconfig-checker | 4.0.1 | .editorconfig conformance (all tracked files) |
| Built-in syntax check, using tomli for TOML | 2.3.1 | JSON, JSONC, and TOML (*.json, *.jsonc, *.toml, Pipfile) |
Built on python:3-alpine3.24.
The target must be a local Git checkout, and its Git directory has to be
available inside the container — the linters enumerate files with git ls-files:
gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
-v "$PWD":/code \
-v "$gitdir":"$gitdir" \
zaventh/lintorama:9
The image works out of /code (its WORKDIR), which is already registered as a
Git safe.directory. That is the only requirement — run the command from the
root of any Git repository, or any of its worktrees, and it lints every
supported file type.
In a regular checkout, .git is a directory and the first mount already brings
it into /code. In a linked worktree (git worktree add), .git is a file
pointing into the main repository's Git directory, for example
gitdir: /home/me/project/.git/worktrees/feature. The second mount puts that
directory at the same path inside the container, so the pointer resolves. In a
regular checkout it's redundant but harmless, so the same command works in both
cases.
Worktrees created with git worktree add --relative-paths (or
worktree.useRelativePaths) store a relative pointer instead, which only
resolves if the worktree keeps its host path in the container. Mount it there
and mark that path as a Git safe.directory:
gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
-v "$PWD":"$PWD" -w "$PWD" \
-v "$gitdir":"$gitdir" \
-e GIT_CONFIG_COUNT=1 \
-e GIT_CONFIG_KEY_0=safe.directory \
-e GIT_CONFIG_VALUE_0="$PWD" \
zaventh/lintorama:9
lint-extras is the image's entrypoint, so most CI systems need only a few
lines. The examples below all run the full linter suite over the checked-out
repository and fail the job on any lint error.
lint:
image: zaventh/lintorama:9
script:
- lint-extras
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run lintorama
run: |
gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
-v "$PWD":/code \
-v "$gitdir":"$gitdir" \
zaventh/lintorama:9
gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
-v "$PWD":/code \
-v "$gitdir":"$gitdir" \
zaventh/lintorama:9
The lint-extras entrypoint operates on the Git-tracked files in the
working directory (it uses git ls-files), so the target must be a Git
repository. In order, it:
yamllint -s (strict) over all tracked *.yml / *.yaml files, except generated lockfiles (pnpm-lock.yaml, conda-lock.yml).check-jsonschema, when present. See Schema-validated files.package-lock.json, npm-shrinkwrap.json). See JSON and TOML.actionlint over all tracked .github/workflows/*.yml / *.yaml files (delegating embedded run: scripts to the bundled ShellCheck).package.json exists, requires a lockfile to accompany it: package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock, or bun.lockb.shellcheck over all tracked *.sh / *.bash files, plus extensionless files whose shebang names sh, bash, dash, or ksh (directly or through env), such as Git hooks. The generated gradlew and mvnw wrappers are skipped.luacheck over all tracked *.lua files.ruff check over the Python in directories named scripts, build, or .claude/hooks at any depth: *.py files plus extensionless files with a python (or uv run) shebang. Change the directories with --python-dirs.hadolint over every Dockerfile and Containerfile at any depth, including variants such as Dockerfile.dev and app.Dockerfile. Per-Dockerfile ignore files (Dockerfile.dockerignore) and templates (Dockerfile.j2, Dockerfile.in, Dockerfile.tmpl, …) are skipped.README.md (case-sensitive) to exist.mdl over all tracked *.md / *.markdown files..editorconfig exists, runs editorconfig-checker to verify every tracked file conforms to it.The exit code is the sum of the individual linter results — any failure fails the run.
Files under package-manager install directories are never linted, at any depth,
even when they are committed or missing from .gitignore: node_modules,
bower_components, jspm_packages, .yarn, .pnpm-store, vendor, .venv,
venv, site-packages, __pypackages__, .tox, .nox, Pods, Carthage,
.build, .gradle, .dart_tool, elm-stuff, and .terraform. Git submodules
are skipped too, since git ls-files doesn't descend into them.
check-jsonschema validates each of these files against the schema bundled with
it, so validation runs offline. File names follow check-jsonschema's own
pre-commit hooks.
| Schema | Files |
|---|---|
| GitLab CI | .gitlab-ci.yml |
| GitHub Actions (composite and other actions) | action.yml at the root, .github/actions/**/action.yml |
| GitHub issue forms | .github/ISSUE_TEMPLATE/*.yml, except config.yml |
| GitHub issue template chooser | .github/ISSUE_TEMPLATE/config.yml |
| GitHub discussion forms | .github/DISCUSSION_TEMPLATE/*.yml |
| Dependabot | .github/dependabot.yml |
| Renovate | renovate.json, renovate.json5, .renovaterc, .renovaterc.json, .renovaterc.json5, and renovate.json / renovate.json5 under .github/ or .gitlab/ |
| Docker Compose | compose.yaml, docker-compose.yml, and variants such as compose.prod.yaml, at any depth. Files that use Compose's !reset or !override merge tags are skipped, since check-jsonschema can't load them. |
| Azure Pipelines | azure-pipelines.yml, .azure-pipelines.yml |
| Bamboo Specs | bamboo-specs/**/*.yml |
| Bitbucket Pipelines | bitbucket-pipelines.yml, at any depth |
| Buildkite | buildkite.yml, buildkite.*.yml, .buildkite/pipeline.yml, .buildkite/pipeline.*.yml (also .json) |
| CircleCI | .circleci/config.yml |
| Google Cloud Build | cloudbuild.yaml (also .yml, .json) |
| Travis CI | .travis.yml |
| Woodpecker CI | .woodpecker.yml, .woodpecker/**/*.yml |
| Changie | .changie.yaml |
| Citation File Format | CITATION.cff |
| Codecov | codecov.yml or .codecov.yml at the root, in .github/, or in dev/ |
| Meltano | meltano.yml at any depth, meltano-manifest.json, meltano-manifest.*.json |
| Mergify | .mergify.yml, .mergify/config.yml, .github/mergify.yml |
| Read the Docs | .readthedocs.yaml |
| Snapcraft | snapcraft.yaml, at any depth |
| Taskfile | Taskfile.yml, taskfile.yml, Taskfile.dist.yml, taskfile.dist.yml |
Each .yml or .yaml name in the table matches both spellings, except Meltano
and Mergify (.yml only) and Snapcraft (.yaml only).
JSON files (*.json) must be strict RFC 8259
JSON: no comments, trailing commas, NaN or Infinity, duplicate keys, or byte
order mark. JSONC files may also use // and /* */ comments, trailing commas,
and a byte order mark, as TypeScript and VS Code allow. These files count as
JSONC, at any depth, because the tools that read them accept comments:
*.jsonc, *.code-workspace, *.code-snippets, and .vscode/*.jsondevcontainer.json and .devcontainer.jsontsconfig*.json and jsconfig*.json, such as tsconfig.build.jsondeno.json, biome.json, turbo.json, wrangler.json, and typedoc.json.eslintrc.json, .oxlintrc.json, .babelrc.json, babel.config.json,
.markdownlint.json, cspell.json, and .cspell.jsonappsettings*.json (.NET)A strict JSON file that would parse as JSONC gets a hint saying so.
TOML files (*.toml and Pipenv's Pipfile) must be valid
TOML 1.0, checked with
tomli, which also rejects duplicate keys and
tables. TOML 1.1 additions, such as trailing commas and line breaks in inline
tables, are rejected: Python's tomllib (which reads pyproject.toml) and
Gradle (which reads libs.versions.toml) only understand 1.0, and every 1.1
reader accepts 1.0.
Each problem is reported as path:line:column: message.
lint-extras takes these command-line options:
| Option | Default | Meaning |
|---|---|---|
--python-dirs DIRS | scripts,build,.claude/hooks | Comma-separated directory names or repository-relative paths whose Python Ruff lints, matched at any depth. Pass '' to skip Python. |
-h, --help | Print usage and exit. |
Python is limited to these directories on purpose: lintorama covers a project's supporting files, and its main Python code is better served by the project's own tooling. To lint scripts kept somewhere else, list those directories instead.
In GitLab CI, pass options to lint-extras in the job's script:
lint:
image: zaventh/lintorama:9
script:
- lint-extras --python-dirs scripts,tools
With docker run, put them after the image name:
gitdir=$(git rev-parse --path-format=absolute --git-common-dir)
docker run --rm \
-v "$PWD":/code \
-v "$gitdir":"$gitdir" \
zaventh/lintorama:9 --python-dirs scripts,tools
Each linter honors its standard configuration file when present in the repository root, so consumers can tune the rules without changing this image:
| File | Linter |
|---|---|
.yamllint | yamllint |
.hadolint.yaml | hadolint |
.mdlrc | markdownlint |
ruff.toml, .ruff.toml, or pyproject.toml with [tool.ruff] | Ruff |
.github/actionlint.yaml | actionlint |
.editorconfig | editorconfig-checker (also the file it enforces) |
The .yamllint, .hadolint.yaml, .mdlrc, and .editorconfig files in this
repository are the ones lintorama applies to itself. check-jsonschema needs
no configuration — it picks each file's schema by name — and
editorconfig-checker only runs when the repository provides an .editorconfig.
When the repository has no Ruff configuration, Ruff uses lintorama's bundled
defaults: Ruff's own default rule set,
tuned lightly for readability. Line length, complexity, naming, and docstring
rules stay off, and formatting isn't enforced. A Ruff config in the repository
replaces those defaults entirely, since Ruff doesn't merge configs, so copy the
bundled file as a starting point. Ruff's exclude and extend-exclude settings
are honored for the files lintorama passes it.
Published to Docker Hub as
zaventh/lintorama:
| Tag | Meaning |
|---|---|
9.0.0 | Exact, immutable version |
9 | Rolling major tag (recommended for most pipelines) |
latest | The most recent build |
Each tag is a multi-arch image for linux/amd64 and linux/arm64; Docker picks
the right one for the host automatically.
What is lintorama?
lintorama is a Docker image that bundles yamllint, ShellCheck, hadolint,
markdownlint (mdl), luacheck, Ruff, actionlint, check-jsonschema, and
editorconfig-checker behind a single command, lint-extras, for linting a Git
repository in CI pipelines. It also syntax-checks JSON, JSONC, and TOML.
Which linters does lintorama include?
Nine: yamllint (YAML), ShellCheck (shell scripts), hadolint (Dockerfiles),
markdownlint / mdl (Markdown), luacheck (Lua), Ruff (Python scripts),
actionlint (GitHub Actions workflows), check-jsonschema (schema validation for
CI and tooling config), and editorconfig-checker (.editorconfig conformance).
A built-in check covers JSON, JSONC, and TOML syntax. See
Bundled linters for exact versions.
How do I run lintorama locally?
Run the Quick start command from the root of any Git repository
or worktree: it mounts the checkout at /code and the repository's Git
directory at its host path.
How do I use lintorama in CI?
In GitLab CI, use it as the job image and call lint-extras. In GitHub
Actions or any other system, run the image with docker run. See
Continuous integration.
Does lintorama require configuration?
No. It works with sensible defaults, but each linter honors its standard config
file (.yamllint, .hadolint.yaml, .mdlrc, ruff.toml) when present. See
Configuration.
Why does lintorama only lint Python in scripts/ and build/?
lintorama checks a project's supporting files, not its main code. Python in
scripts/, build/, and .claude/hooks/ is usually helper tooling with no
linter of its own, while a Python codebase has its own lint setup. Use
--python-dirs to choose different directories.
Why does lintorama need the .git directory?
lint-extras discovers files with git ls-files, so the target must be a Git
repository with its Git directory available inside the container. For a
worktree, that means the main repository's Git directory too. See
Git worktrees.
How does lintorama report failures? The exit code is the sum of the individual linters' results, so a non-zero exit means at least one check failed — exactly what a CI pipeline needs.
Does lintorama run on Apple Silicon or other arm64 hosts?
Yes. Every tag includes a native linux/arm64 image next to linux/amd64, so
Docker on an Apple Silicon Mac runs it without Rosetta or QEMU emulation.
Where is the image published?
On Docker Hub as
zaventh/lintorama. The source
lives on GitHub.
The image is built and pushed by .gitlab-ci.yml on every push to the default
branch. Each architecture is built natively on its own runner — linux/amd64 on
a Linux runner, linux/arm64 on an Apple Silicon Mac runner (tagged macos) —
and linted with that same image before anything is published. To cut a new
release, bump the BUILD_VER variable in that file (semver, e.g. 7.1.0); the
pipeline then combines both builds into one multi-arch image, publishes it as the
full version, the major tag, and latest, stamps the version, build date, and
commit SHA into the image's OCI labels, and syncs this README to the Docker Hub
repository description.
Released under the MIT License. Copyright (c) 2016-2026 Jeff Mixon.
Content type
Image
Digest
sha256:efefc0599…
Size
168.5 MB
Last updated
15 days ago
docker pull zaventh/lintorama