Configuration Reference
Sensez reads sensez.toml at the repository root. If that file is absent, it
falls back to a [tool.sensez] table in pyproject.toml; when both exist,
sensez.toml wins.
sensez init writes a starter file with every supported knob commented at its
default — that generated file is the complete surface. This page explains the
concepts behind it and the common tuning workflows. The
sensez.toml Sensez uses on itself
is a heavily commented real-world example.
Validation
Sensez checks the whole file before it scans. Every problem is printed as
sensez.toml:LINE:COL: message, with a "did you mean" hint where one fits.
- Errors make Sensez ignore the whole file and scan with the default
configuration: TOML syntax errors, wrong value types, invalid values such as
cycles = "critical", and unknown keys inside[smells](including[smells.rules.*]and the language tables) and[action]. - Warnings ignore only the offending key and the scan continues: unknown
keys in other tables (
[duplication],[dead_code],[cycles], …) and unknown top-level tables.
Neither changes the exit code: a --fail-on / --fail-on-new run is still
decided by its findings, and prints a gate: line on stderr noting that
configuration issues were reported. Check the printed problems (or
meta.issues, see below) in CI to catch a broken file.
MCP scan tools return the same problems as an error result. The end-of-turn gate and the language server keep working on defaults instead.
Action levels
Every finding carries an action level. From quietest to strictest:
info → advisory → warning → must_fix.
Action levels decide how automated workflows react:
sensez noze . --diff --fail-on-new [LEVEL]exits non-zero when diff-scoped findings meet or exceedLEVEL(defaultmust_fix) — this is the CI hook.- Agents and the end-of-turn gate treat each pillar according to its level, so
must_fixfindings lead andinfofindings stay out of the way.
Pillar defaults: boundaries = must_fix, cycles = warning,
duplication = advisory, dead_code = advisory. Smells inherit the
severity of their detector. Override per pillar or per smell:
[action]
duplication = "warning"
[smells.rules.loose_typing]
action = "must_fix"
Smell rules: base vs per-language
[smells.rules.<kind>] applies to every language.
[smells.<lang>.rules.<kind>] — where <lang> is python, javascript,
typescript, or rust — overrides the base for that language only. (The Rust
profile is dogfooding-only and not shipped in the published packages.)
Setting any knob, including action, enables the rule; use enabled = false
for explicit suppression. A rule's own action wins over the
[action.smells] entry for the same smell. A flat threshold under [smells] or
[smells.<lang>] (max_function_lines, max_cyclomatic, max_nesting, …)
also enables its rule, unless the same table sets the rule's toggle (for
example long_function = false) or lists it in disabled.
# All languages: long functions warn past 80 lines.
[smells.rules.long_function]
max_lines = 80
action = "warning"
# ...except TypeScript, which allows 120.
[smells.typescript.rules.long_function]
max_lines = 120
Every rule's knobs and default enabled state are listed in the Finding Reference.
Dead code: teaching Sensez about your framework
Sensez ships no hardcoded framework names. Liveness is modeled from entry points, and your lists are merged with the language-profile defaults — you only add what is specific to your project:
[dead_code]
entrypoints = ["route", "fixture", "task"] # decorator trailing names that register code
entrypoint_names = ["register", "main"] # function/class names invoked dynamically
entrypoint_bases = ["AppConfig"] # base classes loaded dynamically
entry_points = ["**/scripts/**"] # file globs reached outside the import graph
test_sources = ["**/*.e2e.ts"] # extra test-runner globs (test roots)
test_support = ["**/testing/**"] # extra test-helper globs
orphan_modules = true # report modules no root reaches
unimported_dir_share = 50 # % of a directory's modules nothing imports that marks it runtime-loaded (0 = off)
Dead code walks the import graph from two kinds of roots:
- Production roots:
entry_pointsglobs, manifest entries (pyproject.tomlscripts, Cargo binaries andlib.rs,package.jsonmain/module/bin/exports, where adist/,lib/,build/orout/target also names itssrc/twin, and source files apackage.jsonscriptscommand or a.github/workflowsrun:step runs, and JS/TS files with a shebang or a top-level main guard such asimport.meta.mainorrequire.main === module), and tool configs such as*.config.ts. An entry-point file is never reported, and its imports count as usage. - Runtime-loaded directories: when at least
unimported_dir_sharepercent (default 50) of the modules under a directory, and at least five of them, have no runtime importer (tests and type-only imports do not count), Sensez takes the runtime to load that directory by path, as frameworks, plugin folders and serverless function folders do. Those modules become production roots and are not reported. Setunimported_dir_share = 0to turn this off. - Test roots:
test_sourcesglobs plus the profile's test files.
A symbol that only tests import is reported at Medium with reason =
"test_only". Files matching test_support (JS/TS default: **/e2e/**,
**/test-utils/**, **/testUtils/**, **/fixtures/**, **/__fixtures__/**)
are test helpers: their test importers count as users, so only truly unused
helpers are reported.
Reachability is a property of whole modules, so it is reported once per module
with kind module, at Medium, anchored at the first unused symbol, with
symbol listing the unused names. A module that nothing imports and no root
reaches has reason = "unreachable_module"; add its path to entry_points if
a tool runs it, or set orphan_modules = false. A module only tests reach has
reason = "test_only".
Names a barrel re-exports (export { x } from "./x") count as used in their
defining module only when someone imports them from the barrel (chains and
export * included). An unused name is reported once, where it is defined;
the barrel line itself is never reported.
A whole-namespace type import (import type * as jobs, as generated API
manifests use) keeps its exports at Medium. When the same initializer callee
(export const run = task({ ... }), builder chains included) initializes at
least five exports across the modules such a manifest exposes, Sensez treats
that callee as a registration and those exports as live.
Confidence is High unless evidence suggests hidden use, then Medium: a default
export, an importer that binds the module whole or not at all (import(),
import mod), a type-only namespace reference, an import from a tool config,
test-only use, or an unreachable module.
entrypoints and entrypoint_names accept a trailing * as a prefix pattern
(entrypoint_names = ["on*"]). entrypoint_names also applies to methods under
unused_methods. For JS/TS methods only, React lifecycle, custom-element and
test-reporter callback names are live by default, and methods of a class that
extends or implements anything outside the scan are not reported (they may be
framework hooks). In JS/TS, entrypoints matches the callee of an
exported initializer, so entrypoints = ["register", "define*"] keeps
export const send = register({ ... }) alive.
If dead-code output feels incomplete, broader unused-symbol classes are available as opt-ins (your generated config shows their defaults):
[dead_code]
unused_imports = true
unused_methods = true
Unused module-level variables and constants are off by default; set
unused_variables = true to report them.
Prefer adding entry points over suppressing individual findings — one line of config fixes a whole class of false positives.
Duplication
[duplication]
threshold = 50 # minimum structural-token run to report a clone; raise to quieten, 0 disables
max_gap = 25 # stitch clones across small consistent edits; 0 disables
exclude = ["**/tests/**", "**/migrations/**"] # clone-only excludes (files stay in the graph)
include_tests = false # true also compares test files
include_generated = false # true also compares generated files
Duplication and smells skip, on top of exclude:
- Test files matched by the language profile (JS/TS:
__tests__/,*.test.*,*.spec.*,tests/,test/, plus__mocks__/,__fixtures__/,e2e/,test-utils/and*.stories.*) and the built-intests/,test/and pytest file names. The Python profile also countsexamples/anddocs_src/, and the Rust profilebenches/, so those are skipped too. Setinclude_tests = trueunder[duplication]or[smells]to analyse them.weak_test_oracleandfixture_echo_assertionalways inspect test files. - Generated files (JS/TS): paths under
_generated/or__generated__/,*.gen.*and*.generated.*, or a first-1 KB header containing@generated,do not edit,auto-generated,automatically generatedorcode generated by. They stay in the graph for cycles and dead code, but never count as smell hotspots or importers forgod_moduleandshotgun_surgery_hazard. - Migrations:
migrations/, plusalembic/andversions/for Python files only.
sensez noze . --summary lists the effective excludes and skip counts under
scope, and the human report prints one dimmed line under Duplication when
files were skipped.
Structural matching keeps member, property and type names, so a copy that
renames only locals is still a clone. Whole functions at least twice the
threshold are also compared after a consistent rename of every name and
literal, so a copy that renames its members is found too. near_miss = true
extends that rename pass to functions down to threshold.
Opt-in detectors for harder clones: class_name_duplicates,
class_property_overlap_min, and [duplication.semantic] (only in builds with
the eyez feature). If the semantic pass is enabled but its embedding model
cannot load (for example offline with no cached model), the scan prints an
analyze issue on stderr, and --fail-on / --fail-on-new exit 2 with
"incomplete scan" rather than passing. JSON output lists the issue under
meta.issues only when SENSEZ_SCAN_DIAGNOSTICS or SENSEZ_TIMING is set.
[duplication.semantic]
enabled = true
min_shape_score = 30 # token 4-gram overlap (0-100) separating edited copies from rewrites; above 100 is an error
comment_boost_score = 85 # doc similarity (50-100) that confirms an edited copy; below 50 is an error
comment_required = false # true compares documented functions only
Semantic groups only consider functions of at least threshold tokens. Each
pair is scored four ways: structural overlap (4-gram Jaccard), and the cosine,
under the code embedding model, of the two functions' source, of their
signatures (name and parameter words, ignoring words that many functions in
the codebase share) and of their doc comments. A group carries one of two
verdicts:
semantic: edited copy (structure 0.48, code 0.91, names 0.88, docs n/a): overlap at leastmin_shape_score, confirmed by similar source and similar signatures, or by matching doc comments. Reported at the duplication action level.semantic: same intent, structurally different code (structure 0.12, code 0.86, names 0.92, docs n/a): a rewrite of the same function, for example a loop and afilter/mapchain. Reported asinfo.
Pairs are only compared when they share enough uncommon structure, signature words or doc words; words and shapes used by most functions in the codebase never pair two functions on their own.
When several detectors report the same code, the widest group is kept, its
line ranges grow to cover the others, and the folded groups are listed under
absorbed in JSON output. Each group's source is in detector. The hint
says the token count (a range when a folded group is wider), the number of
copies, what to do, and how many overlapping groups were folded in.
Cycles
[cycles]
exclude = ["**/vendor/**"] # import edges here never report a cycle
include_type_only = true # also report cycles closed only by type imports
A cycle that only exists through type-level imports (TypeScript
import type, Python if TYPE_CHECKING:) is reported at info with a "type
coupling" hint. Set include_type_only = false to drop those; this is
recommended for TypeScript, where type imports vanish at build time.
Cycles among test files are reported. Only migrations/ (and Python
alembic/ and versions/) are excluded by default; add
exclude = ["**/tests/**"] to silence test fixtures.
Each cycle finding lists every module of the strongly connected group in
modules; edges is one representative closed loop (the shortest one through
the alphabetically first module), so following it returns to its start.
Boundaries
Each rule forbids any module matching from from importing any module matching
to:
[[boundaries.forbidden]]
from = "app.domain"
to = "app.api"
Matching, per endpoint:
- A plain name is a module-name prefix —
app.domainmatchesapp.domainandapp.domain.*; for JavaScript and TypeScript modules, which are named by path,src/domainmatchessrc/domainandsrc/domain/*. - A glob (
* ? [) is matched against both the module name and the file path, sofrom = "**/domain/**"works even for namespace packages without__init__.py. toalso matches the literal import target, so rules still fire when an import cannot be resolved to an in-repo module.
A rule whose from matches no module is reported under
meta.unmatched_boundary_rules in JSON output — a typo never looks like a
clean pass.
Accepting findings: [accept]
The out-of-line alternative to # noqa — nothing is written into your source.
Keys are pillars or detector ids; values are substrings of the finding label as
shown in a scan. The file is committed, so accepted findings stop flagging for
every teammate and their agents:
[accept]
dead_code = ["legacy.compat::shim"] # a specific accepted symbol
"smells/god_module" = ["app.registry"] # a specific accepted detector hit
duplication = ["legacy-fork/"] # an accepted copy location
duplication patterns work per location. A plain path substring drops the
matching occurrences from every clone group, and a group stays reported while
two or more other copies remain, so accepting a fork never hides duplication
elsewhere. A pattern containing " + " matches the group label
(a.ts + b.ts + c.ts) and hides that whole group.
There are three ways to silence findings — pick by scope:
| Mechanism | Scope | Shared? | Use for |
|---|---|---|---|
exclude globs |
Whole files/dirs, per pillar or global | Yes (committed) | Generated code, fixtures, migrations |
[accept] |
Specific findings | Yes (committed) | Accepted debt the team agrees on |
brainz_triage |
Specific findings | No (local .sensez/) |
Personal review verdicts |
Global scope
roots = [] # package roots; empty = auto-detect from project layout
exclude = ["**/generated/**"] # excluded from every pillar
Gate and local metrics
CLI scans are stateless and never persist parsed source facts. MCP scans run in
a short-lived child process that reuses parsed source facts from
.sensez/parse-cache/, keyed by the sensez build, the configuration and each
file's content and capped at 1 GiB; SENSEZ_PARSE_CACHE=0 turns it off. With
[cache] enabled = true, MCP and LSP processes instead keep analysis state in
memory for their own lifetime:
[cache]
# Enables process-local reuse in MCP/LSP only; CLI scans always remain stateless.
enabled = false
[gate]
repeat_limit = 2 # how often the gate re-reports the same finding before auto-deferring
[self_improvement]
enabled = false # stops even the on-disk recording under .sensez/local-metrics/
Recipes
Adopting Sensez in an existing repository
- Run
sensez noze . --summaryfor aggregated per-rule counts to see what would be noisy. - Gate only on new work: run
sensez noze . --diff --fail-on-newin CI (or use the GitHub Action), so the backlog does not block anyone. - Record the agreed backlog in
[accept]as you review it. - Escalate action levels pillar by pillar as each one cleans up.
Quieting a noisy detector
Prefer raising its threshold over disabling it, and scope the change to the language that is noisy:
[smells.python.rules.god_module]
min_fan = 40
Use enabled = false only as a last resort — a disabled detector is invisible
in future scans.
Fixing framework false positives
Add the framework's registration pattern to [dead_code] (above) instead of
suppressing findings one by one.
Practical advice
- Keep the file small enough that a new contributor can scan it in one sitting.
- Prefer explicit thresholds and exclusions over broad global disablement.
- Treat the config as code: review changes to it the same way you would review a detector change.