agent-linters

lintp · one line per finding

Linter output your coding agent can actually afford

Default linter output is written for a human reading one error, so every finding arrives with a code frame, a caret and a help block. An agent gets fourteen of those lines per finding and spends its context on formatting. lintp prints the instruction and drops the rest.

$ lintp src/
src/api.py:1     remove unused import `os`
src/api.py:14    return str, not int
src/ui.tsx:8     add a `key` prop to the list element
src/theme.css:1  remove the duplicate CSS property

4 findings, 2 auto-fixable (lintp --fix)

Same three findings. 31 lines, or 5.

What your agent reads today

ruff check + mypy · 31 lines · 587 bytes

$ ruff check src/api.py
I001 [*] Import block is un-sorted or un-formatted
 --> src/api.py:1:1
  |
1 | / import os
2 | | import json
  | |___________^
help: Organize imports
  |
1 + import json
2 | import os
  - import json
3 |
  |

F401 [*] `os` imported but unused
 --> src/api.py:1:8
  |
1 | import os
  |        ^^
2 | import json
  |
help: Remove unused import: `os`
  |
  - import os
1 | import json
  |

Found 2 errors.
[*] 2 fixable with the `--fix` option.
$ mypy src/api.py
src/api.py:11: error: Incompatible return value type (got "str", expected "int")  [return-value]
Found 1 error in 1 file (checked 1 source file)

What lintp hands it

lintp · 5 lines · 155 bytes

$ lintp src/api.py
src/api.py:1   sort the import block
src/api.py:1   remove unused import `os`
src/api.py:11  return int, not str

3 findings, 2 auto-fixable (lintp --fix)
Two commands, two output formats, and most of the first one is ASCII art. lintp keeps the file, the line and the verb. Nothing is invented and nothing is swallowed: a rule with no hand-written phrasing falls through to the linter's own message.

One command, four linters

Point lintp at a path. It routes by extension, then prints every finding in the same shape no matter which tool produced it. Exit codes stay useful: 0 clean, 1 findings, 2 a linter failed to run.

File type to linter
ExtensionsLinter
.py .pyiruff + mypy
.js .mjs .cjs .jsx .ts .tsx .mts .ctsoxlint
.css .scss .json .jsoncbiome

biome lints JS and TS as well, so those findings are dropped. oxlint owns them, and duplicate prompts are worse than none.

Flags

lintp flags
FlagEffect
--fixapply the safe autofixes first, then report what remains
--codesappend the rule name to each line
--strictadd oxlint's pedantic and style rules
--toolsrun a subset, e.g. --tools ruff,mypy

Your repo's config still wins

Where a repo has an .oxlintrc.json, lintp passes it with -c and adds no category flags of its own. A forced category switches rules back on that the repo deliberately turned off, which is how a wrapper starts giving advice the project already rejected.

Install

Three steps, in order. Nothing here compiles.

  1. Install the linters you want

    None of them ship with agent-linters.

    $ uv tool install ruff && uv tool install mypy
    $ npm install -g oxlint @biomejs/biome
  2. Clone the repo and run the installer

    $ git clone https://github.com/cocodedk/agent-linters ~/projects/agent-linters
    $ cd ~/projects/agent-linters && ./install.sh

    It symlinks bin/, shims/ and config/ into ~/.local and ~/.config, and adds one source line to your shell profile. A real file already sitting at a target gets moved to <name>.bak first.

  3. Tell Claude Code

    Claude Code reads its own env block rather than your shell profile, so add two variables to ~/.claude/settings.json, or let ./install.sh --claude do it with jq.

    "env": {
      "RUFF_OUTPUT_FORMAT": "concise",
      "RUFF_CACHE_DIR": "/home/YOU/.cache/ruff"
    }

./install.sh --uninstall reverses all of it and puts the backups back. Re-running the installer is safe. Requirements are POSIX sh, Python 3.8 or newer, and the linters themselves.

Four traps that cost real debugging

Every one of these made a linter point at the wrong thing. They are why lintp does more than export an environment variable.

  • Never share one MYPY_CACHE_DIR

    Two projects that each contain a utils.py collide in a shared cache, and mypy then reports the finding against the other project's file. An agent that trusts the path edits a file in the wrong repository. The shim gives every working directory its own cache. An inherited MYPY_CACHE_DIR still wins, as an explicit setting should, so unset yours if you ever exported a shared one.

  • ruff lints whatever file you name

    ruff check app.js returns a screenful of imaginary Python syntax errors. Filter paths by extension before you invoke anything; lintp does it in select().

  • mypy's paths are relative to the target

    A bare a.py in mypy's output can resolve to a different same-named file in your current directory. Anchor the reported paths to the target roots first.

  • oxlint reads config from the cwd only

    ruff, mypy and biome all search upward. oxlint does not, so running it from a subdirectory silently loses every exception the repo's config exists to declare. lintp walks up and passes -c itself.