scitools-hook¶
scitools-hook measures the routines, classes and files that a commit touches, compares
each one against what it was before the commit, and refuses the commit that makes any of
them measurably worse. The measurement comes from
SciTools Understand, so it covers twelve languages rather than the
one the tool happens to be written in. Debt that was already there is reported as
pre-existing and does not block. Only regression blocks.
That line is the whole design. Four errors are real and are printed. None of them blocks, because none of them got worse in this commit.
The argument¶
Situation. A coding agent is very good on a small codebase. The whole relevant neighbourhood of a change fits in the context window, the agent reads it, and the edit it produces is usually right the first time.
Complication. That stops being true as the project grows, and the usual response is to reach for a larger, more expensive model. A larger model buys a larger context window. It does not buy a smaller problem, so it pays to work around the thing that is actually going wrong instead of fixing it.
Question. What is actually degrading? Not the model. Not the prompt.
Answer. The amount of code that has to be held in mind at once in order to change one
routine safely. Call it the working set. A routine with a CyclomaticStrict of 45 and a
CountPath of 9.6 × 108 has a working set no agent holds, at any price. It
does not reason about that routine; it pattern-matches at it and guesses. The guess compiles,
the tests that exist still pass, and a human finds the defect later.
An agent's working set is bounded. A codebase that outgrows it stops being editable by agents at any model price. This gate keeps the shape of the code inside that bound, one commit at a time, with a ratchet.
This is not a fourth linter¶
You already run ruff. You probably run mypy, and there may be a CodeQL workflow on the
repository as well. The reasonable first reaction to another static-analysis tool in CI is
that it will produce more noise on the same axis.
It is a different axis.
| Question | Answered by |
|---|---|
| Is this file syntactically and stylistically correct? | ruff, flake8, clang-tidy |
| Do the types line up? | mypy, pyright, tsc |
| Does this code have a defect or a vulnerability? | CodeQL, Semgrep |
| Is this change safe to make in a routine of this shape, and did it make that shape worse? | this gate |
ruff has no opinion about a routine whose CountPath is 16384, because CountPath is not
a lint. Neither ruff nor mypy compares a file against what it was before the change,
because neither one has a before to compare against. The gate does both, and it does nothing
else: it never comments on style, never rewrites code, and has no view on whether your types
are right.
The two tools are not merely complementary. One of them can create work the other cannot
see: ruff's UP047 rewrites a TypeVar into PEP 695 def f[T](x) syntax, which
Understand 6.5 cannot parse. The parse aborts at the declaration and the rest of the file
silently leaves the database. That is documented, with the measurement, in
What this adds beyond ruff and mypy.
Compare against CodeQL, Semgrep, LOC tools and import-graph tools →
How you would prove this wrong¶
A document that cannot be wrong is not an argument, so here is the disproof condition, stated plainly:
The claim, and what would falsify it
Claim. Agent effectiveness on a codebase decays as the working set of an average change grows, and a complexity ceiling enforced per commit keeps it from growing.
Disproof. If your agents stay effective as your codebase grows, with no complexity ceiling of any kind, the claim is wrong and this tool solves a problem you do not have.
That is testable on your own repository without installing anything: take the ten routines
your agents most often get wrong, and look at their CyclomaticStrict and CountPath
against the median for the project. If there is no relationship, stop reading.
It is written in Python. It does not only check Python¶
The tool is packaged as a Python project and installed with uv. That is the implementation
language, not the coverage. The analysis is Understand's, and Understand build 1204 accepts
twelve languages across 58 file extensions:
Ada .a .ada .adb .ads .gpr
Assembly .asm .s
Basic .vb
C# .cs
C++ .C .H .c .cc .cpp .cu .cuh .cxx .h .hh .hpp .hxx .inl .m .mm
Fortran .F .F90 .f .f03 .f77 .f90 .f95 .for .ftn
Java .java
Jovial .cpl .jov
Pascal .dfm .dpr .fmx .pas .sp .sql
Python .py .upy
VHDL .vhd .vhdl
Web .cjs .css .cts .htm .html .js .mjs .mts .php .ts .tsx .xml
A C++ team or a Java team installs a Python-packaged hook and gates their own language. The
complexity axis is language-agnostic in a way that a rule pack is not: CyclomaticStrict
means the same thing in Fortran as in TypeScript, whereas a CodeQL query pack has to be
written per language and per vulnerability class.
Coverage is not uniform across the twelve, and the gate reports what it cannot measure
rather than skipping it quietly. The details, including why .sql maps to Pascal and why an
HTML asset can fail to parse as source, are on the
Languages page.
Maturity¶
This is version 0.1.0a5. An alpha. The gradient across the twelve languages is real and
worth stating before you decide whether to try it:
| Language | Status |
|---|---|
| Python 3 | Exercised end to end: unit tests, contract tests against a real licensed Understand, and an e2e suite that drives real git commit runs through the installed hook. |
| C++ | Exercised in the contract suite. It is what proves EntityKey tells a real overload pair apart. Not exercised end to end. |
| Ada, Assembly, Basic, C#, Fortran, Java, Jovial, Pascal, VHDL, Web | Wired through the same extension map and expected to work. Untested. |
"Untested" is the accurate word, and it is not "unsupported". If you are deciding whether to point this at a Fortran codebase: it should work, and nobody has run it. The extension map itself is not a guess. A contract test re-measures it against the installed Understand build in both directions, so a wrong or missing suffix fails loudly rather than mapping a file to nothing.
What you get¶
- A commit-time gate.
scitools-hook check --staged, as a native.git/hooksshim or as apre-commitframework hook. It analyses the staged change againstHEAD, not the whole repository, so it is fast enough to sit in front of every commit. - A ratchet. Per-entity before and after, inside one commit. You never pay down the debt you have; you are forbidden from adding to it.
- Review aids.
scitools-hook explain --graphs --impact --out DIRexports SVG dependency and butterfly graphs plus an impact set, so a reviewer can look at the shape of a large agent-authored change before opening a single file. - Machine-readable output.
--format jsonand SARIF 2.1.0, with a remediation hint on every finding. - Rules an agent reads.
scitools-hook agent-rules --write AGENTS.mdwrites the effective limits into your agent instructions file, so the agent knows the numbers before it writes the code rather than after the commit is refused.
Getting it¶
This tool is not on PyPI and will not be. It is distributed as a GitHub release, so every install form names the source:
pip install scitools-hook, uv tool install scitools-hook and a bare uvx scitools-hook
all resolve to nothing. See Install for the wheel-from-a-release form and
for why this distinction was a live defect rather than a documentation nicety.
Requirements¶
An existing SciTools Understand installation, version 6.5 or later, with a licence. The tool never installs Understand and never bundles it.
The PyPI package named understand is not SciTools Understand
Do not pip install understand. It is an unrelated project. This tool uses the API
shipped inside your Understand installation.
Install it → · Run it on a repository in five minutes → · Point it at a real, messy codebase →
If your first run reports hundreds of findings, that is expected
A first check --all on a real repository returns a large number, and it is an inventory
rather than a verdict — on this repository, 99.9% of routines are already inside the
default complexity limit and only 3 of 230 findings are routine.CyclomaticStrict at all.
Rescuing a problematic project is the six-step path from that number to
a gate you can actually commit through.
About the numbers in these documents¶
Every measured claim here carries the command that produced it and the build it was measured against. Where a number came from a repository you cannot see, the document says so. Where something is expected but unverified, the document says "unverified" rather than rounding it up to a fact.
That is not a stylistic preference. It is the same rule the source code is written under: an inference stated as a fact is a defect, and the project's own history includes several corrections where a claim turned out to have been inferred rather than measured.