main ae76d4b

README.md RawHistoryFolder

hexcheck

hexcheck is a Go architecture linter for hexagonal and clean architecture boundaries.

It is built on golang.org/x/tools/go/analysis, runs as a standalone CLI, and can be embedded in a custom golangci-lint binary through the Module Plugin System.

What it checks

  • dependency direction between configurable architecture roles
  • framework or infrastructure types leaking into core/port APIs
  • suspicious business logic in adapters and entrypoints
  • missing generated mocks for port interfaces
  • local fakes or concrete adapters in tests where generated mocks should be used

Architecture roles

hexcheck does not require folders to be named domain, usecase, or adapters. A repository maps its own paths to roles in .hexcheck.yaml.

Supported roles:

  • core — domain/core business logic
  • usecase — application use cases and orchestration
  • ports — interfaces/contracts
  • adapter — infrastructure, persistence, external services, outbound adapters
  • entrypoint — CLI, HTTP handlers, UI entrypoints, app bootstrap
  • ignore — generated code, mocks, vendored paths

Quick config

version: 1
components:
  core:
    role: core
    paths:
      - internal/domain/**
      - internal/core/**
  usecases:
    role: usecase
    paths:
      - internal/application/usecase/**
      - internal/application/usecases/**
      - internal/usecase/**
      - internal/usecases/**
  ports:
    role: ports
    paths:
      - internal/application/port/**
      - internal/domain/repository/**
  adapters:
    role: adapter
    paths:
      - internal/infrastructure/**
      - internal/adapters/**
  entrypoints:
    role: entrypoint
    paths:
      - cmd/**
  generated:
    role: ignore
    paths:
      - '**/mocks/**'
      - '**/generated/**'
      - '**/*_templ.go'
      - '**/*_gen.go'

rules:
  no-adapter-imports-in-core: error
  no-usecase-imports-in-core: error
  no-infra-imports-in-usecase: error
  no-infra-imports-in-ports: error
  no-entrypoint-imports-in-adapter: error
  no-framework-types-in-core: error
  no-infra-types-in-ports: error
  no-adapter-to-adapter-imports: warn
  suspicious-business-logic-in-adapter: warn
  no-local-fakes-for-ports: warn
  missing-generated-mock-for-port: warn
  prefer-generated-mocks: warn

heuristics:
  businessLogicThreshold: 8
  businessLogicMinStrongSignals: 2
  businessLogicMinWeakSignals: 2
  businessLogicMaxFunctionNodes: 2000
  businessLogicMaxDiagnosticsPerPackage: 10
  businessLogicMode: audit # audit|ci
  businessLogicMinConfidence: medium # low|medium|high
  excludeTestFiles: true

mocking:
  generatedMockPaths:
    - internal/mocks/**
    - internal/application/mocks/**
    - internal/application/port/mocks/**
  generatedMockNamePatterns:
    - Mock{{Interface}}
    - '{{Interface}}Mock'

A fuller example lives in examples/hexcheck.yaml.

Business-logic diagnostics include a confidence level. audit mode reports findings at or above businessLogicMinConfidence; ci mode reports only high-confidence findings.

Rules

Rule Default Reports
no-adapter-imports-in-core error core imports an adapter or entrypoint
no-usecase-imports-in-core error core imports a usecase
no-infra-imports-in-usecase error usecase imports an adapter or entrypoint
no-infra-imports-in-ports error ports import an adapter or entrypoint
no-entrypoint-imports-in-adapter error adapter imports an entrypoint
no-framework-types-in-core error exported core API exposes an adapter or framework type
no-infra-types-in-ports error port interface method exposes an adapter or framework type
no-adapter-to-adapter-imports warn adapter imports another adapter component
suspicious-business-logic-in-adapter warn adapter or entrypoint function looks like business logic
business-logic-diagnostics-hidden warn package exceeds the business-logic diagnostic cap; reports the hidden count
unused-ignore-directive warn inline ignore rule suppresses no diagnostics in this run
no-local-fakes-for-ports warn test defines a fake for a port that has a generated mock
missing-generated-mock-for-port warn port interface has no generated mock
prefer-generated-mocks warn usecase/core test imports a concrete adapter

Type-leak rules look through pointers, slices, maps, channels, function signatures, inline structs, aliases, and generic type arguments.

Business-logic evidence treats domain calls and mutations as weak signals. Field-by-field construction of fresh local domain values is not mutation evidence. Domain predicates (methods or package-level boolean functions) driving decisions, numeric domain operands in ordering comparisons, guarded domain errors (other than common storage sentinels), and domain-derived set membership (including comma-ok checks) provide stronger evidence with branching. String/Error formatting and IsZero checks alone do not. Business keywords need decision context rather than merely a domain-typed parameter (except explicit computation/scoring or validation with mutation). The per-package cap applies after inline suppression; one business-logic-diagnostics-hidden warning states how many additional findings it hides.

The config is strict: unknown keys and unknown rule names are errors.

Suppressing a finding

Prefer config allow entries for whole files or directories. For one line, use an inline directive with a reason:

type Rows interface {
	//hexcheck:ignore no-infra-types-in-ports row type is part of the published contract
	Next() sql.Row
}

The directive covers its own line and the next one. It accepts a comma-separated rule list. A directive without a reason, or with an unknown rule, is reported as invalid-ignore-directive. Each listed rule that suppresses nothing in the current run produces unused-ignore-directive (warn by default), including when its rule is disabled or excluded. Business-logic suppression usage is tracked even beyond the package reporting cap. Unclassified, ignored, and generated files are not checked for unused directives.

Standalone CLI

hexcheck init              # write a starter .hexcheck.yaml from detected folders
hexcheck ./...             # check; config is discovered from the root upwards
hexcheck -config .hexcheck.yaml -root . ./...
Flag Default Meaning
-config discovered path to .hexcheck.yaml
-root current dir project root for config-relative paths
-module from go.mod Go module path
-fail-on error lowest severity that fails: error or warn
-json false JSON output on stdout
-test true also analyze test files
-version print the version

Each finding is prefixed with its severity, e.g. [warn] no-adapter-to-adapter-imports: ....

Exit codes: 0 no failing findings, 1 load/config/analysis error, 2 usage error, 3 findings at or above -fail-on.

golangci-lint module plugin

Build a custom golangci-lint binary using the module plugin system:

golangci-lint custom -c examples/custom-gcl.yml

Example builder config:

version: v2.12.2
name: hex-golangci-lint
destination: ./bin
plugins:
  - module: github.com/bnema/hexcheck
    import: github.com/bnema/hexcheck/golangci
    version: v0.1.0

Enable it in a project:

version: "2"
linters:
  enable:
    - hexcheck
  settings:
    custom:
      hexcheck:
        type: module
        description: Checks hexagonal architecture boundaries.
        settings:
          config: .hexcheck.yaml

golangci-lint reports every hexcheck finding as an issue, whatever its configured severity. To fail only on error rules, set the warn rules to off in .hexcheck.yaml, or exclude issues whose text matches ^\[warn\] with linters.exclusions.rules.

Agent configuration guide

SKILL.md explains how an agent should configure hexcheck for a new repository, including non-standard layouts such as core/boundaries.

Local development

make test        # go test -race
make check       # tidy, lint, test, and hexcheck on its own code
HEXCHECK_SMOKE_REPO=/path/to/local/go/repo make smoke-local
source
1# hexcheck
2
3`hexcheck` is a Go architecture linter for hexagonal and clean architecture boundaries.
4
5It is built on `golang.org/x/tools/go/analysis`, runs as a standalone CLI, and can be embedded in a custom `golangci-lint` binary through the Module Plugin System.
6
7## What it checks
8
9- dependency direction between configurable architecture roles
10- framework or infrastructure types leaking into core/port APIs
11- suspicious business logic in adapters and entrypoints
12- missing generated mocks for port interfaces
13- local fakes or concrete adapters in tests where generated mocks should be used
14
15## Architecture roles
16
17`hexcheck` does not require folders to be named `domain`, `usecase`, or `adapters`. A repository maps its own paths to roles in `.hexcheck.yaml`.
18
19Supported roles:
20
21- `core` — domain/core business logic
22- `usecase` — application use cases and orchestration
23- `ports` — interfaces/contracts
24- `adapter` — infrastructure, persistence, external services, outbound adapters
25- `entrypoint` — CLI, HTTP handlers, UI entrypoints, app bootstrap
26- `ignore` — generated code, mocks, vendored paths
27
28## Quick config
29
30```yaml
31version: 1
32components:
33  core:
34    role: core
35    paths:
36      - internal/domain/**
37      - internal/core/**
38  usecases:
39    role: usecase
40    paths:
41      - internal/application/usecase/**
42      - internal/application/usecases/**
43      - internal/usecase/**
44      - internal/usecases/**
45  ports:
46    role: ports
47    paths:
48      - internal/application/port/**
49      - internal/domain/repository/**
50  adapters:
51    role: adapter
52    paths:
53      - internal/infrastructure/**
54      - internal/adapters/**
55  entrypoints:
56    role: entrypoint
57    paths:
58      - cmd/**
59  generated:
60    role: ignore
61    paths:
62      - '**/mocks/**'
63      - '**/generated/**'
64      - '**/*_templ.go'
65      - '**/*_gen.go'
66
67rules:
68  no-adapter-imports-in-core: error
69  no-usecase-imports-in-core: error
70  no-infra-imports-in-usecase: error
71  no-infra-imports-in-ports: error
72  no-entrypoint-imports-in-adapter: error
73  no-framework-types-in-core: error
74  no-infra-types-in-ports: error
75  no-adapter-to-adapter-imports: warn
76  suspicious-business-logic-in-adapter: warn
77  no-local-fakes-for-ports: warn
78  missing-generated-mock-for-port: warn
79  prefer-generated-mocks: warn
80
81heuristics:
82  businessLogicThreshold: 8
83  businessLogicMinStrongSignals: 2
84  businessLogicMinWeakSignals: 2
85  businessLogicMaxFunctionNodes: 2000
86  businessLogicMaxDiagnosticsPerPackage: 10
87  businessLogicMode: audit # audit|ci
88  businessLogicMinConfidence: medium # low|medium|high
89  excludeTestFiles: true
90
91mocking:
92  generatedMockPaths:
93    - internal/mocks/**
94    - internal/application/mocks/**
95    - internal/application/port/mocks/**
96  generatedMockNamePatterns:
97    - Mock{{Interface}}
98    - '{{Interface}}Mock'
99```
100
101A fuller example lives in [`examples/hexcheck.yaml`](examples/hexcheck.yaml).
102
103Business-logic diagnostics include a confidence level. `audit` mode reports findings at or above `businessLogicMinConfidence`; `ci` mode reports only high-confidence findings.
104
105## Rules
106
107| Rule | Default | Reports |
108| --- | --- | --- |
109| `no-adapter-imports-in-core` | error | core imports an adapter or entrypoint |
110| `no-usecase-imports-in-core` | error | core imports a usecase |
111| `no-infra-imports-in-usecase` | error | usecase imports an adapter or entrypoint |
112| `no-infra-imports-in-ports` | error | ports import an adapter or entrypoint |
113| `no-entrypoint-imports-in-adapter` | error | adapter imports an entrypoint |
114| `no-framework-types-in-core` | error | exported core API exposes an adapter or framework type |
115| `no-infra-types-in-ports` | error | port interface method exposes an adapter or framework type |
116| `no-adapter-to-adapter-imports` | warn | adapter imports another adapter component |
117| `suspicious-business-logic-in-adapter` | warn | adapter or entrypoint function looks like business logic |
118| `business-logic-diagnostics-hidden` | warn | package exceeds the business-logic diagnostic cap; reports the hidden count |
119| `unused-ignore-directive` | warn | inline ignore rule suppresses no diagnostics in this run |
120| `no-local-fakes-for-ports` | warn | test defines a fake for a port that has a generated mock |
121| `missing-generated-mock-for-port` | warn | port interface has no generated mock |
122| `prefer-generated-mocks` | warn | usecase/core test imports a concrete adapter |
123
124Type-leak rules look through pointers, slices, maps, channels, function signatures, inline structs, aliases, and generic type arguments.
125
126Business-logic evidence treats domain calls and mutations as weak signals. Field-by-field construction of fresh local domain values is not mutation evidence. Domain predicates (methods or package-level boolean functions) driving decisions, numeric domain operands in ordering comparisons, guarded domain errors (other than common storage sentinels), and domain-derived set membership (including comma-ok checks) provide stronger evidence with branching. `String`/`Error` formatting and `IsZero` checks alone do not. Business keywords need decision context rather than merely a domain-typed parameter (except explicit computation/scoring or validation with mutation). The per-package cap applies after inline suppression; one `business-logic-diagnostics-hidden` warning states how many additional findings it hides.
127
128The config is strict: unknown keys and unknown rule names are errors.
129
130## Suppressing a finding
131
132Prefer config `allow` entries for whole files or directories. For one line, use an inline directive with a reason:
133
134```go
135type Rows interface {
136	//hexcheck:ignore no-infra-types-in-ports row type is part of the published contract
137	Next() sql.Row
138}
139```
140
141The directive covers its own line and the next one. It accepts a comma-separated rule list. A directive without a reason, or with an unknown rule, is reported as `invalid-ignore-directive`. Each listed rule that suppresses nothing in the current run produces `unused-ignore-directive` (warn by default), including when its rule is disabled or excluded. Business-logic suppression usage is tracked even beyond the package reporting cap. Unclassified, ignored, and generated files are not checked for unused directives.
142
143## Standalone CLI
144
145```bash
146hexcheck init              # write a starter .hexcheck.yaml from detected folders
147hexcheck ./...             # check; config is discovered from the root upwards
148hexcheck -config .hexcheck.yaml -root . ./...
149```
150
151| Flag | Default | Meaning |
152| --- | --- | --- |
153| `-config` | discovered | path to `.hexcheck.yaml` |
154| `-root` | current dir | project root for config-relative paths |
155| `-module` | from `go.mod` | Go module path |
156| `-fail-on` | `error` | lowest severity that fails: `error` or `warn` |
157| `-json` | false | JSON output on stdout |
158| `-test` | true | also analyze test files |
159| `-version` | | print the version |
160
161Each finding is prefixed with its severity, e.g. `[warn] no-adapter-to-adapter-imports: ...`.
162
163Exit codes: `0` no failing findings, `1` load/config/analysis error, `2` usage error, `3` findings at or above `-fail-on`.
164
165## golangci-lint module plugin
166
167Build a custom golangci-lint binary using the module plugin system:
168
169```bash
170golangci-lint custom -c examples/custom-gcl.yml
171```
172
173Example builder config:
174
175```yaml
176version: v2.12.2
177name: hex-golangci-lint
178destination: ./bin
179plugins:
180  - module: github.com/bnema/hexcheck
181    import: github.com/bnema/hexcheck/golangci
182    version: v0.1.0
183```
184
185Enable it in a project:
186
187```yaml
188version: "2"
189linters:
190  enable:
191    - hexcheck
192  settings:
193    custom:
194      hexcheck:
195        type: module
196        description: Checks hexagonal architecture boundaries.
197        settings:
198          config: .hexcheck.yaml
199```
200
201golangci-lint reports every hexcheck finding as an issue, whatever its configured severity. To fail only on `error` rules, set the warn rules to `off` in `.hexcheck.yaml`, or exclude issues whose text matches `^\[warn\]` with `linters.exclusions.rules`.
202
203## Agent configuration guide
204
205[`SKILL.md`](SKILL.md) explains how an agent should configure `hexcheck` for a new repository, including non-standard layouts such as `core`/`boundaries`.
206
207## Local development
208
209```bash
210make test        # go test -race
211make check       # tidy, lint, test, and hexcheck on its own code
212HEXCHECK_SMOKE_REPO=/path/to/local/go/repo make smoke-local
213```

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 5

↑↓ moveenter openesc close