Skip to content

chant audit

llms.txt lists every page for an agent
chant audit [path] [flags]
chant audit <repo-url> [flags]

chant audit runs chant’s security, correctness, and best-practice rules against an existing repository’s CI/CD pipelines, IaC and manifests (Kubernetes, Helm, Docker, CloudFormation, ARM, Config Connector, fountain), including files chant didn’t generate. Point it at a local directory or a public repo URL and it produces a tiered report grouped by category, with findings worth a pull request split from lower-priority hygiene.

It is read-only and runs outside a chant project. It reads the repo’s CI files directly and runs the same post-synth security checks the build pipeline uses (GitHub Actions GHA*, GitLab CI WGL*; Forgejo workflows are GitHub-dialect, so the GitHub tier applies to them too).

What it scans:

  • .github/workflows/*.{yml,yaml} (GitHub Actions / Forgejo)
  • .forgejo/workflows/*.{yml,yaml} (Forgejo / Codeberg / Gitea)
  • .gitlab-ci.yml (GitLab CI)
  • Kubernetes manifests (any *.{yml,yaml} with apiVersion + kind), for a local path — runs the WK8*/ARGO* checks (privileged containers, host namespaces, hardcoded secrets, image pinning, …)
  • Docker — Dockerfile/*.Dockerfile and Compose files (*.{yml,yaml} with services:), for a local path — runs the DKRD* checks (root user, :latest images, exposed SSH, …)
  • AWS CloudFormation — .json/.yaml/.yml/.template with AWSTemplateFormatVersion or AWS::* resources, for a local path — runs the WAW*/COR*/EXT* checks (public S3, open security groups, IAM wildcards, unencrypted storage, …). Both JSON and YAML templates.
  • Azure ARM — .json deployment templates ($schema with deploymentTemplate + resources), for a local path — runs the AZR* checks (public blob access, missing encryption/TDE, HTTPS-only, TLS 1.2, …)
  • GCP Config Connector — *.{yml,yaml} with a cnrm.cloud.google.com apiVersion, for a local path — runs the WGC* checks (public IAM, open firewalls, public Cloud SQL, missing encryption, Workload Identity, …)
  • Helm charts — any directory with a Chart.yaml, for a local path — runs the WHM* checks (privileged/root containers, :latest images, inline Secrets, chart structure, …). The whole chart is audited as one unit.
  • Terraform root modules, any directory with at least one .tf file directly inside it (or .tf.json, .tofu, .tofu.json), for a local path. A root that holds a .tofu file is read as OpenTofu reads it: its .tofu files count, and x.tofu replaces x.tf. Not recursive: a nested directory with its own .tf files (e.g. modules/foo) is its own root module and its own audit unit. Install @intentius/chant-lexicon-terraform like any other lexicon.
  • fountain manifests — *.{yml,yaml} with apiVersion: fountain.dev/v1 (fountain apply files, single or multi-document), for a local path — runs the FTN* checks (unrestricted networking, credential-shaped env_vars, vault shadowing, unresolvable ${VAR} references, …). The documents are parsed back into the entity graph, so these are the same rules chant build runs — and a fountain document is never misread as a Kubernetes manifest.
  • Secrets & credentials (SEC*) — every file above, plus .env*, .pem, .key, .crt, .cer, .pfx, and .p12 files, are scanned for likely secrets regardless of which lexicons are installed. See Secrets & credentials detection below.
  • Wrangler config (WRG*) — wrangler.toml, Cloudflare Workers’ native deploy config. No lexicon parses TOML, so this is audit-only (#446): there is no lexicon package to install, and chant reads Wrangler config for these checks and never writes it. Runs regardless of which lexicons are installed, the same as SEC*. See Wrangler config detection below.
  • nginx config (NGX*) — nginx.conf, and .conf files under nginx-ish directories (nginx/, conf.d/, sites-available/, sites-enabled/, snippets/) whose parsed content confirms they are nginx. Like WRG*, this is audit-only (#1979). See nginx config detection below.

--agents audits the agent configuration installed on the machine instead of a repository, running the AGT* rules. See Auditing agent configuration below.

Every rule a finding cites links to its entry in the audit rules reference.

The report groups findings into:

  • Quick wins (deterministic) — safe mechanical fixes shown as a ready-to-apply unified diff (add a least-privilege permissions: block, pin actions to a commit SHA, pin images to a digest).
  • Needs review (guidance) — findings that need a judgement call (script injection, pull_request_target hardening, secret scope), clustered by the security area they belong to.
  • Report-only (hygiene) — style/perf/deprecation, never something to open a PR over.

The minimized (markdown) report leads with ready-to-apply fix diffs; rule IDs link to the rules reference:

# chant audit
4 findings — 3 quick-win, 0 needs-review, 1 report-only (0 error, 3 warning, 1 info).
## Quick wins (deterministic)
### `.github/workflows/ci.yml`
Addresses [GHA033](/chant/lint-rules/audit-rules/#gha033) (Blanket write-all permissions):
```diff
-permissions: write-all
+permissions:
+ contents: read
```
Needs a value before it can be auto-patched:
- **[GHA021](/chant/lint-rules/audit-rules/#gha021)** (`build`) — Pin `actions/checkout` to a full 40-char commit SHA.
<details><summary>Report-only (hygiene) — 1</summary>
…
</details>

The expanded (HTML) report renders the same findings with badges, cards, and syntax-highlighted diffs. View a sample report ->

FlagDescription
-f, --format <fmt>Output format: stylish (default), json, sarif, markdown, html
--jsonShortcut for --format json
-o, --output <file>Write the report to a file instead of stdout
--tier <tier>all (default) or merge-worthy to show only PR-worthy findings
--fail-on <level>Exit non-zero on merge-worthy, warning, error, or none (default none)
--template <file>html only: a full HTML template override (see below)
--theme <file>html only: a JSON file of theme knobs (title, logo, accent, footer)
--max-files <n>Local path only: how many files the walk takes before it stops (default 1000). See Scan limits
--agentsAudit this machine’s agent configuration instead of a repository (see below)
--scope <list>--agents only: comma-separated system, user, project (default: all)
--runtime <list>--agents only: comma-separated claude, codex, gemini, opencode, cursor (default: all)
--all-projects--agents only: scan every project registered in ~/.claude.json, not just the path argument

chant audit --agents changes the subject from a repository to the machine you are on: the instruction files, MCP servers, skills, plugins, and tool permissions that configure your coding agents.

It is audited apart from CI because its threat model differs. An agent config is executable surface that runs with your own credentials on your own machine, usually with no review step. An MCP server entry is a command line that runs at session start, a skill is instructions the model will follow, and a permission allowlist decides what runs without anyone being asked. All of it is typically installed with a single command and rarely re-read afterwards.

Terminal window
chant audit --agents # every scope, every harness
chant audit --agents --scope user # just the home directory
chant audit --agents --format html -o agents.html # shareable report
chant audit --agents --tier merge-worthy --fail-on merge-worthy # for CI

Three scopes are scanned, in the order harnesses merge them:

ScopeWhereWhy it matters
systemmachine-wide managed policyAdministrator-set, not user-editable
useryour home directoryApplies to every project you open, including repos you don’t own
projectthe path argument (default .), or every registered project with --all-projectsNarrowest, but most likely to have arrived from someone else

--all-projects reads the project list the harness already maintains in ~/.claude.json, so it needs no disk crawl and no configuration. Registrations whose directory no longer exists are skipped. The path argument is still included, so a project you are in but haven’t opened before is still scanned:

Terminal window
chant audit --agents --scope project --all-projects --tier merge-worthy

When a scan does not cover everything, the report says so (“Scanned 1 project root; 64 more are registered in ~/.claude.json and were not visited”), and when it does, it states the breadth instead, because “5 findings” reads very differently across one project than across sixty-five.

Five harnesses are recognized (Claude Code, codex, Gemini, opencode and Cursor), and each is normalized onto one model, so a finding reads the same regardless of which tool’s dialect declared it.

The report leads with an inventory (what is configured, before any judgement about it), because for most users that list has never been seen in one place. It then states its own edges: locations probed, files that could not be parsed, and registered projects it did not visit. The rules it applies are the AGT family in the audit rules reference.

To turn what it finds into chant code, see chant import --agents.

A local audit walks the tree once and stops after 1000 files. Every file the walk reaches counts toward that limit, including the ones it does not audit. A repository with a large docs/ directory can therefore hit it early. Directories the walk skips (node_modules, .git, dist, and most dot-directories) are never counted.

Pass --max-files <n> to raise the limit. A repository URL is fetched with its own limits, so the flag is refused there.

When the walk stops with files left over, the report says so. Text output (stylish, markdown and html) leads with a note:

Note: The scan of ./repo stopped at 1000 files, so part of the tree was not audited. The limit counts every file the walk reaches, not only the ones it audits. Raise it with --max-files <n>.

JSON output carries a truncated field with the limit and the flag that raises it. The field is absent when the whole tree was read:

{ "schemaVersion": "1.0", "status": "ok", "truncated": { "limit": 1000, "flag": "--max-files" }, "...": "..." }

SARIF has no place for the note, so with --format sarif it goes to stderr instead.

Up to v0.80.0 a truncated scan read as a whole one on stdout, and v0.80.0 warned about it on stderr. This is one of the level-0 changes on the exception list (#2528).

TF023 reports Terraform state and .terraform/ directories that are committed. On a local path, a path that any .gitignore between it and the scan root ignores is not committed, so it is not reported. Auditing from the repository root and from a subdirectory such as infra/ agree about infra/terraform.tfstate whenever the .gitignore that ignores it is at or below infra/. Up to v0.80.0 only the scan root’s own .gitignore was read (#2528).

  • Minimized: --format markdown (or stylish/json/sarif). Compact, pastes into a PR or issue; quick wins lead with ready-to-apply diffs, the rest collapses into <details>.
  • Expanded: --format html. A self-contained, presentable HTML document (inline CSS, opens in a browser, shareable as one file): severity badges, tier counts, styled finding cards, syntax-highlighted fix diffs, and a provenance snapshot (host, repo, commit SHA, files scanned, timestamp, chant version) so every finding is anchored to an exact commit.
  • --theme theme.json fills knobs into the default template:

    { "title": "Acme CI Audit", "logo": "https://acme.example/logo.svg", "accent": "#0b7285", "footer": "Internal use only." }
  • --template my.html replaces the whole document. Available slots: {{title}}, {{accent}}, {{logo}}, {{styles}}, {{body}}, {{footer}}. The findings render into {{body}}.

For tooling, --format json emits a versioned envelope (stable contract). The same object is embedded in the HTML report inside <script type="application/json" id="chant-audit-report">, so the expanded report is both human- and machine-readable.

{
"schemaVersion": "1.0",
"tool": { "name": "chant-audit", "version": "0.7.0" },
"status": "ok",
"snapshot": { "target": "...", "host": "github.com", "repo": "owner/repo", "commit": "…", "files": ["..."], "generatedAt": "…", "toolVersion": "0.7.0" },
"summary": { "total": 0, "quickWin": 0, "needsReview": 0, "reportOnly": 0, "errors": 0, "warnings": 0, "infos": 0, "security": 0, "correctness": 0, "bestPractice": 0 },
"findings": [
{ "checkId": "GHA033", "severity": "warning", "message": "…", "file": "…", "entity": "…", "lexicon": "github",
"tier": "merge-worthy", "fixKind": "deterministic", "category": "security", "title": "…", "remediation": "…",
"authority": [{ "name": "…", "url": "…" }],
"lineage": [{ "tool": "zizmor", "rule": "excessive-permissions", "url": "…", "relation": "equivalent" }],
"docUrl": "https://intentius.io/chant/lint-rules/audit-rules/#gha033" }
]
}

Stability contract:

  • Additive changes (new fields) keep the same schemaVersion; renamed or removed fields bump it.
  • Consumers should check the major version and ignore unknown fields.
  • The snapshot anchors findings to an exact commit for reproducibility.
  • status is "ok" when at least one lexicon ran, "no-lexicons" when none could (see Prerequisites). Check it before trusting an empty findings. An optional unclaimed list names files that wanted a lexicon that is not installed.
  • An optional truncated object (limit, flag) means the local walk stopped at its file limit, so findings covers only part of the tree (see Scan limits).
  • lineage credits the open-source tools whose rules check the same condition, with the upstream rule id and how the two relate. It is credit, not authority, and is empty for a rule with no known counterpart. The audit rules reference lists every tool credited.
  • For maximum stability, use --format sarif, an industry-standard format versioned independently of chant.

--format sarif emits a SARIF 2.1.0 document, the interchange format most CI platforms and code-review surfaces (GitHub code scanning, GitLab, Azure DevOps, many SAST/IDE viewers) render as inline, file-and-line annotations instead of scraped text:

  • Each run.tool.driver.rules[] entry carries the finding’s helpUri (the same authority citation as the other formats) plus a help.text with the catalog’s remediation guidance, and a properties.category/properties.dimension pair (security, correctness, or best-practice) for rule-level triage. A rule with prior art also carries properties.priorArt, the same lineage entries the JSON report emits.
  • Each run.results[] entry carries properties.tier (merge-worthy or report-only) alongside the standard ruleId, level, message, and locations (file + line, when known).
  • Secrets findings (SEC001–SEC010) redact the same as every other format — a result’s message.text never carries the matched value, only the rule id, kind, location, and fingerprint.
  • --agents emits the same property bag under a chant audit --agents driver name, so one consumer can triage both. Its rules’ helpUri points at the rule’s own entry in the audit rules reference rather than an external authority, since an AGT finding is about a local file.

Upload it to GitHub code scanning from a workflow:

- name: Audit
run: npx @intentius/chant audit . --format sarif --output audit.sarif
- name: Upload to code scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: audit.sarif

Auditing a URL calls the host’s API. Auditing a local path reaches no network, and packages/core/src/audit/fetch.ts is the only audit module that touches the network at all (Network Egress). Set a host-specific token to lift rate limits (and, for GitHub, to resolve action SHAs and image digests for the inline fix diffs):

  • GITHUB_TOKEN: GitHub, plus action-SHA and image-digest resolution (always queries the GitHub/registry APIs)
  • GITLAB_TOKEN: GitLab
  • CODEBERG_TOKEN: Codeberg

Without a token, unauthenticated GitHub is limited to 60 requests/hour and pin fixes fall back to “needs a value” instead of an inline diff. Tokens are never sent across hosts.

chant audit runs each lexicon’s security checks, so it needs the lexicon packages for the file types you want audited installed alongside @intentius/chant:

Terminal window
# CI/CD pipelines
npm i @intentius/chant-lexicon-github # GitHub Actions (also Forgejo workflows)
npm i @intentius/chant-lexicon-gitlab # GitLab CI
npm i @intentius/chant-lexicon-forgejo # Forgejo dialect
# Infrastructure & manifests
npm i @intentius/chant-lexicon-k8s # Kubernetes manifests
npm i @intentius/chant-lexicon-helm # Helm charts
npm i @intentius/chant-lexicon-docker # Dockerfiles / Compose
npm i @intentius/chant-lexicon-aws # CloudFormation
npm i @intentius/chant-lexicon-azure # ARM templates
npm i @intentius/chant-lexicon-gcp # Config Connector
npm i @intentius/chant-lexicon-fountain # fountain manifests

Without a project to install into, put every lexicon you need on the same npx path:

Terminal window
npx -p @intentius/chant -p @intentius/chant-lexicon-github -p @intentius/chant-lexicon-k8s chant audit .

A bare npx @intentius/chant audit . has no lexicons and cannot detect anything. It does not report clean in that case. It says it had nothing to look with, lists each file that looked like it wanted a lexicon (by path, filename, and a few content markers), prints the npx -p ... line that covers exactly those files, and exits with code 2:

chant audit had nothing to look with: no audit lexicon is installed, so nothing under . was inspected.
This is not a clean result. Detection and checks live in the lexicon packages.
Files that wanted a lexicon:
.github/workflows/ci.yml -> github
Dockerfile -> docker
infra/main.tf -> terraform
k8s/deploy.yaml -> k8s
Run it with the lexicons those files need:
npx -p @intentius/chant -p @intentius/chant-lexicon-github -p @intentius/chant-lexicon-docker -p @intentius/chant-lexicon-terraform -p @intentius/chant-lexicon-k8s chant audit .

With --json the same run emits "status": "no-lexicons" plus unclaimed, missingLexicons, and install fields, and no findings.

When some lexicons are installed and files on disk look like they belong to one that is not, the report leads with a one-line note naming the skipped files and the npm i to fix it, and --json carries them in unclaimed. Terraform is listed the same way, npm i @intentius/chant-lexicon-terraform included.

If a lexicon package is present for detection but its checks fail to load, the command reports an error naming the package.

A chant project can declare a lexicon by module path in chant.config.ts (A lexicon in your own project). Such a lexicon has no package. The install line leaves it out, and the messages above name its path instead. Audit reads the target’s config without running it, since it audits code it must not execute. When the config’s lexicons cannot be read that way, audit prints a note saying so. Every lexicon is then treated as a package. It never loads a lexicon’s module from a path either: its checks always come from installed lexicon packages. See Commands that read chant.config.ts without running it.

The SEC* rules (see the rules reference) scan the raw text of every scanned file for likely credentials. They are independent of lexicon, so they run even for file types no installed lexicon claims. Two layers:

  • Pattern detectors for well-known credential shapes: SEC001 AWS access key ID, SEC002 AWS secret access key, SEC003 GitHub token, SEC004 Slack token, SEC005 Google API key, SEC006 Stripe live secret key, SEC007 PEM private key block, SEC008 bearer/authorization token, SEC009 credentials embedded in a connection string (scheme://user:pass@host).
  • SEC010, a high-entropy heuristic: a catch-all for a random-looking token (real character-class diversity, not a pure-hex run like a git SHA) that doesn’t match a known shape. Its entropy threshold defaults to a conservative 4.0 bits/char and is tunable via .chant-audit.json (below).

Redaction is unconditional. A secrets finding’s message, JSON, SARIF, and HTML report output carry only the rule id, the kind of secret, the file/line location, and a non-reversible fingerprint of the value. The matched value itself never appears.

Two ways, the same as for other false positives:

  • Inline marker: a comment containing chant-audit-ignore on the flagged line (or the line immediately above it, for a PEM block whose -----BEGIN…----- line can’t carry a trailing comment) suppresses every secrets finding on that line. Scope it to specific rules with chant-audit-ignore: SEC001,SEC010; bare chant-audit-ignore suppresses all of them.

    # chant-audit-ignore: SEC010
    fixture_token: "not-a-real-secret-just-looks-random-9f3k2"
  • Config allowlist: a .chant-audit.json at the audited root:

    {
    "secrets": {
    "entropyThreshold": 4.5,
    "allow": [
    { "ruleId": "SEC010", "file": "fixtures/example.env", "fingerprint": "a1b2c3d4e5f6a1b2" }
    ]
    }
    }

    Copy the fingerprint straight from the finding’s message or JSON output. It is a hash of the value, so allowlisting a false positive never means writing the secret into the config. An allow entry needs ruleId and/or fingerprint; file alone never blanket-suppresses everything in a file.

A Terraform finding (TF*) is a different mechanism: it reads # chant-ignore, # chant-ignore-file, and # chant-ignore-block comments straight out of the .tf source, so it works identically through chant audit and chant build. See the terraform lexicon’s Suppressing a finding section.

The WRG* rules (see the rules reference) read wrangler.toml with a small dependency-free TOML parser (packages/core/src/toml.ts) that covers what real deploy configs use (tables, arrays of tables, dotted/nested [env.<name>] blocks, inline tables) short of the full spec. Like SEC*, it runs with no @intentius/chant-lexicon-* package installed. Wrangler config is audit-only, and chant has no authoring surface or serializer for it.

Checks cover: a production-named environment exposed on the public *.workers.dev subdomain (WRG001), a credential-shaped key committed in [vars] instead of wrangler secret (WRG002), observability explicitly disabled (WRG003), an unscoped wildcard route matching every zone on the account (WRG004), a non-production environment sharing a KV/R2/D1 binding id with production (WRG005), and static assets served from the project root (WRG006).

A file that fails to parse (or isn’t found) contributes no findings rather than failing the audit, as elsewhere in the engine.

The NGX* rules (see the rules reference) read nginx’s native directive/block config, parsed with a small dependency-free recursive-descent parser built for this (packages/core/src/audit/nginx.ts), which reads directives, nested blocks, quoted arguments and comments with line numbers. Like SEC*/WRG*, it runs independent of lexicon: nginx config is audit-only, and chant has no authoring surface or serializer for it.

Detection is two-stage, because .conf names are shared with unrelated software (systemd drop-ins live in *.d/ directories too): a path candidate (nginx.conf anywhere, or .conf under nginx/, conf.d/, sites-available/, sites-enabled/, snippets/) only produces findings when its parsed form also carries an nginx marker directive (http, server, location, upstream, …).

Checks cover: deprecated TLS protocols in ssl_protocols (NGX001), weak cipher suites in ssl_ciphers (NGX002), directory listing via autoindex on (NGX003), the alias path-traversal shape — a location prefix without a trailing slash over an alias with one (NGX004), a stub_status endpoint with no access restriction (NGX005), server version disclosure — server_tokens never disabled (NGX006), and access logging disabled at http/server scope (NGX007).

An nginx file that fails to parse is skipped without failing the audit.

Terminal window
# Audit the current repo
chant audit
# Audit a local checkout
chant audit ../some-repo
# Audit a public repo over the network
chant audit https://github.com/owner/repo
chant audit https://gitlab.com/group/project
chant audit https://codeberg.org/owner/repo
# Save a shareable markdown report
chant audit https://github.com/owner/repo -f markdown -o report.md
# Generate a presentable, branded HTML report
chant audit https://github.com/owner/repo -f html --theme theme.json -o report.html
# Only the PR-worthy findings, as JSON, failing CI if any exist
chant audit . --tier merge-worthy --json --fail-on merge-worthy
--fail-onExit 1 when…
none (default)never (read-only)
merge-worthyany merge-worthy finding exists
warningany error- or warning-severity finding exists
errorany error-severity finding exists (chant #2113)

error is a severity floor, decoupled from merge-worthy/warning’s tier- based checks: it’s the value that lets a new rule ship reporting at "warning" and be promoted to "error" later without breaking a pipeline pinned to --fail-on error. See Rule Configuration for the full explanation.

Independent of --fail-on, the command exits 1 when no audit lexicon is installed (nothing was inspected, see Prerequisites) and on a hard failure such as an unreachable repo or a missing path. Exit 2 is the drift code and audit never returns it.

chant