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:pinGitHubActionDigestspreset, a per-managerpinDigests: truefor github-actions,minimumReleaseAgeset, and no top-levelautomerge. → security/repo-config.md - No dead renovate markers — every
# renovate:marker's file is consumed by a live customManager (amanagerFilePatternsentry scopes it and amatchStringsentry 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 inverify-latest-release.yml'sverifyjob 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 verifyinvocation 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
.sigstorebundle 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.jsonlSLSA build-provenance bundle (captured fromactions/attest-build-provenance); satisfies OSSF ScorecardreleasesHaveProvenanceprobe. → 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.lockinput 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 dailyflake-lock-staleness-checkworkflow, 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.nixdoes 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 unlessflake.nixdeclares a differenturlfor that same input between base and head (per input name, top-level only, so a transitive repoint is never corroborated and an unparsableflake.nixis an operational error); the entry point is each lock's own top-level.rootfield (missing/non-string.rootis an operational error, a base/head root-id mismatch fails closed) and top-level input refs resolve throughfollowspaths (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 whoserevmoved undeclared must be a fast-forward per the GitHub compare API (aheadpasses;behind,diverged, or a rev the repo does not know fails; any other API failure or a malformed response is an operational error; aflake.nix-corroborated move is counted and not probed; a non-githubnode 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: blockwith a non-empty per-joballowed-endpoints:. → security/trust-model.md - egress allowlist matches tool inventory — every job's
allowed-endpointslist carries the hosts its tool inventory reaches, the ghcr blob host if it carriesghcr.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 thenotify-workflow-resultcomposite 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/checkoutstep setspersist-credentials: false. → security/workflow-hardening.md - upload-artifact strict — every
actions/upload-artifactstep setsif-no-files-found: error. → security/workflow-hardening.md - Workflow on.branches main-only — every
pull_request:/push:trigger declaresbranches: [main]exactly. → security/workflow-hardening.md - pull_request_target forbidden — no workflow uses the
pull_request_targettrigger. → security/workflow-hardening.md - Auto-merge decline gate — every workflow run-block calling
gh pr merge --autoinspects PR state and aborts on aCLOSED/MERGEDPR 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 bashand usesset -Eeuo pipefail, while every sourced library underscripts/lib/satisfies the inverse: ashellcheck shell=directive, no shebang, no shell options of its own. → security/workflow-hardening.md - Harness preamble — every
tests/*.test.shopens with the canonical preamble: exact shebang,set -Eeuo pipefail, the strict IFS line, and a readonlyREPO_ROOTderived fromgit rev-parse --show-toplevel. → security/workflow-hardening.md - No opaque process-substitution loops in scripts — no script anywhere under
scripts/— including the sourced libraries inscripts/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, andBASH_SOURCEnever appears inside a command substitution anywhere else underscripts/either, so a script's own directory resolves by parameter expansion (${BASH_SOURCE[0]%/*}), never through something that needsreadlink,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 baremktemp—scripts/lib/temp.sh, which holds the one sanctioned invocation, is the sole exception; every other creation runs throughmake_temp(scripts/lib/temp.sh), which reports an unwritableTMPDIRas exit 2 instead of lettingmktemp'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 baregit rev-parse --show-toplevel—scripts/lib/repo.sh, which holds the one sanctioned lookup, is the sole exception; every other lookup runs throughrepo_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 callsrepo_toplevelinside 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 literalexit 2/return 2or a direct call to an exit-2-capable library helper, says so in its header; four contract shapes count, the scan does not recurse intoscripts/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 byrequire_tool, acommand -vtest, 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 theshfmtparse tree so a tool named in a comment, a message string, or asedprogram is not mistaken for an invocation. → security/workflow-hardening.md - Regex ranges hold ASCII — a bash
=~regex, a GNUgreppattern or a GNUsedscript inscripts/*.sh,scripts/lib/*.shortests/*.shholds no locale-bound range between ASCII letters or digits outside a C locale, since a range follows collation and underen_US.UTF-8admits 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
awkinvocation in a script underscripts/(sourced libraries included) carrying a file operand spells it"$(awk_path "${var}")", sinceawkreads a bare operand shapedname=valueas 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-filesandgit ls-treein a repo script or test harness (scripts/*.shandtests/*.test.shas git pathspecs, soscripts/lib/and nested harnesses are included, lesstests/fixtures/) is an argument toenumerate_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
forloop 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 throughglob_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
FILTERor ending_FILTERmay be read at file scope to reachfilter_into(scripts/lib/enumerate.sh) and narrow an already-enumerated set, but not again inside afororwhileloop 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 callsfilter_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 setfilter_intonever narrowed, a filter matching nothing leaves a body that never runs and a clean exit — and a file with nofilter_intocall 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 sanctionedreadonly 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_OVERRIDEvariable, a bare stdin slurp, or a scopedflake.lockread) 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 textual2substring — 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/*.shortests/*.shsets a variable to a bare*_OVERRIDEvariable name, bare or declared (local,readonly,declare,export); every payload source is named bypayload_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 inscripts/*.shis a violation, captured or not, unless the path traces to amake_temp/mktempresult 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 byread_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-*.shhas a matchingtests/check-*.test.shand vice versa, modulo the enforcer's namedEXEMPTlist. → 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 therun-harness-groupHARNESSES array, therefresh-*.test.shdoc-freshness glob, alint-groups.ymlmember, 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_exemptnames for that substring is skipped), and every output-asserting harness is wired to the gate, modulo the enforcer's namedEXEMPTarray. → security/workflow-hardening.md - harness exemption ratchet — no harness registers a
harness_assert_exemptdiscrimination 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_recordapplies) unless a rationale-bearingharness_assert_parity_exemptcovers 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.shharness declares exactly one subject, bySCRIPT=assignment or# @subjectannotation. → 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, includesnix/hooksin itsfilesfilter, 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
filesfilter, every source that evaluation reads: for a generator evaluatingdevTooling.<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 indevShells.lint;lint-doc-invariantsstays ondevShells.defaultbecauserenovate-config-validatorneeds the renovate closure. → security/workflow-hardening.md - lean lint-shell tool guard — every binary the
.#lint-hosted lint groups expect (theEXPECTEDlist inscripts/check-lint-shell-tools.sh) resolves on aPATHbuilt from thelintToolslist innix/devshell-lint.nix; keepsdevShells.lintbuildInputs 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.ymlor 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.ymlbasename resolves to a realscripts/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?mlcomposite, starts withset -Eeuo pipefail. → security/workflow-hardening.md - Fork-guard on guard-required jobs — every job holding
contents/packages/id-token/attestations/actionsatwrite, or minting a GitHub App installation token, includesgithub.repository == 'rvenutolo/linPEAS-flake'in itsif:. → security/workflow-hardening.md - No unpinned
nix run nixpkgs#— nonixsubcommand may take a barenixpkgs#<pkg>ref; must usenix 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
writescopes 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.ymlworkflow, which pages through a dedupedscorecard-drifttracking issue on the results the linked section lists. → security/workflow-scanners.md
Architecture / CI¶
- Pin-diff isolation — only
bump-linpeas.shmutateslinpeas-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 inauto-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.versionregex,pin.urlprefix, andpin.versionpresent as the release-tag path segment ofpin.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.ymlnotify 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
cancelledjob conclusion means either a job the runner pool never picked up ortimeout-minutesexceeded, and the unscheduled case is cancelled near 15 minutes regardless of the declared deadline; readrunner_name,steps, and the log archive against a sibling job before calling an emptystepsarray a retention expiry. → architecture/ci.md - Cron-notify root-cause comments — when a
notify-workflow-resultissue 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_runofrelease-on-bump(any completion) plus manualworkflow_dispatch, with success filtered by thesyncjob'sif:gate; nopush:trigger. → architecture/ci.md - Pages site invariants —
docs/_data/dashboard.ymlis generated and gitignored;gen-dashboard-data.shhard-fails on bad pin shape, on apin.urloutside 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 inpages.ymlin the step before the site build, while thedashboard-data-testsjob 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-checkworkflow 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-contextsmarker after its number must equal the data-row count of the## Required contextstable indocs/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-armsmarker must equal the arms its notify job files on, derived from the job'sif:gate andresult:input, and every notify job in the five scanner workflows carries one in its issue body and one in the docs; a docs marker declaringcancelledshares 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 holdscontents: readand never sees the App key; only thepush-*job mints the App token, and it commits via RESTPUT /contentswith nogit push. → architecture/flake-input-bumps.md - Notify label descriptions — every
label-descriptionhanded 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.lockruns a generator for each freshness hook that declaresflake.locka trigger, and its committable-path list is exactlyflake.lockplus 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'spre-commit-hooksinput URL equalsflake.lock'spre-commit-hooks.locked.rev. → architecture/flake-input-bumps.md
Install / Runbooks¶
- Declared systems evaluate — every system in
flake.lib.systemsforce-evaluates in CI down to each package's derivation, failing with the system named. → install/nix.md - OCI image —
EntrypointnotCmd; bash+coreutils shipped alongsidegrep,sed,awk,findandps, which the smoke jobs assert; theirdocker run <image> -hstep 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 —
_RWvs_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-driftumbrella 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_diffoscopedispatch 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.jsonlsidecars; runbook drives thebackfill-tagworkflow_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.nixwraps the actionlint binary with explicit-shellcheck=...and-pyflakes=...flags, and the hook innix/hooks/linters.nixconsumes that wrapper, so which binaries reachrun: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 theoctoscan.ymlworkflow; digest + version label pinned inscripts/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:underon.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-titlefrom 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 fmtbefore push. → development/linting.md - Flake-show auto-block —
scripts/refresh-flake-show.showns 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
*-freshpre-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.mdas 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.defaultlists the Python bound innix/renderer-python.nixas its firstbuildInputsentry, so thepython3thelint-doc-invariantsgroup's scripts-reference round-trip check runs carries python-markdown, Pygments, pymdown-extensions and PyYAML itself rather than through a propagatedPYTHONPATH; the check's pre-commit hook uses the same binding. → development/linting.md - Python type checking — every
.pyfile underscripts/anddocs/_data/passesmypy --strictwith the options inmypy.ini, checked against the python-markdown and PyYAML versions the round-trip check runs with. → development/linting.md - Markdown anchor resolution — every
#anchorlink inREADME.mdordocs/**/*.mdinto an in-tree.mdfile resolves to a heading slug or an explicit<a name>anchor in its target; a zero-link scan fails unlessLINT_ALLOW_EMPTY_SCAN=1. → development/linting.md - Config schema validation —
renovate.json, workflow YAML, composite-action YAML and.markdownlint.jsonvalidate against pinned JSON Schemas. → development/linting.md - Docs-audit point is recorded —
.github/docs-audit-statenames the commit the semantic docs audit last ran against, written byscripts/mark-docs-audit.sh; thedocs-audit-reminder.ymlworkflow measures drift pressure from it, and a marker that is absent, carries noLAST_AUDIT_SHAline, 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-systemstree; do not hand-edit. → reference/flake-outputs.md - PR auto-labeling — area labels come from
.github/labeler.ymlglobs, size labels from thepascalgn/size-label-actionthresholds, and.github/labels.ymlis the catalog of record for both. → development/labeling.md - Changelog tag-pattern parity —
cliff.tomltag_patternmatches 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.mdis regenerated byrelease-on-bump.yml changelogjob; 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.tomlMUST retain the^docs: update changelogcommit-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-blockpath, and nothing else but the lint's named exemptions. → development/labeling.md - Cache-prune permission scope —
actions-cache-prune.ymljobs declareactionsas their only write scope; thecontents: readhalf is convention, not linted. → development/cache-cleanup.md - Cache-prune fork-guard — every job in
actions-cache-prune.ymlincludesgithub.repository ==fork-guard. → development/cache-cleanup.md