Configuration
lessloc works without any configuration. When a project needs its own rules, its owner writes them in .lessloc.json at the root of the repository. This page lists every key with its default, how to accept or suppress a finding, and the switches that turn lessloc off.
Who owns .lessloc.json
The file is the project owner's policy. Commit it, so that every teammate, every agent and CI use the same settings. mode, interrupt and protect act only in agent sessions: the commit gate, lessloc check and CI do not read them.
- Agents need your approval to edit it. In the default
blockmode, an agent's edit to.lessloc.jsonor.lessloc-baseline.jsongoes to you for approval (the gate). CI and code review are the authority over this file. - Mistakes never reach the agent. An unknown key, a wrong type or broken JSON is reported by
lessloc doctor, and bylessloc checkas a tier-0configfinding, so CI fails until it is fixed. A misspelled key gets a "did you mean" suggestion. The pre-commit gate never refuses a commit over it. - A key with a problem is ignored, and its default applies. If the file is not valid JSON, every default applies. An exception without a reason is dropped; the other exceptions still apply.
- To start one, run
lessloc init --configin your terminal. It writes the file with the test command lessloc found. An agent creates it with its edit tool instead, so you approve it: in a session, a change to.lessloc.jsonor.lessloc-baseline.jsonmade by a shell command, such aslessloc init --configorlessloc baseline, is a tier-0policyfinding and holds the agent's turn.
Example
{
"test": "npm test",
"setup": "npm ci",
"protect": ["migrations/**", ".github/workflows/**"],
"boundaries": [
{ "files": "src/ui/**", "mustNotImport": "/db/", "why": "the UI reads data through src/api" }
],
"approvedDependencies": ["express", "zod", "vitest"],
"exceptions": [
{ "path": "legacy/**", "finding": "narration", "why": "vendored file, kept verbatim" }
],
"trailers": "verified"
}All keys
| Key | Default | Value |
|---|---|---|
test | discovered | a shell command |
setup | none | a shell command |
testTimeout | 300 | seconds |
probe | none | a shell command |
mode | "block" | "block", "warn" or "off" |
interrupt | the kinds marked "stops" in the reference | a list of finding kinds |
trailers | "off" | "verified", "all" or "off" |
protect | [] | a list of path globs |
boundaries | [] | a list of { "files", "mustNotImport", "why" } |
approvedDependencies | null | a list of package names, or null |
allow | [] | a list of path globs |
exceptions | [] | a list of { "path", "finding", "why" } |
maxCommentRatio | 0.25 | a number |
maxNetLines | 150 | a number |
maxNewFiles | 3 | a number |
dupLines | 6 | a number |
instructionBudget | 2000 | a number |
Path globs are relative to the repository root and use /. * matches within one folder, ** across folders, and ? one character. A glob must match the whole path: *.sql matches only files at the root, and **/*.sql matches them anywhere.
Verifying the work
test
{ "test": "python3 -m pytest -q" }The command that runs your whole test suite. An agent's passing whole-suite run on its final state is recorded as evidence for that exact state, which the handoff reports, and the commit trailers too when trailers is set. lessloc test, verify and ci run this command.
When test is not set, lessloc uses the first of these that fits:
npm test, when package.json has a realtestscriptmake test, when the Makefile has atest:targetcargo test, when there is a Cargo.tomlgo test ./..., when there is a go.modpython3 -m pytest -q, when there is a pytest.ini, tox.ini or conftest.py, or a[tool.pytestsection in pyproject.toml- for a
testsortestfolder withtest_*.pyfiles:python3 -m unittest discover -s tests -q(or-s test) when the first file uses unittest, otherwisepython3 -m pytest -q
On Windows, python replaces python3. lessloc init and lessloc doctor show the command in use.
Set test yourself when this guesses wrong, or when your suite runs through a script. A passing run of the configured command counts as the whole suite, and so does a whole-suite run of the same runner: for uv run pytest -q, also pytest, python3 -m pytest -x or uv run pytest. Only arguments that select a part, such as a file path or -k, narrow a run. A script lessloc cannot see into, such as ./scripts/test.sh, counts only when it is run itself. With a discovered command, a run counts when it tests the language the turn changed; set test to make it exact (what counts as evidence).
setup
{ "setup": "npm ci" }How a fresh checkout installs its dependencies. lessloc verify runs it in the clean checkout before the tests, and lessloc ci runs it before anything else. What verify does without it is under verify.
testTimeout
{ "testTimeout": 900 }How many seconds a test run may take in lessloc test, verify and ci. lessloc never runs your tests during an agent session, so this never limits the agent.
probe
{ "probe": "claude -p --output-format json" }The agent command that lessloc probe runs to ask a fresh agent where a behavior lives. The question arrives on stdin. Without probe, check lists the handoff probe under "Not checked".
How lessloc acts
mode
{ "mode": "warn" }"block"(default): the gate before writes and commands is on. It refuses placeholders, commented-out code and junk files, and sends policy edits and the common commands that skip the project's checks to you for approval (the gate)."warn": nothing is refused or sent to you for approval before a write or a command. The notes after writes and the review at the end of each turn still run, and can still send the agent back."off": the agent hooks do nothing in this repository. The git hooks,lessloc checkand CI still run.
interrupt
The finding kinds that hold an agent's turn: the agent is sent back once, with what it left unfinished (at most eight items). The default list is the kinds marked "Stops" in the reference, which describes every kind.
Your list replaces the default; it is not added to it. Findings of other kinds never hold a turn: they are kept for the next session's handoff, and check reports them. A name that is not a finding kind makes the whole list invalid, and the default applies.
This keeps the default, drops untested and adds near-copy:
{
"interrupt": [
"tests-failed", "policy", "protected", "boundary", "dependency", "secret",
"placeholder", "commented-out", "junk-file", "weakened-tests", "swallowed-error",
"suppression", "unsafe", "made-up-api", "test-logic", "stub", "debug-leftover",
"empty-test", "unaddressed", "unfinished", "copy", "versioned-copy", "unverified", "near-copy"
]
}trailers
{ "trailers": "verified" }What the prepare-commit-msg hook from lessloc init adds to each commit made through it: nothing with "off" (default), one line saying what was verified at exactly the committed state with "verified", and also what was left open and not checked with "all". Trailers shows each line.
Project policy
protect
{ "protect": ["migrations/**", ".github/workflows/**", "LICENSE"] }Paths only you change. At session start the agent reads one line about them, such as "In this project: only the user changes migrations/**." In the default block mode, an agent's write to a protected path with its edit tools, a heredoc or a patch goes to you for approval (the gate). A change made in a session without your approval, such as with sed -i, is a tier-0 protected finding, which holds the turn when it ends. In warn mode nothing goes to you for approval, and every change there holds the turn.
This works only in agent sessions. lessloc check, the commit gate and CI do not look at protect, so a protected path changed outside a session, or in a pull request, is not flagged. For a binding rule, set one up on your code host, such as CODEOWNERS with branch protection on GitHub.
boundaries
{
"boundaries": [
{ "files": "src/ui/**", "mustNotImport": "/db/", "why": "the UI reads data through src/api" }
]
}Imports a part of the code must not make. Each entry needs files and mustNotImport:
filesis a path glob.mustNotImportis a regular expression, tested against each import line a change adds in those files (lines that start withimport,from,require,use,usingor#include, andrequire(orimport(calls). In JSON, a literal dot is written\\., as in"app\\.db".whyis optional. It is shown with the rule.
At session start the agent reads the rule, such as "src/ui/** must not import /db/ (the UI reads data through src/api)". A matching import is a tier-0 boundary finding: the agent hears about it right after the write, the turn is held if the import is still there at the end, and check fails.
The check reads the import lines a change adds.
approvedDependencies
{ "approvedDependencies": ["express", "zod", "vitest"] }null(default): a dependency added to a manifest is a tier-2new-dependencynote. The note says when an installed library already does the same job, or when the lockfile does not pin the new one.- A list: a dependency added that is not on the list is a tier-0
dependencyfinding. It holds the turn and failscheck.
lessloc reads package.json, composer.json, requirements*.txt, pyproject.toml, Cargo.toml, go.mod and Gemfile. A new version of a dependency that was already there is not a new dependency. Names are matched as the manifest writes them, such as @types/node or a Go module path.
allow
{ "allow": ["generated/**", "docs/api/**"] }Paths lessloc leaves alone. Writes there are never refused, and the review's line, file and copy checks skip them.
You do not need allow for generated files that say so in their first lines (@generated, "do not edit", "auto-generated"), or for dependency and build folders such as node_modules, vendor, dist and build, or for lockfiles. lessloc skips those already.
exceptions
{
"exceptions": [
{ "path": "legacy/**", "finding": "narration", "why": "vendored file, kept verbatim" },
{ "path": "src/cli.py", "finding": "reads command-line flags by hand", "why": "the target device has no argparse" }
]
}Findings the owner accepts. An accepted finding is not shown in a session, in check or in CI, and does not hold a turn; an accepted tier-0 finding no longer fails check or blocks a commit. This is the only way to accept a tier-0 finding from the review. An entry covers every matching finding under its path; a lessloc-ignore comment covers one line.
whyis required. An entry without one is ignored and reported as aconfigproblem.pathis a path glob. Without it, the entry covers every file.findingis a finding kind. If it is not a kind, it is a regular expression tested against the finding's message, as in the second entry above. Withoutfinding, the entry accepts every finding underpath.
Exceptions apply to the findings of a review. They do not stop the refusals before a write; use allow for that. They also cannot accept config, ratchet, tests-failed, unverified, unaddressed or unfinished, which come from outside the review. The ones the check adds itself are handled where they come from: a worse ratchet measure by fixing it, or by committing a baseline that records it (lessloc baseline, with --allow-worse "<why>" when a baseline exists; the ratchet), a config finding by fixing .lessloc.json, and a missing test run by running the tests.
What check then shows:
checkreviews only the change: by default the working tree against HEAD, with--stagedthe staged change, and with--base <ref>everything since the branch left<ref>. Line rules such asdebug-leftover,swallowed-errorandstublook only at the lines the change adds. Socheckshows or counts such a finding, as in "1 accepted by exception" on its first line, only while the change adds its line. A line already in the base (HEAD, by default) and left as it is shows nothing, before the entry or after.- Only tier-0 findings fail
check(any finding with--strict). For other kinds, such asdebug-leftover, an exception hides the finding and keeps it from holding a turn;checkdid not fail on it before. lessloc auditlists the whole codebase as it is: exceptions andlessloc-ignorecomments do not apply to it.
Thresholds
{ "maxNetLines": 300, "maxNewFiles": 5 }Each threshold sets when a finding appears. Of these findings, only copy holds a turn by default.
| Key | Default | Finding | Reported when |
|---|---|---|---|
maxCommentRatio | 0.25 | comment-density | comments are more than this share of a file's added lines, once at least 8 comment and code lines were added. Doc comments and directives do not count. audit uses the same share for comment-heavy files |
maxNetLines | 150 | net-growth | a change adds more net lines than this. Tests, fixtures, examples, migrations, lockfiles and build output do not count. 0 turns the check off |
maxNewFiles | 3 | new-files | a change adds more new code files than this. Tests, fixtures, examples and migrations do not count |
dupLines | 6 | copy | a block of this many code lines in a row also exists elsewhere. Blank lines, comments, imports and lines such as a lone bracket are skipped. audit uses the same size for duplicate blocks |
instructionBudget | 2000 | long-instructions | always-loaded instruction files (CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules and the like) together hold more than this many tokens, estimated as characters divided by 4. Checked when a change touches one of them |
Suppressing one finding in the code
A lessloc-ignore <kind>: <reason> comment, on a finding's line or on the line above, suppresses that finding wherever lessloc reviews a change: the end-of-turn hold in every agent session, check and CI.
try:
import uvloop
uvloop.install()
# lessloc-ignore swallowed-error: uvloop is an optional speedup
except Exception:
pass- The reason is required.
- Without a kind (
lessloc-ignore: <reason>), the comment suppresses any finding on that line. - Only findings tied to a line can be suppressed this way, and never tier-0 findings. Only the owner accepts those, through
exceptions. - A suppression stays visible.
checkcounts it on its first line and lists each one with its reason, and in GitHub Actions each one shows as a notice on the diff:
suppressed: [swallowed-error] shop/loop.py:5: swallowed error: let it reach a boundary that handles it, or catch only the error you expect (uvloop is an optional speedup)An agent sent back for something it intended can just say why in one line instead; an item held once is not held again in that session. For a case that comes back session after session, use the comment or an exception.
Turning lessloc off
| To turn off | Do this |
|---|---|
| lessloc in the agent, for one run | Start the agent tool with LESSLOC=off, for example LESSLOC=off claude |
| lessloc in the agent, for a repository | Set "mode": "off" in .lessloc.json |
| Refusals and approval prompts before writes | Set "mode": "warn"; the end-of-turn review still runs |
| One part | Set LESSLOC_OFF to a comma-separated list of parts (below) |
| Holds for some kinds | Leave them out of interrupt in .lessloc.json |
| Some paths | List them in allow in .lessloc.json |
| The commit gate, trailers, the rules block in AGENTS.md and lessloc's files | Run lessloc remove in the repository |
| Everything | Also run lessloc remove --agents, and remove the Claude Code plugin. Then, once every agent session has ended, run rm -rf ~/.cache/lessloc ~/.lessloc. That deletes the stored prompt copies and the clone (Remove lessloc) |
LESSLOC=off and "mode": "off"
| Switch | Where | Effect | Takes effect |
|---|---|---|---|
LESSLOC=off | the environment your agent tool starts with | every agent hook does nothing, for that tool's sessions | only in sessions of a tool started with it |
"mode": "off" | .lessloc.json | every agent hook does nothing in this repository, for everyone | at the next hook call, in sessions that are already open too |
Every hook call starts lessloc afresh and reads .lessloc.json again, so any change to the file, "mode" included, applies from the agent's next tool call, without a new session. Written during a session, "mode": "off" silences lessloc from that session's next tool call on; setting "mode": "block" or deleting the key turns it back on the same way. LESSLOC=off is read from the environment the agent tool started with, so it takes effect only when the tool is started with it, as in LESSLOC=off claude.
Neither switch touches the git hooks, lessloc check or CI.
In the default block mode, if an agent puts LESSLOC=off or LESSLOC_OFF=… in front of its own commands, the command goes to you for approval (the gate). For enforcement, make lessloc ci a required check (what each layer stops). An agent's shell can still change .lessloc.json or .git/hooks in ways the hooks do not parse; the required check reviews every pull request either way.
LESSLOC_OFF: single components
LESSLOC_OFF takes a comma-separated list of components to switch off, and leaves the rest on:
LESSLOC_OFF=reuse,handoff claude| Component | What it switches off |
|---|---|
rules | the working standard at session start, and its short form for each subagent. The project's protect and boundaries rules are still given |
handoff | the note at session start about the last session's uncommitted work |
reuse | the hints about existing code with each prompt and in each subagent's prompt |
gate | the refusals and approval prompts before writes and commands, as "mode": "warn" does |
lines | the one-line note right after a Write or Edit, about a credential, a swallowed error or a forbidden import |
shell | the same note after shell commands, patches and other tools that change files. The agent's test runs are still read |
review | the whole review at the end of each turn and of each subagent: no hold, and no handoff record |
tests | judging the agent's test runs at the end of a turn: nothing is held for tests that failed, or for a change to how the tests run |
requests | the check that the functions, options and flags a request asks for exist (unaddressed) |
unfinished | the check of the agent's last reply for part of the request it says still fails (unfinished) |
pace | the one note in a turn that passes 40 tool calls, to finish what works and say what is left |
LESSLOC_OFF is for narrowing down a problem, and for measuring what each component is worth.
The other environment variables lessloc reads are listed in the reference.

