Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

claude-code-linter

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.

Setup

git clone https://github.com/borisdev/claude-code-linter && cd claude-code-linter
uv run python3 -m nobs.concepts.rule

That 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.

One example

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

Without the linter

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.

With the linter

`"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.

Point it at your own concepts

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.rule

Fork 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.

Why

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 lookedUNCHECKED 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.

What it does not do

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.

About

A semantic linter for Claude Code conversations, backed by your repo's concept type system

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages