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 |
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.
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.
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| `no-local-fakes-for-ports` | warn | test defines a fake for a port that has a generated mock |
119| `missing-generated-mock-for-port` | warn | port interface has no generated mock |
120| `prefer-generated-mocks` | warn | usecase/core test imports a concrete adapter |
121
122Type-leak rules look through pointers, slices, maps, channels, function signatures, inline structs, aliases, and generic type arguments.
123
124The config is strict: unknown keys and unknown rule names are errors.
125
126## Suppressing a finding
127
128Prefer config `allow` entries for whole files or directories. For one line, use an inline directive with a reason:
129
130```go
131type Rows interface {
132 //hexcheck:ignore no-infra-types-in-ports row type is part of the published contract
133 Next() sql.Row
134}
135```
136
137The 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`.
138
139## Standalone CLI
140
141```bash
142hexcheck init # write a starter .hexcheck.yaml from detected folders
143hexcheck ./... # check; config is discovered from the root upwards
144hexcheck -config .hexcheck.yaml -root . ./...
145```
146
147| Flag | Default | Meaning |
148| --- | --- | --- |
149| `-config` | discovered | path to `.hexcheck.yaml` |
150| `-root` | current dir | project root for config-relative paths |
151| `-module` | from `go.mod` | Go module path |
152| `-fail-on` | `error` | lowest severity that fails: `error` or `warn` |
153| `-json` | false | JSON output on stdout |
154| `-test` | true | also analyze test files |
155| `-version` | | print the version |
156
157Each finding is prefixed with its severity, e.g. `[warn] no-adapter-to-adapter-imports: ...`.
158
159Exit codes: `0` no failing findings, `1` load/config/analysis error, `2` usage error, `3` findings at or above `-fail-on`.
160
161## golangci-lint module plugin
162
163Build a custom golangci-lint binary using the module plugin system:
164
165```bash
166golangci-lint custom -c examples/custom-gcl.yml
167```
168
169Example builder config:
170
171```yaml
172version: v2.12.2
173name: hex-golangci-lint
174destination: ./bin
175plugins:
176 - module: github.com/bnema/hexcheck
177 import: github.com/bnema/hexcheck/golangci
178 version: v0.1.0
179```
180
181Enable it in a project:
182
183```yaml
184version: "2"
185linters:
186 enable:
187 - hexcheck
188 settings:
189 custom:
190 hexcheck:
191 type: module
192 description: Checks hexagonal architecture boundaries.
193 settings:
194 config: .hexcheck.yaml
195```
196
197golangci-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`.
198
199## Agent configuration guide
200
201[`SKILL.md`](SKILL.md) explains how an agent should configure `hexcheck` for a new repository, including non-standard layouts such as `core`/`boundaries`.
202
203## Local development
204
205```bash
206make test # go test -race
207make check # tidy, lint, test, and hexcheck on its own code
208HEXCHECK_SMOKE_REPO=/path/to/local/go/repo make smoke-local
209```