argotdocs

Configure

A committed argot.toml configures Argot, while a committed fit snapshot carries its learned voice — excludes, generated/data detection, rule severity, accepted hits, and local overrides.

argot is a statistical linter: it will sometimes learn from the wrong files, or flag a line you meant to write. The team-owned configuration lives in argot.toml; its reviewed learned baseline lives alongside it in the committed .argot/ fit snapshot.

argot init writes an argot.toml at your repo root with the effective defaults spelled out, so nothing is hidden. It has four sections:

Inline # argot: ignore comments are a fourth surface (per-line, in the source itself), and a gitignored argot.local.toml lets any one dev layer personal overrides on top. A reference table at the end lists every file argot writes and whether it’s committed.

[exclude] — set the scope

Location: the [exclude] section of <repo-root>/argot.toml. Commit it so the whole team and CI share the same scope.

[exclude] has three gitignore-style pattern lists that differ only in how a match is treated:

[exclude]
# The built-in argot:recommended set — dropped SILENTLY (as if absent). init
# writes the full list; edit it freely (see below).
recommended = [
  "test*/", "docs/", "build/", "dist/",   # … and the rest of the defaults
  "conftest.py", "*.test.*", "*.config.*",
]

# Your repo-specific excludes — still SCORED, but their hits are dropped and
# counted on stderr, so every exclusion stays auditable. One pattern per entry,
# each with a trailing `# reason` comment.
paths = [
  "legacy",                 # bare name → a dir/file named `legacy` at ANY depth
  "vendor/",                # trailing slash → that directory only, never a `vendor` file
  "/generated.py",          # leading slash → anchored to the repo root (this file only)
  "src/proto/",             # an interior slash also anchors to the root
  "src/*.gen.ts",           # `*` matches within ONE segment (does not cross `/`)
  "**/snapshots/**",        # `**` spans directories, at any depth
  "*.min.js",               # bundled/minified output, anywhere
  "!keep.min.js",           # `!` re-includes a path an earlier pattern excluded (last match wins)
]

# Paths that are CHECKED like any other, but never shape the voice. argot learns
# their dependency vocabulary, not their style. Defaults to your tests.
check-only = [
  "test/", "tests/", "__tests__/", "benchmarks/",
  "test_*", "*.test.*", "*.spec.*",
]

The pattern rules (shared by both lists), precisely:

One scope decision argot makes for you: gitignored files never shape the voice. The fit reads files from disk, but anything .gitignore covers and git doesn’t track — editor-history trees like .history/, local worktrees, build output — is dropped from the training corpus automatically. Only committed code needs an [exclude] entry.

Don’t hand-guess the directories to exclude — argot init --suggest surfaces them with evidence, on two grounds: directories whose files are generated or data-heavy, and directories your repository stores but never writes. The second is the one that hides in plain sight — a vendored library, a forked upstream copy, a machine-translated binding. That code carries no marker and reads like anything else, but its history gives it away: it arrived in one commit and the repo has barely touched it since, while the code around it is edited constantly. argot reports the ratio and lets you decide. See Setup.

check-only — checked, but never teaches

The third list answers a question the other two can’t: judge this code, but don’t learn from it.

Tests are the reason it exists. A test file’s phrasing is deliberately unlike production code — arrange/act/assert, fixtures, mocks — so learning style from tests dilutes the voice. But a test’s dependencies are real vocabulary: a harness or a clock-control library that only ever appears in tests is not a foreign dependency, it’s how this repo tests.

A path in check-only is:

By default this list changes nothing, because the same paths are also in recommended, which drops them from checking entirely. To turn your tests into a guarded, non-teaching scope, remove the test patterns from recommended and leave them here:

[exclude]
recommended = [
  "docs/", "build/", "dist/",     # … the rest of the defaults, minus:
  # "test*/", "__tests__/", "test_*", "*.test.*", "*.spec.*",
]
check-only = ["test*/", "__tests__/", "test_*", "*.test.*", "*.spec.*"]

Re-run argot fit afterwards — the test vocabulary is learned at fit time. It is a membership set, not a distribution, so it needs no minimum corpus: three test files are enough for the three imports they name, and a repo with no tests gets an empty set and today’s behaviour exactly.

A bare pattern here names a file (test_* matches test_helpers.py, not a test_util/ directory of production support code); add a trailing slash for a directory (tests/), or use a path shape (**/__tests__/**).

init writes the full built-in argot:recommended set into recommended — the directories and files that are almost never part of a repo’s authored voice:

Because it’s a plain list, you tune it per entry: remove "test*/" to bring tests back into scope (check-only above still keeps them out of the voice), add "vendor/" to drop a vendored tree silently, or set recommended = [] to disable the set entirely and rely on paths alone. (Detecting generated and data files is a separate concern — see [detect] next — and stays on regardless.)

[detect] — how argot spots generated & data files

Location: the [detect] section of argot.toml. These were hardcoded heuristics; now they’re yours to tune. argot init writes today’s defaults out in full.

[detect]
# Share of a file's non-blank lines that must be static data literals before the
# file counts as data, not authored voice (locale tables, fixture arrays).
data-threshold = 0.65

# A file whose head comments contain any of these (case-insensitive) is treated
# as generated and kept out of the voice model. Trim ones you don't want, or add
# your own codegen banner.
generated-markers = [
  "@generated",
  "auto-generated",
  "do not edit",
  "generated by protoc",
  "generated by swagger",
  "generated by openapi",
  # … init writes the full default list (protobuf/gRPC, OpenAPI, ORMs,
  #    binding/codegen tools, bison/flex/moc, and more) …
]

The same resolved [exclude] + [detect] govern what argot learns from, calibrates on, and checks — they never drift apart.

[rules] — rule severities

Location: the [rules] section of argot.toml. Every finding argot emits belongs to one of twelve stable rules, in five groups:

GroupRules
voiceforeign-import · unfamiliar-callee · rare-tokens · convention · superseded
semanticredundant · misplaced
architecturelayering
integritytest-deleted · test-disabled · test-weakened
governancerule-tampered — the guardrail’s self-protection (see Locked rules)

A repo can add a sixth group, custom: repo-local rules dropped under .argot/rules/, discovered fresh on every run and configured exactly like the built-ins — see Custom rules.

Each rule carries a severity: error (reported, fails argot check with exit 1), warn (reported, does not fail the check), or off (the rule does not run). Everything defaults to error (except test-weakened, which ships warn — reported, never failing the check) — argot gates on the rest until you say otherwise. Keys are rule names or whole group names; a rule-specific entry always beats its group entry:

[rules]
misplaced = "warn"    # report placement findings, but don't fail the check
semantic  = "off"     # …or disable the whole embedding-based group
                      # (with the group off, fit/check skip the embedding pass
                      #  and never build or load the semantic index)

Precedence, ascending: built-in defaults → argot.tomlargot.local.toml → CLI (argot check --rule <name|group>=<severity>, repeatable). An unknown rule name or severity is a warning on stderr, never a failed run.

The optional Claude Code pre-write hook has no separate configuration format. It reads this same effective [rules] resolution: a foreign-import rule set to off does not ask, while warn and error retain the hook’s non-blocking prompt. Configure the hook itself through the plugin/setup flow; configure its rule policy here.

Two companions on the CLI:

Severity is about the exit code. It’s distinct from the confidence tier (unusual / suspicious / foreign) a hit displays with, which grades the evidence — see Reading the output.

Path-scoping a rule

Any rule — built-in or custom — can be limited to (or kept out of) a set of paths, from the [rules] inline-table form:

[rules]
layering   = { severity = "error", include = ["src/**"] }   # only enforce layering under src/
convention = { exclude = ["legacy/**", "vendor/**"] }        # everywhere except these trees
custom     = { include = ["packages/api/**"] }               # scope the whole custom group

include keeps only findings whose file matches; exclude drops findings whose file matches (and wins over include). Both use the same glob dialect as [[mute]].path (*/** cross /). A rule-specific entry beats its group’s. This is a filter on findings — the rule still runs, its out-of-scope hits are simply dropped — so it composes with everything else here.

This is distinct from a custom rule’s manifest include/exclude (see Custom rules → which files a rule runs on): the manifest decides which files the rule ever runs on (and is the only way to reach files argot doesn’t score, like .env); the [rules] scope here is the repo owner narrowing any rule’s findings after the fact. rule-tampered is never path-scoped away.

Locked rules — the agent can’t turn off the alarm

Opt-in strict mode, per rule or per group, from the committed argot.toml only:

[rules]
layering = { severity = "error", locked = true }
custom   = { severity = "error", locked = true }   # lock every repo-local rule

A locked rule is frozen against every runtime relaxation an AI agent might reach for when a check fails:

This is tamper-evidence, not tamper-proofing — the same philosophy as the integrity rules: an agent can touch the alarm, but touching the alarm is the alarm. The one quiet path to relaxing a locked rule is a committed argot.toml diff that a human reviews.

Inline comments — mute one line

Locked rules ignore inline ignores. A # argot: ignore comment has no effect on a rule locked with locked = true — see Locked rules.

Location: in the source file itself, on the line above the code (or around the block) you’re excusing. The comment token is the language’s own line comment: # for Python and Ruby, // for everything else (TypeScript/JavaScript, Go, Rust, C, C++, Java, C#, PHP, Pascal) — the language adapter supplies it.

When an exception is deliberate, say so where it lives. This block uses every form:

# argot: ignore-next-line — vendored oddity we keep on purpose
weird_call()

# argot: ignore-next-line rule=foreign-import — deliberate one-off dependency
import boto3

# argot: ignore-block-start — legacy shim, do not judge this region
legacy_thing()
more_legacy()
# argot: ignore-block-end

Adopting argot on an existing codebase with a wall of findings? argot check --add-ignores inserts one of these comments above every current finding (tagged baselined by --add-ignores; review) so the first run goes green and each acceptance stays a greppable, reviewable line — the same move as ruff --add-noqa.

The // languages are identical, just with a different token:

// argot: ignore-next-line rule=rare-tokens - generated glue, keep as-is
oddPhrasing();

The rules:

[[mute]] — accept a specific hit

A [[mute]] can’t silence a locked rule — the entry is refused, and adding one that targets a locked rule is reported by rule-tampered. See Locked rules.

Location: the [[mute]] tables in argot.toml. argot mute appends one; you can also hand-edit it. It’s committed, so a mute is a shared, reviewable audit trail that a teammate and CI inherit.

There are two forms, and picking the wrong one is how a repo ends up with a committed mute per file.

Per hit — by hash. Every hit prints a stable [hash]:

argot mute a1b2c3d4e5f6 --reason "adopting axios repo-wide"
argot mute a1b2c3d4e5f6 --reason "temporary shim" --expires 30d

A hash pins that hit and no other. The identical finding in a sibling file has its own hash and stays flagged — which is what you want for a genuine one-off, and not what you want for a standing decision.

Standing — by path. When the decision covers a tree, name the tree:

argot mute --path 'src/legacy/**' --rule foreign-import --reason "migrating in Q3"
argot mute --path 'vendor/**' --reason "vendored upstream" --expires 90d

--path takes the same globs as [exclude].paths; --rule narrows it to one rule or group (validated against your repo’s full vocabulary, custom rules included, so a typo is refused rather than silently ignored). It covers every future hit under the glob and needs no prior check run.

--reason records why (recommended everywhere argot reports a hit); --expires takes a day count (30d, or a bare 30), which argot resolves to a calendar date in the file. The hash form reads the last check run to learn which file the hash belongs to, so run argot check first. Either append is a format-preserving edit, so your hand-written sections and comments are never rewritten.

Review and prune what you’ve muted:

argot list-mutes            # every active suppression, across all surfaces
argot review-mutes          # report hash-scoped mutes whose file is gone
argot review-mutes --prune  # …and rewrite the [[mute]] tables to drop the dead ones

review-mutes flags a hash-scoped mute as dead once the file it names no longer exists (in the working tree or HEAD) — the point at which it can never fire again. --prune drops only those; a mute guarding a file you still have is always kept.

The [[mute]] format

Each [[mute]] is a TOML table. This example shows every field a rule can carry:

[[mute]]
path = "src/vendored/**"     # REQUIRED — glob (fnmatch; `*` crosses `/`)
rule = "rare-tokens"         # optional — a rule or group name (e.g. "semantic"); scopes the mute to that signal
hash = "a1b2c3d4e5f6"        # optional — pin to one specific hit (argot mute writes this)
expires = "2026-12-31"       # optional — YYYY-MM-DD; ignored ON/AFTER this date
reason = "vendored upstream" # REQUIRED — why (surfaces in list-mutes and code review)

# path + reason alone is the standing form — it covers every hit under the glob.
# `argot mute --path` writes exactly this; hand-editing does the same thing:
[[mute]]
path = "generated/**"
reason = "protobuf stubs, never our voice"

Only path and reason are mandatory. An entry with a missing/invalid field (no path, no reason, an unknown rule, a malformed expires) is skipped with a warning on stderr — one bad entry never voids the file. An entry with expires in the past is treated as expired and ignored; one expiring today is still active.

[[migration]] — declare a migration

This is the config surface for the superseded rule’s declared side. argot also mines the same fact from your accepted history — no config needed — and the rule itself (evidence, severity, the leftover report) is documented end to end in What it catches.

A repo mid-migration has two voices, and a model trained on history alone only hears the loud (old) one until enough commits accumulate. [[migration]] states the replacement yourself, and it’s enforced from the very next check — no refit needed:

[[migration]]
from = "moment"
to = "date-fns"
reason = "Q2 date-handling refactor"
# kind = "callee"   # optional: "import" (default) or "callee"

argot.local.toml — personal overrides

Want a scratch directory excluded on your machine only, without touching the committed config? Put it in argot.local.toml at the repo root — init gitignores it for you. It deep-merges over argot.toml: scalars there win, list entries (paths, generated-markers, [[mute]], [[migration]]) append, and [rules] entries override the committed ones key by key (the CLI’s --rule still beats both).

# argot.local.toml — gitignored, personal, uncommitted.
[exclude]
paths = ["scratch/", "experiments/"]   # appended to the committed excludes

[fit] — snapshot freshness

This is the config surface; the full mechanism — verdict, health record, drift, staleness — is one page: Health & freshness.

A fit is a committed snapshot: as the repo evolves, enough accepted source and layout change can make the learned model stop representing the repository. Argot measures that change directly by comparing the final accepted tree with the tree at fit_sha. It does not use commit count or elapsed time as its default policy. Ten tiny commits, a hundred docs commits, and a change later reverted do not create artificial maintenance.

The deterministic adaptive verdict is fresh, watch, recommended, or strongly_recommended. watch is visible in status and MCP but stays out of routine checks and CI annotations. A recommendation remains advisory: run the argot-refresh skill locally, review its scope/mute proposal, then fit and commit the .argot/ diff deliberately.

“Accepted” is the load-bearing word: staleness is measured at the merge-base with your default branch. A feature branch’s own commits and uncommitted edits never age the baseline it is judged against. Only accepted in-scope source, function, and layout changes shape the adaptive measurement. init writes the accepted-history anchor explicitly:

[fit]
refresh-from = "default-branch"   # auto-detected — see below

There is no default commit threshold. A team may add a deliberate backstop if its own process requires one; it supplements adaptive drift rather than replacing it:

[fit]
refresh-after = 250               # optional; absent by default

refresh-from is a mode, not a blank to fill in: "default-branch" auto-detects your trunk (origin/HEAD, else main, else master), so main- and master-only repos both just work. Name a branch ("develop") when your trunk is non-standard. "current-branch" is available only when branch fits are an intentional policy.

Freshness is separate from calibration drift — a new gen/ dir or a vendored SDK appearing in the tree, or an argot.toml edit the model hasn’t absorbed yet. argot watches for both itself, and you never have to guess when to recalibrate:

A well-configured repo stays quiet on all of it.

Mutes and [rules] are deliberately not part of the fit fingerprint: they change which findings are shown, not what the model learns. argot-refresh reviews them in the same maintenance pass because stale policy can hide useful evidence, but mute-only cleanup does not force an otherwise unnecessary fit.

[update] — the passive update notice

argot prints at most one dim line a day on stderr when a newer release exists (argot X is available — run 'argot update'). It’s a single cached GET to https://argot.tmonier.com/version.json at most once per 24 hours, refreshed in a detached process so it never adds latency, and it’s automatically silent in CI, on a non-tty, under --quiet, in machine formats, and when ARGOT_OFFLINE is set. To opt out entirely:

[update]
check = false          # or set ARGOT_UPDATE_CHECK=0 in the environment

This version check is the only network call argot ever makes on its own. The embedding model ships inside the binary, so there is nothing to fetch: turn the check off and argot never touches the network at all.

Environment variables

VariableEffect
ARGOT_OFFLINE=1Never touch the network. Every rule still runs, the semantic layer included — the embedder is compiled in.
ARGOT_UPDATE_CHECK=0Disable the passive update notice (same as [update] check = false).
ARGOT_THREADS=<n>Cap argot’s worker threads, so a fit doesn’t saturate a shared machine.

Which files argot writes, and where

argot.toml and Argot’s fit snapshot are meant to be committed. init/fit write a selective .argot/.gitignore: it keeps caches and one-run state local, but leaves every artifact a full argot check needs visible to Git. CI consumes the base commit’s snapshot and never fits or rebuilds it.

FileWritten byWhat it isCommitted?
argot.toml (repo root)init / argot mute / by handConfig — [exclude], [detect], [rules], [update], and [[mute]].Yes — commit it.
argot.local.toml (repo root)youPersonal overrides, merged on top.No — gitignored.
.argot/scorer-config.jsonfit / initThe fitted voice model: calibrated threshold(s) + scorer config.Yes — fit snapshot.
.argot/semantic-index.jsonfit / initThe per-repo code-embedding index for reinvention/placement.Yes, when produced — status names an intentional abstention.
.argot/layering.jsonfit / initThe module-dependency graph the layering rule checks.Yes, when produced — status names an intentional abstention.
.argot/integrity.jsonfit / initPer-repo learned gates for the test-integrity rules.Yes, when produced — status names an intentional abstention.
.argot/manifest.jsonfit / initVersioned, hashed record of what was learned.Yes — provenance.
.argot/health.jsonfit / initFit SHA, config fingerprint, drift candidates, and compact corpus denominators for adaptive freshness.Yes — freshness.
.argot/repo-corpus.txtfit / initThe source files counted into the repo distribution.No — rebuildable.
.argot/generic-baseline.jsonfit / initThe bundled generic-baseline reference.Yes — required by voice scoring.
.argot/dataset.jsonlextractRaw training dataset — one record per hunk. The check path doesn’t need it.No — rebuildable.
.argot/last-check.jsoncheckCache of the last check’s hits, so argot mute <hash> can resolve.No — rebuildable.
.argot/.gitignorefit / initSelectively ignores runtime state while exposing snapshot files.Yes — commit it.
.argot/rules/<name>/argot-write-rule / argot-suggest-rules / by handYour custom rulesrule.toml + check.rhai, authored not generated.Yes — ordinary tracked source.

Outside the repo, argot keeps exactly two user-level locations:

LocationWhat it is
~/.argot/settings.jsonThe global repo registry — every repo argot has fitted or checked, powering argot list/status (and telling argot uninstall where artifacts live).
~/.cache/argot/The embedding cache and the once-a-day update-check state (update-check.json) — see the callout below. Curl installs also leave a receipt in ~/.config/argot/.

To remove all of it — every repo’s artifacts, the cache, the registry, and the binary — run argot uninstall: it shows the full inventory first and never touches git-tracked files or your authored custom rules under .argot/rules/.

Consumer repositories should commit their fitted artifacts. The embedded model is separate: it is compiled into the Argot binary, while the snapshot records only repository-specific learned data that CI needs to score consistently.

There is no model to install. The semantic layer’s embedder is a 15.6 MB static table compiled into the argot binary, so a fit needs no download, no cache to warm and no network — it works air-gapped on the first run. What argot does keep in ~/.cache/argot/ is an embedding cache: a content-addressed store keyed by function text, so a re-fit or an audit re-embeds only what changed. It is a pure accelerator — argot cache clear reclaims it and the next local fit rebuilds it. The .argot/semantic-index.json the fit produces is committed as part of the snapshot.

Which to reach for

You want to…Use
Stop argot learning from a directory[exclude].paths in argot.toml
Learn from one recommended dirremove its entry from [exclude].recommended
Turn the recommended set off entirelyrecommended = [] in [exclude]
Catch a repo’s own codegen banneradd it to [detect].generated-markers
Tune how aggressively data files are dropped[detect].data-threshold
Make a rule report-only, or turn it off[rules] in argot.toml (or --rule per run)
Limit a rule to (or out of) some pathsinclude/exclude on the [rules] entry
Accept one deliberate line *inline # argot: ignore-next-line — reason
Accept a specific reported hit for good *argot mute <hash> --reason …
Temporarily silence a hit *argot mute <hash> --expires 30d
Cover many hits under one path *a path: glob [[mute]] rule
Make a rule un-silenceablelocked = true on its [rules] entry
Exclude something on your machine onlyargot.local.toml

* The suppression surfaces (inline ignore, [[mute]], [exclude].paths) do not apply to a locked rule — that’s the point of a lock, so an AI agent can’t quiet a rule it can’t satisfy. Weakening a lock is itself reported by rule-tampered.

Suppressed ≠ deleted: check drops muted hits from its output and exit code but still prints a one-line count on stderr, so a repo full of silent mutes never looks cleaner than it is.