Browse the documentation

assurance

Build a security assurance case: claims backed only by checks that really ran, recorded in a hash-chained ledger, then verified at a commit. Shield tooling, exit codes and failure modes.

On this page
  1. Synopsis
  2. The case file
  3. What it reads and writes
  4. The ledger and its key
  5. How the verdict is built
  6. Exit codes
  7. What the output looks like
  8. Errors and what to do
  9. Examples

assurance is the command behind the Shield pack’s security assurance case. A case lists claims about your system (for example “no production dependency has a known high-severity advisory”), gives an argument for each, and supports it with evidence. The only evidence BMAD+ accepts is a check it ran itself: a command, its exit code, a digest of its output and the digests of the files it wrote, appended to a hash-chained ledger. A statement, a document or a link is refused. You use it when someone, an auditor, a customer or your future self, asks you to show that controls hold at a given commit, and “we believe so” is not enough.

Warning What a verdict is not

A supported case shows that the named checks passed at a commit. It does not certify the system and it is not an audit opinion; a qualified auditor decides what the evidence is worth. Also, assurance run executes the commands written in the case with the same trust as your own scripts: read a case from someone else before running it.

Synopsis

In your project terminal
npx bmad-plus assurance init <case-file>
npx bmad-plus assurance run <case-file> [--check <id>]... [--dir <path>] [--json]
npx bmad-plus assurance verify <case-file> [--ledger-head <sha256>] [--emit-check <file.json>] [--dir <path>] [--json]

Actions
ActionWhat it does
initCopies the Shield template to the case file you name. The file must be called <id>.yaml; the id is taken from the name. It never overwrites an existing file.
runExecutes the case’s checks (all, or the ones named with --check) and appends one record per check to the ledger.
verifyJudges every check on its latest run and every claim on its evidence, and prints the verdict. Runs nothing.
Options of assurance
FlagArgumentDefaultWhat it does
-d, --directory<path>current folderProject folder.
--dir<path>_bmad-output/assuranceEvidence folder inside the project; the ledger lives in <dir>/<case id>/runs.jsonl. Relative, written with /.
--check<id>all checksrun only: run just this check. Repeat the flag for several.
--ledger-head<sha256>noneverify only: fail unless the ledger still holds the record with this digest (64 lowercase hexadecimal characters).
--emit-check<file>noneverify only: also write the verdict as a JSON check result for CI. The name must end in .json and must not be the case or its ledger.
--json-offPrint the result as JSON (schemaVersion: 1) instead of text. Errors become { "status": "error", "message": ... }.

The case file

By convention a case lives at _bmad/assurance/<id>.yaml, but any project-relative path works as long as it contains no .. and no symbolic link. init writes the Shield template, which is a complete working example (four checks, one top claim and four sub-claims). Replace every example with your own, commit the file, then run it. Here is the shape; unknown keys are refused.

Shape of a case (shortened from the template)
schema: bmad-plus/assurance-case/1
id: release-security
title: Release security case
scope: >-
  The service at the commit being released.
freshness:
  maxAgeDays: 30
  sameCommit: true
checks:
  - id: unit-tests
    run: [npm, test]
    timeoutSeconds: 900
  - id: secret-scan
    run: [gitleaks, detect, --no-banner, --redact, --report-path, reports/gitleaks.json]
    artifacts: [reports/gitleaks.json]
claims:
  - id: C1
    claim: The release ships no committed credential and no failing control test.
    argument: >-
      Holds when both sub-claims hold.
  - id: C1.1
    parent: C1
    claim: No credential is committed in the repository history.
    argument: >-
      The scanner reads every reachable commit and exits non-zero on a finding.
    controls: [ISO27001:A.5.17, SOC2:CC6.1]
    evidence:
      - check: secret-scan
        shows: gitleaks finds no secret and writes its report.
  - id: C1.2
    parent: C1
    claim: The behaviour the controls rely on is covered by passing tests.
    argument: >-
      The suite exercises access checks and input validation.
    controls: [ISO27001:A.8.29]
    evidence:
      - check: unit-tests
        shows: The test suite passes at this commit.

Keys of a case
KeyRule
schemaMust be bmad-plus/assurance-case/1.
idLowercase letters, digits, dots and dashes, up to 64 characters.
title, scopeFree text, 1 to 4000 characters each.
freshness.maxAgeDaysWhole number of days from 1 to 366. Default 30. A run older than this is stale.
freshness.sameCommittrue (default) ties the evidence to the commit checked out and to a clean working tree. Set false only for a project outside git.
checks[].idUnique id of the check.
checks[].runThe command as a list of separate arguments. No shell is involved. On Windows, programs such as npm are .cmd files and need [cmd, /c, npm, ...].
checks[].cwdProject-relative folder to run in. Default ..
checks[].envNames of extra environment variables the check may see. By default it inherits only PATH, HOME, the temporary folders and a few Windows system variables. BMAD_PLUS_ASSURANCE_KEY can never be listed.
checks[].timeoutSecondsWhole number from 1 to 21600. Default 600.
checks[].expect.exitCodeExit code that counts as success, 0 to 255. Default 0.
checks[].artifactsProject-relative files the command must write; each is hashed with the run. A file left unchanged by the run does not count as produced.
claims[].id, claim, argumentRequired. The argument says why a passing check shows the claim.
claims[].parentA claim declared earlier. A claim with no evidence must have at least one sub-claim, otherwise it is “only asserted” and the case does not load.
claims[].controlsOptional list of FRAMEWORK:ID references. Accepted frameworks: ISO27001, SOC2, GDPR, EU-AI-Act, NIS2, NIST-800-53. A mistyped or invented control is refused.
claims[].evidenceList of { check, shows }. check must name a declared check.

What it reads and writes

  • init writes one file: the case, at the path you give (parent folders are created).
  • run reads the case, executes each selected check as a child process without a shell, and appends one line per check to <dir>/<case id>/runs.jsonl. While it works it holds runs.jsonl.lock. It reads git (rev-parse, status, ls-files) to record the commit and whether the tree was clean.
  • verify reads the case, the ledger, git and the declared artifacts. It writes nothing except the optional --emit-check file.
  • It opens no network connection itself. The commands you declare may do whatever they do; that is why the data-handling inventory lists assurance-checks as a flow that exists only when you invoke run.
  • Refused everywhere: paths outside the project, and any path that passes through a symbolic link.

The ledger and its key

Each record in runs.jsonl is one JSON line: the check id, a sequence number, the digest of the command, the commit (revision) and dirty (whether tracked changes or untracked, non-ignored files existed), start and end times, the exit code, whether it timed out, the output size and digest with a redacted excerpt of its last 2048 bytes, the digest of each declared artifact (or null when the run did not write it), the digest of the previous record, and its own sha256. Editing, reordering or removing a record before the last one breaks the chain, and verify then judges the whole case unsupported.

The chain alone only reveals careless edits: anyone who can write the file can append a record that verifies, or cut off the last records. Two things kept outside the ledger close that gap.

  • The key. Set BMAD_PLUS_ASSURANCE_KEY (at least 32 characters) from a CI secret. run then adds an HMAC-SHA256 to every record and verify refuses any record the key does not authenticate. The key is never handed to a check. Use the same key for run and verify: a ledger written without the key is refused once the key is set.
  • The head. run prints the digest of its last record. Keep it where the ledger’s writers cannot change it (the CI job log, the audit file) and pass it to verify --ledger-head. A ledger that no longer holds that record is refused, so removed runs are noticed.

How the verdict is built

Status of one check (from its latest run)
StatusMeaning
passedRan, did what the check expects, and is fresh.
failedIt did not start, timed out, exited with another code than expected, or an artifact was not produced by the run.
staleIt passed, but at another commit, on uncommitted changes or untracked files, with a command that has changed since, with an artifact that changed since, older than maxAgeDays, or with an end time in the future.
missingIt never ran.

A claim is supported only when all its evidence checks are passed and all its sub-claims are supported; otherwise it is unsupported, with the reasons. The case is supported only when every claim is, the ledger is intact, any --ledger-head is still in it, and, with sameCommit: true, there is a commit and the working tree has no uncommitted changes or untracked files (the case’s own ledger folder and artifacts do not count). The verdict also lists controls.supported and controls.unsupported: the controls tagged on claims that have, or lack, executed evidence.

Exit codes

Exit codes by action
ActionCodeMeaning
init0Case file written.
run0Every selected check ended as its check expects.
run1At least one selected check failed (did not start, timed out, wrong exit code, or a declared artifact not produced). The records are written anyway.
verify0The case is supported.
verify1The case is unsupported.
any3The command could not do its job: unknown action, case missing or invalid, bad key or head, ledger not intact on run, lock held, check not declared, --emit-check refused. Printed as assurance: <message> on the error stream.

What the output looks like

The three transcripts below are composed from the message templates in the source, with illustrative values (the paths, timings and digests are made up). They are marked as simulated for that reason.

Start a case, then run itSimulated
npx bmad-plus assurance init _bmad/assurance/release-security.yaml
_bmad/assurance/release-security.yaml: case release-security started from the Shield template.
Replace every example claim and check with those of the project, commit it, then run it.
npx bmad-plus assurance run _bmad/assurance/release-security.yaml
/home/me/app/_bmad-output/assurance/release-security/runs.jsonl
  passed unit-tests               exit 0 in 41.3 s
  passed dependency-audit         exit 0 in 6.8 s
  passed secret-scan              exit 0 in 3.1 s
  passed review-gate              exit 0 in 1.2 s
Recorded 4 run(s); ledger head 3f9c1b7e5a2d4c6e8b0a1d3f5c7e9b2a4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c.
Keep the head outside the ledger and verify with bmad-plus assurance verify _bmad/assurance/release-security.yaml --ledger-head 3f9c1b7e5a2d4c6e8b0a1d3f5c7e9b2a4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c.

Illustrative transcript, not captured from a real run. Source: tools/cli/commands/assurance.js (initAction, runAction message templates)

Verify at the same commitSimulated
npx bmad-plus assurance verify _bmad/assurance/release-security.yaml --ledger-head 3f9c1b7e5a2d4c6e8b0a1d3f5c7e9b2a4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c --emit-check ci/assurance.json
release-security: supported — 5/5 claim(s) supported, 4 recorded run(s)
  controls with executed evidence: ISO27001:A.8.8, NIST-800-53:RA-5, ISO27001:A.5.17, SOC2:CC6.1, ISO27001:A.8.32, SOC2:CC8.1, ISO27001:A.8.29
  check: /home/me/app/ci/assurance.json

Illustrative transcript, not captured from a real run. Source: tools/cli/commands/assurance.js (verifyAction message template)

Verify after the code moved onSimulated
npx bmad-plus assurance verify _bmad/assurance/release-security.yaml
release-security: unsupported — 3/5 claim(s) supported, 4 recorded run(s)
  stale   unit-tests: it ran at 1a2b3c4d5e6f, not at 9f8e7d6c5b4a
  unsupported claim C1: sub-claim C1.4 is unsupported
  unsupported claim C1.4: evidence unit-tests is stale
  controls with executed evidence: ISO27001:A.8.8, NIST-800-53:RA-5, ISO27001:A.5.17, SOC2:CC6.1, ISO27001:A.8.32, SOC2:CC8.1
Exit code 1. Run the stale check again at the new commit, then verify.

Illustrative transcript, not captured from a real run. Source: tools/cli/commands/assurance.js (verifyAction), tools/cli/lib/assurance.js (judgeCheck reasons)

With --emit-check, the file written is a JSON object with the schema bmad-plus/assurance-check/1: the status, the revision, the claims with their reasons, the controls, and a github entry (name, conclusion of success or failure, a title such as 3/5 claims supported and a Markdown summary table) that a CI step can post as a check run. It holds no timestamp, so the same verdict produces the same file.

Errors and what to do

Messages printed after assurance: (exit code 3)
MessageCauseWhat to do
unknown action "x" (init, run, verify)The first argument is not one of the three actions.Use init, run or verify.
no assurance case at <path>The case file does not exist.Check the path, or create it with init.
<path> already existsinit never overwrites.Pick another name or edit the existing case.
case file: name it <id>.yaml ...The file name is not <id>.yaml with a valid id.Use lowercase letters, digits, dots and dashes in the id.
<case>: claim X is only asserted; support it with evidence from a check or with sub-claimsA claim has neither evidence nor sub-claims.Add evidence naming a check, or sub-claims with parent.
evidence must name a check that runs (check: <id>); a statement, document or link is only assertedAn evidence item is not { check, shows }.Write a check whose command shows the claim, and point to it.
BMAD_PLUS_ASSURANCE_KEY must hold at least 32 charactersThe key is set but too short.Generate a longer secret, or unset the variable.
the ledger <file> is not intact (record N ...)run found an edited, reordered or unauthenticated record (for example written before the key was set).Keep the file for inspection and record into another --dir.
another run holds <file>.lock; delete it only if no run is in progressA run is active, or one was interrupted.Wait; if none is running, delete the lock file.
check X is not declared in <case>--check names an unknown check.Use an id from the checks list.
the ledger head must be a record digest of 64 lowercase hexadecimal characters--ledger-head is malformed.Copy the head exactly as run printed it.
--emit-check writes a .json file / --emit-check must not overwrite the case or its ledgerBad output name.Choose a .json path that is neither the case nor the ledger.
<path> passes through a symbolic link / must be a relative path inside the project, written with /The path leaves the project or crosses a link.Use a normal relative path with / separators.

Reasons that make verify exit 1

  • the ledger is not intact: ... or ... is not authenticated by BMAD_PLUS_ASSURANCE_KEY: a record was altered, or the key does not match. Rebuild the ledger by running the checks again into a fresh --dir.
  • the ledger no longer holds the head ... it was anchored to: records were removed or the ledger replaced.
  • the working tree has uncommitted changes or untracked files: commit, then run and verify again. The evidence describes the commit, not your working tree.
  • there is no commit to bind the evidence to: the folder is not a git repository with a commit. Set freshness.sameCommit: false only for a project outside git.
  • A check is missing, failed or stale: the line <status> <check>: <reasons> says why. Run that check again at the current commit.

Examples

Have Shield build and run a case for a release

You are about to release and a customer asked for evidence that security controls hold at that exact commit. The working tree is committed.

In your AI assistant
Shield, start an assurance case for this release. Keep each claim small enough that one command shows it, tag the claims with the controls they support, then run the checks at this commit and verify the case. Tell me which claims are not supported and why.

What you should get Shield runs bmad-plus assurance init _bmad/assurance/<id>.yaml, replaces the template’s example claims and checks with ones for your project (one top claim, sub-claims each backed by a command that exits non-zero when the claim fails), and asks you to commit the file. It then runs assurance run, keeps the printed ledger head, and runs assurance verify --ledger-head <head> --emit-check .... It reports each unsupported claim with the reason printed by verify and lists the controls with executed evidence. It states whether a key and a head were used; without them it says the evidence is recorded, not tamper-proof. It never calls an unsupported claim met.

Explain an unsupported verdict

A CI job failed on assurance verify, and the case was green last week.

In your AI assistant
Shield, verify the release-security case and explain every unsupported claim. Do not re-run anything until you have told me what is stale and why.

What you should get Shield runs bmad-plus assurance verify _bmad/assurance/release-security.yaml (read-only) and reads the output: the status line, each non-passed check with its reasons (another commit, uncommitted changes, a changed command or artifact, older than maxAgeDays), and each unsupported claim. It explains them in plain words, tells you which checks to run again with assurance run --check <id>, and waits for your go-ahead before executing anything, because run executes the commands written in the case.