Skip to main content
The JSON emitter is the source of truth. SARIF, JUnit, Markdown and PDF are all derived from the same Scan model that --output json serialises. When in doubt, read the JSON. Source: reports/json_report.py.

When to use this page

  • You want the raw machine-readable scan output for a custom dashboard or threshold checker.
  • You’re writing a parser and need the canonical-form guarantees (sorted keys, deterministic shape).
  • You need a signed artifact for an audit trail.

Generate one

agent-guardian scan --system-prompt prompt.txt --model stub \
  --output json --output-path scan.json
The same JSON is always persisted to ~/.agentguardian/scans/<scan_id>/scan.json regardless of which --output you ask for — even a --output pdf run writes the canonical JSON alongside the PDF. --output json --output-path scan.json just adds a second copy at the path you name. You can also regenerate the JSON from a stored scan:
agent-guardian report cli-3a4c1d9c2840 --output json --output-path scan.json

What you get

The schema is agentguardian-scan-v1 (SCHEMA_VERSION in reports/json_report.py:52). Top-level keys, in canonical (sorted) order:
KeyTypeNotes
aivssint 0–100The headline score.
aivss_formula_versionstraivss-v1. Bumps on any scoring change.
asi_scores{str: float}Per-category 0–100, keyed by ASI01ASI10.
bandstrEXCELLENT / GOOD / WARNING / POOR / CRITICAL / not_evaluated.
budgetobject | nullRuntime USD outcome.
completenessobject | nullHow much of the planned attack work ran.
cost_usdfloatActual LLM spend.
coverageobjectCounters reconstructed from memory.jsonl.
coverage_gradestrAF. A = every ASI category covered by real evidence.
created_atstr (ISO 8601)UTC if no tzinfo, else local-with-offset.
duration_secondsfloatWall-clock runtime.
engineobject | nullLLM model spec per role ({commander, attacker, evaluator} → model id).
evaluation_modestrreal ⇔ a real LLM judged turns; stub forces band: not_evaluated.
findingsFinding[]One entry per landed attack. See Evidence timeline.
findings_summaryobjectPre-aggregated counts by ASI / severity.
modestrfast / smart / full.
mode_authoritativebooltrue only for full. Veto for --fail-under.
package_versionstrThe agent-guardian PyPI version that emitted this.
probe_library_versionstr2026.05 etc. — bumps when the corpus changes.
scan_idstrGenerated id (e.g. cli-3a4c1d9c2840).
scoring_validboolfalse for stub LLM / empty corpus.
signaturesobjectHMAC + Ed25519 blocks. Stripped before recomputing the signing input.
stopped_reasonstrcompleted / budget / early_stop / cancelled.
sub_scores{str: float}The six PRD §6 sub-scores.
targetobject{mode, ref, inferred_goal, profile_source}.
tierstrDetected or forced tier (T1T4).
tokens_totalintTotal input+output tokens across all roles.
undertestedstr[]ASI categories the scan launched but exercised too thinly.
The audit envelope is added under contract-driven scans (contract sha256, authorization ref, suppressed tool attempts, egress-refused turns). See reports/json_report.py:169.

Canonical form (the rules that make it deterministic)

Same inputs always produce byte-identical output. Three rules guarantee that:
  1. json.dumps(..., indent=2, sort_keys=True) — every object’s keys are sorted lexicographically before serialisation.
  2. signatures is excluded from the signing input. The HMAC + Ed25519 blocks are computed over _strip_signatures(payload) (everything except the signatures field) so signing an already-signed payload twice yields the same bytes.
  3. PII redaction is on by default (redact_pii=True). The shared redact_finding helper scrubs all five fields before serialisation — see Evidence timeline.
Together these mean: same scan, same redaction policy, same signing key → same bytes. That’s the property agent-guardian verify relies on.

Signatures

Every JSON report carries two signature channels under signatures:
{
  "signatures": {
    "hmac_sha256": {
      "alg": "hmac-sha256",
      "value": "…",
      "secret_id": "default"
    },
    "ed25519": {
      "alg": "ed25519",
      "public_key": "…",
      "value": "…"
    }
  }
}
  • HMAC-SHA256 — uses AGENT_GUARDIAN_SIGNING_SECRET (or the documented default for local-only smoke). The public default is never accepted on verify — without the real secret, the HMAC channel is integrity-only.
  • Ed25519 — uses a long-lived signer key under ~/.agentguardian/keys/. The public key is embedded so the report is self-verifying for integrity; trust requires anchoring (see below).
The signing input is canonical JSON of the payload minus the signatures key. Verifiers reconstruct the same bytes and recompute both channels.

Verifying a JSON report

verify is fail-closed: a green result requires a pinned trust anchor. Without one, integrity passes but the result is UNANCHORED and the command exits non-zero.
agent-guardian verify scan.json \
  --pubkey-file ./trusted-signer.pub \
  --secret "$AGENT_GUARDIAN_SIGNING_SECRET"
OutcomeWhat it meansExit
schema: OK + HMAC: OK + Ed25519: OK + trust anchor: PINNEDBytes match + anchored to your pinned key. Quotable.0
schema: OK + integrity green + trust anchor: UNANCHOREDBytes match but no anchor supplied. Don’t quote.1
Any channel FAIL or schema invalidTampered or wrong-anchor.1
The verify exit code is the same EXIT_FAIL_UNDER (1) a failed --fail-under gate uses — CI gates treat tamper and risk-floor identically.

Read it in this order

When you first open a scan.json:
  1. band — first glance. Human label.
  2. aivss — score behind the band. Trust only when scoring_valid: true AND mode_authoritative: true.
  3. evaluation_modereal = a real LLM judged turns. stub forces band: not_evaluated and the number is meaningless.
  4. mode_authoritativetrue only for --mode full.
  5. coverage_grade + undertested — categories launched but too thinly tested for “no findings” to be safety evidence.
  6. findings — sorted by severity then descending confidence.
The Markdown emitter’s top-5 follows the same rank, so a JSON-first reader and a Markdown-PR-comment reader see the same top findings in the same order.

Anti-patterns

Don’t treat aivss as authoritative without first checking mode_authoritative AND scoring_valid. A --mode fast stub run can still emit a 100 — and --fail-under will refuse to gate-pass on it for exactly that reason.
Don’t re-sign a modified payload to mask edits. The Ed25519 public key is embedded; anchoring on the real trusted public key will reject a forged re-sign. The verify command’s UNANCHORED state is not an OK — gate on ok=true, not integrity_ok=true.
Don’t parse aivss out of stdout. Read the JSON. The canonical artifact is always written; stdout is a UX surface that may change.

Next step

SARIF export

The same findings as a SARIF 2.1.0 file for GitHub Code Scanning.

Markdown export

The same findings as a flat Markdown report for PR comments.

Report schema

Field-by-field schema reference.

Upload SARIF to GitHub

Walk-through for github/codeql-action/upload-sarif@v3.