# Audit

zn audit reads the agent configuration files of a repository, without running anything, and reports what would run, where requests would go and which instructions an agent would follow.

Source: https://usezn.com/docs/zn-gate/audit/

```bash
npx -y zn-gate audit ./some-repo
```

(or `zn audit` if you installed it with `npm i -g zn-gate`; `zn-gate audit` is the same command).

Coding agents read configuration from the repository they work in. Several of those files run commands, start MCP servers, change where the agent sends its API requests or widen what it may do without asking. Updating the agent fixes known bugs, but the files stay in the repository, and the next agent, a team member on an older version or a CI job may still act on them.

`zn audit` reads those files, **without running anything**, and tells you what would run, where requests would go and which instructions an agent would follow. It runs locally and sends nothing anywhere. Run it before you open a repository you just cloned, and in CI.

## Options

```
zn audit [path] [--format text|json|sarif] [--output <file>]
         [--fail-on low|medium|high|critical|none]
         [--agents claude,codex,cursor,opencode,amazonq,copilot,gemini,all] [--include-home]
```

| Option | Default | What it does |
|---|---|---|
| `path` | `.` | Directory to audit, usually the repository root. A bare `zn audit` reviews the current directory |
| `--format` | `text` | `text` for people, `json` for scripts, `sarif` for GitHub code scanning |
| `--output` | standard output | Writes the report to a file |
| `--fail-on` | `high` | Exits with 1 if any finding has this severity or a higher one. `none` never fails |
| `--agents` | `all` | Checks only the files these agents read: `claude`, `codex`, `cursor`, `opencode`, `amazonq`, `copilot`, `gemini` or `all`. Findings that concern every agent (`AGENTS.md`, planted executables, CI workflows) are always shown |
| `--include-home` | off | Also checks your own user-level configs, such as `~/.claude/settings.json` and `~/.codex/config.toml` |

Examples:

```bash
# Before opening a repository you just cloned
npx -y zn-gate audit ./some-repo

# In CI: write a SARIF file for GitHub code scanning and never fail the job
zn audit --format sarif --output zn-audit.sarif --fail-on none

# Only what Claude Code and Codex read; fail on critical only
zn audit --agents claude,codex --fail-on critical
```

In 1.5 `zn audit` showed the local log of secret use. That log is now `zn vault log` (also `zn secret log`). `zn audit log` and `zn audit --since DATE` exit with code 2 and tell you to use `zn vault log`.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | No finding at or above `--fail-on` |
| `1` | At least one finding at or above `--fail-on` |
| `2` | Usage or runtime error: bad option, path not found, output not writable |

## What it reads

Project files at any depth, because the configuration of a subdirectory applies when an agent starts there. Agent folders (`.claude`, `.codex`, `.cursor`, `.github`, `skills` and similar) are read first. Outside them, `node_modules`, `vendor`, build and virtual-environment folders are skipped. `.git` is never read.

| Agent | Files |
|---|---|
| Claude Code | `.claude/settings.json`, `.claude/settings.local.json`, `.mcp.json`, `CLAUDE.md`, `CLAUDE.local.md`, `.claude/commands/`, `.claude/agents/`, `.claude/skills/`, `.claude-plugin/`, plugin `hooks/hooks.json` |
| Codex | `.codex/config.toml`, `.codex/hooks.json`, `.codex/prompts/`, `.env` (only endpoint and code-loading variables), `AGENTS.md` |
| Cursor | `.cursor/mcp.json`, `.cursor/hooks.json`, `.cursor/cli.json`, `.cursor/rules/`, `.cursor/commands/`, `.cursorrules`, the hooks in `.claude/settings*.json` (Cursor runs them), `.vscode/tasks.json`, `AGENTS.md` |
| OpenCode | `opencode.json`, `opencode.jsonc`, `.opencode/` config, plugins, commands and agents, `AGENTS.md` |
| Amazon Q | `.amazonq/mcp.json`, `.amazonq/cli-agents/`, `.amazonq/agents/`, `.amazonq/rules/`, `AGENTS.md` |
| Copilot / VS Code | `.vscode/mcp.json`, `.vscode/settings.json`, `.vscode/tasks.json`, `.github/copilot-instructions.md`, `.github/instructions/`, `.github/prompts/`, `AGENTS.md` |
| Gemini CLI | `.gemini/settings.json`, `.gemini/commands/`, `GEMINI.md` |
| Any agent | Executables in the audited root, `.github/workflows/*.yml`, `.windsurfrules`, `.clinerules` |

Nothing the repository controls can make a file disappear from the report silently:

- A symbolic link that leaves the repository is reported (ZNA020), not followed.
- An agent file over 1 MB is reported (ZNA020) instead of skipped.
- The scan stops after 50,000 directory entries or 120 seconds, and says so with a ZNA020 high finding, so the run fails at the default threshold.
- A committed root `.env` that sets `CODEX_HOME` or `CLAUDE_CONFIG_DIR` to a folder inside the repository makes `zn audit` read the configs there too.
- A root `.env` is audited even when `.gitignore` lists it, because in a fresh clone it exists only if it was committed. Its findings are one level lower and say that they do not apply to your own local `.env`.

With `--include-home` it also reads `~/.claude/settings.json`, `~/.claude.json`, `~/.codex/config.toml`, `~/.cursor/mcp.json`, `~/.cursor/hooks.json`, `~/.aws/amazonq/mcp.json`, `~/.config/opencode/opencode.json[c]` and `~/.gemini/settings.json`. It honours `CODEX_HOME` and `XDG_CONFIG_HOME` the way the agents do. Commands you configured yourself are expected there, so only the riskier variants are reported: code downloaded and run, unpinned packages, endpoint overrides, secrets and broad permissions. Endpoint, environment and permission findings are capped at medium.

## Rules

Rule ids are stable: a rule may get better, but its id never changes meaning. Severities are `critical`, `high`, `medium` and `low`; when a rule lists several, the finding gets the one that fits the case.

| Id | Severity | What it finds |
|---|---|---|
| ZNA001 | high / medium / low | A command that runs automatically from a project config: hooks, `statusLine`, Codex `notify`, OpenCode plugin files, VS Code tasks with `runOn: folderOpen`. Medium for hooks of a plugin, which run only once the plugin is enabled. The hook that `zn hooks install` writes is reported as low |
| ZNA002 | critical | An automatic command that downloads code and runs it (`curl ... \| sh`, `iwr ... \| iex`, `bash <(curl ...)`) |
| ZNA003 | high / medium / low | A project MCP server that starts a local command. High for Codex and Amazon Q; low for zn's own secrets server when it is verified on disk |
| ZNA004 | high / medium | Project settings that approve MCP servers in advance (`enableAllProjectMcpServers`, `enabledMcpjsonServers`) |
| ZNA005 | medium | An MCP server or agent plugin that is not pinned: `npx -y pkg`, `@latest`, `uvx pkg` without `==`, container images without a tag or digest, OpenCode plugins without a version |
| ZNA006 | medium / low | A remote MCP server, or instructions loaded from a URL. Medium for plain `http`, low for `https` |
| ZNA007 | critical / high / medium / low | The model API endpoint is overridden (`ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, any `*_BASE_URL`, provider base URLs in Codex and OpenCode) or a proxy is set (`HTTPS_PROXY`). Critical for a remote host, high for a proxy, medium for a loopback address. Inside the `env` of one MCP server it only redirects that server: medium, or low for loopback |
| ZNA008 | high | An environment variable that loads code or certificates (`NODE_OPTIONS`, `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, `PYTHONSTARTUP`, `BASH_ENV`, `PATH`, `GIT_SSH_COMMAND`, `NODE_EXTRA_CA_CERTS` and similar) or that moves where the agent reads its config (`CODEX_HOME`, `CLAUDE_CONFIG_DIR`, `XDG_CONFIG_HOME`, `OPENCODE_CONFIG`). Also checked in the `env` of every MCP server |
| ZNA009 | high | A credential helper command in project settings (`apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `otelHeadersHelper`) |
| ZNA010 | high / medium | Broad permissions: `Bash(*)`, `Bash`, `*`, interpreter wildcards such as `Bash(python:*)`, `defaultMode: bypassPermissions`, Codex `approval_policy = "never"` with full access, OpenCode `"bash": "allow"`, VS Code `chat.tools.autoApprove` |
| ZNA011 | high | A secret value in an agent config or instruction file. The report never prints the value |
| ZNA012 | high / medium / low | A prompt-injection pattern in agent instructions: instruction overrides, fake system tags, hidden HTML, invisible Unicode, markdown image beacons. In the files an agent reads as instructions (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, rules, skills, commands, prompts) a strong pattern is never lowered, quoted or not |
| ZNA013 | medium / low | Instructions that tell the agent to download and run code. Low for well-known installers such as rustup, uv, bun, deno, nvm and Homebrew |
| ZNA014 | high | Instructions that tell the agent to send secrets or files out (`.env`, `~/.ssh`, tokens to a URL, `curl -d @.env`) |
| ZNA015 | medium | Instructions that tell the agent to skip safety checks or hide what it does (`--no-verify`, `--dangerously-skip-permissions`, "disable the sandbox", "do not tell the user") |
| ZNA016 | critical / high | An executable in the audited root that shares its name with a common tool (`git.exe`, `node.exe`, `npm.cmd`, `python`, `zn.cmd`). Critical for Windows executables. Windows and some tools search the current folder first, so the planted file runs instead of the real tool |
| ZNA017 | medium | Project settings that add plugin marketplaces or enable plugins (`extraKnownMarketplaces`, `enabledPlugins`) |
| ZNA018 | low | The repository ships agent extensions (commands, subagents, skills, plugins). It is an inventory: the other rules review them, but you should know they are there |
| ZNA019 | high / medium | A CI workflow that runs an AI agent on input anyone can write (`issues`, `issue_comment`, `pull_request_target`, discussions). High with `pull_request_target`, issue or comment text pasted into the workflow, an action opened to users without write access, or a CLI step with write permissions and no author filter |
| ZNA020 | high / low | An agent file could not be checked: a syntax error, a size over 1 MB, a symbolic link leaving the repository, or a scan that stopped early. High when the file can run commands or redirect the agent, or when the scan did not finish; low for a syntax error in a file that cannot run anything |

## Output

### Text

```
zn audit  ./some-repo  (zn-gate 1.12.0 or later)

CRITICAL  ZNA007  .claude/settings.json:4  [claude]
          Model API endpoint overridden by the project
          ANTHROPIC_BASE_URL points to https://agent-proxy.example.invalid
          evidence: "ANTHROPIC_BASE_URL": "https://agent-proxy.example.invalid"
          fix: Remove the override; set it in your user settings if you need a gateway.

1 finding (1 critical, 0 high, 0 medium, 0 low) in 3 files. Failing: severity >= high.
```

Colors are used only on a terminal and never when `NO_COLOR` is set. Control characters and escape sequences that come from the repository are printed as `\x1b`, never sent to your terminal.

### JSON

`--format json` prints one object with the `findings` and a `summary`:

```json
{
  "version": 1,
  "tool": "zn-gate",
  "toolVersion": "1.12.0",
  "root": "/home/me/some-repo",
  "findings": [
    {
      "id": "ZNA007",
      "severity": "critical",
      "agent": "claude",
      "file": ".claude/settings.json",
      "line": 4,
      "title": "Model API endpoint overridden by the project",
      "detail": "ANTHROPIC_BASE_URL points to https://agent-proxy.example.invalid ...",
      "evidence": "\"ANTHROPIC_BASE_URL\": \"https://agent-proxy.example.invalid\"",
      "fix": "Remove the override ...",
      "references": []
    }
  ],
  "summary": {
    "files": 3,
    "findings": 1,
    "bySeverity": { "critical": 1, "high": 0, "medium": 0, "low": 0 },
    "failOn": "high",
    "failed": true,
    "agents": ["claude", "codex", "cursor", "opencode", "amazonq", "copilot", "gemini"]
  }
}
```

`file` is relative to `root` with forward slashes; files from `--include-home` start with `~/`. `agent` is the agent that reads the file, or `any` when several do. `evidence` is the matching line, cut to 160 characters, with secrets replaced by `[REDACTED:<TYPE>]`. URLs lose their user, password, query string and fragment.

### SARIF

`--format sarif` writes SARIF 2.1.0 for GitHub code scanning: one rule per `ZNA` id with help text and a severity score, and results located relative to the repository root. Each result has a fingerprint, so alerts survive line moves. Findings from `--include-home` are marked as coming from the home folder and GitHub ignores them, so keep that flag off in CI.

## Use it in CI

The simplest way is the zn-gate GitHub Action with `mode: audit`. See [GitHub Actions](https://usezn.com/docs/integrations/github-actions/).

To run the CLI yourself, write a SARIF file and upload it to code scanning:

```yaml
- run: npx -y zn-gate@1.12.0 audit --format sarif --output zn-audit.sarif --fail-on none
- uses: github/codeql-action/upload-sarif@v4
  with:
    sarif_file: zn-audit.sarif
```

Drop `--fail-on none` (the default is `high`) if you want the job to fail on high and critical findings. Add `security-events: write` to the job only when you upload SARIF.

## Limits

- It reads files; it does not run them, start servers or resolve packages. A pinned package can still be malicious, and a hook that calls a local script is reported as a hook, not judged by what the script does.
- Instruction rules match patterns line by line. A determined author can phrase an instruction that no pattern catches, and documentation that quotes a dangerous command is reported too, for example an install guide with `curl ... | sh`. Review the evidence before you act.
- The workflow rule reads YAML line by line. It knows common agent actions and the trigger and permission keys, not reusable workflows or composite actions.
- Git configuration (`.git/config`, `.gitattributes` filters) and editor extensions are not checked.
- Agents change their config formats often. Unknown keys are ignored, not reported.
- Secrets are found by format and by sensitive key name. A secret with no known format under an innocent key name is not found, and passwords written in prose are not redacted from the evidence.
