Skip to content

Invariant index

Binding rules of the project. Each entry points to the tracked doc that holds the canonical wording for that rule. Linted by scripts/check-orphan-invariants.sh — every entry here must resolve to an existing file under docs/, and every docs/**/*.md (minus an explicit EXEMPT allowlist) must appear here. That allowlist covers generator-owned pages that state no rule of their own — being generated is not itself the criterion, since reference/flake-outputs.md is generated and still carries an entry below; the live-status template pages (dashboard.md, releases.md), whose content is a rendering rather than a rule; overview pages, which route to rules held elsewhere (security/threat-model.md, index.md); install/consume-from-flake.md, which only tells a consumer how to use the flake; and this index itself, which cannot hold an entry pointing at itself. install/nix.md is on the allowlist too, but unlike the others it still carries an entry below (its declared-systems rule), so its exemption is inert. install/docker.md is not exempt: it holds the OCI-image and manifest digest-pinning rules and is indexed below like any other rule-bearing page. The array in scripts/check-orphan-invariants.sh is the authoritative list.

Entry paths are lowercase ASCII and are read under a C locale against the link-path class in scripts/check-orphan-invariants.sh: a doc whose path falls outside that class (an uppercase or non-ASCII letter, say) never matches an entry, so it is reported as an unreferenced doc in every locale. A .md link target here that falls outside that class (an uppercase or non-ASCII letter, a leading digit, a URL or a ../ path) is reported as an off-class link rather than skipped, so a broken pointer of that shape fails the lint too.

Each entry carries an enforcer: ...; ci: ...; hook: ... annotation that scripts/refresh-enforcement-matrix.sh renders into security/enforcement-matrix.md. ci: names the ci.yml job(s) that enforce the rule — for a script enforcer, the job that runs it; for a rule held by evaluation, a smoke test, or a third-party action, the job that carries it. For most values the generator validates only that the name is a key in that file, so an enforcer that runs only in a scheduled workflow says ci: - and names the workflow in its prose. A job that reaches an enforcer only through its test harness is not recorded there either: harness-group counts only for the rules whose roster entry in scripts/run-harness-group.sh names an enforce script to run after the harness passes, so a rule whose harness merely exercises the enforcer — even against the checkout, as check-flake-lock-staleness.test.sh does — says ci: -. That one value is asserted rather than described: the generator reads the roster through scripts/run-harness-group.sh --print-roster and fails if an entry claims ci: harness-group without naming an enforce script the roster runs, or if a roster enforce script is named by no entry that says ci: harness-group. doc-freshness is different by construction: its tests/refresh-*.test.sh harnesses run the generators over the real tree and are the job's whole purpose, so the generator-owned rules record it. hook: names the pre-commit hook(s) that enforce the rule the same way; the generator validates it against the flake's devTooling.<system>.preCommitHooks keys.

Behavior rules for the AI assistant and other non-binding guidance live mostly in an untracked assistant-tooling tree. The committed part of .claude/ is assistant tooling rather than a binding rule, so it is out of scope for this index either way.

Security

  • Workflow SHA pinning — every non-local uses: SHA-pinned (path-relative ./… refs excepted). → security/repo-config.md
  • action pin comment — every SHA-pinned uses: carries an exact patch-tag comment (# vX.Y[.Z] — at least two numeric components) — never absent, never a non-version comment, never a floating major (# vX) — with inline # patch-tag-exception: <reason> markers for refs without a patch tag. → architecture/pin-convention.md
  • Pin digest provenance — an action-pin SHA or container digest may not move under an unchanged version label (a repointed released tag); floating-major (# vN) digest moves must be reachable from the upstream default branch. → security/repo-config.md
  • Renovate invariants — the helpers:pinGitHubActionDigests preset, a per-manager pinDigests: true for github-actions, minimumReleaseAge set, and no top-level automerge. → security/repo-config.md
  • No dead renovate markers — every # renovate: marker's file is consumed by a live customManager (a managerFilePatterns entry scopes it and a matchStrings entry matches a line). → security/repo-config.md
  • Bump-script integrity — URL prefix, .digest, atomic write, API-version header. → security/verification.md
  • verify-latest-release parity + attribution — SRI drift = incident; per-reason notify bodies. → security/verification.md
  • Failure-reason ladder coverage — every id:-carrying step in verify-latest-release.yml's verify job is mapped to a reason token by the attribution ladder, in execution order, with every token documented; a step may opt out only with an inline # reason-ladder-exempt: <reason> marker, and a marker on a step the attribution env already reads, or on the attribution step itself, excuses nothing and is reported as drift rather than accepted. → security/verification.md
  • Gitleaks / TruffleHog / Dependency review / Trivy / Grype / SBOM — scanner scope, thresholds, and notify-job split; Gitleaks, TruffleHog, and Dependency review are required checks, the CVE scans and SBOM attestation are advisory. → security/verification.md
  • Cosign keyless signing — per-arch images and the multi-arch index are cosign-signed keyless at release time; the verify-side identity pin is its own entry below. → security/verification.md
  • gh attestation verify --repo pin — every gh attestation verify invocation passes --repo rvenutolo/linPEAS-flake. → security/verification.md
  • cosign verify identity + issuer pin — every cosign verify* subcommand pins --certificate-identity[-regexp] AND --certificate-oidc-issuer. → security/verification.md
  • Release-asset blob signatures — every primary GitHub Release asset has a sibling .sigstore bundle signed via cosign keyless; verified at release time and weekly. → security/verification.md
  • Release-asset provenance sidecar — every primary GitHub Release asset has a sibling .intoto.jsonl SLSA build-provenance bundle (captured from actions/attest-build-provenance); satisfies OSSF Scorecard releasesHaveProvenance probe. → security/verification.md
  • Bump credentials blast-radius — GitHub App, no PAT, no git push. → security/trust-model.md
  • PR-triggered workflow secret allowlist — only secrets.GITHUB_TOKEN. → security/trust-model.md
  • flake.lock input staleness — every top-level flake.lock input must have moved within the bound declared for it (14 days for the branch-tracked high-churn pair, 120 days for the rest), so a refresh mechanism that has silently stopped surfaces as a filed issue rather than as a frozen-but-green tree; the age probe runs as the daily flake-lock-staleness-check workflow, which is not a required check; scope is top-level inputs only because a transitive node's age reports on its parent's pin, and an input the bound table does not name is an operational error rather than a pass. → architecture/auto-update.md
  • flake.lock input provenance — a flake.lock bump flake.nix does not declare may only move rev/narHash/lastModified; a top-level input add/remove/repoint or any node repointed at any depth fails the gate unless flake.nix declares a different url for that same input between base and head (per input name, top-level only, so a transitive repoint is never corroborated and an unparsable flake.nix is an operational error); the entry point is each lock's own top-level .root field (missing/non-string .root is an operational error, a base/head root-id mismatch fails closed) and top-level input refs resolve through follows paths (string = node id, array = path walked from that root, bounded by a nesting ceiling and a shared step budget so a cycle exhausts the ceiling like any over-deep chain) before comparison, an unresolvable ref fails closed, and a ref-shape change whose resolved source is unchanged passes by design; once identity is clean, every node present on both sides whose identity is unchanged but whose rev moved undeclared must be a fast-forward per the GitHub compare API (ahead passes; behind, diverged, or a rev the repo does not know fails; any other API failure or a malformed response is an operational error; a flake.nix-corroborated move is counted and not probed; a non-github node is named as unprobed rather than skipped silently). → security/trust-model.md
  • harden-runner — first step in every job. → security/trust-model.md
  • harden-runner block egress — every harden-runner step uses egress-policy: block with a non-empty per-job allowed-endpoints:. → security/trust-model.md
  • egress allowlist matches tool inventory — every job's allowed-endpoints list carries the hosts its tool inventory reaches, the ghcr blob host if it carries ghcr.io, the complete Docker Hub pull host set if it carries any Docker Hub host (plus the push host when the job logs in or pushes), a complete sigstore host set if it carries any sigstore host, the set declared beside the notify-workflow-result composite if the job runs it, and none of a fixed denylist of hosts nothing in this repo reaches. → security/trust-model.md
  • Per-job timeout-minutes — every job declares explicit timeout-minutes (reusable-workflow caller jobs excepted — the key is invalid on that shape). → security/workflow-hardening.md
  • Workflow YAML holds no Unicode space — workflow and composite-action YAML holds none of the listed Unicode spaces or line separators in any position, so text scanners that read blankness with [[:space:]] and the YAML consumers read one thing. → security/workflow-hardening.md
  • Workflow concurrency group — every workflow declares top-level concurrency.group. → security/workflow-hardening.md
  • Checkout persist-credentials false — every actions/checkout step sets persist-credentials: false. → security/workflow-hardening.md
  • upload-artifact strict — every actions/upload-artifact step sets if-no-files-found: error. → security/workflow-hardening.md
  • Workflow on.branches main-only — every pull_request:/push: trigger declares branches: [main] exactly. → security/workflow-hardening.md
  • pull_request_target forbidden — no workflow uses the pull_request_target trigger. → security/workflow-hardening.md
  • Auto-merge decline gate — every workflow run-block calling gh pr merge --auto inspects PR state and aborts on a CLOSED/MERGED PR so a declined or merged PR is never resurrected. → security/workflow-hardening.md
  • scripts/*.sh shebang + pipefail — every executable under scripts/ starts with #!/usr/bin/env bash and uses set -Eeuo pipefail, while every sourced library under scripts/lib/ satisfies the inverse: a shellcheck shell= directive, no shebang, no shell options of its own. → security/workflow-hardening.md
  • Harness preamble — every tests/*.test.sh opens with the canonical preamble: exact shebang, set -Eeuo pipefail, the strict IFS line, and a readonly REPO_ROOT derived from git rev-parse --show-toplevel. → security/workflow-hardening.md
  • No opaque process-substitution loops in scripts — no script anywhere under scripts/ — including the sourced libraries in scripts/lib/ — feeds a redirection from a process substitution, whatever the producer, because the producer's exit status stays in its own subshell; diff <(...) <(...) stays legal. → security/workflow-hardening.md
  • Library sourcing is tool-free — a source/. library path never resolves through a command substitution of any kind, and BASH_SOURCE never appears inside a command substitution anywhere else under scripts/ either, so a script's own directory resolves by parameter expansion (${BASH_SOURCE[0]%/*}), never through something that needs readlink, dirname, or any other tool on PATH. → security/workflow-hardening.md
  • Could-not-run guards exit 2 — no script anywhere under scripts/, libraries included, exits 1 out of a guard whose test is purely an availability check (! command -v X, ! require_tool X, [[ ! -f|-r|-e|-d|-s|-x PATH ]], [[ -f PATH ]] || { … }), and none takes a required value through a ${var:?} expansion, whose failure is the shell default status of 1; a guard whose missing input is itself the finding opts out with an inline # exit-code-exempt: <rationale> marker, and a marker excusing no guard this rule matches is reported as its own finding. → security/workflow-hardening.md
  • Temp files created through the guarded helper — no script anywhere under scripts/, libraries included, creates a temp file with a bare mktemp — scripts/lib/temp.sh, which holds the one sanctioned invocation, is the sole exception; every other creation runs through make_temp (scripts/lib/temp.sh), which reports an unwritable TMPDIR as exit 2 instead of letting mktemp's own exit 1 read as a content violation, and a call whose failure is itself the finding opts out with an inline # exit-code-exempt: <rationale> marker, and a marker excusing no creation this rule matches is reported as its own finding. → security/workflow-hardening.md
  • Repository root resolved through the guarded helper — no script anywhere under scripts/, libraries included, resolves the repository root with a bare git rev-parse --show-toplevel — scripts/lib/repo.sh, which holds the one sanctioned lookup, is the sole exception; every other lookup runs through repo_toplevel, which reports a directory outside a git work tree as exit 2 instead of letting git's own 128 end the caller or a fallback read the wrong tree, and no script calls repo_toplevel inside the arguments of a declaration builtin, which would lose that 2; matching is per physical line, a site that never runs the lookup (a string literal) or whose failure is itself the finding opts out with an inline # exit-code-exempt: <rationale> marker, and a marker excusing no site this rule matches is reported as its own finding. → security/workflow-hardening.md
  • Exit contracts document could-not-run — every script directly under scripts/ that can reach exit 2, whether through a literal exit 2/return 2 or a direct call to an exit-2-capable library helper, says so in its header; four contract shapes count, the scan does not recurse into scripts/lib/, and there is no exemption marker. → security/workflow-hardening.md
  • Third-party tool use is guarded — every tool a script directly under scripts/ (top level only; scripts/lib/ is out of scope) invokes that a shell can genuinely lack (jq, yq, gh, nix, docker, and the rest of the third-party set) is guarded in that same script by require_tool, a command -v test, or a library helper that guards it for the caller, so an absent binary is reported as a could-not-run rather than becoming a malformed-input verdict, a clean pass over input nobody read, or an uncataloged exit code; command words are read from the shfmt parse tree so a tool named in a comment, a message string, or a sed program is not mistaken for an invocation. → security/workflow-hardening.md
  • Regex ranges hold ASCII — a bash =~ regex, a GNU grep pattern or a GNU sed script in scripts/*.sh, scripts/lib/*.sh or tests/*.sh holds no locale-bound range between ASCII letters or digits outside a C locale, since a range follows collation and under en_US.UTF-8 admits characters the CI runner refuses; a grep or sed call locale-bound on purpose carries a reasoned marker; what the lint reads is stated in its header. → security/workflow-hardening.md
  • No control characters in tracked paths — no path git reports for the tree (tracked, or untracked-and-unignored) carries a byte in the 0x01-0x1F control range or DEL (0x7F). → security/workflow-hardening.md
  • awk file operands spelled explicit — every awk invocation in a script under scripts/ (sourced libraries included) carrying a file operand spells it "$(awk_path "${var}")", since awk reads a bare operand shaped name=value as a variable assignment rather than a filename and silently scans nothing. → security/workflow-hardening.md
  • Filesystem enumerations run through the helper — every find, git ls-files and git ls-tree in a repo script or test harness (scripts/*.sh and tests/*.test.sh as git pathspecs, so scripts/lib/ and nested harnesses are included, less tests/fixtures/) is an argument to enumerate_into, or runs inside a function that helper is handed by name, or carries an inline # enumerate-exempt: <rationale> marker; the helper asserts scan breadth, which a producer's exit status cannot, since one that enumerates nothing still exits 0. Copying a producer word to a variable is banned at the assignment, since the use site's command word is then a value no single pass can resolve; the array form is read only at element 0, its command head, so a producer word at any other index is data, and a producer word planted at a non-head index and later spliced into command position by index arithmetic still evades. A marker excusing no producer this rule matches is reported as its own finding. → security/workflow-hardening.md
  • Glob-driven scans run through the helper — across the same scan set, no for loop expands a glob at its own loop head, no array assignment expands one in its element list, and neither expands, unquoted, a variable this same file assigns a glob pattern; every glob-driven scan fills its array through glob_into (scripts/lib/enumerate.sh), or carries an inline # glob-exempt: <rationale> marker naming an empty match set as that site's normal state, since a pattern matching nothing expands to nothing and neither a loop body that never runs nor an array that came back empty has a violation to report; a marker excusing no glob site this rule matches is reported as its own finding. → security/workflow-hardening.md
  • Filter-narrowed scans run through the helper — across the same scan set, a variable named FILTER or ending _FILTER may be read at file scope to reach filter_into (scripts/lib/enumerate.sh) and narrow an already-enumerated set, but not again inside a for or while loop over the narrowed selection, nor in a function that loop calls by name — one hop out; a function called only by another function the loop calls still evades and stays outside what this pass decides — and not at all in a file that never calls filter_into, unless carrying an inline # filter-exempt: <rationale> marker; a read outside every loop and every hop-reached function stays legal without one, since an in-loop read re-applies the filter test where nothing asserts it matches anything — over a set filter_into never narrowed, a filter matching nothing leaves a body that never runs and a clean exit — and a file with no filter_into call asserts that selection's size nowhere. Copying a filter value into a variable whose own name does not match the filter pattern is banned at the assignment, wherever the copy is later read, because every arm above keys on the name of the variable being read; the sanctioned readonly FILE_FILTER="${WORKFLOW_FILE_FILTER:-}" stays legal because its own target matches the pattern. A marker excusing no filter site this rule matches is reported as its own finding. → security/workflow-hardening.md
  • Payload shape-gate scenario coverage — every script matching the four-arm external-payload predicate (gh api, a *_JSON_OVERRIDE variable, a bare stdin slurp, or a scoped flake.lock read) carries a harness scenario asserting exit 2 against a malformed payload — anchored to the scenario call's own bare positional exit-code argument, never a textual 2 substring — or a # payload-subject-exempt: <rationale> marker; a marker on a script the predicate does not match is reported as drift. → security/workflow-hardening.md
  • Payload source-naming runs through the helper — no assignment in scripts/*.sh, scripts/lib/*.sh or tests/*.sh sets a variable to a bare *_OVERRIDE variable name, bare or declared (local, readonly, declare, export); every payload source is named by payload_source_into (scripts/lib/payload.sh), which fills a caller variable rather than printing, since a namer read as $(...) in argument position discards its own status and hands the shape gate an empty source name — or the file carries an inline # payload-source-exempt: <rationale> marker whose rationale is non-empty, and a marker on a file holding no assignment the rule matches is reported as drift. → security/workflow-hardening.md
  • Payload reads run through the helper — every cat -- <path> command in scripts/*.sh is a violation, captured or not, unless the path traces to a make_temp/mktemp result the same script created, or the file carries an inline # payload-read-exempt: <rationale> marker whose rationale is non-empty; every payload read is filled by read_json_payload_into (scripts/lib/payload.sh), and a marker on a file holding no such command is reported as drift. → security/workflow-hardening.md
  • check-script ↔ test pairing — every scripts/check-*.sh has a matching tests/check-*.test.sh and vice versa, modulo the enforcer's named EXEMPT list. → security/workflow-hardening.md
  • test-runner reachability — every test harness is executed by some runner, across two scan sets: tests/*.test.sh, and the harnesses tracked under .claude/. The runners are the run-harness-group HARNESSES array, the refresh-*.test.sh doc-freshness glob, a lint-groups.yml member, and a direct workflow invocation. → security/workflow-hardening.md
  • harness assertion discrimination — a harness scenario's expected substring must appear in its own recorded output and in no sibling scenario's output (a sibling asserting the same substring, one whose output is identical after clock normalization, or one that harness_assert_exempt names for that substring is skipped), and every output-asserting harness is wired to the gate, modulo the enforcer's named EXEMPT array. → security/workflow-hardening.md
  • harness exemption ratchet — no harness registers a harness_assert_exempt discrimination exemption, and a harness counts as wired only when it calls the verify function rather than naming it. → security/workflow-hardening.md
  • harness census parity — no two scenarios in a harness record the same output (the files each passes to the gate, compared after the clock normalization harness_assert_record applies) unless a rationale-bearing harness_assert_parity_exempt covers the pair on the terms the linked section sets, and only an allowlisted harness may register one. → security/workflow-hardening.md
  • harness subject declaration — every tests/*.test.sh harness declares exactly one subject, by SCRIPT= assignment or # @subject annotation. → security/workflow-hardening.md
  • manifest-reaching hook watches nix/hooks — every pre-commit hook that reaches the Nix hook manifest (preCommitHooks), whether through the script its entry runs or through a flake attribute its entry evaluates, includes nix/hooks in its files filter, so a hook-definition edit re-triggers the freshness check on the per-changed-file commit path. → security/workflow-hardening.md
  • freshness hook watches evaluated sources — every pre-commit hook whose entry evaluates a flake attribute names, in its files filter, every source that evaluation reads: for a generator evaluating devTooling.<system>.<attr>, the nix modules the attribute is defined or transposed by; for an entry evaluating an attribute directly, flake.nix, flake.lock, the modules assigning it, and the paths those modules reference. A source edit then re-triggers the freshness check on the per-changed-file commit path. → security/workflow-hardening.md
  • lean lint-shell routing — light lint groups (lint-workflow-security, lint-script-hygiene) run in devShells.lint; lint-doc-invariants stays on devShells.default because renovate-config-validator needs the renovate closure. → security/workflow-hardening.md
  • lean lint-shell tool guard — every binary the .#lint-hosted lint groups expect (the EXPECTED list in scripts/check-lint-shell-tools.sh) resolves on a PATH built from the lintTools list in nix/devshell-lint.nix; keeps devShells.lint buildInputs from silently dropping a tool. → security/workflow-hardening.md
  • ci.yml job ↔ summary category — every ci.yml job appears in docs/_data/ci-check-categories.yml or on the lint's self-policed EXEMPT list; every category-map key names a real job in some .github/workflows/ file; and every .github/lint-groups.yml basename resolves to a real scripts/check-<basename>.sh. → security/workflow-hardening.md
  • Multi-line run: strict-mode prelude — every block-scalar or newline-carrying run: block, in a workflow job or a .github/actions/**/action.ya?ml composite, starts with set -Eeuo pipefail. → security/workflow-hardening.md
  • Fork-guard on guard-required jobs — every job holding contents/packages/id-token/attestations/actions at write, or minting a GitHub App installation token, includes github.repository == 'rvenutolo/linPEAS-flake' in its if:. → security/workflow-hardening.md
  • No unpinned nix run nixpkgs# — no nix subcommand may take a bare nixpkgs#<pkg> ref; must use nix shell .#<pkg>, nix run .#<pkg>, or pin a revision. → security/workflow-hardening.md
  • setup-nix composite required — workflows install Nix only via .github/actions/setup-nix with github-token. → security/workflow-hardening.md
  • Nix-host reachability — every job that allowlists a Nix host must reach Nix or carry the exemption marker the linked section defines, and every job using the setup-nix composite must allowlist both Nix hosts. → security/workflow-hardening.md
  • GITHUB_TOKEN min-permissions — top-level permissions: {}; every job declares own scopes. → security/min-permissions.md
  • GITHUB_TOKEN write-scope allowlist — every job's write scopes are pinned per job in .github/permission-scopes.yml. → security/min-permissions.md
  • Workflow-scanner division of labor — layered DiD model (pre-commit → PR/push → weekly sweep → watchdog) across zizmor/codeql/octoscan/scorecard plus in-tree lints; records why the overlap is budgeted. → security/workflow-scanners.md
  • Scorecard drift check — weekly OSSF Scorecard run across a curated check allowlist; any included check scoring \<10 fails the scorecard-drift-check.yml workflow, which pages through a deduped scorecard-drift tracking issue on the results the linked section lists. → security/workflow-scanners.md

Architecture / CI

  • Pin-diff isolation — only bump-linpeas.sh mutates linpeas-pin.json. → architecture/auto-update.md
  • Pin-shape parity set — every tracked file outside tests/ carrying the canonical pin-shape regex is derived from the tree and rendered into a managed block in auto-update.md; the copies are permanent, so the list of them is generated rather than remembered. → architecture/auto-update.md
  • nix/linpeas.nix pin invariants — pin.version regex, pin.url prefix, and pin.version present as the release-tag path segment of pin.url. → architecture/auto-update.md
  • Release VERSION shape — ^[0-9]{8}-[0-9a-f]{7,40}$. → architecture/auto-update.md
  • Linpeas-pin release-trigger — pin change must cut release. → architecture/auto-update.md
  • Stale-pin failure attribution — stale-pin-check.yml notify body distinguishes four reason tokens (pin-malformed, upstream-api-failure, upstream-tag-malformed, stall-detected); never collapse into a single failure class. → architecture/ci.md
  • Cancelled-job attribution — a cancelled job conclusion means either a job the runner pool never picked up or timeout-minutes exceeded, and the unscheduled case is cancelled near 15 minutes regardless of the declared deadline; read runner_name, steps, and the log archive against a sibling job before calling an empty steps array a retention expiry. → architecture/ci.md
  • Cron-notify root-cause comments — when a notify-workflow-result issue auto-closes after a transient failure recovers, a one-line root-cause comment goes on the closed issue; maintainer discipline, no lint. → architecture/ci.md
  • dockerhub-sync trigger — triggers are workflow_run of release-on-bump (any completion) plus manual workflow_dispatch, with success filtered by the sync job's if: gate; no push: trigger. → architecture/ci.md
  • Pages site invariants — docs/_data/dashboard.yml is generated and gitignored; gen-dashboard-data.sh hard-fails on bad pin shape, on a pin.url outside the upstream release-download prefix, and on missing JSON fields, writes the YAML atomically, and shape-checks every soft this-repo lookup so an API error body degrades to the documented fallback instead of publishing a literal "null"; the generator runs in pages.yml in the step before the site build, while the dashboard-data-tests job runs only its failure-mode harness, not the generator. → architecture/ci.md
  • Cron schedule — cron schedule table in docs/architecture/ci.md must match workflow cron triggers, and the ordering paragraph must match the table's daily rows in strictly increasing UTC order; a push- and dispatch-triggered cron-table-drift-check workflow re-runs the same script, which is not a required check. → architecture/ci.md
  • Prose CI-job names resolve — every CI job or required check named in prose, in any of the adjacency shapes the lint recognises, resolves to a real workflow job or, under the job noun, a whole workflow; a name that runs nowhere (ghost), or that resolves but not to the kind of thing the sentence calls it (mislabel) — a group or roster member called a standalone job, or a workflow called a required status check — fails. Names inside fenced blocks are shown rather than claimed and are skipped; a file whose fence never closes is a precondition failure. → security/workflow-hardening.md
  • Required-check counts match the table — a prose count of the required set that carries a count: required-contexts marker after its number must equal the data-row count of the ## Required contexts table in docs/security/required-checks.md; a mismatched declared count, an unmarked count in one of the backstop's phrasings, or a marker that follows no standalone digit count, names an unknown key or is misshapen fails, and a missing, duplicated, malformed or empty table is a precondition failure. Counts phrased any other way are not read. → security/workflow-hardening.md
  • Scanner notify arms match their workflow — every notify-arms marker must equal the arms its notify job files on, derived from the job's if: gate and result: input, and every notify job in the five scanner workflows carries one in its issue body and one in the docs; a docs marker declaring cancelled shares its paragraph with the word "cancel" in visible text. → security/workflow-hardening.md
  • Single-source cron schedules — workflow schedules live only in the ci.md schedule table; other docs link it and never put a literal HH:MM or a numeric cadence beside a workflow's name (ci.md itself and the README ci-summary block excepted). → architecture/ci.md
  • update-flake-lock credential split / renovate-flake-lock-refresh — the Nix-evaluating compute-* job holds contents: read and never sees the App key; only the push-* job mints the App token, and it commits via REST PUT /contents with no git push. → architecture/flake-input-bumps.md
  • Notify label descriptions — every label-description handed to the notify composite fits the labels API's 100-character cap and no label name is filed with two different descriptions, so the wording in the tree is what the composite writes to the label. → security/workflow-hardening.md
  • Lock-derived doc regeneration — every workflow that writes a flake.lock runs a generator for each freshness hook that declares flake.lock a trigger, and its committable-path list is exactly flake.lock plus the outputs those generators declare with @generates / @generates-block, asserted in both directions. → architecture/flake-input-bumps.md
  • pre-commit-hooks SHA parity — the revision embedded in flake.nix's pre-commit-hooks input URL equals flake.lock's pre-commit-hooks.locked.rev. → architecture/flake-input-bumps.md

Install / Runbooks

  • Declared systems evaluate — every system in flake.lib.systems force-evaluates in CI down to each package's derivation, failing with the system named. → install/nix.md
  • OCI image — Entrypoint not Cmd; bash+coreutils shipped alongside grep, sed, awk, find and ps, which the smoke jobs assert; their docker run <image> -h step is what proves the entrypoint. → install/docker.md
  • Manifest digest-pinning — every multi-arch manifest-creating docker command takes its source refs as immutable digests (an @sha256: literal or an @${…DIGEST} expansion), never mutable tags. → install/docker.md
  • DOCKERHUB_TOKEN split — _RW vs _DELETE, never unsuffixed, in workflows, and the delete-scoped token in the docs' fenced tag-delete snippets the linked section scopes. → runbooks/dockerhub-recovery.md
  • Docker Hub notify-body parity — issue body mirrors runbook. → runbooks/dockerhub-recovery.md
  • Ratchet pin audit — daily detection of tag-vs-SHA drift on pinned action refs; deduped ratchet-drift umbrella issue auto-opens/closes. → runbooks/ratchet-pin-audit.md
  • Ratchet version parity — every documented ratchet <X.Y.Z> literal names the version the devShell ships; dropping the literal fails too. → runbooks/ratchet-pin-audit.md
  • Reproducibility check — weekly dual-build comparison; flake-pinned diffoscope diff + auto-issue on mismatch, with a force_diffoscope dispatch input that exercises the diagnostic steps alone; burn-in → required promotion criteria. → runbooks/reproducibility-check.md
  • Scorecard PAT scope and rotation — the run uses a fine-grained SCORECARD_PAT (scoped to linPEAS-flake only, Read-only on Contents/Metadata/Webhooks) because the Webhooks check exceeds GITHUB_TOKEN scope; rotated on PAT expiry (bounded at one year), on suspected compromise, and on GitHub revocation. → runbooks/scorecard-pat-rotation.md
  • Scorecard Signed-Releases backfill — recovery procedure when historic releases lack .sigstore / .intoto.jsonl sidecars; runbook drives the backfill-tag workflow_dispatch input. → runbooks/scorecard-signed-releases-backfill.md
  • CI retry watchdog — re-runs failed jobs on open bot-authored auto-merge PRs, bounded to 3 attempts per run via run_attempt (no stored state); escalates to a deduped-by-title issue on exhausting the bound; a per-PR error is confined to that PR and fails the job at the end rather than aborting the sweep. → runbooks/ci-watchdog.md

Development

  • actionlint embedded-linter pins — nix/wrappers.nix wraps the actionlint binary with explicit -shellcheck=... and -pyflakes=... flags, and the hook in nix/hooks/linters.nix consumes that wrapper, so which binaries reach run: blocks is this repo's decision rather than nixpkgs' packaging; a canary per linter smoke-tests each pin against a fixture carrying a planted finding. → actionlint-embedded-linters.md
  • octoscan workflow scanning — synacktiv/octoscan scans .github/workflows/ via a pinned ghcr.io container image, as a pre-commit hook and in the octoscan.yml workflow; digest + version label pinned in scripts/octoscan-scan.sh (Renovate-managed). → development/linting.md
  • ci-summary-category-map — every required status check is categorized for the README CI summary. → security/required-checks.md
  • Required-check workflows carry no paths filter — no workflow that sources a required status check declares paths: / paths-ignore: under on.pull_request:, so a path-filtered skip can never leave a PR waiting on a check that will not run. → security/required-checks.md
  • Merging PRs — merge-commit only, signed, PR title = subject (held by lint-pr-title from its own workflow), every commit Conventional-Commits clean. → development/git.md
  • Commitlint config is explicit — every commitlint action step names an existing configFile, and the merge ruleset relaxes only the two line-length rules. → development/git.md
  • Ephemeral-reference ban — every Markdown, shell, Nix and YAML source in the repo outside the skip allowlist carries no PR/issue refs, prose dates, planning/review labels, or literal .claude/ paths. → development/linting.md
  • Ephemeral-refs scope gap — the file types no extractor claims are derived by inverting the lint's own type records, rendered into a managed block in linting.md, and hold no blocking shape. → development/linting.md
  • Treefmt YAML quote gotcha — prettier rewrites single-quoted YAML scalars to double-quoted; run nix fmt before push. → development/linting.md
  • Flake-show auto-block — scripts/refresh-flake-show.sh owns the generated block; do not hand-edit it. → development/linting.md
  • Generated-document ownership — the generator-written regions without a dedicated entry of their own are never hand-edited; a fix means editing the generator, and the *-fresh pre-commit hooks reject a stale one on the commit path. → development/linting.md
  • Scripts-reference round trip — every piece of script-header text the generator is meant to publish is visible, as written, in its own part of its entry in docs/reference/scripts.md as the site renders it, with no word dropped or altered and an indented block after a colon line kept as exactly one preformatted block, indentation included, under the per-unit comparison rules the section states; header text in the shapes the generator never reads fails too. → development/linting.md
  • Round-trip renderer Python leads the doc-lint shell — devShells.default lists the Python bound in nix/renderer-python.nix as its first buildInputs entry, so the python3 the lint-doc-invariants group's scripts-reference round-trip check runs carries python-markdown, Pygments, pymdown-extensions and PyYAML itself rather than through a propagated PYTHONPATH; the check's pre-commit hook uses the same binding. → development/linting.md
  • Python type checking — every .py file under scripts/ and docs/_data/ passes mypy --strict with the options in mypy.ini, checked against the python-markdown and PyYAML versions the round-trip check runs with. → development/linting.md
  • Markdown anchor resolution — every #anchor link in README.md or docs/**/*.md into an in-tree .md file resolves to a heading slug or an explicit <a name> anchor in its target; a zero-link scan fails unless LINT_ALLOW_EMPTY_SCAN=1. → development/linting.md
  • Config schema validation — renovate.json, workflow YAML, composite-action YAML and .markdownlint.json validate against pinned JSON Schemas. → development/linting.md
  • Docs-audit point is recorded — .github/docs-audit-state names the commit the semantic docs audit last ran against, written by scripts/mark-docs-audit.sh; the docs-audit-reminder.yml workflow measures drift pressure from it, and a marker that is absent, carries no LAST_AUDIT_SHA line, or names a commit outside this history is a could-not-run rather than a fallback to a fixed window. → development/docs-audit.md
  • Flake outputs reference — auto-regenerated nix flake show --all-systems tree; do not hand-edit. → reference/flake-outputs.md
  • PR auto-labeling — area labels come from .github/labeler.yml globs, size labels from the pascalgn/size-label-action thresholds, and .github/labels.yml is the catalog of record for both. → development/labeling.md
  • Changelog tag-pattern parity — cliff.toml tag_pattern matches the canonical pin-shape regex enforced across the bump/release/dashboard scripts. → development/changelog.md
  • Changelog link-duplication guard — the regenerated changelog contains zero adjacent duplicate identical PR links and retains the scorecard-count preprocessor. → development/changelog.md
  • Changelog released-section freshness — CHANGELOG.md's released sections match a fresh git-cliff regeneration for every release tag predating the last changelog commit, so a release that ships without the changelog job landing (or a manual edit to a released section) is caught rather than accruing silently. → development/changelog.md
  • Changelog generator-owned — CHANGELOG.md is regenerated by release-on-bump.yml changelog job; manual edits are a review-blocker. → development/changelog.md
  • Changelog App identity — changelog commit-back uses the same GitHub App identity as update-linpeas.yml push-and-merge; no new secret surface. → development/changelog.md
  • Changelog self-reference skip — cliff.toml MUST retain the ^docs: update changelog commit-parser skip rule. → development/changelog.md
  • Size-label ignore-list parity — the size-label ignore list holds exactly the paths generators declare with @generates, never a @generates-block path, and nothing else but the lint's named exemptions. → development/labeling.md
  • Cache-prune permission scope — actions-cache-prune.yml jobs declare actions as their only write scope; the contents: read half is convention, not linted. → development/cache-cleanup.md
  • Cache-prune fork-guard — every job in actions-cache-prune.yml includes github.repository == fork-guard. → development/cache-cleanup.md