Guardrails
A guardrail is a reusable check that runs in your pipeline and produces a verdict. To enable one, list it in buildnote.json and run buildnote guardrails. The CLI fetches the guardrail, runs its check on the runner, records the verdict in Buildnote as a guardrail event, comments the result on the pull request, and can fail the build.
Guardrails cover the rules your team agrees on but has no good place to enforce, such as a commit message format, no merge commits, a dependency policy or a minimum coverage level. You configure guardrails instead of writing them, and every run is recorded. That way you can see how a rule trends across builds, not just whether today's build passed.
Browse the library
Buildnote ships 125 guardrails. You can filter them by name, by what they check, by category or by severity. Open a guardrail to see its inputs, an example configuration and how to fix what it finds.
125 guardrails. Showing the first 40.
azure1
build4
- Dependency updates are automatedbuild/dependency-updates-configuredThe repository configures a tool that opens dependency updates, so upgrades arrive as pull requests you can review instead of as a chore nobody has time for.warning
- The Gradle wrapper is no older than the floorbuild/gradle-version-floorThe Gradle version the wrapper pins is at or above the floor your team sets, so the build runs on a toolchain that is still supported.warning
- The build runs on a managed CI runnerbuild/runs-on-hosted-ciThe artifact was built on a CI platform, not on someone's workstation, so the build is attributable and repeatable.error
- The build tool distribution is verifiedbuild/wrapper-distribution-verifiedThe Gradle wrapper pins the checksum of the distribution it downloads, so nobody can swap the toolchain underneath the build.error
claude3
- Claude runs in CI without bypassing its permissionsclaude/cli-safe-flagsNo Claude CLI invocation the build ran asked to skip its approval step.error
- Claude answers CI in a format a program can readclaude/cli-structured-outputEvery headless Claude CLI invocation the build ran asked for structured output.warning
- Claude reads the instruction file the repository keepsclaude/instructions-symlinkedCLAUDE.md is a symbolic link to AGENTS.md, not a second copy of it or missing altogether.warning
clojure4
- Clojure dependencies are pinnedclojure/dependencies-pinnedEvery dependency in the manifests names a single released version, or the exact commit a git dependency builds from.warning
- No dependencies on a local rootclojure/no-local-dependenciesNo dependency comes from a :local/root, so the build resolves everything from this commit and not from the machine it runs on.warning
- Repositories are ones the team allowsclojure/repositories-allowedEvery Maven repository the manifest adds beyond the defaults resolves from a host on the allowed list.error
- Clojure version is declaredclojure/version-declaredThe project depends on the Clojure release it builds against, and that release is no older than the configured floor.warning
commands1
cpp4
- CMake floor is recent enoughcpp/cmake-minimum-floorA CMake build names the CMake version whose policy defaults it uses, and that version is no older than the configured floor.warning
- Dependencies come from a package managercpp/package-manager-declaredThe checkout declares its dependencies through vcpkg or Conan, so it names the versions it builds against instead of taking whatever the machine has installed.warning
- C++ standard is declaredcpp/standard-declaredA CMake build that enables C++ declares the standard it compiles against, and that standard is no older than the configured floor.warning
- C++ standard is required, not preferredcpp/standard-requiredA CMake build that enables C++ requires the standard it declares, so a compiler that doesn't support it fails instead of falling back to an older one.warning
docker5
- Dockerfile base images are pinneddocker/base-pinnedEvery image a Dockerfile builds from is pinned to a digest, so rebuilding the same commit starts from the same bytes.error
- Dockerfiles declare a health checkdocker/healthcheckEvery Dockerfile declares a HEALTHCHECK, so the orchestrator can tell a container that is running from one that is actually working.warning
- Dockerfile lint findings are cleandocker/lint-cleanEvery finding the Dockerfile linter reported is below the severity your team gates on.warning
- Dockerfiles declare no credential argumentsdocker/no-build-secretsNo ARG or ENV declared in a Dockerfile has a name that looks like a credential.error
- Dockerfiles run as a non-root userdocker/nonroot-userEvery Dockerfile ends with a USER that is not root, so the container runs its process without privileges.error
docs7
- Repository instructs the coding agents working in itdocs/agent-instructionsThe repository has an instruction file for coding agents, long enough to say something and short enough to be read in full.info
- Repository keeps a changelogdocs/changelogThe repository has a changelog, so what changed between two releases is written down instead of pieced together from commits.info
- Repository publishes a code of conductdocs/code-of-conductThe repository has a CODE_OF_CONDUCT.md, so the standard contributors are held to, and who enforces it, is written down.info
- Repository says how to contribute to itdocs/contributingThe repository has a CONTRIBUTING.md explaining how a change is proposed, built and reviewed.info
- Repository states its licencedocs/license-presentThe repository has a licence file, so what people may do with the code is written down, not assumed.warning
- Repository has a READMEdocs/readmeThe repository has a README that says more than just its title.warning
- Repository documents how to report a vulnerabilitydocs/security-policyThe repository has a SECURITY.md saying where to report a security problem.warning
dotnet4
- Package versions are managed centrallydotnet/central-package-managementA single Directory.Packages.props sets the version of every package, instead of each project file setting its own.warning
- Package references carry a versiondotnet/dependencies-versionedEvery PackageReference names its own version or takes one from a central PackageVersion.error
- .NET SDK is pinneddotnet/sdk-pinnedglobal.json names the SDK the build uses and holds it there, so a newer SDK on the runner can't take over.warning
- Target framework is no older than the floordotnet/target-framework-floorEvery net<major>.<minor> framework a project targets is no older than the configured floor.warning
elixir3
- Elixir dependencies are lockedelixir/lockfile-committedThe project commits mix.lock, so fetching the same commit resolves the same packages and verifies them.warning
- No dependency is fetched from gitelixir/no-git-dependenciesNo dependency the project declares is fetched from a git repository.warning
- Elixir version is declaredelixir/version-declaredThe project declares the Elixir version it compiles against, and that version is no older than the configured floor.warning
git4
- Commits come from a known identitygit/author-identity-domainEvery commit in the build range was authored and committed with an email address on a domain your organisation controls.warning
- The change is small enough to reviewgit/changed-files-budgetThe build range changes few enough files that a reviewer can hold the whole change in their head.info
- The change is few enough lines to reviewgit/changed-lines-budgetThe build range adds and removes few enough lines that a reviewer can read the whole change before approving it.info
- Commits follow Conventional Commitsgit/conventional-commitsEvery commit message in the build range follows the Conventional Commits specification, including the subject line, the blank line before the body, and the BREAKING CHANGE footer.error
Categories
Each category is one directory in the library. Its guardrails share a version and any inputs they have in common. A category's page lists all of its guardrails and their shared inputs.
| Category | Guardrails |
|---|---|
azure | 1 |
build | 4 |
claude | 3 |
clojure | 4 |
commands | 1 |
cpp | 4 |
docker | 5 |
docs | 7 |
dotnet | 4 |
elixir | 3 |
git | 7 |
github | 11 |
gitlab | 7 |
golang | 4 |
java | 3 |
jenkins | 2 |
kotlin | 4 |
maven | 4 |
nodejs | 4 |
php | 3 |
python | 4 |
ruby | 4 |
rust | 4 |
scala | 4 |
secrets | 4 |
security | 4 |
supply-chain | 6 |
terraform | 6 |
tests | 4 |
To write your own, see Writing a guardrail. A guardrail rarely gathers its own evidence. Instead, it declares a collector, which gathers information about the repository, the build and the files once per run, and passes the same document to every guardrail that asked for it.
Configuration
Configuring guardrails describes every option.
Running
buildnote guardrails --org company --project web --module ci --build 1234In a CI environment that Buildnote recognises, the organisation, project, module, build, commit and ref are detected automatically, so usually all you need is:
buildnote guardrailsUse --only <id> to run a single guardrail, --dry-run to print the verdicts without recording or reporting anything, and --fail-on never to report results without failing the build. The command reference lists every option.
Verdicts
Every guardrail run ends with one of three verdicts.
| Verdict | Meaning | Event status |
|---|---|---|
passed | The check ran and found nothing over the guardrail's threshold. | successful |
failed | The check ran and found more violations than the guardrail allows. | failed |
skipped | The check couldn't gather its evidence, so it can't give a result. | skipped |
A skip is a normal, expected result. If a guardrail can't see what it needs, it never fails your build. The CLI reports skipped when:
- the check reports that it can't run, for example because a base ref doesn't resolve on a shallow clone
- the check produces no readable result
- the interpreter is missing on the runner, or the check runs past its timeout
- the guardrail's source can't be reached, so its definition or script couldn't be read
- the pinned
sha256doesn't match the script that was fetched (the CLI prints a warning instead of passing quietly)
After a check runs, the exemptions in your check entry are applied to the violations it reported. The remaining violations are compared with the guardrail's maxViolations, and that decides between failed and passed.
Failing the build
failOn decides which verdicts fail the build.
| Value | The build fails when |
|---|---|
error (default) | a guardrail with severity error fails |
warning | a guardrail with severity error or warning fails |
never | never |
The command always records the verdicts and posts the pull request comment before it exits, so a failed build still reports everything it found. A guardrail can still fail the build even if its events can't be submitted.
A guardrail that can't be loaded at all is not a verdict, and failOn doesn't apply to it. If a definition is malformed, declares an id other than the one referenced, or names a script whose extension doesn't match an interpreter, that's a configuration error. The command reports it, runs every other guardrail, and then exits with a non zero code whatever failOn says. A source that simply can't be reached is different: it counts as missing evidence, so it's recorded as skipped.
Reporting
Guardrails use the same reporters as buildnote report. You configure them in the same report section of buildnote.json and adjust them with the same --slack-*, --discord-*, --github-* and --html-* options. Every reporter receives the same verdict table, most severe first, with the evidence and remediation for each failed guardrail. The verdict table is the whole report, so components and template have no effect.
| Reporter | What it does |
|---|---|
github | Writes the markdown to the job summary and posts it on the pull request as a comment titled Guardrails. Every report.github option works the same as for buildnote report: enabled and condition decide whether anything is reported to GitHub, commentEnabled and commentUpdates decide whether the comment is posted and updated, and commentTitle replaces the default title. A re-run updates the existing comment instead of adding a new one, and the default title never clashes with the test summary comment. |
html | Writes a standalone Guardrails document. Without an outputFile, the file is buildnote-guardrails.html. With one, it's written next to it with a -guardrails suffix, so build/report.html gives build/report-guardrails.html and your test report isn't overwritten. |
slack | Posts a notification with one line and a status icon per guardrail. |
discord | Posts the same notification as a Discord message. |
A reporter runs when it's enabled and its condition passes, just like with buildnote report. The condition is evaluated against the guardrail run instead of the test run, so the same expressions refer to the guardrail results:
| Variable | Meaning |
|---|---|
successful failed skipped | Booleans for the run's overall verdict. |
successful.count failed.count skipped.count | How many guardrails reached each verdict. |
total | How many guardrails ran. |
violations | Violations across the whole run. |
status | successful, failed, skipped or unknown. |
always never | true and false. |
org project module build sha ref | The build coordinates. |
If an expression can't be resolved, it becomes empty instead of stopping the run. If a reporter can't be resolved at all, the command reports the problem and skips that reporter. Neither changes the exit code.
The pull request comment needs a GitHub token, from report.github.token or the --github-token option. If there's no token, no pull request, or "commentEnabled": false is set, the command skips the comment and carries on. The GitHub reporter is enabled unless report.github says otherwise, so a repository with no report block still gets the job summary and the comment. Set "github": { "enabled": false } to turn both off. A reporter failure never changes the exit code. Only the verdicts decide that.
Querying verdicts
Every run is recorded as two kinds of guardrail event, which you can query through the guardrails table. One run event summarises the whole command. It holds the total violation count and, as attachments, the facts every collector gathered. Each guardrail also gets a check event with its own verdict, linked back to the run:
SELECT guardrail, status, violations FROM guardrails WHERE build = '1234' AND category = 'check'SELECT guardrail, COUNT(*) AS failures FROM guardrails WHERE status = 'failed' AND category = 'check' GROUP BY guardrail ORDER BY failures DESCRunning a guardrail again in the same build replaces its earlier verdict, so each build has one row per guardrail. The entity_id stays the same across builds, so you can track one guardrail over time.