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 nosocket,urllib,http.client,ssl,smtplib,webbrowserorrequestsimport. 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.iniand.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.cfgjust 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_BUILTINSis 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):
| Tool | Capabilities | Declared mounts | What it is (catalogue) |
|---|---|---|---|
flake8 | ro | none | Check Python source for logic errors, PEP 8 style and complexity — the worked example above |
mypy | rw | ~/.config/mypy/config (ro), ~/.mypy.ini (ro) | Optional static typing for Python |
pylint | ro | ~/.cache/pylint (writable dir), ~/.pylintrc, ~/.config/pylintrc (ro) | Static code analyser for Python — read-only CWD, writable cache: the split as designed |
black | rw | ~/.config/black (ro), ~/.cache/black (writable dir) | Reformat Python source files to a single deterministic style — it writes the files it reformats |
bandit | rw | none | Scan Python source for common security issues |
pytest | rw | none | Run 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.