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.
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.
Copies 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.
run
Executes the case’s checks (all, or the ones named with --check) and appends one record per check to the ledger.
verify
Judges every check on its latest run and every claim on its evidence, and prints the verdict. Runs nothing.
Options of assurance
Flag
Argument
Default
What it does
-d, --directory
<path>
current folder
Project folder.
--dir
<path>
_bmad-output/assurance
Evidence folder inside the project; the ledger lives in <dir>/<case id>/runs.jsonl. Relative, written with /.
--check
<id>
all checks
run only: run just this check. Repeat the flag for several.
--ledger-head
<sha256>
none
verify only: fail unless the ledger still holds the record with this digest (64 lowercase hexadecimal characters).
--emit-check
<file>
none
verify 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
-
off
Print the result as JSON (schemaVersion: 1) instead of text. Errors become { "status": "error", "message": ... }.
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
Key
Rule
schema
Must be bmad-plus/assurance-case/1.
id
Lowercase letters, digits, dots and dashes, up to 64 characters.
title, scope
Free text, 1 to 4000 characters each.
freshness.maxAgeDays
Whole number of days from 1 to 366. Default 30. A run older than this is stale.
freshness.sameCommit
true (default) ties the evidence to the commit checked out and to a clean working tree. Set false only for a project outside git.
checks[].id
Unique id of the check.
checks[].run
The 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[].cwd
Project-relative folder to run in. Default ..
checks[].env
Names 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[].timeoutSeconds
Whole number from 1 to 21600. Default 600.
checks[].expect.exitCode
Exit code that counts as success, 0 to 255. Default 0.
checks[].artifacts
Project-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, argument
Required. The argument says why a passing check shows the claim.
claims[].parent
A 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[].controls
Optional 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[].evidence
List of { check, shows }. check must name a declared check.
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.
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.
It did not start, timed out, exited with another code than expected, or an artifact was not produced by the run.
stale
It 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.
missing
It 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.
At least one selected check failed (did not start, timed out, wrong exit code, or a declared artifact not produced). The records are written anyway.
verify
0
The case is supported.
verify
1
The case is unsupported.
any
3
The 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.
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 sRecorded 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.
npx bmad-plus assurance init _bmad/assurance/release-security.yaml
npx bmad-plus assurance run _bmad/assurance/release-security.yaml
Illustrative transcript, not captured from a real run. Source: tools/cli/commands/assurance.js (initAction, runAction message templates)
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.yamlrelease-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.
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.
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.
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.