Skip to main content

Lint My Headers

Lint My Headers

Fast copyright and license header checks, powered by Rust.
One policy for your codebase. Clear findings. Small, reviewable repairs.

PyPI version Tests Apache-2.0 license

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.

Native check latency

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.

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: write and pull-requests: write; it does not approve or merge its PR.
  • PR workflows triggered by GITHUB_TOKEN require 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.

Report a bug · Read the docs · View releases

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)

Source distribution for lint-my-headers 0.7.0
File Size Uploaded
lint_my_headers-0.7.0.tar.gz 183.8 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for lint-my-headers 0.7.0
File
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}

Release history Release notifications | RSS feed

This release

0.7.0 This release

9 release files

0.6.0

9 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page