---
title: Setting up AI features
description: What lawlint init does, which providers are supported, and where API keys are stored.
icon: settings
---

Linting is deterministic and offline. Two features are not — the soft-rule [AI
judge](/docs/guides/judge) and [`lawlint learn`](/docs/guides/learn) — and
neither does anything until you configure a model.

```sh
lawlint init
```

The walkthrough covers AI-model choice, whether the judge runs on every lint,
Markdown handling for stdin, and custom rule directories. It writes
`.lawlint/config.json`, plus an optional starter rules package under
`.lawlint/rules/`.

:::note
Until a model is configured, AI features exit with guidance instead of running.
Nothing is ever downloaded silently.
:::

## Hosted providers

The recommended path: better quality, no multi-gigabyte download.

| Provider | Model spec | Key |
| -------- | ---------- | --- |
| Anthropic *(recommended)* | `anthropic:<model>` | `ANTHROPIC_API_KEY` |
| OpenAI-compatible | `openai:<base-url>#<model>` | `OPENAI_API_KEY` |
| Azure AI Foundry | `foundry:<deployment>` | `AZURE_FOUNDRY_API_KEY` |

API keys go to a user-level credential file — `~/.lawlint/credentials`, mode
`0600` — or to the environment. **The environment wins.** Keys are never
written into your project, so `.lawlint/config.json` is safe to commit.

Once a key and a model are configured, the soft rules run on every lint,
including in directories with no project config. `--no-ai` skips them for a
run; `"judge": { "enabled": false }` turns them off for good.

Before 0.8 the credential file lived at `~/.config/lawlint/credentials`. It is
still read from there, and the next `lawlint init` moves it to `~/.lawlint/`
and reports the move.

## Keeping text on your machine

lawlint has no built-in model. If the document must not leave your machine —
privileged material, a client matter under an engagement term that forbids it —
run an OpenAI-compatible server yourself and point lawlint at it:

```sh
ollama serve && ollama pull qwen3:30b
lawlint init --ai openai:http://localhost:11434/v1#qwen3:30b
```

That spec needs **no API key**, and nothing but your own server ever sees the
text. Ollama, vLLM, llama.cpp and LM Studio all speak this protocol.

:::note[Judgement quality still depends on the model]
The soft rules ask for judgement a small model cannot make. lawlint used to
embed a 1.5B model for this; it was removed in 0.9 because it scored F1 0.111
on `empty-hedge` and 0.000 on `padded-elaboration`, failing 38 of 330 sections
outright. Serving a larger model yourself is both more private and far better
than that was — but a 1–3B model behind `openai:` will do no better. Prefer the
largest model your hardware serves comfortably, and consider lowering
`judge.contextChars` and setting `judge.perRule: false` for a small one.
:::

## Non-interactive setup

```sh
lawlint init --yes                          # accept defaults (hosted)
lawlint init --ai anthropic:claude-haiku-4-5-20251001
lawlint init --ai openai:http://localhost:11434/v1#qwen3:30b  # self-hosted
lawlint init --force                        # overwrite an existing config
```

| Prop | Type | Default | Description |
| - | - | - | - |
| `--yes?` | `flag` | - | Accept the default answer for every prompt. |
| `--force?` | `flag` | - | Overwrite an existing .lawlint/config.json. |
| `--ai?` | `string` | - | Model spec, skipping the catalog prompt (anthropic:<model>, openai:<base-url>#<model>, foundry:<deployment>). |

## What it writes

```jsonc
// .lawlint/config.json — every field optional, camelCase
{
  "ai": {
    "model": "anthropic:claude-haiku-4-5-20251001",
    "features": { "judge": "...", "learn": "..." },
  },
  "judge": { "enabled": false },
  "markdown": false,
  "ruleDirs": [".lawlint/rules"]
}
```

`ai.model` is the default for every AI feature; `ai.features` overrides it per
feature. The full schema is in
[Configuration](/docs/reference/configuration).

## Where config is found

The CLI walks up from the current directory looking for `.lawlint/config.json`,
falling back to the legacy `lawlint.config.json` at each level. The first
directory containing either is the project root, and relative `ruleDirs`
resolve against it.

A config file that exists but does not parse is a configuration error — exit
`2` — never a silent skip.
