Lint My Headers
Fast copyright and license header checks, powered by Rust.
One policy for your codebase. Clear findings. Small, reviewable repairs.
Documentation · Quick start · Integrations · Releases
Keep copyright and license notices consistent across your codebase, and turn an annual
refresh into a small diff. Run lmh locally, in CI, or from a coding agent using the
same explicit policy.
Highlights
- Native Rust execution. A compiled CLI with no Python, Node.js, or language toolchain required to run the installed binary.
- Nine languages, one policy. Python, JavaScript, TypeScript, Rust, Go, Swift, Bash, C, and C++.
- Leading-header checks. Recognizes the opening comment region and requires notices before code, keeping header-like text in program bodies out of repairs.
- Precise, reviewable fixes. Refreshes recognized stale years while preserving the creation year, owner, all other bytes, and file permissions.
- Fits your workflow. First-party pre-commit/prek hook, GitHub Action, and stable JSON diagnostics for scripts and coding agents.
Performance
On 10,000 mixed-language files (1 KiB/file), the release CLI measured:
| Task | LMH | HawkEye 7.2.0 |
|---|---|---|
| Routine check | 36 ms | 76 ms |
| Check stale headers | 62 ms | 76 ms |
| Repair stale years | 147 ms | 125 ms |
Five timing trials after warmup on the same shared Linux runner. Both tools receive the same CPU budget; LMH uses up to four available workers for large trees.
See the performance guide for repair time, peak memory, all nine languages, CSVs, methodology and reproduction. These are synthetic, warm-cache results; CPU limits and storage affect the comparison.
Installation
Install the published release with uv:
uv tool install lint-my-headers==0.7.0
Or install with pip: python -m pip install lint-my-headers==0.7.0.
Version 0.7.0 supports all nine languages and policy in .lmh.toml,
pyproject.toml, Cargo.toml, or package.json. Select your project's languages
in the policy; the default remains Python.
To build from source, use Rust 1.93 and a C compiler:
cargo install --git https://github.com/frgfm/lint-my-headers --locked
PyPI tooling requires Python 3.11+. Wheels ship native lmh and lint-my-headers
executables; building a source distribution also requires Rust and a C compiler.
See the installation guide for source checkout
instructions and launcher details.
Quick start
Declare your project's actual owner and license in pyproject.toml, keep the matching
LICENSE at the project root, and select the source folders that exist in your project:
[tool.lint-my-headers]
owner = "Example Organization"
starting-year = 2024
license = "Apache-2.0"
paths = ["src", "tests"]
Check existing headers:
lmh check
Findings tell you exactly where to look and whether a repair is available:
src/example.py:1:1: LMH004 copyright year ends at 2025; expected 2026 [fixable]
Refresh recognized stale years and review the result:
lmh fix
lmh check
git diff
A 2026 refresh produces a diff like this:
-# Copyright (C) 2024-2025, Example Organization.
+# Copyright (C) 2024-2026, Example Organization.
Only the end year changes. check never writes source files. Missing headers and other
findings stay available for manual review. See the
getting-started guide for a complete header example.
Benchmarks
Run bash scripts/benchmark.sh to generate CSVs, SVG/PNG charts and an HTML
report with a workflow-overhead calculator. Add --compare /path/to/hawkeye for
a locally measured comparison or --baseline /path/to/previous/lmh to show the
improvement. See requirements and reproduction.
Configuration and safety
Keep the policy in the file your project already uses:
| File | Configuration |
|---|---|
.lmh.toml |
Top-level keys. |
pyproject.toml |
[tool.lint-my-headers]. |
Cargo.toml |
[package.metadata.lint-my-headers] or [workspace.metadata.lint-my-headers]. |
package.json |
A "lint-my-headers" object. |
The CLI discovers the nearest policy by searching upward, with priority in
that order; Cargo package metadata takes precedence over workspace metadata. Manifests
without LMH settings are skipped. Invalid policies fail and configurations are never
merged. --config selects an exact file; CLI options override file settings.
For multilingual projects, select the languages used by your codebase:
languages = ["python", "typescript", "rust"]
The default is Python. Configured paths are relative to the policy file; explicit CLI
paths are relative to the invocation directory. Unsupported extensions are skipped.
Exclude generated, dependency, and build folders with ignore-folders; LMH does not
infer exclusions from .gitignore.
Repairs require one recognized stale year for the configured owner. Ambiguous layouts, wrong owners, future years, unsupported encodings, symlinks/reparse points, multiple hard links, and concurrently changed targets are refused. Ownership and licensing always come from your policy; LMH does not insert missing headers or establish legal, SPDX, or REUSE compliance.
See the configuration guide for all options, language aliases, supported extensions, notice formats, and byte-preservation rules.
Agents and JSON
Use the same CLI from scripts and coding agents:
lmh check --output-format json
JSON schema version 1 includes the checked count, completed changes, and diagnostics with path, line, column, code, message, and repair eligibility. Parsed JSON-mode commands write only JSON to stdout; malformed CLI syntax remains a stderr usage error.
| Exit | Meaning |
|---|---|
0 |
No unresolved findings. |
1 |
Findings need review. |
2 |
Invocation, configuration, or I/O failure. |
An agent should read the declared policy, run check, repair only when source changes
are authorized, then recheck and inspect the diff. A failed repair may follow earlier
completed changes; review changed and the working tree before retrying.
Use the first-party agent skill and the diagnostics guide for the full contract. Retained agent evaluations are historical Python-era results, not measurements of the Rust CLI.
Integrations
pre-commit and prek
Use the published native wheel in an isolated Python 3.11+ hook environment.
Select languages in your policy to enable multilingual checks:
repos:
- repo: local
hooks:
- id: lmh
name: Lint My Headers
entry: lmh check
language: python
types: [file]
additional_dependencies:
- --only-binary=lint-my-headers
- lint-my-headers==0.7.0
Alternatively, use the first-party source hook at the release tag:
repos:
- repo: https://github.com/frgfm/lint-my-headers
rev: v0.7.0
hooks:
- id: lmh
The source hook compiles Rust on first installation with the pinned toolchain.
pre-commit's Rust installer does not pass --locked. Both hooks run read-only checks.
GitHub Action
Add the published release to your existing workflow:
steps:
- uses: actions/checkout@v7
- uses: frgfm/lint-my-headers@v0.7.0
The Action reads repository policy and uses uv to install the exact PyPI version with
source builds disabled. Prefer immutable release SHAs; the
v0.7.0 release notes
provide its commit SHA. For an unreleased multilingual revision, pin its reviewed SHA
and set version: source to build that Action checkout using Cargo.lock.
Inputs override policy. The issues and changed outputs are compact JSON path arrays;
on exit 2, issues is empty and changed retains completed writes. An I/O error stops
new repairs; already-running repairs finish before results are returned. See the
integration guide and Action reference for all inputs,
outputs, and installation behavior.
Annual copyright refresh
Create a review PR each January using the maintained workflow below.
Annual workflow template, setup, and recovery
Schedule a review PR for January 1 at 00:01 Europe/Paris. The scheduler and job use the same timezone so the repair year is January 1's year even while UTC is still December 31. GitHub schedules run from the default branch and can be delayed or dropped under high load; the scheduled minute is not an execution-time guarantee.
First, declare LMH in your project's quality dependencies, then add a headers-fix Make target using the same environment as your header check in CI. For a quality dependency group, append the package to the existing group in pyproject.toml:
[dependency-groups]
quality = ["lint-my-headers==0.7.0"]
Run uv lock and commit the updated uv.lock, then add:
headers-fix:
uv run --locked --group quality lmh fix
For a quality extra, add the package to quality in [project.optional-dependencies] and use uv run --extra quality lmh fix instead; retain --locked when the repository commits its uv lockfile. The target must install/synchronize its environment because scheduled runners start fresh. A global uv tool install does not install LMH in the runner's project environment. The LMH version stays in pyproject.toml, without a second pin in the workflow.
Save this consumer template as .github/workflows/update-copyright-years.yml. This repository's own workflow builds its locked Rust source instead of installing a published package; the publication scripts match:
name: update copyright years
on:
schedule:
- cron: "1 0 1 1 *"
timezone: Europe/Paris
permissions:
contents: read
concurrency:
group: annual-copyright-years
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
TZ: Europe/Paris
BASE_BRANCH: ${{ github.event.repository.default_branch }}
UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/lmh-annual-env
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.12.5"
- name: Create the annual pull request
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
copyright_year="$(date +%Y)"
branch_name="automation/update-copyright-years-$copyright_year"
test "$branch_name" != "$BASE_BRANCH"
pr_number="$(gh pr list --base "$BASE_BRANCH" --head "$branch_name" --state all --json number --jq '.[0].number // empty')"
if test -n "$pr_number"; then
echo "Annual pull request #$pr_number already exists."
exit 0
fi
remote_sha="$(git ls-remote --heads origin "$branch_name" | cut -f1)"
if test -z "$remote_sha"; then
make headers-fix
if git diff --quiet -- ':(glob)**/*.py'; then
echo "Copyright years are already current."
exit 0
fi
git switch -c "$branch_name"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -u -- ':(glob)**/*.py'
git commit -m "chore: update copyright years for $copyright_year"
git push --set-upstream origin "$branch_name"
fi
gh pr create --base "$BASE_BRANCH" --head "$branch_name" \
--title "chore: update copyright years for $copyright_year" \
--body "Annual refresh of recognized Python copyright years using the declared header policy."
The external UV environment keeps installed dependency files outside the scan and PR. Repairs follow [tool.lint-my-headers] paths/exclusions and update only recognized stale years for its declared owner in supported Python files. They preserve creation years and all other bytes/modes. Missing, ambiguous, unsafe, or wrong-owner notices require manual review; a failed repair stops before any branch or PR is published. To cover more Python source files, extend the declared paths while preserving generated/vendor exclusions. This annual workflow stages Python changes only.
Each year gets one automation/update-copyright-years-YYYY branch. Existing PRs, including closed PRs, are left untouched; reopen the existing PR if it was closed by mistake. If a push succeeded but PR creation failed, rerunning resumes PR creation from that branch without overwriting it. If repairs make no changes and no annual branch already exists, no PR is created. Only tracked Python changes are committed; generated untracked files are excluded. Pushes never target the default branch or force-update an existing branch.
Repository setup:
- Merge the policy, Make target and workflow into the default branch. The workflow has only a schedule trigger, so it does not run on pushes, PRs or manual dispatch.
- Enable Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests. The job requests only
contents: writeandpull-requests: write; it does not approve or merge its PR. - PR workflows triggered by
GITHUB_TOKENrequire a writer to click Approve workflows to run before their CI starts. Use an existing GitHub App installation token if unattended PR checks are required. - GitHub disables schedules in public repositories after 60 days without activity; verify the schedule remains enabled before the annual run.
See GitHub's schedule behavior and workflow-token triggering rules.
For a failed scheduled run, fix the cause and use Actions → Re-run failed jobs within 30 days of the original run. If no run was created or that window has expired, re-enable a disabled schedule for future runs, then run make headers-fix on a fresh branch from the default branch, inspect the diff, and open a manual PR. The workflow has no manual dispatch trigger.
Contributing
Lint My Headers is open source under Apache-2.0. Contributions, bug reports, and feedback are welcome. See CONTRIBUTING.md for local development and checks, AGENTS.md for runtime contracts, and RELEASE_NOTES.md for release and migration notes.
Metadata
Release files for lint-my-headers 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lint_my_headers-0.7.0.tar.gz | 183.8 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| lint_my_headers-0.7.0-py3-none-win_arm64.whl | Python 3 | none | Windows ARM64 | Details |
| lint_my_headers-0.7.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| lint_my_headers-0.7.0-py3-none-musllinux_1_2_x86_64.whl | Python 3 | none | Linux musl 1.2+ x86-64 | Details |
| lint_my_headers-0.7.0-py3-none-musllinux_1_2_aarch64.whl | Python 3 | none | Linux musl 1.2+ ARM64 | Details |
| lint_my_headers-0.7.0-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| lint_my_headers-0.7.0-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| lint_my_headers-0.7.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| lint_my_headers-0.7.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 21.1 MB
Release files / lint_my_headers-0.7.0.tar.gz
| Download URL | lint_my_headers-0.7.0.tar.gz |
|---|---|
| Size | 183.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ef004b7b0897dfba5d5fd0989393c93db99f73c2c9750df43318e9bffde93738
|
|
BLAKE2b-256 checksum How to use checksums |
eb9788b88f05c781cbc2dc1ed1e2931829d2ef33df5de55c837802a65a2ed8ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-win_arm64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-win_arm64.whl |
|---|---|
| Size | 2.4 MB |
| Tags | Python 3 Windows ARM64 |
|
SHA-256 checksum How to use checksums |
8ad64d2516ce7b3a0ef4bbad3cc231df52701d01495fb6a84e6a9c746d7bac7f
|
|
BLAKE2b-256 checksum How to use checksums |
e68bf418f5d3b685207ac168be8b2bbea1adbca3d9a4a0a16e827a9faf9c902a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-win_amd64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-win_amd64.whl |
|---|---|
| Size | 2.5 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
33eb07ba766fbb65f1166e8404243c02b232592f780a00cd2abd0b0cbc6fd731
|
|
BLAKE2b-256 checksum How to use checksums |
6439a4af992ad5dfeacda78c17adef48529bd13aa447de74711bc2592da822db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.9 MB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
f9a5f33db8ed4e1b28b7b95c856f074796a88c51c9bcb8a4a590c98c5dc035aa
|
|
BLAKE2b-256 checksum How to use checksums |
ff2325936673bc7d22fac119829cb5799e54c9a7bed156eb79198ab24cb66e45
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-musllinux_1_2_aarch64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | Linux musl 1.2+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
ac5d84800559b0c74ad796a447a3b76e4f9c1b114ac9a1a9ce1419f9708fcf22
|
|
BLAKE2b-256 checksum How to use checksums |
65e74c0a81c6d9f77c4f874fe4cb65a0d26057618485b2d26af87aefd1bd42a0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-manylinux_2_28_x86_64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.8 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
20a6137abe14d63a3f29971ecf68639ca7cc20bcd48fb45339109acd4acb1162
|
|
BLAKE2b-256 checksum How to use checksums |
4c54c1504f742f3bf1270ca969ef5de7187d55dbaecf7a82fced469bfe8030b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-manylinux_2_28_aarch64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
6d05e7ad2217fc0c03db155acc6873cf1aa353d84e9efdfa635532f00dcf6fb7
|
|
BLAKE2b-256 checksum How to use checksums |
3335d6f3eee68ec63a25cc401aea6aea8c8ca8063c975eaec820aca71c1e9c38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-macosx_11_0_arm64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.5 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
845a38e0538548a5c976b09ef0a69e24ee466fb023df8afded503289ccba1919
|
|
BLAKE2b-256 checksum How to use checksums |
ee47a8c759181b816e4d40d469932e94d030335112d98e4166228f34c0009fcc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / lint_my_headers-0.7.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | lint_my_headers-0.7.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
f0534f0a018677fcbef654e361a55c91ff65cff52325d6a6cbe79af03bfc0b28
|
|
BLAKE2b-256 checksum How to use checksums |
a7128d4c7107abf0cbf4f98e3fa948e5863cde193a1d568c634bc8994c91cd74
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|