A semantic linter for Claude Code conversations, backed by your repo's concept type system.
Free-form English is fine. But when a word in your domain has two meanings, an agent should say so instead of picking one and building.
git clone https://github.com/borisdev/claude-code-linter && cd claude-code-linter
uv run python3 -m nobs.concepts.ruleThat writes ~/.claude/rules/domain-concept-interpreter.md, which Claude Code loads at the start of
every session — in every repo on the machine. Declare a Concept anywhere, rerun the command,
and the rule an agent reads changes with it.
Keep it fresh with a SessionStart hook, so a stale rule survives at most one session:
// ~/.claude/settings.json
{"hooks": {"SessionStart": [{"hooks": [
{"type": "command", "command": "/path/to/regen-concept-rule.py"}]}]}}The hook fails silently by design — a convenience must never stop a session from starting.
A repo with a declared Step in src/acme/steps.py and two real .github/workflows/*.yml.
Both senses of the word are physically present. The directive names neither:
make me a workflow where input is string person name and output is
"hello <person name>"— you figure out the code that goes into the middle step
It picks a meaning silently. Here it invented a third one — a new Workflow class — in a repo
that already had .github/workflows/ sitting next to it:
**New file `src/acme/workflow.py`** — the missing chain concept:
```python
@dataclass(frozen=True)
class Workflow:
"""An ordered chain of Steps, threaded output-to-input."""
name: str
steps: Sequence[Step]
```
Good code. Nothing in it records that "workflow" already meant something else in this repo, or that a choice was made at all.
`"workflow"` is AMBIGUOUS — and both senses are physically present in this repo, so I won't guess.
```
"workflow" is AMBIGUOUS
candidates : DataFlow (a declared graph of typed steps — src/acme/steps.py)
GitHub Actions workflow (CI config triggered by repo events — .github/workflows/)
why it matters : a DataFlow is a typed declaration with no trigger and no scheduler;
a GitHub workflow is a trigger and a scheduler with no declared types.
Identical word, disjoint concepts.
need : which one — or a reason the distinction doesn't apply here
```
Your phrasing leans hard toward **DataFlow** — input type, output type, and a Step between them.
But `.github/workflows/nightly.yml` already exists, so "make me a workflow" plausibly means
"add another one of those." I'd rather ask than write the wrong file.
Same request, same repo, same model. The difference is that the flag arrives in a fixed shape, with the candidates named and the files cited — so a human can answer it in one word, and a tool downstream can parse it.
The rule describes a role; the concepts are whatever package is installed. Four environment variables, all optional:
| default | ||
|---|---|---|
DCI_SOURCE_PACKAGE |
nobs-concepts |
named in the generated rule |
DCI_SOURCE_URL |
this repo | linked from the generated rule |
DCI_RULE_PATH |
~/.claude/rules/domain-concept-interpreter.md |
where it is written |
DCI_GENERATE_CMD |
uv run python3 -m nobs.concepts.rule |
what the file tells a reader to run |
DCI_SOURCE_PACKAGE=acme-concepts \
DCI_SOURCE_URL=https://github.com/acme/concepts \
uv run python3 -m nobs.concepts.ruleFork it, subclass Concept with your own domain terms, and the generated rule names your vocabulary
rather than ours.
DCI_RULE_PATH can point anywhere, including into a repo. Don't. The default is outside every
repo on purpose: a rule committed to a branch stops existing when anything else is checked out, and
nothing reports it.
A rule an agent reads is a suggestion. A type it cannot express is a constraint.
A rule saying "provenance is a field, not a vibe" sat in an agent's context while the code shipped a constant contradicting it — twice in one afternoon, once understating and once, while fixing that, overstating. Both compiled. Both passed review.
When a concept has no type, it gets expressed as a constant — and a constant cannot be wrong out loud.
Three things follow. The rule is derived — declaring a Concept subclass is the registration,
so it cannot describe concepts that no longer exist. It lives outside any repo, because a rule
committed to a branch stops existing when anything else is checked out and nothing reports it.
Silence means nobody looked — UNCHECKED is the default relation, and anything else requires a
note or construction fails.
Prior art: skos · SSSOM (predicate_modifier: Not) · OBO · FHIR ElementDefinition.mapping
(cmd-1) · LinkML · Evans' Ubiquitous Language, made machine-checkable.
Emitting a semantic error instead of guessing is agent behaviour, not a check — it holds only while the rule is being read. Everything else here fails a test when violated.
The example above is one real request run both ways. It is an illustration, not a measurement:
Claude often notices such collisions unprompted, and what the rule reliably changes is the shape
of the flag, not whether one appears. evals/ runs it if you want to look.