Skip to content

CLI Options

Qualimetrix provides the check command for code analysis and several utility commands for baseline management, git hooks, and dependency graph visualization.

check command

bin/qmx check [options] [--] [<paths>...]

Paths argument

Specify one or more directories or files to analyze:

# Analyze specific directories
bin/qmx check src/ lib/

# Analyze a single file
bin/qmx check src/Service/UserService.php

If you omit paths, Qualimetrix auto-detects them from the autoload section of your composer.json.


File options

--config, -c

Path to a YAML configuration file:

bin/qmx check src/ --config=qmx.yaml

--exclude

Exclude directories from analysis. Can be repeated:

bin/qmx check src/ --exclude=src/Generated --exclude=src/Legacy

A value that removes no directory is reported as discovery.unmatched-exclude — a warning at project level, not a refusal, and only on a run whose paths cover the project's production autoload roots. On a narrower run the pattern may bind nothing simply because the code it names lies outside the slice.

--include-generated

By default, Qualimetrix automatically skips files that contain a @generated annotation in the first 2 KB. This flag overrides that behavior and includes generated files in the analysis:

bin/qmx check src/ --include-generated

Can also be set in qmx.yaml:

include_generated: true

--suppress-path

Suppress violations for files matching a glob pattern. The files are still analyzed (their metrics contribute to namespace-level calculations), but violations are not reported. Can be repeated:

bin/qmx check src/ --suppress-path="src/Entity/*" --suppress-path="src/DTO/*"

Merged with suppress_paths from qmx.yaml — both sources are combined.

Does not apply to architecture.* rules

architecture.layer-violation and architecture.circular-dependency violations are never suppressed by this option — see Suppress Paths for why and for the alternatives.

--suppress-namespace

Suppress violations for classes in namespaces matching a prefix or glob pattern. The classes are still analyzed (their metrics contribute to aggregated calculations), but violations are not reported. Can be repeated:

bin/qmx check src/ --suppress-namespace="App\Entity" --suppress-namespace="App\DTO\*"

Merged with suppress_namespaces from qmx.yaml — both sources are combined.

Does not apply to architecture.* rules

architecture.layer-violation and architecture.circular-dependency violations are never suppressed by this option — see Suppress Namespaces for why and for the alternatives.


Preset options

--preset

Apply a named preset or a custom YAML file. Can be repeated or comma-separated:

# Built-in presets
bin/qmx check src/ --preset=strict
bin/qmx check src/ --preset=legacy

# Combine presets (merged left-to-right)
bin/qmx check src/ --preset=strict,ci
bin/qmx check src/ --preset=strict --preset=ci

# Custom preset file
bin/qmx check src/ --preset=./my-preset.yaml

Available built-in presets: strict, legacy, ci.

Presets are applied after composer.json auto-detection but before qmx.yaml, so your config file always takes precedence. See Configuration > Presets for details.


Output options

--format, -f

Choose the output format. Default: summary.

bin/qmx check src/ --format=json
bin/qmx check src/ --format=sarif

Available formats: summary, text, text-verbose, json, metrics, checkstyle, sarif, gitlab, github, health, html, suppressed.

See Output Formats for details on each format.

--group-by

Group violations in the output. Default depends on the formatter.

bin/qmx check src/ --format=text-verbose --group-by=rule

Available values: none, file, rule, severity, class, namespace.

--format-opt

Pass formatter-specific options as key=value pairs. Can be repeated:

bin/qmx check src/ --format-opt=key=value

A key no formatter reads is refused with exit 3. A key some other formatter reads stays accepted, so a script that sweeps one option set across formats still works.

JSON format options:

Option Default Description
violations=N\|all all Max violations in output (0=none)
limit=N all Alias for violations
top=N 10 Number of worst offenders to include
rank-by=count\|density count Reorder worst-offender lists by violation count (default) or by violation density
bin/qmx check src/ --format=json --format-opt=limit=100
bin/qmx check src/ --format=json --format-opt=violations=all
bin/qmx check src/ --format=json --format-opt=rank-by=density

Summary format options:

Option Default Description
top=N 3 Number of worst offenders to include
rank-by=count\|density count Reorder worst-offender lists by violation count (default) or by violation density
bin/qmx check src/ --format-opt=rank-by=density

Two unrelated options named top

--format-opt=top=N (JSON and summary formats) caps the worst-namespace/worst-class offender lists. The global --top flag is a different knob: it caps the separate "Top issues by impact" list. The two can be set independently.

Health format options:

Option Default Description
contributors=N 3 Number of worst contributors listed per health dimension

HTML format options:

Option Default Description
project-name=NAME auto-detected Overrides the project name shown in the HTML report
bin/qmx check src/ --format=html --format-opt=project-name="My Project" -o report.html

--fail-on

Set the minimum severity that causes a non-zero exit code. Default: error.

# Default behavior: only errors cause non-zero exit code
bin/qmx check src/

# Also fail on warnings
bin/qmx check src/ --fail-on=warning

# Never fail on violations
bin/qmx check src/ --fail-on=none

By default, warnings are shown in the output but do not cause CI failure. Use --fail-on=warning to also fail on warnings.

Can also be set in qmx.yaml:

fail_on: warning   # also fail on warnings

--exclude-health

Exclude specific health dimensions from scoring. The excluded dimensions are not shown in the health summary and do not contribute to the overall score. Can be repeated:

# Exclude typing from health scoring
bin/qmx check src/ --exclude-health=typing

# Exclude multiple dimensions
bin/qmx check src/ --exclude-health=typing --exclude-health=maintainability

Available dimensions: complexity, cohesion, coupling, typing, maintainability.

Can also be set in qmx.yaml:

exclude_health:
  - typing

--detail

Show a grouped violation list after the summary. Only affects summary format.

# Default limit (200 violations)
bin/qmx check src/ --detail

# Show all violations (no limit)
bin/qmx check src/ --detail=all

# Custom limit
bin/qmx check src/ --detail=50

Auto-enabled when --namespace or --class is used.

--top

Number of top-impact issues to show. Default: 10; 0 disables the section.

# Default: top 10
bin/qmx check src/

# Show the top 25
bin/qmx check src/ --top=25

# Disable the section
bin/qmx check src/ --top=0

Controls the "Top issues by impact" section of the summary format and the topIssues key of --format=json — a list of findings ranked by impact (combining ClassRank, severity and remediation time), separate from the worst-namespace and worst-class offender lists. No other format renders it.

This is a different knob from --format-opt=top=N, which caps the worst-offender lists instead — see the format options tables above.

--all

Show all violations without truncation. This is a shorthand for --format-opt=violations=all --detail=all.

# Show all violations in JSON format
bin/qmx check src/ --format=json --all

# Show all violations in summary format
bin/qmx check src/ --all

Cannot be combined with --format-opt=violations=N (numeric limit) — this produces a clear error. Combining --all with --format-opt=violations=all is allowed (they are synonyms).

--namespace

Filter output to a specific namespace subtree. The value is a namespace pattern, not a literal prefix:

  • Without glob characters it matches on namespace boundaries — App\Service matches App\Service and everything under it, but not App\ServiceBus.
  • With *, ? or [ it is matched as a glob, so App\*\Order selects App\Billing\Order and App\Sales\Order rather than a namespace literally spelled with an asterisk.
  • A trailing \ is cosmetic: App\Service\ and App\Service are the same pattern.
  • An empty value matches nothing at all, the global namespace included.
bin/qmx check src/ --namespace=App\\Service
bin/qmx check src/ --namespace='App\*\Order'

Filters violations and worst offenders to the selected namespaces. Shows subtree health scores. Auto-enables --detail.

Project-wide findings (architecture.coverage-gap and the other diagnostics that judge the run as a whole) are never selected by a namespace pattern, not even *: they belong to no namespace.

The same matching rule governs the health drill-down and the worst-offender lists this option turns on, and the include_namespaces option of coupling.distance.

A pattern that selects no analysed namespace is refused with exit 3, and the refusal says how many namespaces the run did have. A pattern that does select something and still reports nothing prints the ordinary empty result — that is the half of the pair worth telling apart.

Mutually exclusive with --class.

--class

Filter output to a specific class by exact FQCN match.

bin/qmx check src/ --class=App\\Service\\UserService

Filters violations to the specified class. Auto-enables --detail.

An FQCN matching no analysed class is refused with exit 3, for the same reason --namespace is: an empty report otherwise reads as a clean class.

Mutually exclusive with --namespace.


Cache options

Qualimetrix caches parsed ASTs to speed up repeated runs.

--no-cache

Disable caching entirely:

bin/qmx check src/ --no-cache

--cache-dir

Set a custom cache directory. Default: .qmx-cache.

bin/qmx check src/ --cache-dir=/tmp/qmx-cache

The directory is created when missing. A path that cannot be created or is not writable is refused with exit 3 rather than silently disabling the cache, and the door closes the same way on every command that resolves a cache directory, not only on check.

--clear-cache

Clear the cache before running analysis:

bin/qmx check src/ --clear-cache

Baseline options

See Baseline for the lifecycle and file format.

--baseline=BASELINE

Use a baseline file to apply accepted ceilings to live findings:

bin/qmx check src/ --baseline=baseline.json

--show-resolved

Count entries whose complete identity no longer appears in the measured set:

bin/qmx check src/ --baseline=baseline.json --show-resolved

Stale and inert entries are reported without failing the run or disabling other baseline entries. A group that still fires with fewer members is not resolved.

Baseline lifecycle commands

The commands below are the complete baseline write and inspection surface:

bin/qmx baseline:generate <baseline> [<paths>...] [--mode=MODE] [--force]
bin/qmx baseline:update   <baseline> [<paths>...] [--force]
bin/qmx baseline:cleanup  <baseline> [<paths>...] [--remove=REMOVE]... [--force]
bin/qmx baseline:explain  <symbol> [<paths>...] [--baseline=BASELINE] [--channel=CHANNEL]
bin/qmx baseline:rename-channels <baseline> <map> [--format=FORMAT]

The first four commands accept --config=CONFIG, --preset=PRESET, --disable-rule=DISABLE-RULE, --only-rule=ONLY-RULE, and --rule-opt=RULE-OPT. They do not accept any exclusion or suppression option. baseline:rename-channels accepts none of them: it runs no analysis, so there is no measured set for them to define.

  • baseline:generate captures the current measured findings. --mode=ratchet is the default; --mode=suppress records unconditional acceptance for captured identities. Its --force overwrites an existing file.
  • baseline:update tightens existing entries only. Its --force overrides the recorded-scope coverage guard.
  • baseline:cleanup lists candidates by default and removes only repeated --remove=REMOVE selectors. Its --force also overrides the scope guard.
  • baseline:explain shows the configured threshold, accepted baseline level, and source override for a canonical symbol; --channel=CHANNEL narrows the answer.
  • baseline:rename-channels rewrites the channel field of the entries a declared tab-separated map names, and nothing else, without analysing anything. Exit 1 covers a refusal on content or an unreadable baseline or map file; 2 is a malformed --format value. Either way the baseline is left byte-identical, and the refusal is reported in the chosen format — under --format=json as an object with an error key. See Carry a baseline onto renamed channels — note that carrying an entry changes its selector.

The four analysing commands refuse incomplete analysis with exit 4 before interpreting or writing a baseline. --force overrides file/scope guards only; it cannot make a partial measured set acceptable. Existing destinations remain byte-identical, and baseline:generate does not create a missing destination.

Loadable baseline versions and the migration procedure for an older file are documented under Replace an older baseline.

The removed --generate-baseline and --baseline-ignore-stale options have no aliases. Use baseline:generate and explicit baseline:cleanup --remove instead.


Suppression options

--show-suppressed

Show violations that were suppressed by @qmx-ignore tags, and violations suppressed by a per-rule suppress_namespaces / suppress_namespace_channels / suppress_paths entry in qmx.yaml (see Rules):

bin/qmx check src/ --show-suppressed

Independently of --show-suppressed, running with -v prints a per-rule count of how many violations were suppressed this way. The namespace bucket includes both namespace options and is separate from suppress_paths; each is broken down by rule name. Unlike @qmx-ignore, this suppression is otherwise silent: nothing in the default output indicates it happened.

--show-suppressed renders part of this as prose on the text surface. --format=suppressed reports the full composition — all seven suppression mechanisms, not only these two — as machine-readable JSON; see Output Formats. Either --show-suppressed or selecting --format=suppressed (including format: suppressed in qmx.yaml) is enough to arm the per-rule exclusion capture; you do not need both. The two surfaces are not otherwise equivalent — see suppressed for what each one shows.

Suppression is a closed set of seven mechanisms. Several neighboring decisions also make a finding invisible but are not suppression, and neither surface covers them: a rule that never ran (--disable-rule, --only-rule, enabled: false) produced nothing to suppress; a disabled channel for a classless producer (visible in qmx rules) is removed the same way, before the ledger runs; a threshold that keeps a finding from being produced at all (@qmx-threshold) is audited separately rather than through this surface; formatter truncation (--detail, violations=N) keeps the finding in the payload and only flags it truncated; and --namespace/--class drill-down narrows presentation per invocation without removing anything from the underlying result.

--no-suppression-annotations

Report every violation, including the ones @qmx-ignore tags suppress:

bin/qmx check src/ --no-suppression-annotations

It does not change what a baseline measures

The flag affects the report only. A baseline measures the findings your configuration and your source annotations leave standing, so a finding an @qmx-ignore tag removes is never captured into a baseline and never compared against one — whether or not this flag is passed.

The visible consequence: under this flag an annotated finding is shown at its own severity and is never promoted to an error, because no baseline entry covers it. A flag can narrow what a baseline measures (--suppress-path, --suppress-namespace); none can widen it.


Git scope options

Report only violations from changed files. See Git Integration for the full guide.

--report

Control which violations to report. Analyzes the full project but only shows violations from changed files:

bin/qmx check src/ --report=git:main..HEAD
bin/qmx check src/ --report=git:origin/develop..HEAD

--report-strict

In diff mode, only show violations from the changed files themselves. Without this flag, violations from parent namespaces are also shown:

bin/qmx check src/ --report=git:main..HEAD --report-strict

Execution options

--workers, -w

Control parallel processing. Default: auto-detect based on CPU count.

# Disable parallel processing (single-threaded)
bin/qmx check src/ --workers=1

# Disable parallel processing (sequential)
bin/qmx check src/ --workers=0

# Use exactly 4 workers
bin/qmx check src/ --workers=4

Tip

Use --workers=1 for debugging or single-process environments. --workers=0 disables parallelism (sequential execution); auto-detect is the default when the option is omitted.

--memory-limit

Set the PHP memory limit for analysis. By default, PHP's memory_limit from php.ini is used.

# Set memory limit to 1GB for large projects
bin/qmx check src/ --memory-limit=1G

# Unlimited memory
bin/qmx check src/ --memory-limit=-1

Valid formats: -1 (unlimited), or a positive integer with optional K/M/G suffix (e.g., 512M, 2G).

Equivalent YAML: memory_limit: 1G

--log-file

Write a debug log to a file:

bin/qmx check src/ --log-file=qmx.log

--log-level

Set the minimum log level. Default: info.

bin/qmx check src/ --log-file=qmx.log --log-level=debug

Available levels: debug, info, warning, error. A value outside them is refused with exit 3 instead of falling back to info.

--no-progress

Disable the progress bar. Useful in CI pipelines:

bin/qmx check src/ --no-progress

Accepted by every command that shows a progress bar: check, directives, debug:layer-assignment, baseline:generate, baseline:update, baseline:cleanup and baseline:explain. graph:export analyses a tree too but draws no bar, so it does not take the option.

The progress bar is written to standard error, so the report on standard output stays machine-readable even on a terminal — bin/qmx check src/ --format=json > report.json produces valid JSON without this flag. It is drawn only when standard error is a terminal; redirecting standard error silences the bar without touching the report, rather than writing control bytes into the file.

The bar shares the error stream with detailed logging (-v, -vv, -vvv) and with warnings emitted during a run. Both are drawn through one owner: a diagnostic line pushes the bar down and stays on the screen, and the bar is redrawn beneath it. Raising verbosity therefore does not turn the bar off, and the bar does not eat log lines.

--silent, -q/--quiet

Symfony console flags that suppress normal output. Both are accepted by every command:

bin/qmx check src/ --silent
bin/qmx check src/ -q

--silent no longer guarantees zero bytes on every exit code

--silent and -q/--quiet currently behave identically: both suppress the report on standard output, but a configuration or input refusal (e.g. a path that does not exist) is still written to standard error. A run refused before analysis starts produces 0 bytes on stdout and a human-readable error line on stderr, whichever of the two flags was passed:

bin/qmx check src/DoesNotExist --silent
# exit code 3, empty stdout, error message on stderr

A CI wrapper built on the older assumption that --silent means "zero output on any exit code" will see that stderr text on a refusal. Redirect standard error too (--silent 2>/dev/null) if that assumption must hold.


Profiling options

--profile

Enable the internal profiler. Optionally specify a file to save the profile:

<!-- llms:skip-end -->

# Show profiling summary on screen
bin/qmx check src/ --profile

# Save profile to file
bin/qmx check src/ --profile=profile.json

--profile-format

Choose the profile export format. Default: json.

bin/qmx check src/ --profile=profile.json --profile-format=chrome-tracing

Available formats: json, chrome-tracing.

Tip

Use chrome-tracing format and open the file in Chrome DevTools (chrome://tracing) for a visual timeline.


Rule options

--disable-rule

Disable a producer rule, an entire group, or a finding channel. A selector is either an exact name (a producer rule, a group like complexity, or a channel), or X.* for strictly the descendants of XX itself is not included. A bare prefix without the star is an error. A channel selector can be narrowed to one level of the aggregation tree with :level, same as --only-rule. Disabling one channel keeps its producer active so that other channels can still be reported. Can be repeated:

# Disable one rule
bin/qmx check src/ --disable-rule=size.class-count

# Disable all complexity rules
bin/qmx check src/ --disable-rule=complexity.*

# Disable multiple
bin/qmx check src/ --disable-rule=complexity.* --disable-rule=cohesion.lcom

# Disable only one computed finding channel
bin/qmx check src/ --disable-rule=health.complexity

Memory optimization

Disabling the duplication.clone rule also skips the memory-intensive duplication detection phase entirely. On large codebases (500+ files), this can significantly reduce memory usage. Use --disable-rule=duplication.clone if you encounter out-of-memory errors. The level-narrowed spelling --disable-rule=duplication.clone:project skips it too: the channel reports at that one level, so silencing the level silences the rule. A producer stops as soon as the disable selectors together cover every level of every channel it emits — one level of a two-level channel leaves it running, since the other level still has findings to report.

--only-rule

Run only matching producer rules or finding channels. A selector is either an exact name (a producer rule, a group like complexity, or a channel), or X.* for strictly its descendants, either optionally narrowed to one level of the aggregation tree with :level. A selector carrying a level keeps its producer running, since a producer filtered out would never emit the level that was asked for. Can be repeated:

# Run only complexity rules
bin/qmx check src/ --only-rule=complexity.*

# Run two specific rules
bin/qmx check src/ --only-rule=complexity.ccn --only-rule=size.method-count

# Select one channel of a built-in health dimension: producer and channel
# share the name, since each of the six dimensions is its own producer
bin/qmx check src/ --only-rule=health.complexity

Selectors must match a registered producer, group, or emitted channel exactly, or resolve an X.* to at least one descendant. Unknown selectors — including a bare prefix without the star, or an X.* that matches nothing — fail closed with exit 3 before stdout receives a report payload:

Rule selector "complexity" does not match any registered producer, group, or channel.

Likewise, the owner before : in --rule-opt=RULE:OPTION=VALUE must be an exact producer rule, not a group or channel — a group or channel there is an error. The same rule governs the rules: YAML section keys.

Every channel obeys selection, including the one assembled last

annotation.unused-directive — the "this suppression silenced nothing" verdict — can only be reached once every other rule has produced its findings, so a run assembles it after rule execution. It is selected like any other channel all the same: --disable-rule=annotation.unused-directive (or annotation.unused-directive:file) silences it, and an --only-rule that names other channels of annotation.directive without naming this one does not report it.

The producer's exclusion options are a separate matter and do not reach this channel: rules.annotation.directive.suppress_paths gates its early channels only, and suppress_namespaces reaches none of them, since these findings are reported against the file the annotation was written in.

--rule-opt

Override rule options from the command line. Format: rule-name:option=value, where rule-name must be an exact producer rule — never a group, never a channel, and never a wildcard. This is the same constraint that governs the owner before : in --only-rule/--disable-rule and the rules: YAML section keys. Can be repeated:

bin/qmx check src/ --rule-opt=complexity.ccn:callable.warning=15
bin/qmx check src/ --rule-opt=complexity.ccn:callable.error=30

All three ways of getting it wrong are refused with exit 3, where the option pair used to be dropped without a word: a value written without =VALUE, a rule name no registered producer answers to, and an option name that rule does not accept. The refusal for the last one lists the options the rule does accept.

suppress_namespace_channels is configured in YAML, not through --rule-opt: each selector requires a non-empty list of namespace patterns, while --rule-opt carries scalar values. Its keys are channel selectors and follow the same exact-or-X.* rule as @qmx-ignore — a bare prefix like health is now an error, not a shorthand for health.*. A key may add :namespace and no other level: the option is offered namespace aggregates only, so any other level would name a filter that can never fire.

Rule-specific shortcut flags

Many rules have dedicated CLI flags for quick rule-option configuration:

Flag Rule Option
--cyclomatic-warning=N complexity.ccn callable.warning
--cyclomatic-error=N complexity.ccn callable.error
--cyclomatic-class-warning=N complexity.ccn class.max_warning
--cyclomatic-class-error=N complexity.ccn class.max_error
--cognitive-warning=N complexity.cognitive callable.warning
--cognitive-error=N complexity.cognitive callable.error
--cognitive-class-warning=N complexity.cognitive class.max_warning
--cognitive-class-error=N complexity.cognitive class.max_error
--npath-warning=N complexity.npath callable.warning
--npath-error=N complexity.npath callable.error
--npath-class-warning=N complexity.npath class.max_warning
--npath-class-error=N complexity.npath class.max_error
--wmc-warning=N complexity.wmc warning
--wmc-error=N complexity.wmc error
--wmc-exclude-data-classes complexity.wmc excludeDataClasses
Flag Rule Option
--cbo-warning=N coupling.cbo class.warning
--cbo-error=N coupling.cbo class.error
--cbo-ns-warning=N coupling.cbo namespace.warning
--cbo-ns-error=N coupling.cbo namespace.error
--distance-warning=N coupling.distance max_distance_warning
--distance-error=N coupling.distance max_distance_error
--instability-class-warning=N coupling.instability class.max_warning
--instability-class-error=N coupling.instability class.max_error
--instability-ns-warning=N coupling.instability namespace.max_warning
--instability-ns-error=N coupling.instability namespace.max_error
--class-rank-warning=N coupling.class-rank warning
--class-rank-error=N coupling.class-rank error
Flag Rule Option
--class-count-warning=N size.class-count warning
--class-count-error=N size.class-count error
--method-count-warning=N size.method-count warning
--method-count-error=N size.method-count error
--property-count-warning=N size.property-count warning
--property-count-error=N size.property-count error
Flag Rule Option
--dit-warning=N design.dit warning
--dit-error=N design.dit error
--lcom-warning=N cohesion.lcom warning
--lcom-error=N cohesion.lcom error
--lcom-min-methods=N cohesion.lcom minMethods
--lcom-exclude-readonly cohesion.lcom excludeReadonly
--lcom-exclude-methods=NAME cohesion.lcom excludeMethods
--noc-warning=N design.noc warning
--noc-error=N design.noc error
--param-type-coverage-warning=N design.type-coverage.param warning
--param-type-coverage-error=N design.type-coverage.param error
--return-type-coverage-warning=N design.type-coverage.return warning
--return-type-coverage-error=N design.type-coverage.return error
--property-type-coverage-warning=N design.type-coverage.property warning
--property-type-coverage-error=N design.type-coverage.property error
--property-exclude-readonly size.property-count excludeReadonly
--property-exclude-promoted-only size.property-count excludePromotedOnly
Flag Rule Option
--mi-warning=N maintainability.mi warning
--mi-error=N maintainability.mi error
--mi-min-statements=N maintainability.mi minStatements
--mi-exclude-tests maintainability.mi excludeTests
Flag Rule Option
--constructor-overinjection-warning=N code-smell.constructor-overinjection warning
--constructor-overinjection-error=N code-smell.constructor-overinjection error
--data-class-woc-threshold=N design.data-class wocThreshold
--data-class-wmc-threshold=N design.data-class wmcThreshold
--data-class-min-members=N design.data-class minMembers
--data-class-exclude-readonly design.data-class excludeReadonly
--data-class-exclude-promoted-only design.data-class excludePromotedOnly
--data-class-exclude-exceptions design.data-class excludeExceptions
--god-class-wmc-threshold=N design.god-class wmcThreshold
--god-class-lcom-threshold=N design.god-class lcomThreshold
--god-class-tcc-threshold=N design.god-class tccThreshold
--god-class-class-loc-threshold=N design.god-class classLocThreshold
--god-class-min-criteria=N design.god-class minCriteria
--god-class-min-methods=N design.god-class minMethods
--god-class-exclude-readonly design.god-class excludeReadonly
--long-parameter-list-warning=N code-smell.long-parameter-list warning
--long-parameter-list-error=N code-smell.long-parameter-list error
--long-parameter-list-vo-warning=N code-smell.long-parameter-list vo-warning
--long-parameter-list-vo-error=N code-smell.long-parameter-list vo-error
--unreachable-code-warning=N code-smell.unreachable-code warning
--unreachable-code-error=N code-smell.unreachable-code error
Flag Rule Option
--circular-deps architecture.circular-dependency enabled
--max-cycle-size=N architecture.circular-dependency maxCycleSize
--layer-violation architecture.layer-violation enabled
--layer-violation-severity=SEVERITY architecture.layer-violation severity
--unassigned-class-mode=MODE architecture.unassigned-class mode

Other commands

baseline:cleanup

Inspect stale candidates in a baseline. Without --remove, it only lists them and never writes the file; remove an explicitly reviewed selector as described in Baseline:

bin/qmx baseline:cleanup baseline.json src/
bin/qmx baseline:cleanup baseline.json src/ --remove=<selector>

debug:layer-assignment

Report which architecture layer a class is assigned to, and every other layer whose criteria would also have matched it (a potential shadow source). See Inspecting layer assignment for a single class for the full walkthrough.

bin/qmx debug:layer-assignment 'App\Service\Foo'
bin/qmx debug:layer-assignment 'App\Service\Foo' --config qmx.yaml

# Machine-readable output — for agents and scripts, not for parsing the text report
bin/qmx debug:layer-assignment 'App\Service\Foo' --format=json
Option Description
-c, --config=FILE Path to qmx.yaml (default: qmx.yaml in the current directory)
--format=FORMAT text (default) or json

--format=json serializes the same resolution the text report renders. The command answers only for classes the run analysed: an FQN naming no analysed declaration — a typo, or a class kept out of the run by paths, exclude or the generated-file filter — exits with code 3 and the error envelope instead of being classified. Schema:

{
  "fqn": "App\\Service\\Foo",
  "assigned": { "layer": "any-foo", "criteria": ["pattern \"App\\**\\Foo\""] },
  "shadowed": [
    { "layer": "service", "criteria": ["pattern \"App\\Service\\**\""] }
  ],
  "hasLayers": true
}
  • assigned is null when no layer matched (empty shadowed follows).
  • shadowed lists every other matching layer in declaration order — each entry would have won the assignment had it been declared before assigned.
  • hasLayers distinguishes "no layers configured" (false) from "layers configured but none matched this class" (true with assigned: null).
  • On error, --format=json prints {"error": "...", "exit_code": N} to stdout instead of the human <error> line, and an unrecognized --format value exits with code 3 regardless of format.

directives

Report what every inline @qmx-ignore and @qmx-threshold in the analysed tree actually does. A suppression is judged by what it silenced; a threshold directive is judged by removing it and executing the rules again over the run's own measurements — by default only the rule it addresses, one execution per directive.

bin/qmx directives src/

# Re-execute every enabled rule instead of just the addressed one — the control the narrow default is measured against
bin/qmx directives src/ --sweep=full

# Machine-readable output — for agents, scripts and CI
bin/qmx directives src/ --format=json
Option Description
-c, --config=FILE Path to qmx.yaml (default: qmx.yaml in the current directory)
--format=FORMAT text (default) or json
--sweep=SCOPE How much of the rule layer each counterfactual re-executes: narrow (default) or full
--preset=PRESET Apply a named preset (repeatable)
--only-rule=RULE Judge under a run that ran only these rules (repeatable)
--disable-rule=RULE Judge under a run with these rules off (repeatable)
--rule-opt=RULE:OPT=VAL Judge under a run with this rule option (repeatable)

The four selection options exist because a verdict is relative to the run that produced it: point the command at the same rules and boundaries your CI checks with, or it will answer about a different run.

A @qmx-threshold names exactly one rule, so under --sweep=narrow a counterfactual re-executes only that rule. --sweep=full re-executes every enabled rule for the same verdicts, at far higher cost — it is not a slower fallback but the control that measures, rather than assumes, that removing a directive of one rule cannot move another rule's findings: the two scopes are swept over the same tree and compared verdict for verdict. On this project's own src/ the narrow sweep is several times cheaper and the two scopes agree on every verdict. Both the text report and --format=json state the sweep the verdicts were measured under.

Exit codes: 0 nothing inert, 2 at least one inert directive whose boundary was observable, 3 bad input or configuration — including a scope that analysed no PHP files at all (a directory with no PHP in it, an exclude that swallowed everything, or nothing but @generated files), 4 the run failed to parse part of the tree, 1 the command itself failed unexpectedly.

Four verdicts, of which three are answers and one is the absence of one:

Verdict What it states
effective Removing it changes what the rules produce.
applied-boundary-only It applied, and nothing moved except the boundary the finding prints.
inert Removing it changes nothing. This is the only verdict that moves the exit code.
unmeasured No answer is available, and the report says why: the producer did not run, the directive was already refused elsewhere, it carries no rule filter, or another directive of the same rule covers the same subject.

A verdict is relative to the analysed scope

A threshold on a metric computed over the analysed subgraph — coupling above all — can be alive over the whole project and dead over one directory of it, and neither answer is wrong. Point the command at what the project actually analyses. The report prints the scope it measured under, and a run that failed to parse part of the tree exits 4 instead of calling anything dead.

Judged against what the rules produced, not against the report

suppress_paths, suppress_namespaces and suppress_namespace_channels suppress publication, not measurement. A directive that moved a finding inside an excluded namespace still did something, so the audit asks its question against every finding the rules produced, not against the report. The one channel outside that universe is annotation.unused-directive, which a run assembles after the rules have run — no directive may address it, so no verdict is judged against it.

The one thing a suppression is not credited with is silencing a configuration error (annotation.unresolved-directive and its two siblings). Those channels are exempt from annotation suppression by construction, not by configuration, so a directive aimed at one is reported inert however it is written.

annotation.unused-directive is exempt in a louder way: a directive addressing it is refused rather than judged, and the audit reports it unmeasured / already-refused — the same answer check gives as an annotation.unresolved-directive on that line.

The applied-boundary-only verdict deliberately makes no claim about direction. The rule layer has no notion of which way is stricter — coupling.instability is worse when higher, cohesion.tcc when lower — so a directive that tightens a boundary and one that raises a boundary the measured value had already passed are the same observable. In --format=json this verdict keeps the stable key overrun.

Where a rule publishes no boundary alongside its finding, an inert verdict carries a note saying so, and does not fail the build: a boundary the value had already passed would have looked identical, so demanding the directive be deleted would report an unasked question as proven debt. --format=json reports it as "boundary_observable": false.

On error, --format=json prints {"error": "...", "exit_code": N} to stdout instead of the human <error> line.

graph:export

Export the dependency graph for visualization:

# Export as DOT (default)
bin/qmx graph:export src/ -o graph.dot

# Export as JSON (aggregated adjacency list with metadata)
bin/qmx graph:export src/ --format=json -o graph.json

# Filter by namespace
bin/qmx graph:export src/ --namespace=App\\Service --namespace=App\\Repository

# Exclude namespaces
bin/qmx graph:export src/ --exclude-namespace=App\\Generated

# Change layout direction
bin/qmx graph:export src/ --direction=TB

# Disable namespace grouping
bin/qmx graph:export src/ --no-clusters
Option Description
-o, --output=FILE Output file (default: stdout)
-f, --format=FORMAT dot (default) or json
-d, --direction=DIR Graph direction: LR, TB, RL, BT (default: LR)
--no-clusters Do not group nodes by namespace
--namespace=NS Include only these namespaces (repeatable)
--exclude-namespace=NS Exclude these namespaces (repeatable)

A --namespace value matching no vertex is refused with exit 3 rather than exporting an empty graph. --exclude-namespace keeps its silence on purpose: a missed exclusion leaves the picture whole, so the viewer loses nothing.

If any discovered file fails parsing or processing, graph:export exits 4 and emits no partial graph. It does not create a missing output file and preserves an existing destination byte-for-byte.

hook:install

Install a git pre-commit hook:

bin/qmx hook:install

# Overwrite existing hook
bin/qmx hook:install --force

hook:status

Show the current status of the pre-commit hook:

bin/qmx hook:status

hook:uninstall

Remove the pre-commit hook:

bin/qmx hook:uninstall

# Restore the original hook from backup
bin/qmx hook:uninstall --restore-backup

rules

List all available rules with their descriptions and CLI options:

# List all rules
bin/qmx rules

# Filter by group
bin/qmx rules --group=complexity

Example output (for --group=complexity):

4 rules available

Complexity
  complexity.cognitive                     Checks cognitive complexity at method and class levels
    --cognitive-warning (--rule-opt=complexity.cognitive:callable.warning=...)
    --cognitive-error (--rule-opt=complexity.cognitive:callable.error=...)
    --cognitive-class-warning (--rule-opt=complexity.cognitive:class.max_warning=...)
    --cognitive-class-error (--rule-opt=complexity.cognitive:class.max_error=...)
  complexity.ccn                    Checks cyclomatic complexity at method and class levels
    --cyclomatic-warning (--rule-opt=complexity.ccn:callable.warning=...)
    --cyclomatic-error (--rule-opt=complexity.ccn:callable.error=...)
    --cyclomatic-class-warning (--rule-opt=complexity.ccn:class.max_warning=...)
    --cyclomatic-class-error (--rule-opt=complexity.ccn:class.max_error=...)
  ...

Usage: bin/qmx check --disable-rule=<name> | --only-rule=<name>
        bin/qmx check --rule-opt=<name>:<option>=<value>

Rules are grouped by category, and each CLI alias is listed with the long --rule-opt form it expands to. Default threshold values are not part of this output — see Default thresholds.