Configuration Reference
This page provides a comprehensive reference for all configuration options available in Sheriff. The configuration is defined in a sheriff.config.ts file located in your project's root directory.
💡 Quick Start: Use
npx sheriff initto automatically generate asheriff.config.tsfile with sensible defaults. See the CLI documentation for more details.
Configuration Structure
import { SheriffConfig } from '@lambda-solutions/sheriff-core';
export const config: SheriffConfig = {
// Your configuration options here
};
Alternatively, use defineConfig to get the same autocompletion and type
checking without the type annotation:
import { defineConfig } from '@lambda-solutions/sheriff-core';
export const config = defineConfig({
// Your configuration options here
});
defineConfig returns its argument unchanged, so both styles are equivalent.
The export must still be named config.
Mandatory Options
These options are required for Sheriff to function properly. You need to understand and configure these for Sheriff to work effectively.
modules
- Type:
ModuleConfig - Description: Defines the modules and assigns tags. This is the primary way to structure your project. If you don't define modules, you must enable
autoTaggingfor Sheriff to work. See Module Boundaries for detailed examples.
Module values can be a tag string, a tag array, a tag matcher function, or an explicit module definition with tags and optional file-level exports.
Module keys match directory paths segment by segment: a literal, a * wildcard, a <placeholder> and a /regex/ each match exactly one segment. A ** segment matches zero or more segments, so modules at varying depths need only one key — see Recursive Globs for the full semantics.
export const config: SheriffConfig = {
modules: {
'libs/**/feature-<name>': ['type:feature', 'feature:<name>'],
},
};
A key whose last segment ends with a source-file extension defines single-file modules: every matching file becomes its own module, e.g. 'src/stores/<name>.store.ts': ['type:store', 'store:<name>'].
export const config: SheriffConfig = {
modules: {
'domains/booking/api': {
tags: ['type:api', 'port'],
exports: ['*.port.ts'],
},
},
};
exports is only needed when a barrel-less module should expose a smaller public API than "everything except internal". The patterns are matched against paths relative to the module folder. A * matches within one path segment only, so *.port.ts matches booking.port.ts but not internal/admin.port.ts; use internal/*.port.ts when a subfolder is intentional. If exports is omitted, existing barrel-less behavior is unchanged; if it is an empty array, no files are public outside the module.
Because tags is also a valid folder name, an object with only tags or with tags and exports is interpreted as an explicit module definition. If a leaf folder is literally named tags, express it with a path key or include it in a nested config that also has another child:
export const config: SheriffConfig = {
modules: {
'src/app/tags': ['type:tags-folder'],
},
};
exports
- Type:
string[] - Default:
undefined - Description: Defines the public files of one barrel-less module when that module uses the object form in
modules.
export const config: SheriffConfig = {
modules: {
'domains/booking/api': {
tags: ['type:api', 'port'],
exports: ['*.port.ts', 'public-*.ts'],
},
},
};
exports patterns are matched against paths relative to the module folder. A
wildcard is segment-local: *.port.ts exports booking.port.ts, while
sub/*.ts exports sub/public.ts but not sub/deep/public.ts.
Without exports, barrel-less modules keep the default behavior: every file is
public except files below the configured encapsulation folder. With exports,
the list is the public API and takes precedence over that default convention,
so exports: ['internal/public.ts'] intentionally exposes that file. With
exports: [], no files are public outside the module.
depRules
- Type:
DependencyRulesConfig - Description: Defines dependency rules between modules. Even with defaults, you should understand how this affects your project structure. See Dependency Rules for detailed examples.
denyRules
- Type:
DependencyRulesConfig - Default:
{} - Description: Defines dependency rules that forbid imports even when
depRuleswould otherwise allow them. Sheriff evaluatesdenyRulesafterdepRules; a matching deny rule wins over clearance fromdepRules.
export const config: SheriffConfig = {
modules: {
'src/domain': ['domain:booking', 'type:domain'],
'src/shared': ['shared'],
},
depRules: {
'*': 'shared',
'type:domain': 'type:domain',
},
denyRules: {
'type:domain': ({ to }) => to !== 'type:domain',
},
};
denyRules never grant access. If no deny rule matches, the depRules result stands. A tag without a denyRules entry is normal and does not raise a missing-rule error.
externalRules
- Type:
Record<string, string[] | ExternalRuleMatcherFn> - Default:
{} - Description: Restricts imports from external libraries in
node_modulesaccording to the importing module's tags.
export const config: SheriffConfig = {
modules: {
'src/domain': ['type:domain'],
'src/api': ['type:api'],
'src/infra': ['type:infra'],
},
depRules: {
'*': '*',
},
externalRules: {
'type:domain': [],
'type:api': ['@angular/core'],
'type:infra': ['@angular/*', 'rxjs'],
},
};
Rule keys support wildcards and are matched against source tags. Library
patterns are matched against the full import string, so @angular/core does
not match @angular/core/testing; use @angular/* to allow both. When an
importing module has multiple tags with matching rules, every rule must allow
the import. One veto is enough to report a violation.
An empty array forbids every external import for the matching tag. A tag with
no matching key is unrestricted, which keeps configurations without
externalRules unchanged.
Uninstalled packages are also governed by externalRules when the nearest
package.json declares them in dependencies, peerDependencies, or
optionalDependencies. Undeclared imports that cannot be resolved remain
unresolvable and are not treated as external libraries.
Optional Options
These options have sensible defaults and are typically only customized for specific use cases.
Recommended Options
entryFile
- Type:
string - Default:
'' - Description: Single entry file path for Sheriff to start traversing imports. Cannot be used together with
entryPoints.
entryPoints
- Type:
Record<string, string> - Default:
undefined - Description: Multiple named entry points for workspaces with multiple applications. Cannot be used together with
entryFile.
Recommendations:
- Use
entryFilefor single applications or simple projects - Use
entryPointsfor monorepos, workspaces, or projects with multiple applications - Example monorepo structure:
entryPoints: {'app-web': './apps/web/src/main.ts','app-mobile': './apps/mobile/src/main.ts','lib-shared': './libs/shared/src/index.ts'}
configs
- Type:
Record<string, string> - Default:
{} - Description: Selects an additional Sheriff config for an explicitly
mapped workspace directory. This opt-in model keeps existing workspaces
unchanged, even if nested
sheriff.config.tsfiles already exist.
The keys are workspace-relative directories and the values are config paths,
relative to the workspace root or absolute. Matching happens at directory
boundaries, so apps/a does not match apps/ab. If mappings overlap, the
deepest matching directory wins. Files outside all mappings use the root
config. Absolute directory keys, keys that escape the workspace root, and keys
mapping the workspace root itself ('.') are invalid — root rules belong in
the root config.
export const config: SheriffConfig = {
entryPoints: {
hexagonal: './apps/hexagonal-demo/src/main.ts',
vertical: './apps/vertical-demo/src/main.ts',
},
configs: {
'apps/hexagonal-demo': './apps/hexagonal-demo/sheriff.config.ts',
'apps/vertical-demo': './apps/vertical-demo/sheriff.config.ts',
},
modules: {
// Modules outside the mapped applications use this root config.
},
depRules: {},
};
Sheriff resolves the config after finding the root config and before parsing
an entry point. ESLint therefore resolves it independently for every linted
file. The CLI resolves it independently for every entryPoints value and
prints the selected config in list and verify output when configs is in
use.
Sub-config modules keys are still workspace-root-relative, not relative to
the sub-config file:
// apps/demo/sheriff.config.ts
export const config: SheriffConfig = {
modules: {
'apps/demo/src/domain/<domain>': ['domain:<domain>'],
},
depRules: {
'*': '*',
},
};
Only the root config's configs, entryFile, and entryPoints are used for
selection. The same fields inside a sub-config are ignored after that sub-config
has been selected.
A sub-config is standalone
A sub-config is not merged with the root config. It is merged with
Sheriff's defaults, exactly like a root config would be. Every
workspace-wide option must therefore be repeated in every sub-config —
otherwise it silently reverts to its default for everything that sub-config
governs, and sheriff verify still reports success.
The trap, written out. The root config below turns on barrel-less mode and forbids barrels workspace-wide:
// sheriff.config.ts
export const config: SheriffConfig = {
configs: {
'apps/demo': './apps/demo/sheriff.config.ts',
},
enableBarrelLess: true,
barrelPolicy: 'forbid',
moduleIdentity: 'config',
depRules: { '*': '*' },
};
❌ Before — the sub-config looks harmless, but apps/demo runs on
enableBarrelLess: false, barrelPolicy: 'allow' and
moduleIdentity: 'auto'. Barrel-less encapsulation is not enforced there, and
a stray index.ts still creates modules:
// apps/demo/sheriff.config.ts
export const config: SheriffConfig = {
modules: {
'apps/demo/src/domain/<domain>': ['domain:<domain>'],
},
depRules: { '*': '*' },
};
✅ After — the workspace-wide options are repeated, so apps/demo is
governed by the same rules as the rest of the workspace:
// apps/demo/sheriff.config.ts
export const config: SheriffConfig = {
enableBarrelLess: true,
barrelPolicy: 'forbid',
moduleIdentity: 'config',
modules: {
'apps/demo/src/domain/<domain>': ['domain:<domain>'],
},
depRules: { '*': '*' },
};
The options which need repeating are the ones that shape the whole workspace:
enableBarrelLess, moduleIdentity,
barrelPolicy, allowBarrelsIn,
encapsulationPattern,
barrelFileName, excludeRoot and
autoTagging.
Setting an option in a sub-config to the same value as the default is a
deliberate choice and stays fine. To find the ones you forgot, run
npx sheriff doctor: it reports every option where the
root config sets a non-default value and a sub-config does not set the option
at all.
An import graph initialized from one entry point currently keeps that entry
point's config for the complete traversal. Use separate entryPoints for
architectures with separate configs. Applying different configs inside one
cross-boundary traversal requires a future mixed-config project graph; this is
tracked on the roadmap.
Other Options
plugins
- Type:
SheriffPlugin[] - Default:
undefined - Description: Registers additional Sheriff CLI commands by instantiating plugins directly in
sheriff.config.ts.
Plugins are only loaded from the root sheriff.config.ts. A plugins entry in a sub-config referenced via configs is ignored.
Example:
import { SheriffConfig } from '@lambda-solutions/sheriff-core';
import { JunitReporterPlugin } from 'mberger-junit-sheriff';
import { SheriffUiPlugin } from '@lambda-solutions/sheriff-ui';
export const config: SheriffConfig = {
version: 1,
modules: {
'src/feature': 'feature',
'src/shared': 'shared',
},
depRules: {
feature: 'shared',
},
plugins: [
new SheriffUiPlugin(),
new JunitReporterPlugin({ junitVersion: 1, reporters: ['html'] }),
],
};
autoTagging
- Type:
boolean - Default:
true - Description: When enabled, Sheriff automatically detects modules and assigns the
noTagtag to them. Useful for initial setup, but becomes optional when you define explicitmodules.
enableBarrelLess
- Type:
boolean - Default:
false - Description: Enables barrel-less modules where files are directly available except those in the
internalfolder.
barrelPolicy
- Type:
'allow' | 'warn' | 'forbid' - Default:
'allow' - Description: Only effective with
enableBarrelLess: true. Controls whether barrel files (index.ts, or the configuredbarrelFileName) are allowed inside the module tree.
In barrel-less mode the absence of a barrel file is load-bearing configuration: a single stray index.ts — created by an IDE, a schematic, or habit — silently turns a barrel-less module into a barrel module and changes its encapsulation semantics.
'allow'(default): keeps the current behaviour, barrels stay legal.'warn': observation phase, surfaced bysheriff verifyonly — it prints a warning line for every barrel module (and a warning-aware success line) but still exits successfully. Thebarrel-policyESLint rule stays silent.'forbid': every barrel module becomes a violation — thebarrel-policyESLint rule reports on the barrel file, andsheriff verifyexits with a non-zero code.
Teams that want editor visibility during an observation phase can set barrelPolicy: 'forbid' and downgrade the rule severity in their ESLint config instead: '@lambda-solutions/sheriff/barrel-policy': 'warn'.
Setting barrelPolicy to 'warn' or 'forbid' without enableBarrelLess: true is a configuration error, because the policy would silently have no effect.
A barrel file in the project root is reported as well. The root module is always barrel-less, so a root-level index.ts never affects encapsulation — it is inert and only misleads readers. Allow it via the pattern . in allowBarrelsIn if it is intentional (e.g. a package entry point).
export const config: SheriffConfig = {
enableBarrelLess: true,
barrelPolicy: 'forbid',
// ... other configuration
};
Intentional barrels can be excluded via allowBarrelsIn.
allowBarrelsIn
- Type:
string[] - Default:
[] - Description: Glob patterns, relative to the project root and matched against the module path, for barrel modules that stay legal despite a restrictive
barrelPolicy.**matches any number of path segments;*matches within a single segment. Leading and trailing path separators in a pattern are ignored, sosrc/api/behaves likesrc/api. The project root itself matches the pattern..
Use this for intentional bucket-level barrels, e.g. an api folder whose index.ts acts as a port with a short import path:
export const config: SheriffConfig = {
enableBarrelLess: true,
barrelPolicy: 'forbid',
allowBarrelsIn: ['**/api'],
// ... other configuration
};
With this configuration, libs/domains/booking/src/api/index.ts stays legal while a library-level barrel such as libs/domains/booking/src/index.ts is still flagged.
Setting a non-empty allowBarrelsIn while barrelPolicy is absent or 'allow' is a configuration error, because the exceptions would be dead configuration. An explicitly set empty array (allowBarrelsIn: []) is legal — it simply keeps the defaults.
moduleIdentity
- Type:
'auto' | 'config' - Default:
'auto' - Description: Decides what makes a directory a module.
By default a directory becomes a module in two independent ways: it matches a modules pattern, or it simply contains a barrel file. The second way means module identity is derived from a file existing. Dropping a stray index.ts into a directory that no modules pattern covers creates a brand-new, untagged (noTag) module and re-routes which module — and therefore which tags — an import is attributed to. The layer matrix silently stops governing that code path.
'auto'(default):modulesand barrel files both create modules.'config': only directories matching amodulespattern are modules. A barrel file never creates one. Files inside a barrel directory which is not a configured module belong to their nearest enclosing configured module.
export const config: SheriffConfig = {
enableBarrelLess: true,
moduleIdentity: 'config',
modules: {
'libs/domains/<domain>/src/api': ['domain:<domain>', 'type:api'],
'libs/domains/<domain>/src/data': ['domain:<domain>', 'type:data'],
'libs/domains/<domain>/src/ui': ['domain:<domain>', 'type:ui'],
},
// ... other configuration
};
With this configuration, a stray libs/domains/booking/src/index.ts no longer creates a module: libs/domains/booking/src/booking.routes.ts keeps the module — and the tags — it had before the file appeared.
Setting moduleIdentity: 'config' without enableBarrelLess: true is a configuration error (SH-021), because without barrel-less mode modules are defined by barrel files by definition.
What it looks like
Take the configuration above and a ui bucket which has grown a sub-folder — an everyday refactoring, and legal: widgets/ is simply part of the ui module.
libs/domains/booking/src/ui/booking-card.ts
libs/domains/booking/src/ui/widgets/booking-badge.ts
npx sheriff verify reports No issues found. Well done!.
Now a single file appears, from an IDE, a schematic, or habit:
// libs/domains/booking/src/ui/widgets/index.ts
export { BookingBadge } from './booking-badge';
With moduleIdentity: 'auto', npx sheriff list shows a module nobody configured:
├── ui (domain:booking, type:ui)
└── widgets (noTag)
and verify reports four violations across three files:
|-- libs/domains/booking/src/ui/booking-card.ts
| |-- Encapsulation Violations
| | |-- ./widgets/booking-badge
| |-- Dependency Rule Violations
| | |-- from tag domain:booking to tags noTag
|-- libs/domains/booking/src/ui/widgets/booking-badge.ts
| |-- Dependency Rule Violations
| | |-- from tag noTag to tags domain:booking, type:types
|-- libs/domains/booking/src/ui/widgets/index.ts
| |-- Barrel Policy Violations
| | |-- index.ts turns a barrel-less module into a barrel module ...
Two of those files were never touched. They are reported because the layer matrix is now being evaluated against a module that does not exist in the architecture: domain:booking importing noTag, and noTag importing back.
With moduleIdentity: 'config' and the very same file tree, sheriff list is unchanged — there is no widgets node — and verify reports one violation, on the file that actually causes it:
Total Encapsulation Violations: 0
Total Dependency Rule Violations: 0
Total Barrel Policy Violations: 1
|-- libs/domains/booking/src/ui/widgets/index.ts
| |-- Barrel Policy Violations
| | |-- index.ts sits outside any module configured via `modules`.
| | | With moduleIdentity: 'config' it creates no module and has no
| | | effect on encapsulation. Remove it, add its directory to
| | | `modules`, or add it to `allowBarrelsIn`.
The rest of the code is judged by its configured identity again, and the remaining message names the cause and the ways out. This scenario is pinned end-to-end by the nx-i integration project.
Barrels still decide exposure
moduleIdentity changes module identity, not module exposure. Inside a configured module a barrel file still means "only this file is importable from outside":
- A configured module containing a barrel file keeps its path, its tags and its dependency rules, but exposes only the barrel file.
- The barrel takes precedence over a module's
exports: where both are present, the barrel alone decides what is importable — exactly as in'auto'mode.
This residual blast radius is what barrelPolicy reports. Under moduleIdentity: 'config' the policy also reports barrel files which sit outside every configured module — otherwise the case this option exists to defuse would become invisible to sheriff verify. Those barrels obey allowBarrelsIn in the same way, matched against their directory.
Migration
'auto' is the default and unchanged, so existing projects are unaffected. Switching to 'config' is behavior-changing:
- Modules disappear. Every module that existed only because of a barrel file, without a matching
modulesentry, is gone. Its files move to the nearest enclosing configured module (or to the root module). Runnpx sheriff listbefore and after to see the difference. - Tags and dependency rules move with them. Files that were governed by
noTagrules are now governed by the enclosing module's tags. If you relied onnoTag: noDependenciesas a tripwire, the tripwire moves. - Encapsulation relaxes for those directories. A barrel that no longer creates a module no longer restricts imports into its directory; the enclosing module's rules apply instead. Where that enclosing module is the root module, there are effectively no encapsulation restrictions on those files at all:
moduleIdentity: 'config'requiresenableBarrelLess: true, and a barrel-less root module exposes every file that does not match theencapsulationPattern. Such files then become deep-importable from anywhere, andexcludeRootmakes no difference to that — it only relaxes access to the root module, which is already fully exposed. Add amodulesentry for those directories if you want them to stay encapsulated.
The recommended path is to run npx sheriff doctor first, add a modules entry for every barrel module you want to keep, and only then switch moduleIdentity to 'config'.
encapsulationPattern
- Type:
string | RegExp - Default:
'internal' - Description: Pattern for files that are not available outside their barrel-less module. A string encapsulates a file when the module-relative path starts with the pattern, or when any directory segment of the path equals the pattern exactly — so an
internalfolder is encapsulated at any depth of the module (e.g.data/foo/internal/secret.ts). The filename itself does not count as a segment:data/internal.tsstays public, while a top-levelinternal.tsremains encapsulated via the prefix rule. A string pattern containing a path separator (e.g.'internal/'or'sub/internal') can never equal a single directory segment and therefore only gets the legacy prefix behavior. A regular expression is matched against the module-relative path.exportsof a module definition take precedence over this pattern.
barrelFileName
- Type:
string - Default:
'index.ts' - Description: Name of the barrel file that exports public APIs from a module.
ignoreFileExtensions
- Type:
string[] | ((defaults: string[]) => string[]) - Default: See [Default Ignored Extensions]
- Description: Controls which file extensions are ignored during import traversal. Sheriff will not follow imports to files with these extensions.
Default Ignored Extensions
Default Ignored Extensions:
- Images:
svg,png,jpg,jpeg,gif,webp,ico - Styles:
css,scss,sass,less - Fonts:
woff,woff2,ttf,eot,otf - Audio:
mp3,wav,ogg - Video:
mp4,webm,mov - Data/Misc:
json,csv,xml,txt,md
Legacy Options
excludeRoot
- Type:
boolean - Default:
false - Description: When enabled, removes the implicit root project from all checks. Useful for incremental integration of Sheriff into existing applications.
log
- Type:
boolean - Default:
false - Description: Enables detailed logging for debugging purposes.
version
- Type:
number - Default:
1 - Description: Configuration version. Currently only version 1 is supported. This option is rarely needed as Sheriff automatically uses the latest supported version.
Configuration Validation
Sheriff validates your configuration and will throw helpful errors if:
- Both
entryFileandentryPointsare specified autoTaggingis disabled but nomodulesare defined- Invalid dependency rules are configured
- Required properties are missing
Migration from Previous Versions
If you're upgrading from an older version of Sheriff, check the Release Notes for any breaking changes or new configuration options.
Generally speaking, we really try hard to avoid breaking changes.