Skip to main content

lintex

PyPI Python CI License: MIT

A fast linter for Python written by LLM coding agents. lintex catches the mistakes agents actually make, such as hallucinated imports, removed APIs, SQL built with f-strings, except Exception: pass, and # ... rest of the code unchanged. It reports each one with an explanation an agent can act on directly.

app/db.py  2 errors
  7:21    ✖ error   LX201  SQL statement built with an f-string; interpolated values are not escaped.
         7 │ cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
           │             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         why Interpolating values into SQL text allows SQL injection whenever any value is
             influenced by user input, and breaks on quotes in ordinary data.
         fix Use bound parameters and pass values separately, e.g. `cur.execute("SELECT * FROM
             users WHERE id = %s", (user_id,))` (placeholder style depends on the driver: `?` for
             sqlite3, `%s` for psycopg/MySQLdb, `:name` with SQLAlchemy `text()`). ...

  13:9    ✖ error   LX401  Placeholder comment '... rest of the implementation unchanged' stands
                           in for code that is not there.
         13 │ # ... rest of the implementation unchanged
            │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         why This looks like an elided edit: an LLM summarised existing code with a comment
             instead of reproducing it, so saving the file deletes that code.
         fix Restore the omitted code from the previous version of the file (e.g. `git diff` /
             `git show HEAD:<path>`), then make only the intended change.

✖ Found 2 errors in 1 file  · checked 1 file in 4 ms
  LX201 sql-string-interpolation  ×1
  LX401 placeholder-code          ×1
  Each finding's `fix` line says what to write instead. `lintex explain CODE` has the full story;
  `# lintex: ignore[CODE]` silences a line on purpose.

Findings are grouped by file with the exact span underlined. The rationale (why) is shown once per rule so a long report stays readable; pass -v to see it every time. Output is coloured on a terminal and falls back to plain ASCII where Unicode or colour isn't available (NO_COLOR and FORCE_COLOR are honoured).

Where it sits

flowchart LR
    subgraph loop["While the agent works"]
        direction LR
        A["Agent edits a .py file"] --> H["Claude Code<br/>PostToolUse hook"]
        H --> L["lintex<br/>one parse, one walk, a few ms"]
        L -- "findings, each with<br/>why + fix" --> A
        L --> G[("session ledger")]
        G --> S["status line<br/>lintex ▮▮▮▮▮ ✖ 1 · 3 fixed"]
        G --> R["lintex report<br/>caught · fixed · still open"]
    end
    subgraph gate["Before code lands"]
        direction LR
        C["git commit"] --> P["pre-commit hook<br/>lintex check --staged"]
        CI["CI"] --> GH["lintex check --changed --format github<br/>annotations on the PR diff"]
    end

The hook loop is where lintex earns its keep: the agent sees a finding and its fix in the same step it made the mistake, so most problems never reach a commit. The commit and CI gates catch what slips through and show people only the lines they changed.

  • Fast enough for the agent loop. One ast.parse and one tree walk per file, with rules dispatched by node type. The cost is close to parsing alone. The CPython stdlib (~300k lines) lints in about 3 seconds on a modest machine, and a single edited file takes a few milliseconds.
  • Light. The only dependency is tomli, and only on Python < 3.11. YAML config support is an optional extra.
  • Output an agent can act on. Every finding has a why and a fix. Use --json for harnesses.
  • Hooks included. lintex install-hook git adds a pre-commit hook. lintex install-hook claude-code sets up a Claude Code PostToolUse hook that reports problems back to the model right after it edits a file.

Install

pip install lintex            # or: uv tool install lintex / pipx install lintex
pip install "lintex[yaml]"    # if you want .lintex.yaml configuration

Usage

lintex                          # lint the current directory
lintex check src/ app.py        # lint specific paths
lintex check --staged           # lint what is staged in git (the index, not the working tree)
lintex check --changed          # only files changed since the base branch, only changed lines
lintex check --changed=HEAD~3   # ...since a specific ref
lintex check --json             # machine-readable output (same as --format json)
lintex check --format concise   # one `path:line:col: CODE message` per finding, for editors/grep
lintex check --format github    # GitHub Actions annotations, shown inline on the PR diff
lintex check --format full      # every finding with its full why/fix; what the hooks give agents
lintex check -v                 # pretty output with the rationale repeated for every finding
lintex check --select LX1,LX2   # only hallucination + banned-pattern rules
lintex check --fail-on warning  # non-zero exit on warnings too
lintex rules                    # list rules and whether they are enabled
lintex explain LX101            # documentation for one rule

Exit status: 0 means clean (or only findings below fail-on), 1 means violations, 2 means a usage or configuration error.

--changed is how to adopt lintex on a codebase that already has findings: it compares the working tree with the merge base of the default branch (origin/HEAD, else main/master), lints only the Python files that differ, and reports only findings that touch a changed line. New and untracked files are checked in full. Pass a ref (--changed=main, --changed=HEAD~3) to choose the base.

Rules

Code Name Default What it catches
LX001 syntax-error error The file does not parse (often a truncated edit)
LX1xx Hallucination guard
LX101 hallucinated-api error/warn Removed or renamed APIs: collections.Mapping, distutils, numpy.float, openai.ChatCompletion, pydantic.BaseSettings, dict.iteritems(), datetime.utcnow() (warn), ...
LX102 nonexistent-stdlib-name error Names that don't exist in a stdlib module (os.path.joinpath, from typing import Json), with "did you mean" suggestions
LX103 unresolvable-import opt-in Imports that aren't installed, stdlib, or local modules
LX2xx Banned patterns
LX201 sql-string-interpolation error SQL built with f-strings, .format(), %, or +
LX202 raw-sql opt-in Any raw SQL in .execute() / text() for ORM-only projects
LX203 dangerous-call error eval/exec, pickle.loads, yaml.load, os.system, shell=True, verify=False
LX204 insecure-file-operation error tempfile.mktemp, world-writable modes, tar extractall() without filter=, hard-coded /tmp/ paths
LX205 banned-api error Project-specific banned modules and functions, with your own message
LX3xx Agent bloat
LX301 function-too-long warning More than 50 lines of code (says which blocks to extract)
LX302 high-complexity warning Cyclomatic complexity above 10
LX303 too-many-parameters warning More than 7 parameters
LX304 deep-nesting warning Blocks nested more than 4 levels deep
LX305 file-too-long warning Module with more than 600 lines of code
LX4xx Agent artifacts
LX401 placeholder-code error # ... existing code ... and other elided edits; # TODO: implement (warning)
LX402 swallowed-exception error except Exception: pass
LX403 bare-except warning except:

Hallucination rules skip code inside compatibility guards, such as try: ... except ImportError and if sys.version_info ... / sys.platform / hasattr(...). LX102 imports only stdlib modules and never third-party code. It checks against the Python version that runs lintex.

Where the API knowledge comes from

LX101's list of removed, deprecated and Python-2-only names lives in two data files under lintex/knowledge/, loaded once per run (about a millisecond):

  • curated.toml: hand-written entries with specific fixes (openai.ChatCompletion, pydantic.BaseSettings, numpy.float, PEP 594 modules, ...). Adding one is a five-line TOML entry with a fix; no Python required.
  • typeshed.json: generated from typeshed, the type stubs mypy and pyright run on. stdlib/VERSIONS gives every stdlib module removal, and @deprecated decorators give deprecated functions and methods with the upstream's own message. Regenerate it with python -m lintex.knowledge.sync --typeshed /path/to/typeshed (or --from-mypy for the stdlib part). Curated entries win where both know a name.

Removals are judged against the project's minimum Python: lintex reads requires-python from pyproject.toml (or target-version = "py311" under [tool.lintex]). import cgi is an error when the minimum is 3.13 or later, and a warning ("still exists on your minimum, breaks on 3.13+") before that. Without either, the interpreter running lintex is the target.

Suppressing

value = eval(expr)  # lintex: ignore[LX203]
value = eval(expr)  # lintex: ignore            (every rule on this line)
# lintex: skip-file                             (anywhere in the file, on its own line)

Configuration

lintex looks for configuration starting in the linted path's directory and moving up, and uses the first match among .lintex.yaml, .lintex.yml, .lintex.toml, and pyproject.toml ([tool.lintex]). Keys can be written in kebab-case or snake_case.

[tool.lintex]
exclude = ["migrations", "scripts/legacy/*.py"]   # added to the defaults (.venv, build, ...)
ignore = ["LX305"]
enable = ["LX103", "raw-sql"]                     # turn on opt-in rules
fail-on = "error"                                 # or "warning"
severity = { LX301 = "error" }                    # change a rule's severity
per-file-ignores = { "tests/**" = ["LX203", "LX301"] }

[tool.lintex.rules.function-too-long]
max-lines = 40

[tool.lintex.rules.raw-sql]
hint = "Use the repository classes in app/db/repos.py instead of hand-written SQL."

[tool.lintex.rules.banned-api]
apis = { "requests" = "Use app.http.client (it adds retries, timeouts and tracing).", "print" = "Use `log = structlog.get_logger()`." }

[tool.lintex.rules.unresolvable-import]
known-modules = ["google.colab"]

The same settings in .lintex.yaml:

ignore: [LX305]
enable: [LX103]
rules:
  banned-api:
    apis:
      requests: Use app.http.client.

Hooks

Claude Code

lintex install-hook claude-code              # writes .claude/settings.json
lintex install-hook claude-code --scope local   # .claude/settings.local.json (not committed)

This adds the following entry, merged with any existing settings:

{"hooks": {"PostToolUse": [{"matcher": "Edit|Write|MultiEdit",
  "hooks": [{"type": "command", "command": "lintex hook claude-code"}]}]}}

After each edit, lintex checks the changed file. If it finds problems, it exits with status 2 and prints them to stderr, and Claude Code passes that output back to the model. Errors in the hook itself, such as a bad payload or config, never block the agent.

Session ledger and status line

Every time the Claude Code hook checks an edit, it appends the result to a per-project ledger (kept in your user cache directory, never in the repo). Two things read it:

lintex statusline puts a live health strip in Claude Code's status line while the agent works. One block per checked edit in this session (green clean, yellow warnings, red errors), then what is still open and how much the agent has already fixed:

Opus · myapp · lintex ▮▮▮▮▮▮▮▮▮▮  ✖ 1 ▲ 2 · 9 fixed
lintex install-hook statusline        # sets statusLine in .claude/settings.json

If you already have a status line script, lintex won't replace it; add lintex statusline --segment to that script's output instead.

lintex report is the ledger itself: what the hook caught, what the agent fixed on its next edit, and what is still open, per rule.

lintex ledger · myapp · last 24h
lintex ▮▮▮▮▮▮▮▮▮▮ ✖ 1 ▲ 2 · 9 fixed
12 caught across 14 edits of 5 files · 9 fixed by the agent · 3 still open

  LX201 sql-string-interpolation  caught   4  fixed   4  all fixed
  LX402 swallowed-exception       caught   3  fixed   2  1 open
  LX301 function-too-long         caught   2  fixed   0  2 open
  ...

Still open
  ✖ app/db.py:41  LX402 Exception handler catches Exception and silently discards it.

--since 7d, --all, --session ID, --json and --clear are available. Set ledger = false under [tool.lintex] to turn recording off, or point LINTEX_LEDGER_DIR somewhere else.

Git pre-commit

lintex install-hook git        # .git/hooks/pre-commit (honours core.hooksPath)

The hook runs lintex check --staged, which lints the staged content that will actually be committed. To use the pre-commit framework instead:

repos:
  - repo: https://github.com/700799/lintex
    rev: v0.1.0
    hooks:
      - id: lintex

lintex only catches known agent mistakes. It does not check that code is correct, well designed or does what you asked. Pair it with a general linter, a type checker and your tests:

Layer Catches When to run
lintex Hallucinated APIs, injection and security shortcuts, elided code, silenced errors, bloat After every agent edit (hook) and at commit
ruff Hundreds of general bug and style patterns, unused imports, undefined names After every agent edit and at commit
mypy / pyright Wrong argument counts, invented methods on typed objects, None misuse At commit and in CI (too slow for every edit on large projects)
pytest Whether the code actually works At commit and in CI

Claude Code hooks

Install both fast checks as PostToolUse hooks so the agent fixes cheap mistakes immediately (lintex install-hook claude-code adds the first one). In .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {"type": "command", "command": "lintex hook claude-code"},
          {"type": "command", "command": "jq -r '.tool_input.file_path // empty' | grep '\\.py$' | xargs -r ruff check --fix --exit-non-zero-on-fix 1>&2 || exit 2"}
        ]
      }
    ]
  }
}

Each hook exits 2 on findings, and Claude Code passes its stderr back to the model. The ruff hook auto-fixes what it safely can and reports the rest. If jq is not installed, use a short Python script instead of the shell pipeline.

Pre-commit framework

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
  - repo: https://github.com/700799/lintex
    rev: v0.1.0
    hooks:
      - id: lintex
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.13.0
    hooks:
      - id: mypy

Then pip install pre-commit && pre-commit install. Pin rev values to the versions you actually want; the ones above are examples.

Agent instructions

Add this to your CLAUDE.md (or equivalent) so the agent knows the checks exist and how to react:

## Code quality
- `lintex check <file>` and `ruff check <file>` run automatically after each edit.
  Fix every reported issue as its `fix:` line says before moving on.
- Never suppress a finding (`# lintex: ignore`, `# noqa`) to make it go away;
  suppress only when the code is deliberately an exception, and say why in the comment.
- Run `mypy src` and `pytest` before declaring a task done.

Library API

from lintex import Linter, lint_source, load_config

violations = lint_source(code, path="generated.py")
for v in violations:
    print(v.code, v.line, v.message, v.fix)

linter = Linter(load_config("."))       # reuse for many files
result = linter.lint_paths(["src"])
result.to_dict()                        # same shape as --json

Development

uv venv && uv pip install -e . pytest pyyaml ruff
pytest
ruff check src tests && ruff format --check src tests
lintex check .                          # lintex lints itself

A new rule subclasses lintex.rules.base.Rule. It sets code, name, summary, and node_types, implements check_node and/or check_file, and is added to ALL_RULES in lintex/rules/__init__.py.

License

MIT

Metadata

Release files for lintex 0.1.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 lintex 0.1.0
File Size Uploaded Provenance
lintex-0.1.0.tar.gz 81.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for lintex 0.1.0
File Interpreter ABI Platform
lintex-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 157.6 kB

Release files / lintex-0.1.0.tar.gz

Download URL lintex-0.1.0.tar.gz
Size 81.3 kB
Tags Source
SHA-256 checksum
How to use checksums
987471fe37af26b1d8761b2591cadd00a7e2123bcbfee34bd3eae6b2cd1bd364
BLAKE2b-256 checksum
How to use checksums
71912c83e4a68683ab162cc6149665461d0ac38caf161618809b56f2be4e6eb1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Release files / lintex-0.1.0-py3-none-any.whl

Download URL lintex-0.1.0-py3-none-any.whl
Size 76.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8a9b96fadea4c0d1c9b9e82926dcfdf6e86722a8325faddebb7cb544440cff7
BLAKE2b-256 checksum
How to use checksums
a73470f14a68b6626ced56b757ef55cf455419f12ad21b743ca2bcb2d7e626ee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

Security for lintex 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for lintex-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

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