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 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 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)
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.
| Extensions | Linter |
|---|---|
.py .pyi | ruff + mypy |
.js .mjs .cjs .jsx .ts .tsx .mts .cts | oxlint |
.css .scss .json .jsonc | biome |
biome lints JS and TS as well, so those findings are dropped. oxlint owns them, and duplicate prompts are worse than none.
Flags
| Flag | Effect |
|---|---|
--fix | apply the safe autofixes first, then report what remains |
--codes | append the rule name to each line |
--strict | add oxlint's pedantic and style rules |
--tools | run 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.
-
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
-
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/andconfig/into~/.localand~/.config, and adds onesourceline to your shell profile. A real file already sitting at a target gets moved to<name>.bakfirst. -
Tell Claude Code
Claude Code reads its own
envblock rather than your shell profile, so add two variables to~/.claude/settings.json, or let./install.sh --claudedo it withjq."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.pycollide 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 inheritedMYPY_CACHE_DIRstill 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.jsreturns a screenful of imaginary Python syntax errors. Filter paths by extension before you invoke anything; lintp does it inselect(). -
mypy's paths are relative to the target
A bare
a.pyin 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
-citself.