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:
[exclude]— what argot learns from and looks at. The built-in recommended pattern list (editable) plus your own path patterns.[detect]— how argot spots code it shouldn’t learn from: the generated-file comment markers and the data-dominance threshold, now visible and editable.[rules]— how loud each rule is:error,warn, oroff, per rule or per group.[[mute]]— specific hits you’ve judged fine, a durable audit trail.
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:
*,?, and[…]match within a path segment;**spans directories.- A leading
/— or any interior/— anchors the pattern to the repo root; a bare name (legacy) matches at any depth. - A trailing
/(vendor/) matches a directory and everything under it, never a same-named file. - A leading
!re-includes a path an earlier pattern excluded — last match wins.
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:
- out of the voice corpus — no surprisal, no callee clusters, no thresholds. Test style never mixes with production conventions.
- in the vocabulary — the import specifiers those files use are learned into a separate set, consulted only when scoring one of those same files. A dependency only your tests use stays foreign in production code.
- checked — but by the import signal alone. On these paths the voice reports
only
foreign-import;rare-tokens,unfamiliar-calleeandconventionare withheld, because the model never read a file like this one. Every other rule — custom rules,layering, the test-integrity rules — behaves normally.
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__/**).
Editing the recommended set
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:
- Directories (matched at any depth):
test*/(any dir startingtest),__tests__/,doc/docs/example/examples/migration/migrations/benchmark/benchmarks/fixtures/scripts/build/dist/__pycache__/.git/.history/.tox/.eggs/. - Files:
test_*andconftest.py;*.test.*/*.spec.*/*.config.*(sox.test.ts,vite.config.ts); and dotfile.*rc.*configs (.babelrc.js).
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.
generated-markersare scanned only in a file’s head comments, so an “auto-generated” phrase inside a docstring or mid-file string can’t trip a false positive. Set it to[]to turn comment-based generated detection off.data-thresholdis file-level; a per-row value-dominance test (a fixed internal 0.80) still skips individual data-literal rows inside an otherwise code file. Setdata-threshold = 1.0to effectively disable file-level data detection.- Go, Rust, and C# generated files are also caught by language-structural
tells built into argot (Go’s
// Code generated … DO NOT EDIT., Rust’s@generated, C#‘s<auto-generated>/[GeneratedCode]) — those aren’t comment phrases, so they’re not in this list and aren’t disabled by emptying it. (C# honors yourgenerated-markerson top of its structural tells; Go and Rust are purely structural.)
[rules] — rule severities
Location: the [rules] section of argot.toml. Every finding argot emits
belongs to one of twelve stable rules, in five groups:
| Group | Rules |
|---|---|
voice | foreign-import · unfamiliar-callee · rare-tokens · convention · superseded |
semantic | redundant · misplaced |
architecture | layering |
integrity | test-deleted · test-disabled · test-weakened |
governance | rule-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.toml → argot.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:
argot rules(--format jsonfor machines) lists every rule with its group, effective severity for this repo, and a one-line description.argot check --error-on-warningsmakeswarn-severity findings fail the check too — strict-CI mode without touching the committed config.
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:
- Severity is pinned at the committed value —
argot.local.tomland--ruleoverrides are refused, with a warning on stderr. - Every suppression surface is refused for its findings — inline
# argot: ignore…comments,[[mute]]entries, and[exclude].pathsdo not apply. The lock means locked. - Weakening the lock is itself a finding. The
rule-tamperedrule (groupgovernance) reads both sides of the change being checked: a diff that removes a lock, downgrades a locked rule’s severity, adds a[[mute]]targeting a locked rule, or edits a locked custom rule’s script/manifest fires an error with the exact weakening named — and a run-level warning that CI surfaces loudly (--format githubturns it into a PR annotation). rule-tampereditself is pinned: alwayserror, always locked, never suppressable. An alarm you can configure off is not an alarm.
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: ignorecomment has no effect on a rule locked withlocked = 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:
- A reason is mandatory. A suppression comment without one is reported as a
warning and ignored (it’s also the note your future self reads). Separate the
reason from the directive with
—,-, or:. rule=<name>(optional) scopes the mute to one rule (foreign-import,rare-tokens,unfamiliar-callee,convention,redundant,misplaced,layering,test-deleted,test-disabled,test-weakened) or a whole group (voice,semantic,architecture,integrity). Omit it to mute the hunk whatever fired. An unknown rule name is a warning, and the directive is ignored.ignore-next-linemutes the single line below the comment.ignore-block-start…ignore-block-endmute everything between them (the-endneeds no reason). An unclosed block suppresses to end of file with a warning.
[[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 byrule-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"
from,to, andreasonare mandatory — the same discipline as[[mute]]. An entry missing one, or naming an unknownkind, is skipped with a warning on stderr; one bad entry never voids the file.kindis optional:"import"(the default) for a module specifier, or"callee"for a method/function call.argot.local.tomlentries append to the committed ones, exactly like[[mute]].- Effective immediately, no refit needed. A declared migration is read at check
time, not baked into the fitted model:
tostops reading as foreign andfromstarts raisingsupersededfrom the very nextcheck. Declaring one doesn’t even require a refit —[[migration]]isn’t part of the config fingerprint that fit-snapshot freshness watches (see[fit]). - Scoping reuses the existing per-rule path scopes — no separate mechanism:
[rules] superseded = { severity = "warn", include = ["src/**"] }limits where the rule fires (mined or declared alike), exactly like scoping any other rule. - The finding it produces is a rule like any other:
[rules] superseded = "off"turns it off (per repo or per run),[[mute]]and inline# argot: ignore-next-line rule=superseded — reasonsuppress one hit, andargot ruleslists its effective severity (warnby default — reported, never failing the check on its own).
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:
- every
fit/initre-scans the tree and persists its verdict to.argot/health.json;argot checkreads it and prints one-line notes when new generated, data-heavy, or vendored directories are shaping the voice or whenargot.tomlchanged since the fit (in--format github, these become run-level PR annotations), - editing
[exclude]/[detect]is itself a refresh trigger: the next check notices the config fingerprint changed and tells you to refit locally, argot status(--repo <path>for a repository you are not sitting in) is the one-stop health view: fitted SHA, adaptive drift score and evidence, accepted commits as context only, config compatibility, and unexcluded noisy directories,refresh.next_actiondistinguishesfitfromreview_scope_then_fit. The latter is emitted for material new areas/languages/layout changes or a fit-relevant config mismatch; useargot-refreshbefore rebuilding,argot status --format jsonalso reports whether every required snapshot artifact is present and tracked, so CI can refuse a misleading partial run.
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
| Variable | Effect |
|---|---|
ARGOT_OFFLINE=1 | Never touch the network. Every rule still runs, the semantic layer included — the embedder is compiled in. |
ARGOT_UPDATE_CHECK=0 | Disable 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.
| File | Written by | What it is | Committed? |
|---|---|---|---|
argot.toml (repo root) | init / argot mute / by hand | Config — [exclude], [detect], [rules], [update], and [[mute]]. | Yes — commit it. |
argot.local.toml (repo root) | you | Personal overrides, merged on top. | No — gitignored. |
.argot/scorer-config.json | fit / init | The fitted voice model: calibrated threshold(s) + scorer config. | Yes — fit snapshot. |
.argot/semantic-index.json | fit / init | The per-repo code-embedding index for reinvention/placement. | Yes, when produced — status names an intentional abstention. |
.argot/layering.json | fit / init | The module-dependency graph the layering rule checks. | Yes, when produced — status names an intentional abstention. |
.argot/integrity.json | fit / init | Per-repo learned gates for the test-integrity rules. | Yes, when produced — status names an intentional abstention. |
.argot/manifest.json | fit / init | Versioned, hashed record of what was learned. | Yes — provenance. |
.argot/health.json | fit / init | Fit SHA, config fingerprint, drift candidates, and compact corpus denominators for adaptive freshness. | Yes — freshness. |
.argot/repo-corpus.txt | fit / init | The source files counted into the repo distribution. | No — rebuildable. |
.argot/generic-baseline.json | fit / init | The bundled generic-baseline reference. | Yes — required by voice scoring. |
.argot/dataset.jsonl | extract | Raw training dataset — one record per hunk. The check path doesn’t need it. | No — rebuildable. |
.argot/last-check.json | check | Cache of the last check’s hits, so argot mute <hash> can resolve. | No — rebuildable. |
.argot/.gitignore | fit / init | Selectively ignores runtime state while exposing snapshot files. | Yes — commit it. |
.argot/rules/<name>/ | argot-write-rule / argot-suggest-rules / by hand | Your custom rules — rule.toml + check.rhai, authored not generated. | Yes — ordinary tracked source. |
Outside the repo, argot keeps exactly two user-level locations:
| Location | What it is |
|---|---|
~/.argot/settings.json | The 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
argotbinary, 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 clearreclaims it and the next local fit rebuilds it. The.argot/semantic-index.jsonthe 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 dir | remove its entry from [exclude].recommended |
| Turn the recommended set off entirely | recommended = [] in [exclude] |
| Catch a repo’s own codegen banner | add 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 paths | include/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-silenceable | locked = true on its [rules] entry |
| Exclude something on your machine only | argot.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.