Skip to content

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 block mode, an agent's edit to .lessloc.json or .lessloc-baseline.json goes 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 by lessloc check as a tier-0 config finding, 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 --config in 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.json or .lessloc-baseline.json made by a shell command, such as lessloc init --config or lessloc baseline, is a tier-0 policy finding and holds the agent's turn.

Example ​

json
{
  "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 ​

KeyDefaultValue
testdiscovereda shell command
setupnonea shell command
testTimeout300seconds
probenonea shell command
mode"block""block", "warn" or "off"
interruptthe kinds marked "stops" in the referencea list of finding kinds
trailers"off""verified", "all" or "off"
protect[]a list of path globs
boundaries[]a list of { "files", "mustNotImport", "why" }
approvedDependenciesnulla list of package names, or null
allow[]a list of path globs
exceptions[]a list of { "path", "finding", "why" }
maxCommentRatio0.25a number
maxNetLines150a number
maxNewFiles3a number
dupLines6a number
instructionBudget2000a 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 ​

json
{ "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:

  1. npm test, when package.json has a real test script
  2. make test, when the Makefile has a test: target
  3. cargo test, when there is a Cargo.toml
  4. go test ./..., when there is a go.mod
  5. python3 -m pytest -q, when there is a pytest.ini, tox.ini or conftest.py, or a [tool.pytest section in pyproject.toml
  6. for a tests or test folder with test_*.py files: python3 -m unittest discover -s tests -q (or -s test) when the first file uses unittest, otherwise python3 -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 ​

json
{ "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 ​

json
{ "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 ​

json
{ "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 ​

json
{ "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 check and 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:

json
{
  "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 ​

json
{ "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 ​

json
{ "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 ​

json
{
  "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:

  • files is a path glob.
  • mustNotImport is a regular expression, tested against each import line a change adds in those files (lines that start with import, from, require, use, using or #include, and require( or import( calls). In JSON, a literal dot is written \\., as in "app\\.db".
  • why is 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 ​

json
{ "approvedDependencies": ["express", "zod", "vitest"] }
  • null (default): a dependency added to a manifest is a tier-2 new-dependency note. 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 dependency finding. It holds the turn and fails check.

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 ​

json
{ "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 ​

json
{
  "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.

  • why is required. An entry without one is ignored and reported as a config problem.
  • path is a path glob. Without it, the entry covers every file.
  • finding is 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. Without finding, the entry accepts every finding under path.

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:

  • check reviews only the change: by default the working tree against HEAD, with --staged the staged change, and with --base <ref> everything since the branch left <ref>. Line rules such as debug-leftover, swallowed-error and stub look only at the lines the change adds. So check shows 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 as debug-leftover, an exception hides the finding and keeps it from holding a turn; check did not fail on it before.
  • lessloc audit lists the whole codebase as it is: exceptions and lessloc-ignore comments do not apply to it.

Thresholds ​

json
{ "maxNetLines": 300, "maxNewFiles": 5 }

Each threshold sets when a finding appears. Of these findings, only copy holds a turn by default.

KeyDefaultFindingReported when
maxCommentRatio0.25comment-densitycomments 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
maxNetLines150net-growtha change adds more net lines than this. Tests, fixtures, examples, migrations, lockfiles and build output do not count. 0 turns the check off
maxNewFiles3new-filesa change adds more new code files than this. Tests, fixtures, examples and migrations do not count
dupLines6copya 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
instructionBudget2000long-instructionsalways-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.

python
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. check counts 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:
text
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 offDo this
lessloc in the agent, for one runStart the agent tool with LESSLOC=off, for example LESSLOC=off claude
lessloc in the agent, for a repositorySet "mode": "off" in .lessloc.json
Refusals and approval prompts before writesSet "mode": "warn"; the end-of-turn review still runs
One partSet LESSLOC_OFF to a comma-separated list of parts (below)
Holds for some kindsLeave them out of interrupt in .lessloc.json
Some pathsList them in allow in .lessloc.json
The commit gate, trailers, the rules block in AGENTS.md and lessloc's filesRun lessloc remove in the repository
EverythingAlso 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" ​

SwitchWhereEffectTakes effect
LESSLOC=offthe environment your agent tool starts withevery agent hook does nothing, for that tool's sessionsonly in sessions of a tool started with it
"mode": "off".lessloc.jsonevery agent hook does nothing in this repository, for everyoneat 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:

sh
LESSLOC_OFF=reuse,handoff claude
ComponentWhat it switches off
rulesthe working standard at session start, and its short form for each subagent. The project's protect and boundaries rules are still given
handoffthe note at session start about the last session's uncommitted work
reusethe hints about existing code with each prompt and in each subagent's prompt
gatethe refusals and approval prompts before writes and commands, as "mode": "warn" does
linesthe one-line note right after a Write or Edit, about a credential, a swallowed error or a forbidden import
shellthe same note after shell commands, patches and other tools that change files. The agent's test runs are still read
reviewthe whole review at the end of each turn and of each subagent: no hold, and no handoff record
testsjudging 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
requeststhe check that the functions, options and flags a request asks for exist (unaddressed)
unfinishedthe check of the agent's last reply for part of the request it says still fails (unfinished)
pacethe 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.

Released under the Apache-2.0 license. · llms.txt