chant audit
Synopsis
Section titled “Synopsis”chant audit [path] [flags]chant audit <repo-url> [flags]Description
Section titled “Description”chant audit runs chant’s security, correctness, and best-practice rules against an existing repository’s config — CI/CD pipelines plus 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 requires no 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}withapiVersion+kind), for a local path — runs theWK8*/ARGO*checks (privileged containers, host namespaces, hardcoded secrets, image pinning, …) - Docker —
Dockerfile/*.Dockerfileand Compose files (*.{yml,yaml}withservices:), for a local path — runs theDKRD*checks (root user,:latestimages, exposed SSH, …) - AWS CloudFormation —
.json/.yaml/.yml/.templatewithAWSTemplateFormatVersionorAWS::*resources, for a local path — runs theWAW*/COR*/EXT*checks (public S3, open security groups, IAM wildcards, unencrypted storage, …). Both JSON and YAML templates. - Azure ARM —
.jsondeployment templates ($schemawithdeploymentTemplate+resources), for a local path — runs theAZR*checks (public blob access, missing encryption/TDE, HTTPS-only, TLS 1.2, …) - GCP Config Connector —
*.{yml,yaml}with acnrm.cloud.google.comapiVersion, for a local path — runs theWGC*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 theWHM*checks (privileged/root containers,:latestimages, inline Secrets, chart structure, …). The whole chart is audited as one unit. - Terraform root modules, any directory with at least one
.tffile directly inside it, for a local path. Not recursive: a nested directory with its own.tffiles (e.g.modules/foo) is its own root module and its own audit unit. Install@intentius/chant-lexicon-terraformlike any other lexicon. - fountain manifests —
*.{yml,yaml}withapiVersion: fountain.dev/v1(fountain applyfiles, single or multi-document), for a local path — runs theFTN*checks (unrestricted networking, credential-shapedenv_vars, vault shadowing, unresolvable${VAR}references, …). The documents are parsed back into the entity graph, so these are the same ruleschant buildruns — 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.p12files, 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. TOML is not a format any lexicon parses, so this is audit-only (#446): no lexicon package to install, no authoring surface — chant never writes Wrangler config, it only reads it for these checks. Runs regardless of which lexicons are installed, the same asSEC*. See Wrangler config detection below. - nginx config (
NGX*) —nginx.conf, and.conffiles under nginx-ish directories (nginx/,conf.d/,sites-available/,sites-enabled/,snippets/) whose parsed content confirms they are nginx. Audit-only likeWRG*(#1979): chant never writes nginx config, it only reads it for these checks. See nginx config detection below.
A different subject entirely, behind its own flag: --agents audits the agent configuration installed on the machine rather than a repository — instruction files, MCP servers, skills, plugins, and tool permissions, 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-privilegepermissions: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_targethardening, secret scope), clustered by the security area they belong to. - Report-only (hygiene) — style/perf/deprecation, never something to open a PR over.
Example
Section titled “Example”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 ->.
| Flag | Description |
|---|---|
-f, --format <fmt> | Output format: stylish (default), json, sarif, markdown, html |
--json | Shortcut 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) |
--agents | Audit 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 |
Auditing agent configuration (--agents)
Section titled “Auditing agent configuration (--agents)”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 worth auditing separately from CI because it is a different threat model. 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.
chant audit --agents # every scope, every harnesschant audit --agents --scope user # just the home directorychant audit --agents --format html -o agents.html # shareable reportchant audit --agents --tier merge-worthy --fail-on merge-worthy # for CIThree scopes are scanned, in the order harnesses merge them:
| Scope | Where | Why it matters |
|---|---|---|
system | machine-wide managed policy | Administrator-set, not user-editable |
user | your home directory | Applies to every project you open, including repos you don’t own |
project | the path argument (default .), or every registered project with --all-projects | Narrowest, 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 sitting in but haven’t opened before is not
dropped:
chant audit --agents --scope project --all-projects --tier merge-worthyWhen 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.
Report modes
Section titled “Report modes”- Minimized —
--format markdown(orstylish/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.
Customizing the HTML report
Section titled “Customizing the HTML report”-
--theme theme.jsonfills knobs into the default template:{ "title": "Acme CI Audit", "logo": "https://acme.example/logo.svg", "accent": "#0b7285", "footer": "Internal use only." } -
--template my.htmlreplaces the whole document. Available slots:{{title}},{{accent}},{{logo}},{{styles}},{{body}},{{footer}}. The findings render into{{body}}.
Machine-readable output
Section titled “Machine-readable output”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
snapshotanchors findings to an exact commit for reproducibility. statusis"ok"when at least one lexicon ran,"no-lexicons"when none could (see Prerequisites). Check it before trusting an emptyfindings. An optionalunclaimedlist names files that wanted a lexicon that is not installed.lineagecredits 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, integrate via
--format sarif— an industry-standard, independently-versioned format.
--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’shelpUri(the same authority citation as the other formats) plus ahelp.textwith the catalog’s remediation guidance, and aproperties.category/properties.dimensionpair (security,correctness, orbest-practice) for rule-level triage. A rule with prior art also carriesproperties.priorArt, the samelineageentries the JSON report emits. - Each
run.results[]entry carriesproperties.tier(merge-worthyorreport-only) alongside the standardruleId,level,message, andlocations(file + line, when known). - Secrets findings (
SEC001–SEC010) redact the same as every other format — a result’smessage.textnever carries the matched value, only the rule id, kind, location, and fingerprint. --agentsemits the same property bag under achant audit --agentsdriver name, so one consumer can triage both. Its rules’helpUripoints at the rule’s own entry in the audit rules reference rather than an external authority, since anAGTfinding 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.sarifTokens
Section titled “Tokens”Auditing a URL calls the host’s API — auditing a local path reaches nothing, 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— GitLabCODEBERG_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.
Prerequisites
Section titled “Prerequisites”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:
# CI/CD pipelinesnpm i @intentius/chant-lexicon-github # GitHub Actions (also Forgejo workflows)npm i @intentius/chant-lexicon-gitlab # GitLab CInpm i @intentius/chant-lexicon-forgejo # Forgejo dialect
# Infrastructure & manifestsnpm i @intentius/chant-lexicon-k8s # Kubernetes manifestsnpm i @intentius/chant-lexicon-helm # Helm chartsnpm i @intentius/chant-lexicon-docker # Dockerfiles / Composenpm i @intentius/chant-lexicon-aws # CloudFormationnpm i @intentius/chant-lexicon-azure # ARM templatesnpm i @intentius/chant-lexicon-gcp # Config Connectornpm i @intentius/chant-lexicon-fountain # fountain manifestsWithout a project to install into, put every lexicon you need on the same npx path:
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 surfaces that as an error naming the package.
Secrets & credentials detection
Section titled “Secrets & credentials detection”The SEC* rules (see the rules reference) scan the raw text of every scanned file for likely credentials — independent of lexicon, so they run even for file types no installed lexicon claims. Two layers:
- Pattern detectors for well-known credential shapes:
SEC001AWS access key ID,SEC002AWS secret access key,SEC003GitHub token,SEC004Slack token,SEC005Google API key,SEC006Stripe live secret key,SEC007PEM private key block,SEC008bearer/authorization token,SEC009credentials 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 conservative4.0bits/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 — never the matched value itself.
Suppressing a secrets finding
Section titled “Suppressing a secrets finding”Two ways, matching the other false-positive escape hatches in the tool:
-
Inline marker — a comment containing
chant-audit-ignoreon 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 withchant-audit-ignore: SEC001,SEC010; barechant-audit-ignoresuppresses all of them.# chant-audit-ignore: SEC010fixture_token: "not-a-real-secret-just-looks-random-9f3k2" -
Config allowlist — a
.chant-audit.jsonat the audited root:{"secrets": {"entropyThreshold": 4.5,"allow": [{ "ruleId": "SEC010", "file": "fixtures/example.env", "fingerprint": "a1b2c3d4e5f6a1b2" }]}}Copy the
fingerprintstraight from the finding’s message or JSON output — it’s a hash of the value, so allowlisting a false positive never means writing the secret into the config. An allow entry needsruleIdand/orfingerprint;filealone 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.
Wrangler config detection
Section titled “Wrangler config detection”The WRG* rules (see the rules reference) read wrangler.toml — parsed with a small dependency-free TOML parser built for this (packages/core/src/toml.ts), not a full spec implementation, but enough for real deploy configs (tables, arrays of tables, dotted/nested [env.<name>] blocks, inline tables). Like SEC*, this runs independent of lexicon — it does not need any @intentius/chant-lexicon-* package installed, since Wrangler config is audit-only: 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 — same tolerance as the rest of the engine.
nginx config detection
Section titled “nginx config detection”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) — directives, nested blocks, quoted arguments, comments, with line numbers. Like SEC*/WRG*, this runs independent of lexicon: nginx config is audit-only — 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).
A file that fails to parse contributes no findings rather than failing the audit — same tolerance as the rest of the engine.
# Audit the current repochant audit
# Audit a local checkoutchant audit ../some-repo
# Audit a public repo over the networkchant audit https://github.com/owner/repochant audit https://gitlab.com/group/projectchant audit https://codeberg.org/owner/repo
# Save a shareable markdown reportchant audit https://github.com/owner/repo -f markdown -o report.md
# Generate a presentable, branded HTML reportchant 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 existchant audit . --tier merge-worthy --json --fail-on merge-worthyExit codes
Section titled “Exit codes”--fail-on | Exit 1 when… |
|---|---|
none (default) | never (read-only) |
merge-worthy | any merge-worthy finding exists |
warning | any error- or warning-severity finding exists |
error | any 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 2 when no audit lexicon is installed (nothing was inspected, see Prerequisites) and 1 on a hard failure such as an unreachable repo or a missing path.