The agent-native CLI linter. Audits whether your CLI follows the 8 agent-readiness principles.
anc dogfoods the spec it enforces. The badge above is its own live score.
The crate is published as agentnative. The binary is called anc.
# Homebrew (installs anc)
brew install brettdavies/tap/agentnative
# From crates.io
cargo install agentnative
# Pre-built binary via cargo-binstall
cargo binstall agentnative
# Pre-built binaries from GitHub Releases
# https://github.com/brettdavies/agentnative-cli/releasesThe Homebrew formula installs the pre-built archive the release publishes for your platform, checked against the checksum the formula pins, and compiles nothing. The tap pins a checksum only after it has verified the archive against the release's build-provenance attestation; Verifying an archive in the tap's README has the command that repeats the check.
Each release archive carries a build-provenance attestation and an attested SBOM, signed by the release workflow. To check that an archive you downloaded was built by it, from this repository:
gh attestation verify agentnative-x86_64-unknown-linux-gnu.tar.gz --repo brettdavies/agentnative-cli \
--signer-workflow brettdavies/.github/.github/workflows/rust-release.ymlReleases up to 0.6.0 carry no attestation; compare those against the release's sha256sum.txt.
anc ships a companion skill bundle (agentnative-skill) that teaches AI coding agents how to operate the linter and
where to apply the principles. Install it with one command per host:
anc skill install claude_code # ~/.claude/skills/agent-native-cli
anc skill install codex # ~/.codex/skills/agent-native-cli
anc skill install cursor # ~/.cursor/skills/agent-native-cli
anc skill install factory # ~/.factory/skills/agent-native-cli (Factory Droid)
anc skill install kiro # ~/.kiro/skills/agent-native-cli (Kiro)
anc skill install opencode # ~/.config/opencode/skills/agent-native-cliInspect the resolved command before running it:
anc skill install --dry-run claude_code
# git clone --depth 1 https://github.com/brettdavies/agentnative-skill.git /home/you/.claude/skills/agent-native-cliJSON output (modes dry-run and install, success and error) shares one envelope shape. Agents parse the same fields
on every outcome:
anc skill install --dry-run claude_code --output jsonIf the site adds a host before this anc release knows about it, fall back to a manual git clone:
git clone --depth 1 https://github.com/brettdavies/agentnative-skill.git <host-skills-dir>/agent-native-cliThe host map is hardcoded in this binary; new hosts ship via patch release after the site updates skill.json.
# Audit the current project (`audit` is the default subcommand)
anc .
# Audit a specific binary
anc ./target/release/mycli
# Resolve a command on PATH and run behavioral audits against it
anc --command rg
# Run only behavioral audits (skip source analysis)
anc . --binary
# Grade one of several binaries the repository builds
anc . --bin mycli
# Run only source audits (skip the compiled binary)
anc . --source
# JSON output for CI
anc . --output json
# Print the scorecard JSON Schema (draft 2020-12)
anc emit schema
# Filter by principle
anc . --principle 3
# Quiet mode (warnings and failures only)
anc . -q.anc.toml declares a CLI's own vocabulary so audits stop counting it against the CLI:
[p2]
json_probe = ["version", "--client", "-o", "json"]
schema_command = ["explain"]
[p5]
confirm_flags = ["--noconfirm"]
not_destructive = ["clean"]
[p6]
domain_verbs = ["post", "like", "repost", "timeline"]| Setting | Audit | What it declares |
|---|---|---|
[p2] json_probe |
p2-must-output-flag |
A read-only call that prints JSON, for anc to run and check |
[p2] schema_command |
p2-must-schema-print |
The subcommand that prints the output schema, when it is not schema |
[p5] confirm_flags |
p5-must-force-yes |
Flags that confirm a destructive subcommand, beside the built-in names |
[p5] not_destructive |
p5-must-force-yes |
Subcommands whose names read as destructive but are not |
[p6] domain_verbs |
p6-may-standard-names |
Verbs added to the standard list that subcommand names are checked against |
json_probe: p2-must-output-flag accepts an output flag that takes a format, --output, --format, or
--output-format where its definition names json, and the --json and --jsonl aliases the spec names, when an
option definition in the help declares one. It checks the flag by passing json to it (an alias goes alone), spelled as
the help prints it, beside --help and --version, the only calls anc makes unprompted. A --json that takes a
value, such as a field list, is not the alias. Most tools answer those calls in text, and then the row is skip: anc
could not check, so the row is not scored. A declared probe is the call anc runs instead, exactly as written: no
shell, with the timeout, closed stdin, and NO_COLOR=1 of every other probe. The row passes when the call exits 0 and
its stdout parses as JSON, and fails otherwise, with evidence naming the call and the file:
`kubectl version --client -o json` printed JSON; probe declared via .anc.toml [p2].json_probe. Declare a call that
only reads and exits on its own; kubectl version -o json contacts a cluster and exits 1 without one, while
version --client stays local. A probe that prints JSON also shows p2-must-schema-print that the tool emits
structured output when its help does not say so. The probe never stands in for the flag: when no option definition in
the help declares one of those flags, the row stays opt_out. The nearest file that declares json_probe supplies it,
and an empty list declares nothing.
schema_command: p2-must-schema-print looks for a schema subcommand or a --schema flag, at the top level and one
level down. A tool whose schema surface has another name, such as kubectl's explain, declares the subcommand path, one
token per level (["emit", "schema"] for <bin> emit schema). The declared path counts when each token is listed in
its parent's --help; anc reads a parent's help with --help and never runs the command itself. The row then passes,
and its evidence names the command and the file:
`kubectl explain` is the schema command declared via .anc.toml [p2].schema_command. A declared path the help does
not list leaves the row failing, and the evidence says so. A built-in schema surface takes priority. Entries are
lowercase. The nearest file that declares schema_command supplies it, and an empty list declares nothing.
confirm_flags: p5-must-force-yes requires each destructive subcommand's own --help to list a confirmation flag.
The built-in names are --force, --yes, -y, -f, --auto-approve, --assume-yes, and --confirm. A declared
flag counts beside them, and only where the subcommand's --help lists it, so a declaration names the flag and cannot
stand in for one. Built-in and declared names match whole: -f does not match -force-copy. In a help that declares no
double-dash name, a single-dash name matches its double-dash spelling, so terraform's -auto-approve meets the built-in
--auto-approve without a declaration, and a declared --noconfirm meets a listed -noconfirm. An entry written
without dashes is read as a flag: noconfirm is --noconfirm, and y is -y. A pass that needed a declared flag says
so in the row's evidence, naming the subcommand, the flag, and the file:
destroy accepts --noconfirm via .anc.toml [p5].confirm_flags.
not_destructive: p5-must-force-yes treats a subcommand as destructive by its name (delete, rm, purge, clean,
and names built on them). A tool whose clean clears regenerable caches, for one, declares it here, and the audit
leaves it out of the destructive set. Entries are lowercase; they are compared with the lowercased subcommand name. The
row's evidence names each subcommand left out and the file that declared it:
declared not destructive: clean via .anc.toml [p5].not_destructive. When every destructive subcommand is declared, the
row is skip, as for a tool with none. A declared subcommand still counts as a write for
p5-must-read-write-distinction, because clearing a cache changes state.
domain_verbs: p6-may-standard-names passes when most subcommand names are standard verbs. A declared verb counts
beside the built-in list. Entries are lowercase; they are compared with the lowercased subcommand name. A pass that
needed a declared verb carries using_domain_verbs and domain_match_count on the row.
A key anc does not know is ignored. A known key with a value of the wrong type, such as
confirm_flags = "--noconfirm", is a parse error.
anc finds .anc.toml from the audit target's location, never from the directory you run it in. The files that apply,
lowest precedence first:
| Target | Files |
|---|---|
| A directory inside a git repository | ~/.anc.toml, then each .anc.toml from the repository root down to the target |
| A binary inside a git repository | The same, starting from the directory that holds the binary's real file |
Any target outside a repository, under ~ |
~/.anc.toml, then each .anc.toml from ~ down to the target |
Any target outside a repository, outside ~ |
~/.anc.toml, then the .anc.toml in the target's directory |
Any target with --repo <PATH> |
~/.anc.toml, then <PATH>/.anc.toml |
A binary counts the same whether you pass its path or --command resolves it on PATH, and symlinks resolve first, so
a dev build linked onto PATH keeps its repository's config. The repository root is the nearest directory holding a
.git entry, so a linked git worktree checkout or a submodule is its own root.
The files merge. A list setting gathers every file's entries, each file's after the ones above it, and an entry listed
twice keeps its first position; evidence credits it to the nearer file, so a flag both your ~/.anc.toml and the tool's
repository declare reads as the repository's. If any file in the chain cannot be read or parsed, no config applies: the
p6-may-standard-names warning names the failing file, such as could not parse .anc.toml at crates/cli/.anc.toml, and
a row another setting could have shaped ends its evidence with No .anc.toml setting applied: and the same message.
The file in your home directory applies under every audit, inside repositories too, as the lowest layer. Keep personal
vocabulary there. Two machines with different home files can score the same tool differently; CI runners have none.
AGENTNATIVE_HOME_CONFIG relocates the file. Evidence and the hint then name it $AGENTNATIVE_HOME_CONFIG, never the
path it holds, and when it names a file that does not exist, anc prints a warning: line on stderr and applies no
user-level file. A json_probe or schema_command here applies to every tool you audit, so point
AGENTNATIVE_HOME_CONFIG at a one-off file to declare either for a single run.
A binary installed by a package manager or into a sandbox does not sit inside its repository. Fetch the repository, or
just its .anc.toml, and point anc at the directory; anc never fetches anything itself:
anc audit --command xr --repo /tmp/xr-src
AGENTNATIVE_REPO=/tmp/xr-src anc audit --command xr--repo takes an existing directory. Anything else is a usage error (exit 2).
When p6-may-standard-names warns and no domain_verbs applied, anc prints the line that clears the warning and the
two files it can go in, with what each one is for:
[WARN] Subcommand verbs follow community-standard names (p6-may-standard-names) (may)
16/41 subcommand(s) follow standard verb names. Non-standard: post, like, ...
hint: add `domain_verbs = ["post", "like", ...]` under `[p6]` in either file below; see https://github.com/brettdavies/agentnative-cli#configuration-anctoml
- .anc.toml at the tool's repository root: travels with the tool; anc reads it from a checkout, or through --repo <checkout> for an installed copy
- ~/.anc.toml: your own vocabulary; applies to every tool you audit on this machine
The repository's .anc.toml is where a tool's own vocabulary belongs: it travels with the tool, so every audit of the
repository applies it. For a target inside a repository or under --repo, the line names that repository's root. For a
target outside any repository, such as the installed tool above, it names the tool's own repository root, which anc
reads from a checkout or through --repo. ~/.anc.toml holds your own vocabulary and applies to every tool you audit,
so a verb added there for one tool passes for all of them. --output json carries the same hint as config_hint on the
row.
A directory audit inventories every package in the repository, grades one binary that a package declares and has built, and anchors the rest of the audit on that binary's package.
anc reads the workspace declarations at the audit root: Cargo [workspace] members and exclude, npm or yarn
workspaces, the packages list of pnpm-workspace.yaml with its ! exclusions, go.work use, and uv
[tool.uv.workspace] members and exclude. It then scans the tree for Cargo.toml, pyproject.toml, go.mod, and
package.json, so a repository with no root workspace file, or one mixing languages, is inventoried too.
The scan honors the repository's .gitignore files and .git/info/exclude, never your global excludes file, so a
repository inventories the same on every machine. It skips hidden directories, target, node_modules, vendor,
venv, dist, build, and tests unless --include-tests is set, and does not follow symlinked directories. A
directory holding two manifests is two packages; a Cargo manifest without [package] is a workspace root, not a
package. A workspace file anc cannot read prints a warning: line, and the scan still finds its packages.
A candidate is a bin that a package declares and that exists at its language's build location. Nothing counts just for
sitting in node_modules/.bin, dist/, or build/.
| Language | Declared bins | Built location |
|---|---|---|
| Rust | [[bin]] names; src/main.rs gives the package name; src/bin/<name>.rs and src/bin/<name>/main.rs give <name>; autobins = false turns the implicit ones off |
release/<name> or debug/<name> in cargo's target directory; the newer of the two |
| Node | a bin string gives the package name without its scope; a bin object gives its keys |
the declared file when executable, else node_modules/.bin/<name> in the package, then at the audit root |
| Python | [project.scripts] keys |
.venv/bin/<name> in the package, then at the audit root |
| Go | each directory holding package main gives its directory name |
<name> in that directory, then at the module root |
On Windows, Rust, Go, and Python names end in .exe, and the virtual environment's directory is Scripts.
The target directory is the one cargo builds into: CARGO_TARGET_DIR when it is set (a relative value is relative to
the current directory); otherwise the target_directory that cargo metadata reports at the audit root, which follows
build.target-dir in cargo's config files and the workspace the root belongs to; otherwise target at the audit root,
when cargo is not installed or gives no answer within 5 seconds. Without CARGO_TARGET_DIR, a package below the audit
root is looked for in its own target first, where cargo builds a crate outside the root's workspace.
With one candidate, anc grades it. With several, it exits 2 before running any audit and prints the command that
grades each one:
error: found 2 built binaries here, and anc grades one per run; run one of these
anc audit . --bin xr # xurl-rs, target/debug/xr
anc audit . --bin xdk-consumer-check # xdk-consumer-check, target/debug/xdk-consumer-check
Usage: anc audit [OPTIONS] [PATH]
That holds for a single crate with several [[bin]] targets too: a repository or crate that builds more than one binary
exits 2 until --bin names one. Each printed command keeps the flags you passed, so anc audit . --principle 6 offers
anc audit . --principle 6 --bin xr. Under --output json the error is the usage envelope with
"error": "binary-ambiguous", a candidates array of name, package, path (relative to the audit root when the
build is under it), and command, and a next_step of choose-bin whose template ends in --bin <name>. When two
candidates share a name, each is offered by its path.
--source runs no binary, so it needs no choice: with several candidates and no --bin, the source and project audits
run with nothing graded.
With none, the source and project audits run, and the warning names any bins the packages declare but have not built,
the cargo target directories searched for the Rust ones, and the ways forward: build one, audit a built binary by path,
or audit an installed one with --command.
--bin <NAME>, or AGENTNATIVE_BIN, picks a candidate by name or by the path anc printed for it. It is unrelated to
--binary, which runs only the behavioral audits:
anc audit . --bin xr # grade xr when the repository builds several binaries
anc audit . --binary # behavioral audits only, of the one built binary
anc audit . --bin xr --binary # both--bin needs a directory target: beside --command or a binary path it is a usage error.
Behavioral audits probe the graded binary. Source audits run for every language present, Rust and Python today, over
that language's files under the audit root, with the scan's skip rules; when both languages run an audit for the same
requirement, the scorecard keeps the more severe result. They also skip example programs: an examples directory beside
a package manifest or at the audit root builds apart from the binary and never ships in it, so it is not read, with or
without --include-tests. A source module named examples deeper in a package is still read. Manifest-reading project
audits, and the scorecard's tool name and version, read the graded binary's package. With nothing graded they read the
one package that declares a binary; when several do, those audits skip and name the packages.
agentnative audits your CLI against eight agent-readiness principles:
| # | Principle | What It Means |
|---|---|---|
| P1 | Non-Interactive by Default | No prompts, no browser popups, stdin from /dev/null works |
| P2 | Structured Output | --output json exists, produces valid JSON, and the shape is discoverable via schema subcommand or --schema flag |
| P3 | Progressive Help | --help has examples, --version works |
| P4 | Actionable Errors | Structured error types, named exit codes, no .unwrap() |
| P5 | Safe Retries | --dry-run for write operations |
| P6 | Composable Structure | SIGPIPE handled, NO_COLOR respected, shell completions, AGENTS.md, SIGTERM cleanup |
| P7 | Bounded Responses | --quiet flag, no unbounded list output, clamped pagination |
| P8 | Discoverable Skill Bundle | Top-level AGENTS.md / SKILL.md with YAML frontmatter, tool skill install [<host>] for agent runtimes |
P1 — Non-Interactive by Default
[PASS] Non-interactive by default (p1-non-interactive)
[PASS] Flags advertise env-var bindings in --help (p1-env-hints)
[PASS] Secret-bearing flags expose stdin or *-file companion (p1-secret-non-leaky-path)
[PASS] TTY detection for color output (p1-tty-detection-source)
[PASS] No interactive prompt dependencies (p1-non-interactive-source)
P2 — Structured Output
[PASS] Structured-output CLI exposes its schema at runtime (p2-schema-print)
[PASS] --json / --jsonl short aliases for --output (p2-json-aliases)
[PASS] Output schema exported to a stable file path (p2-schema-file)
P6 — Composable Structure
[PASS] Handles SIGPIPE gracefully (p6-sigpipe)
[PASS] Subcommand verbs follow community-standard names (p6-standard-names)
[PASS] Long-running CLI handles SIGTERM (p6-sigterm)
[PASS] Shell completions support (p6-completions)
P8 — Discoverable Skill Bundles
[PASS] Skill bundle has install path (`tool skill install [<host>]`) (p8-bundle-install)
[PASS] Top-level AGENTS.md / SKILL.md bundle present (p8-bundle-exists)
Code Quality
[PASS] No .unwrap() in source (code-unwrap)
69 audits: 59 pass, 0 warn, 0 fail, 0 opt_out, 0 n_a, 10 skip, 0 error
🏆 Score: 100% — your tool qualifies for the agent-native badge.
Embed in your README:
[](https://anc.dev/score/anc)
Convention: https://anc.dev/badge
The badge hint appears in text output when a tool scores at or above the 70% eligibility floor. Below the floor, anc
prints nothing badge-related. The convention is to surface the embed only when earned.
agentnative uses three layers to analyze your CLI:
- Behavioral: runs the compiled binary, checks
--help,--version,--output json, SIGPIPE, NO_COLOR, SIGTERM, exit codes. Language-agnostic. Isolate withanc . --binary. - Source: ast-grep pattern matching on source code. Detects
.unwrap(), missing error types, nakedprintln!, closed-set rejection, and more. Supports Rust and Python. Isolate withanc . --source. - Project: inspects files and manifests. Checks for
AGENTS.md/SKILL.mdbundle, recommended dependencies, dedicated error/output modules, output-schema file at the repo root. Runs alongside the other layers; no isolation flag.
--binary and --source are useful when one layer regresses and you want a focused gate (CI step for source quality,
release-gate against the compiled artifact). Without either flag, all three layers run together.
Several behavioral audits ask whether a help declares a flag (--quiet, --force, --output). anc answers from
definition lines: lines that start with a flag name and, usually, describe it. A usage synopsis, an example, or a
sentence that mentions a flag mid-line declares nothing, so a flag that appears only in usage: tool [-q] does not
count.
To have a flag count, print it on a line of its own, names first, with its description set off by two or more spaces or
a marker (#, =, --, or a colon), and spell it the way the requirement does. One dash stands for two only in a help
that declares no double-dash name, and a single letter counts only where its own description says what the audit asks
for. The rest of this section is the detail behind those rules.
Every name on a definition line is kept whole and spelled as the help prints it, and a row that names a flag quotes that spelling:
| Help line | Names read |
|---|---|
-force-copy Suppress prompts about copying state data (terraform) |
-force-copy |
-n, --quiet, --silent (GNU sed) |
-n, --quiet, --silent |
-q, --[no-]quiet suppress summary after successful commit (git) |
-q, --quiet, --no-quiet |
-h,-H,--help,-help,-usage,/? = Print usage information and exit. (cmake) |
-h, -H, --help, -help, -usage, /? |
-W<category> = Enable the specified category of warnings. (cmake) |
-W |
-0, --http1.0 Use HTTP/1.0 (curl) |
-0, --http1.0 |
-cd --print-config-dir Print the config directory (lazygit) |
-cd, --print-config-dir |
-f FORMAT, -r FORMAT --from=FORMAT, --read=FORMAT (pandoc) |
-f, -r, --from, --read |
--ignore-files-only --ignore option only ignores files (files-to-prompt) |
--ignore-files-only |
A value placeholder (<FILE>, =false, N, [=WHEN]) is never part of a name. A single letter matches only the same
letter: -f is not -force-copy, and -v is not -version. A single letter also counts for an audit only when its
own definition means what the audit asks for, through a long name the audit accepts on the same line or a description
that names the meaning: -v Increase verbosity is a verbose flag, and -v, --version Print version is not. A row
that sets a declared letter aside quotes the definition it read.
Names can sit in a second column with no comma between the columns, as lazygit and pandoc print them. After a gap, text
that leads with a flag name and holds only names and placeholders is another column; text that reads on as a sentence is
the description, so the files-to-prompt line above declares one name. A description can also be set off by a marker in
place of a gap: # (Thor), = (cmake), -- (ffmpeg), -, or a colon that ends the names, as in nmap's
-iL <inputfilename>: Input from list of hosts/networks.
A wrapped description line is not a definition, even when it starts with a dash. A line indented to the description
column of the definition above it carries on that description, so docker's -1 for unlimited) and the second line of
-v, --verbose... Increase logging verbosity (-v for warnings, -vv for info, -vvv for debug,
-vvvv for trace)
declare nothing. The same holds for a description that starts on the line after its definition, as clap's long help, the
Go flag package and kubectl print it, and it holds across blank lines, because a description can run over several
paragraphs. A line left of the description column ends it. A flag that a help mentions only inside another flag's
description is not declared. Under a definition with no description, a line that declares a name at a column where names
start is a definition, not that description: -x, --long puts the long name four columns right of the short, so
--format <FORMAT> under an undescribed -x declares --format.
A definition can sit at any indent. rsync, ffmpeg and Miller print theirs at column 0, typer and broot print them as
rows of a box table with the short name in a cell of its own (typer marks a required option with * in a first cell,
which is not part of the name), Thor brackets its long names (-f, [--force]), and fzf has shorts that start with a
plus (+x, --no-extended). Text is read as a terminal shows it: without ANSI escapes, with groff's overstruck bold
collapsed, and with TABs expanded.
Two shapes need more than a leading dash, because usage lines wrap into them. Dash-led text at column 0 is a definition
only when a gap or a marker sets a description off from its names, or its description starts at the column the
definition above it uses; --hide-secret flag. Please carefully consider ... at column 0 is prose, and python's
--help-all: print complete help information and exit is a definition. A line that starts with a bracketed flag is a
definition only with a description set off the same way, so [--recursive] on a synopsis line of its own declares
nothing.
Three more dash-led shapes are text, not definitions, at any indent. A line under one that ends in a backslash carries
on an example command, so the --recursive under aws s3 ls s3://amzn-s3-demo-bucket \ declares nothing. A line
indented to the arguments of the usage: line above it carries on the synopsis, where argparse and git wrap a long one,
unless a gap or a marker sets off a description of its own. A line that reads on as a sentence, directly under prose at
the same indent that has not ended its sentence, is the rest of that sentence:
--human-readable and --summarize options. .... A row whose description a gap or a marker sets off is not a sentence,
so nmap's colon-described rows under its Ex: ... line are definitions.
One dash and two are the same name only where the help's own convention makes them so. In a help that declares no
double-dash name, the Go flag convention, a single-dash word meets an audit that asks for its double-dash spelling:
terraform's -force on force-unlock counts as --force. In a help that declares any double-dash name, a single-dash
word is a name of its own, and a row that misses for that reason says so:
`-verbose` is declared, but this help also declares double-dash names, so it does not count as `--verbose`.
These layouts are read the way they look rather than the way the tool means them:
- A paragraph that opens with a flag and reads as a sentence is read as a definition of the flags it opens with:
terraform's
-state, state-out, and -backup are legacy options supported for the localdeclares-state. No audit asks for those flags. - An example command at definition indent, with no backslash on the line above it, is read as a definition.
- python3's rows with a placeholder and a spaced colon, such as
-c cmd : program passed in as string, are not read, becausecmdopens like a sentence at column 0. - A synopsis printed on a line of its own under
Usage:, and argparse's wrap of a long program name to column 7, declare their dash-led lines. p1-must-env-vardoes not count an environment variable printed bare in a column between a flag's names and its description (--token GH_TOKEN API token), because a value placeholder prints the same way (--config-file -c CONFIG_FILE Path).$GH_TOKENin that column,[env: GH_TOKEN], or the name in the description counts.
If anc misreads your tool's help, file a
false positive / false negative report
with the output of <tool> --help. The help text becomes a test fixture.
The public score reflects shipped-binary behavior only: only behavioral-layer audits enter the formula. Source- and project-layer audits still run and still appear in the scorecard, but they do not move the score: what a tool's source looks like does not change how an agent experiences the installed binary.
Every audit result lands in one of seven statuses. The score is a credit-weighted ratio over the behavioral rows that verified something one way or the other (the denominator set):
| Status | Credit | In denominator | Meaning |
|---|---|---|---|
pass |
1.0 | yes | Behavior present and correct. |
warn |
0.5 | yes | Behavior present, partially correct. |
fail |
0.0 | yes | Behavior expected, absent or broken. |
opt_out |
0.0 | yes | Behavior deliberately declined (counts against). |
n_a |
n/a | no | Inapplicable: a conditional antecedent is unmet. |
skip |
n/a | no | Unmeasurable: the probe could not determine. |
error |
n/a | no | The probe itself raised an exception. |
With per-tier weights w (currently flat, with MUST = SHOULD = MAY = 1), score_pct = round(100 × Σ w·credit / Σ w)
over the denominator set. Badge eligibility floor: 70%. The floor is deliberately low so the badge spreads the
standard; exclusivity is carried by four cohort bands the site renders: Exemplary (≥ 85), Strong (80–84), Solid (75–79),
Qualified (70–74). The eligibility floor is not a high gate. The formula, weights, floor, and bands are defined in the
spec's principles/scoring.md and held stable for at least six months from publication.
Each spec requirement is tagged MUST / SHOULD / MAY. A missing requirement maps to a different result status depending on tier:
| Tier | On miss | Example |
|---|---|---|
| MUST | fail |
p1-must-no-interactive |
| SHOULD | warn |
p2-should-schema-file |
| MAY | warn |
p8-may-install-all |
anc runs the same scoring on itself. Because the public score is behavioral-only, a source-only run has no rows to
score. It reports 0% and ineligible by design, since there is no shipped-binary behavior to measure:
| Mode | Audits | Pass | Skip | Behavioral rows | Score | Eligible |
|---|---|---|---|---|---|---|
anc audit . --binary |
43 | 34 | 9 | 43 | 100% | yes |
anc audit . --source |
26 | 25 | 1 | 0 | 0% | no |
anc audit . (full) |
69 | 59 | 10 | 43 | 100% | yes |
The score is identical in --binary and full mode: source and project audits add scorecard rows but never change the
number. The nine behavioral skips are probe limitations (skip), excluded from the denominator, so they neither help
nor hurt the score.
When the first non-flag argument is not a recognized subcommand, audit is inserted automatically. anc ., anc -q .,
and anc --command rg all resolve to anc audit …. Bare anc (no arguments) still prints help and exits 2: a
deliberate fork-bomb guard for when agentnative dogfoods itself.
Usage: anc audit [OPTIONS] [PATH]
Arguments:
[PATH] Path to project directory or binary [default: .]
Options:
--command <NAME> Resolve a command from PATH and run behavioral audits against it
--binary Run only behavioral audits (skip source analysis)
--bin <NAME> Grade this binary when the directory builds several
[env: AGENTNATIVE_BIN=]
--repo <PATH> Read `.anc.toml` from this directory instead of the target's
repository [env: AGENTNATIVE_REPO=]
--source Run only source audits (skip behavioral)
--principle <PRINCIPLE> Filter audits by principle number (1-8)
--output <OUTPUT> Output format [default: text] [possible values: text, json]
-q, --quiet Suppress non-essential output [env: AGENTNATIVE_QUIET=]
--include-tests Include test code in source analysis
--audit-profile <CATEGORY> Exemption category for the target [possible values:
human-tui, file-traversal, posix-utility, diagnostic-only]
-h, --help Print help
--command and [PATH] are mutually exclusive; pick one. --command runs behavioral audits only. Source and project
audits are skipped because there is no source tree to analyze.
--audit-profile suppresses audits that legitimately do not apply to a class of tool. Profiles: human-tui for TUI
apps like lazygit whose contract IS the TTY, posix-utility for stdin-primary tools like cat/sed/awk,
diagnostic-only for read-only tools like nvidia-smi, file-traversal reserved for upcoming subcommand-structure
relaxations on fd/find-class tools. Suppressed audits emit Skip with structured evidence. The full per-category
mapping lives in coverage/matrix.json under audit_profiles[]. Agents should read that file rather than scrape
--help.
One table for every verb: each returns the same four codes, and every text report closes by naming the code it returns and what earned it.
| Code | Meaning |
|---|---|
| 0 | Clean: every applicable check passed |
| 1 | Warnings only: a SHOULD or MAY check missed |
| 2 | Failures present: a MUST check missed, or a usage error |
| 3 | Could not check: an audit or probe errored or was cut short, or nothing applied |
On anc audit, each audit reports fail or warn itself; an audit that errored returns 3 even when another failed,
and a suppressed (--audit-profile) or not-applicable audit counts toward no code.
Exit 2 also covers usage errors (bare anc, an unknown flag, mutually exclusive flags). Agents distinguishing a usage
error from a check failure should parse stderr, where a usage error prints Usage: in text mode and a typed envelope
in --output json mode, or call anc --help first to confirm the invocation shape.
Exit 3 separates a run that could not reach a verdict from one that found failures, which is the distinction a CI gate needs.
Every error prints one JSON envelope on stderr: kind (usage or runtime), error (a stable kebab-case reason),
message, exit_code, and next_step, the recovery an agent can act on without parsing message. next_step.action
is one of a closed set, and a newer release can add to it:
action |
Carries | Raised by |
|---|---|---|
show-help |
command: the help of the subcommand the invocation reached, such as anc audit --help |
parse errors, missing-subcommand, app-error |
choose-bin |
template: the invocation with --bin <name>; candidates holds one runnable command each |
binary-ambiguous, unknown-bin |
rerun |
command: the same invocation without --bin |
bin-needs-directory |
A step carries command, runnable as printed, or template, whose placeholder you fill; never both. docs links the
README section that explains the error. The offending input is echoed beside the reason: bin for unknown-bin and
bin-needs-directory, and argument and value for parse errors when the parser reports them.
# Bash
anc completions bash > ~/.local/share/bash-completion/completions/anc
# Zsh (writes to the first directory on your fpath)
anc completions zsh > "${fpath[1]}/_anc"
# Fish
anc completions fish > ~/.config/fish/completions/anc.fish
# PowerShell
anc completions powershell > anc.ps1
# Elvish
anc completions elvish > anc.elvPre-generated scripts are also available in completions/.
anc audit . --output jsonProduces a self-describing scoring run record (schema_version: "0.5") with results, summary, coverage against the
eight principles, plus contextual metadata identifying which tool was scored, by which anc build, on which platform,
and how. Each scorecard conforms to the JSON Schema emitted by anc emit schema (also committed at
schema/scorecard.schema.json):
{
"schema_version": "0.5",
"results": [
{
"id": "p3-help",
"label": "Help flag produces useful output",
"group": "P3",
"layer": "behavioral",
"status": "pass",
"evidence": null,
"confidence": "high"
}
],
"summary": {
"total": 33,
"pass": 28,
"warn": 1,
"fail": 0,
"skip": 4,
"error": 0
},
"coverage_summary": {
"must": { "total": 27, "verified": 21 },
"should": { "total": 20, "verified": 6 },
"may": { "total": 10, "verified": 3 }
},
"audience": "agent-optimized",
"audit_profile": null,
"spec_version": "0.4.0",
"tool": { "name": "ripgrep", "binary": "rg", "version": "ripgrep 15.1.0" },
"anc": { "version": "0.4.0" },
"run": {
"invocation": "anc audit --command rg --output json",
"started_at": "2026-04-29T16:00:00Z",
"duration_ms": 412,
"platform": { "os": "linux", "arch": "x86_64" }
},
"target": { "kind": "command", "path": null, "command": "rg" },
"badge": {
"eligible": true,
"score_pct": 97,
"embed_markdown": "[](https://anc.dev/score/ripgrep)",
"scorecard_url": "https://anc.dev/score/ripgrep",
"badge_url": "https://anc.dev/badge/ripgrep.svg",
"convention_url": "https://anc.dev/badge"
}
}-
coverage_summary: how many MUSTs/SHOULDs/MAYs the audits that ran actually verified, against the spec registry's totals. Seedocs/coverage-matrix.mdfor the per-requirement breakdown. Audits suppressed by--audit-profiledo not count towardverified. Suppression means the requirement was not verified, even when the audit shows as Skip rather than running. -
audience: derived classification from 4 signal behavioral audits (p1-non-interactive,p2-json-output,p7-quiet,p6-no-color-behavioral). Emitsagent-optimized(0-1 Warns),mixed(2 Warns), orhuman-primary(3-4 Warns). Returnsnullwhen any signal audit failed to run (source-only mode, missing runner, or--audit-profilesuppression). Informational only; never gates totals or exit codes. Values serialize as kebab-case to matchaudit_profile's format within the same JSON document. -
audience_reason: present only whenaudienceisnull. Values:suppressed(at least one signal audit was masked by--audit-profile) orinsufficient_signal(signal audit never produced, e.g. source-only run). Additive to schema0.2; older consumers feature-detect. -
audit_profile: echoes the applied--audit-profile <category>flag value (human-tui,file-traversal,posix-utility, ordiagnostic-only).nullwhen no profile is set. Seecoverage/matrix.jsonunderaudit_profilesfor the committed per-category mapping of which audit IDs each profile suppresses. -
tool: identifies what was scored.nameis always present and follows a four-tier fallback (command name, binary basename, manifest package name, project directory basename) that matches the site registry's slug convention.binaryis the executable basename when one is located;nullfor project-mode runs without a built artifact.versionis best-effort: project-mode prefers the manifest version (Cargo.toml/pyproject.toml), command/binary mode probes<bin> --versionthen-V.nullwhen probing failed or was declined by the self-spawn guard. The site'sregistry.yamlversion_extractsnippets remain authoritative for tools whose self-report is unreliable. Schema0.4addition. -
anc: identifies theancbuild that produced the scorecard.versionis the crate version at compile time. Informational, not a signed provenance signal. Pair with a Sigstore-signed release artifact when provenance is required. Schema0.4addition. -
run: run-level facts.invocationis the user's argv joined with shell-safe quoting, captured before default-subcommand injection so it reflects what the user typed (anc ., notanc audit .).started_atis RFC 3339 UTC.duration_msis wall-clock milliseconds.platform.os/platform.archcome fromstd::env::consts. Schema0.4addition. -
target: whatancwas pointed at.kindis"project"(directory),"binary"(executable file), or"command"(PATH-resolved name from--command).pathis the basename of the resolved target (project directory name or binary file name), never the absolute path, so home-dir usernames and employer directory layouts do not leak into scorecards committed to repos or posted by agents.commandcarries the user-supplied name for command mode. The unused field is alwaysnull, never missing. Consumer code can access both fields unconditionally. Schema0.4addition. -
badge: agent-native badge derivation from the live run.score_pctis the credit-weighted, behavioral-only leaderboard score (see Scoring);n_a/skip/errorand all non-behavioral rows are excluded from the ratio.eligibleis true iffscore_pct >= 70and a tool slug was derivable.embed_markdownisnullbelow the floor (the convention is "do not nag" until earned).scorecard_urlandbadge_urlare populated whenever a slug exists, even below the floor, so the site renders an SVG for every scored tool (a regression below the floor shifts color rather than 404s).convention_urlalways points athttps://anc.dev/badge. Schema0.5addition. -
evidenceon apassrow: what the audit matched, for an audit that names it (p7-limitnames each list command and its limit flag), then, when an.anc.tomlsetting decided the pass, the setting, what it contributed, and the file that supplied it, such asdestroy accepts --noconfirm via .anc.toml [p5].confirm_flags. The two parts are joined by a semicolon.nullwhen the pass carries neither. See Configuration. -
config_hint: present only on ap6-may-standard-nameswarning when no.anc.tomldeclareddomain_verbs.fileslists where the setting can go, each as{file, scope}:fileis.anc.toml,~/.anc.toml, or$AGENTNATIVE_HOME_CONFIG(never an absolute path), andscopeisrepository(the root of the repository this audit found, or the--repodirectory),tool-repository(the root of the tool's own repository, for a target outside any repository; read from a checkout or through--repo), oruser(applies to every audit on the machine). The repository's file comes first.domain_verbslists the flagged verbs to add under[p6], anddocslinks Configuration. Schema0.9addition.
Publishing a scorecard?
run.invocationmay carry usernames or absolute paths from the machine that produced the scorecard.target.pathis intentionally the basename only and is safe to commit. Reviewrun.invocationbefore publishing.ancdoes not silently redact, since that would surprise users debugging their own runs.
Three shapes of contribution, in order of cost:
- Signal (false-positive report, scoring bug, feature request, registry submission): file an issue with the matching template at github.com/brettdavies/agentnative-cli/issues/new/choose.
- Proposal (new language auditor, scoring-engine rework, registry expansion): open a design issue first; the maintainer signs off before code lands.
- Code: PR against
dev(per branch discipline).
Local setup:
git clone https://github.com/brettdavies/agentnative-cli
cd agentnative-cli
git config core.hooksPath scripts/hooks # fast checks on commit, full CI mirror on push
cargo test
cargo run -- audit .The full tier breakdown, pre-push hook contents, and commit-message conventions live in
CONTRIBUTING.md. Cross-repo routing: principle-level discussion (MUST/SHOULD/MAY tier changes,
new principles, applicability clauses) goes to the
spec repo; site bugs (rendering, performance) to
brettdavies/agentnative-site.
MIT OR Apache-2.0