Patterns
Every way lessloc knows that coding agents bloat code, comments, files, folders, dependencies and tests, with the standard that calls each one a bad practice and what lessloc does about it.
Each pattern's page, or its section on this page, says what lessloc does about it:
- Rule: the line in RULES.md that prevents it. Every session gets the rules; in Claude Code, each subagent gets a one-line working standard.
- Kind: the finding lessloc reports when it happens anyway. Stops marks a kind that holds an agent's turn once, sending it back with what is unfinished. Tier-0 kinds also fail
lessloc check. All other kinds are notes to the user. - When: write means right after each edit, while the code is still open; turn means when the agent says it is done; check means commit and CI; audit means the whole codebase.
Every detected pattern is proven by a test: bad and good forms in test/cases.mjs, where the bad form must be caught and the good form must not be, so the push for less code never flags what the code needs; or a named test. test/catalog.test.mjs fails when a pattern has neither.
The standards cited are the ones engineers already use:
- CWE weaknesses. Those in the maintainability measure of ISO/IEC 5055:2021, the international standard for automated source-code quality, are 561, 1041, 1047, 1064, 1074, 1080, 1085 and 1121.
- Linter rules that ship enabled in common configurations.
- Refactoring, clean-code and testing literature.
- The model vendor's own guidance. Anthropic's prompting guide says recent Claude models "have a tendency to overengineer by creating extra files, adding unnecessary abstractions, or building in flexibility that wasn't requested."
Sonar now ships a quality profile for agent-written code that targets "code duplication and structural drift" and "hallucinated APIs and supply-chain risks".
Code that already exists
- Reimplements a helper the project already has, under the same or another name
- Copies a rule another function owns: a format, regex, SQL or date pattern
- Keeps old and new versions side by side (
parse_date_v2,fooFixed,legacyFoo,app_new.py) - Rebuilds what the standard library or framework has: counters, flag loops, CSV, date math, argument parsing
- Copy-pasted test cases instead of one parametrized test
- Adds a second library for a job an installed one already does (dayjs beside moment)
- The same data shape in several layers (DTO, model, entity)
- The same CSS declarations under another selector; the same long utility-class string again and again
Code nobody needs
- Options, flags, parameters or config for needs nobody named
- An interface, trait, protocol or abstract class with one implementation
- Wrappers that only forward; barrel files that only re-export
- A new function or import that nothing calls or tests
- Obsolete code kept behind a guard or flag instead of deleted
- Tombstones:
// removed X, "kept for backward compatibility", comments citing the task - Stubs and fake success: "in a real app you would...",
raise NotImplementedError,todo!(),throw new Error("Not implemented"),mockUsersin production code - Placeholders:
# ... existing code ..., "your code here" - Commented-out code
- Debug leftovers:
debugger,breakpoint(),print("here"),print(f"{x=}"),dbg! - A demo
if __name__ == "__main__":block in a module other code imports; an examples or demo folder nobody asked for - Hand-written boilerplate a language feature writes (an
__init__that only stores its parameters) - A one-line helper used once
- TODO and FIXME left behind: work the agent did not finish and did not mention
Code longer than it needs to be
- Brute-force idioms:
== True, an if/else that returns a boolean, index loops that only readxs[i],.filter(...).length > 0,indexOf(x) !== -1,x ? true : false,x ? x : y,.map().flat(),strings.Index(...) != -1,.len() == 0 - Hand-built counters and flag loops
- A catch that only rethrows; logging an error and rethrowing it
- A promise wrapped in
new Promise - Annotating types the compiler infers
- A React Effect that only sets state from other values
- Functions with many parameters (8 or more)
- Deep nesting
- Long functions
- If-chains on literal values
- A file that keeps growing
- Reformatting lines the task did not change
- Net growth past a budget
- Golfed code: nested ternaries, comprehensions with several
forclauses, lines over 160 characters
Errors and checks hidden
- Swallowed errors:
except: pass, an empty catch,catch (Exception e) { return null; }, Go'sif err != nil { return nil } - Silenced checks:
@ts-ignore,any, blanket# type: ignoreor# noqa,eslint-disablewithout a rule,#[allow(dead_code)],//nolintwithout a reason - Checks, fallbacks and catches for cases that cannot happen
- Removing validation, error handling, logging or stated rules to shrink code
Shortcuts that trade away safety
Hand-written versions of what the platform does safely. They are short, which is why agents reach for them.
- SQL built from values with f-strings, concatenation or template strings
- HTML built or escaped by hand:
innerHTMLfrom values,.replace('<', '<') Math.random()orrandomfor tokens, passwords and session ids- Fast hashes (MD5, SHA-1, SHA-256) for passwords
- Certificate checks turned off:
verify=False,rejectUnauthorized: false,InsecureSkipVerify: true - Running text as code:
eval,exec,new Function
Calls and packages that do not exist
- Another language's API:
nullor===in Python,.strip(),Noneorlen()in JavaScript,nullin Go - Imports of packages no manifest declares: a made-up name, or one never installed
Comments and text
- Comments that narrate what the code does ("Loop through the users")
- Banners, dividers, closing-brace comments, emoji decoration
- Docs that restate the name (
"""Get the user.""",name: The name.,@returns The result.) - History and messages to the requester ("Updated the parser to...", "As requested", "Generated by AI", dated change notes)
- Comment blocks and comment-heavy changes
- Log lines that narrate each step ("Starting...", "Successfully saved")
- Docs that still name what a change removed
- Summary, report and notes files (
IMPLEMENTATION_SUMMARY.md,FIXES.md) - Doc files nobody asked for, and READMEs that grow by 40 or more lines nobody asked for
- Docs deleted wholesale to shrink a change
- Always-loaded instructions that grow, or that include a directory tour
- Commit messages that recap the diff file by file
Summary, report and notes files
Summary, report and notes files (IMPLEMENTATION_SUMMARY.md, FIXES.md). lessloc catches it as junk-file and refuses it before it lands.
junk-file(tier 0): A new backup, copy, scratch or report file (refused before it lands, unless the request names it), or cache files, build output or logs that would be committed.
Files and folders
- Scratch, backup, copy and harness files (
debug_x.py,.bak,utils_old.py,tmpclaude-*) - Caches, build output and logs about to be committed
- Dockerfiles, compose files, CI workflows and deploy configs nobody asked for
- Tiny one-function files; file-per-concern splits
- New folders for one or two files; layer trees for small apps
- Junk-drawer
utilsandhelpersmodules and folders - Vague names (
process_data,handle) - Moving or renaming files without a reason
- Secrets: a committed
.env, API keys, private keys
Scratch, backup, copy and harness files
Scratch, backup, copy and harness files (debug_x.py, .bak, utils_old.py, tmpclaude-*). lessloc catches it as junk-file and refuses it before it lands. A root test_*.py is allowed when the project has no tests folder.
junk-file(tier 0): A new backup, copy, scratch or report file (refused before it lands, unless the request names it), or cache files, build output or logs that would be committed.
Caches, build output and logs about to be committed
lessloc catches it as junk-file.
junk-file(tier 0): A new backup, copy, scratch or report file (refused before it lands, unless the request names it), or cache files, build output or logs that would be committed.
Tests and verification
- Behavior added with no test changed in the same change
- Tests that assert nothing, or assert a constant
- Deleted, skipped, focused (
.only), exited (sys.exit(0)) or force-passed tests - Test-only branches in production code; code shaped to pass a test
- Tests that mock the code they test
- Production code that branches on a value copied from the tests
- Stopping short: a function, option or flag the request asked for is never written
- Stopping short: leaving part of the reported problem failing, and saying so
- Saying "done" without a passing run on the final state
- A refactor that changes existing tests
Saying "done" without a passing run on the final state
lessloc catches it as tests-failed and unverified: the agent's own test runs, as its host reported them, are the evidence for the exact tree; a run that fails on the final state, or a change to how the tests run, holds the turn once, and a final state no run verified is written to the record; lessloc test, verify and ci record evidence too.
tests-failed(tier 0): A test run on the final state failed.unverified(tier 1): In a session, the turn changed what the tests run, so a pass no longer shows the code works. Withcheck --require-evidence, no passing test run is recorded for exactly the state checked, at tier 0.
Getting around the checks
git commit --no-verify,-n,HUSKY=0,SKIP=,LESSLOC=off- Editing lessloc's policy or a protected path
- Suppressing a finding without a reason
- Fixing a review by adding layers, checks or tests
Skipping commit hooks with --no-verify or LESSLOC=off
git commit --no-verify, -n, HUSKY=0, SKIP=, LESSLOC=off. What lessloc does: The gate asks the user before the command runs.
Editing lessloc's policy or a protected path
lessloc catches it as policy and protected; the gate asks the user.
policy(tier 0):.lessloc.jsonor.lessloc-baseline.jsonchanged in an agent session without your approval. Only in a session:check, the commit gate and CI do not report it.protected(tier 0): A path inprotectchanged in an agent session without your approval. Only in a session:check, the commit gate and CI do not report it.
Suppressing a finding without a reason
What lessloc does: lessloc-ignore works only with a reason. check and CI list every suppression.
Fixing a review by adding layers, checks or tests
What lessloc does: The turn's hold ends with "Fix only these and add nothing else for this note".
Project policy
These kinds hold what the project's owner decided, so an agent cannot change them on its own.
- A broken or misspelled
.lessloc.jsonthat would silently fall back to defaults - Imports across a boundary the project forbids (for example, UI importing the database)
- A dependency outside the approved list
- Duplication, dead code, comment weight, swallowed errors or test assertions getting worse
A broken or misspelled .lessloc.json
A broken or misspelled .lessloc.json that would silently fall back to defaults. lessloc catches it as config.
config(tier 0):.lessloc.jsonis not valid JSON, or has an unknown key, a wrong type or an exception without a reason. It never refuses a commit.
Imports across a boundary the project forbids
Imports across a boundary the project forbids (for example, UI importing the database). lessloc catches it as boundary.
boundary(tier 0): An added import breaks a rule inboundaries.
A dependency outside the approved list
lessloc catches it as dependency.
dependency(tier 0): An added dependency is not inapprovedDependencies.
Duplication, dead code or swallowed errors getting worse
Duplication, dead code, comment weight, swallowed errors or test assertions getting worse. lessloc catches it as ratchet. It warns, comparing with the base commit; with a committed baseline, it fails.
ratchet(tier 0): A codebase measure got worse than its line: more duplication, copies, similar names, unreferenced definitions, comments, commented-out code, swallowed errors, pass-through wrappers, junk files or instruction tokens, or fewer test assertions. Tier 0 only against a baseline this version recorded.
Context and handoff
- Test output re-read again and again
- The next agent cannot tell what was verified or left open
- Hunting for where things live
- Several agents in one checkout, editing the same files
- Tool state cluttering the repository, its history or teammates' setups
Test output re-read again and again
What lessloc does: lessloc test, for people and CI (agents run the project's own command), prints one line on success and only the failing parts on failure.
The next agent cannot tell what was verified or left open
What lessloc does: The next session in the checkout gets the last one's uncommitted work, verification and open problems in its own words; evidence tied to the exact tree; commit trailers when the team turns them on.
Hunting for where things live
What lessloc does: Existing code named on a prompt when a specific word of its name matches; the last session's uncommitted work handed over.
Several agents in one checkout, editing the same files
What lessloc does: Each session reviews only what its own tools wrote, so two agents in one checkout never review each other's work.
Tool state cluttering the repository
Tool state cluttering the repository, its history or teammates' setups. What lessloc does: Nothing in the working tree unless asked; state in the user's cache; one trailer line; shared hook folders left alone (what lessloc writes).
What lessloc never pushes an agent toward
"Less code" taken too far is its own failure. Each of these has a counterweight:
| Over-correction | Counterweight |
|---|---|
| Cutting tests, validation, error handling or logging | "Never cut correctness, security, tests" in the rules; removed-safeguard, weakened-tests |
| Golfing code into one-liners | clever (nested ternaries, comprehensions with several for clauses; Google Python style: optimize for readability, not conciseness), dense-lines |
| Inlining everything to avoid files and helpers | long-function, deep-nesting, swollen-file |
| Merging code that only looks alike | The rule keeps code that changes for different reasons separate (Metz, "The Wrong Abstraction": duplication is far cheaper than the wrong abstraction). Copy findings show how widely each copy is used |
| Deleting comments that carry a reason | narration spares comments with a why (because, so that, must, never, a colon) |
| Hiding errors in fewer lines | contextlib.suppress is excluded from simpler and still counts as a swallowed error |
| Syntax the project's runtime does not support | ruff follows the project's target version; truthiness rules are skipped in NumPy and pandas code |
| Breaking framework conventions | Route folders and convention files (page.tsx, +page.svelte, __init__.py, mod.rs) never count as needless |
| Blocking the tests the rules ask for | A root test file counts as scratch only when the project keeps tests in a folder |
| Review loops that add code | At most one hold per turn, and it says to fix only what it lists and add nothing else |

