IDEs and linters

The pain being removed

A linter installed the native way pollutes the machine it is installed on: a pip install flake8 lands in your user site-packages (or the global tree, if you had the courage), an npm i -g eslint mutates the global npm tree, and the venv route — the polite one — means hacking a venv's bin/ onto the editor's PATH and hoping the project's Python and the tool's Python agree. Every route is state you did not ask for, on a machine the linter does not need.

The shape

Point the editor's lint, format and diagnostics command at the tool itself:

kapsl flake8 .
kapsl mypy src/
kapsl black --check .

Or at the shim — which is what an editor invocation actually is. No install, no venv, no pollution: the tool is current, it was scanned before it ran, and its entire filesystem view is the project tree. The editor gets the exit code and the stdout it already parses; nothing about the integration changes.

The worked example: flake8's declared boundary

This is the tool to walk, because its boundary is the tightest in the catalogue and every part of it was measured — read out of the installed tree and re-checked by running the tool in the sandbox, not taken from flake8's documentation.

kapsl --info flake8
   Check Python source for logic errors, PEP 8 style and complexity

   IMAGE        ghcr.io/kapsl-sh/flake8:latest
   digest       sha256:9b84…3d46   1 MB · 2 layers

   LINES        RESOLVES TO
 @latest      7.3.0             in use
                lines float — kapsl carries security updates onto them
                pin one with flake8@<version> · a digest never moves

   CAPS          ro

ro, and that is the whole story of the capability:

  • No network, and it is a proof, not a policy. The entire installed tree — 90 files, every one a .py — contains no socket, urllib, http.client, ssl, smtplib, webbrowser or requests import. There is no code in the image capable of opening a connection, so the editor's subprocess cannot phone home and there is no vendor opt-out to configure.
  • No mounts at all. flake8 discovers configuration only from the working directory: its config lookup walks upward from the CWD looking for setup.cfg, tox.ini and .flake8, and stops at $HOME. There is no user-level config file and no cache — so the working directory the sandbox already mounts is the whole of the tool's input, and any mount would be a hole bought for nothing. The consequence for you: a project's .flake8/setup.cfg just works, and a user-level one in your home directory does not — that is the declared behaviour, not a bug.
  • No env to set, one var forwarded. Nothing for kapsl to set; PYFLAKES_BUILTINS is forwarded because a user who has it exported and does not get it forwarded sees false F821s inside the sandbox and correct results outside it.

Run it for real. A probe.py with deliberate PEP8 violations, and a setup.cfg in the same directory:

kapsl flake8 .
./probe.py:1:1: F401 'os' imported but unused
./probe.py:2:1: F401 'sys' imported but unused
./probe.py:4:1: E302 expected 2 blank lines, found 1
./probe.py:4:15: E201 whitespace after '('
./probe.py:4:17: E231 missing whitespace after ','
./probe.py:7:1: E305 expected 2 blank lines after class or function definition, found 1

Exit code 1, the editor shows the failures. Note what is not in the output: probe.py line 7 is 90 characters long, and setup.cfg sets max-line-length = 100 — so no E501. The same file in a directory with no project config gets the default:

kapsl flake8 probe.py
probe.py:1:1: F401 'os' imported but unused
probe.py:2:1: F401 'sys' imported but unused
probe.py:4:1: E302 expected 2 blank lines, found 1
probe.py:4:15: E201 whitespace after '('
probe.py:4:17: E231 missing whitespace after ','
probe.py:7:1: E305 expected 2 blank lines after class or function definition, found 1
probe.py:7:80: E501 line too long (90 > 79 characters)

Same tool, same file, one extra finding — the difference is the config flake8 found by walking up from the CWD. That is the config-discovery caveat working as declared: the project's config is authoritative inside the sandbox, because the project is the sandbox.

The --output-file spec gap, and the redirect that costs nothing

The one flag that does not work is --output-file — flake8's single write path, and the CWD is read-only:

kapsl flake8 --output-file=report.txt .
OSError: [Errno 30] Read-only file system: 'report.txt'

The declaration calls this out as a spec gap: the boundary flake8 actually wants is "ro, and rw only when --output-file or --tee is given", which is a per-flag narrowing, and the mechanism narrows per subcommand. The declaration takes the tighter posture and lets the flag fail loudly. The equivalent that costs the sandbox nothing is the host-side redirect — it happens outside the container, and the exit status comes through:

kapsl flake8 . > report.txt   # exit 1, report.txt holds the full report

The tighter boundary wins, and nobody widens every invocation to rw so that one non-default flag can write into the tree being linted.

The rest of the Python tooling

The other linters the editors point at, and what the live catalogue carries for each (checked 2026.35.16, 2026-08-25):

ToolCapabilitiesDeclared mountsWhat it is (catalogue)
flake8rononeCheck Python source for logic errors, PEP 8 style and complexity — the worked example above
mypyrw~/.config/mypy/config (ro), ~/.mypy.ini (ro)Optional static typing for Python
pylintro~/.cache/pylint (writable dir), ~/.pylintrc, ~/.config/pylintrc (ro)Static code analyser for Python — read-only CWD, writable cache: the split as designed
blackrw~/.config/black (ro), ~/.cache/black (writable dir)Reformat Python source files to a single deterministic style — it writes the files it reformats
banditrwnoneScan Python source for common security issues
pytestrwnoneRun a Python test suite and report the results; forwards PYTEST_* and BUILD_NUMBER

Two of them, run for real against the same probe file:

kapsl black --check probe.py
would reformat probe.py

Oh no! 💥 💔 💥
1 file would be reformatted.
kapsl mypy probe.py
/kapsl/home/.config/mypy/config: No [mypy] section in config file
Success: no issues found in 1 source file

mypy's warning is the declared ~/.config/mypy/config mount doing its job: the host's own mypy config reached the container read-only through the mount the declaration made, and mypy complained about its contents — a host-state leak, visible, that the boundary let happen on purpose.

What is available

Published and runnable today (catalogue 2026.35.16): flake8, mypy, pylint, black, bandit, pytest. Declared in the index but not yet published: ruff and tsc (TypeScript). Language servers (gopls, pyright, …) follow as their projects land — each with the same shape: a declaration that names the mounts, the network class and the env it needs, and the index page that shows the same boundary to everyone else.

The pattern generalizes past Python: the languages section covers each ecosystem's runtimes and tooling, and the catalogue page is where a tool's declared boundary is the entry, not an annotation on it.