GitHub
What GitHub itself reads out of the repository: the Actions workflows in .github/workflows, read as they are written, and the CODEOWNERS file, read as rules. For a workflow, which events start it, what token permissions it hands a job, which runner each job asks for, and every action a step reaches for and how tightly it is pinned. For CODEOWNERS, every pattern in file order with the owners it names, and the owners that apply to the paths a guardrail asks about, resolved the way GitHub resolves them, where the last rule to match a path is the one that owns it. Names and shapes only, never a secret, an input value or an environment value.
| Facts key | github |
| Version | v1 |
| Script | github.py |
| Timeout | 60 seconds |
Inputs
| Input | Description | Default | Environment |
|---|---|---|---|
workflows | Comma separated globs matched against every file in the working directory and against bare file names, naming the workflows to read. Narrows what discovery finds; a file it matches that is not under .github/workflows is still left out. | .github/workflows/*.yml,.github/workflows/*.yaml | GUARDRAIL_INPUT_WORKFLOWS |
paths | Comma separated paths to resolve owners for, relative to the repository root. A guardrail that cares about the files a pull request changed names them here. | README.md,LICENSE | GUARDRAIL_INPUT_PATHS |
A guardrail that declares an input of the same name passes it through, so the guardrail's configuration in buildnote.json is what decides these values.
A guardrail asks for these facts by name, and reads them back the same way:
{
"collect": ["github"]
}github = guardrail.facts("github")Facts
The document github collects. A path carrying [] is an entry of the list before it, and one carrying [path] is a key of the object before it.
| Fact | Meaning |
|---|---|
source | report when a workflow or a CODEOWNERS file was read, none when neither was. |
reason | Why nothing was read, present only when source is none. |
reports | Every file that was read, workflows in path order and the CODEOWNERS file after them. |
reports[].path | Path of the workflow, relative to the directory the CLI runs in. |
reports[].modified | When it was last written, ISO 8601. |
reports[].format | workflow for an Actions workflow, codeowners for the CODEOWNERS file. |
workflows | The workflows themselves, one entry per file that was read. Absent when the repository carries none, so a check reads its absence rather than an empty list. |
workflows[].path | Path of the file, relative to the directory the CLI runs in. |
workflows[].name | Name the workflow gives itself, or null when it names none. |
workflows[].triggers | The on keys: the events that start the workflow. |
workflows[].permissions | The workflow level permissions value, exactly as written. null when the file declares none, which is the token being whatever the repository defaults to rather than an empty set. |
workflows[].defaults | The workflow level defaults value, exactly as written. null when there is none. |
workflows[].concurrency | Whether the file declares a workflow level concurrency group, so an in flight run is cancelled or queued rather than racing. |
workflows[].jobs | The jobs the workflow declares. |
workflows[].jobs[].id | Key the job is declared under, which is what needs names. |
workflows[].jobs[].name | Display name the job gives itself, or null. |
workflows[].jobs[].runsOn | The runs-on value, exactly as written: a label, a list of labels or a runner group. |
workflows[].jobs[].selfHosted | false when the runner is one of GitHub's hosted images, true when it is explicitly self-hosted or names no hosted image, and null when the label is an expression. |
workflows[].jobs[].permissions | The job level permissions value, exactly as written. null when the job declares none and inherits whatever the workflow or the repository gives it. |
workflows[].jobs[].timeoutMinutes | The declared timeout-minutes, exactly as written, or null when the job declares none and takes GitHub's six hour default. |
workflows[].jobs[].environment | The deployment environment the job runs in, exactly as written, or null. An environment is what carries a protection rule and its own secrets. |
workflows[].jobs[].needs | Jobs this one waits for. |
workflows[].jobs[].if | The raw condition guarding the job, or null. |
workflows[].jobs[].uses | The reusable workflow this job calls instead of declaring steps, exactly as written, or null. |
workflows[].jobs[].secretsUsed | Names of the secrets referenced anywhere in the job, and the keys of a secrets block it passes on. Names only, never values: the facts are attached to the run event. |
workflows[].jobs[].steps | The steps the job declares, in order. |
workflows[].jobs[].steps[].name | Display name of the step, or null. |
workflows[].jobs[].steps[].uses | The action the step runs, exactly as written, or null for a step that only runs a script. |
workflows[].jobs[].steps[].action | The part of uses before the @: owner/repo with any subdirectory for a published action, the path for a ./ local action, the image for a docker:// action. |
workflows[].jobs[].steps[].ref | What follows the @ in uses, whatever it names, or null when it names nothing. A tag, a branch and a sha are indistinguishable here beyond pinned. |
workflows[].jobs[].steps[].pinned | Whether ref is a 40 character hex commit sha, which is the only ref a tag cannot be moved out from under. |
workflows[].jobs[].steps[].local | Whether the action is a ./ path in this repository rather than a published one. |
workflows[].jobs[].steps[].docker | Whether the action is a docker:// image rather than a published one. |
workflows[].jobs[].steps[].run | The shell body the step runs, truncated to 2000 characters, or null. |
workflows[].jobs[].steps[].shell | The shell the step declares for itself, or null when it takes the default the workflow or GitHub sets. |
workflows[].jobs[].steps[].withKeys | Names of the inputs the step passes to the action. Names only, never values. |
workflows[].jobs[].steps[].interpolations | Every ${{ }} expression appearing in the run body, read from the whole body before it was truncated. An expression interpolated into a shell body is the script injection surface. |
triggers | Every event declared across the workflows that were read. |
untrustedTriggers | One entry per workflow and trigger that runs with the repository's own token on a change somebody outside it wrote: pull_request_target, issue_comment and workflow_run. |
untrustedTriggers[].path | Path of the workflow declaring it. |
untrustedTriggers[].trigger | The trigger itself. |
counts.workflows | Workflows that were read. |
counts.jobs | Jobs across every workflow. |
counts.steps | Steps across every job. |
unparsed | Files that matched but were not read, because the YAML they are written in is outside the subset the collector can represent. A file here is not a file with nothing in it: nothing was read from it and no verdict should be drawn about it. |
unparsed[].path | Path of the file that was not read. |
unparsed[].reason | The construct that could not be represented, and the line it is on. |
codeowners | The CODEOWNERS file the repository carries. Absent when it carries none at any of the three locations GitHub reads one from, so a check reads its absence rather than an empty rule list. |
codeowners.path | Path of the CODEOWNERS file that was read: the first of .github/CODEOWNERS, CODEOWNERS and docs/CODEOWNERS that exists, which is the order GitHub searches them in. A repository carrying more than one of them is owned by the first, and the rest are text GitHub never reads. |
codeowners.rules | Every rule the file declares, in file order, which is the order GitHub resolves them in. |
codeowners.rules[].pattern | Pattern exactly as it is written, so an anchored /docs/ reads differently from a docs/ matched at any depth. A bracketed line such as [Docs] is a pattern too, not a heading: GitHub CODEOWNERS has no section headers, so the line is resolved like any other rule and owns whatever it matches. |
codeowners.rules[].owners | Owners the rule names, as GitHub users (@dana), GitHub teams (@acme/platform) or email addresses. Empty when the rule names none, which removes ownership from everything it matches. |
codeowners.rules[].line | Line the rule is declared on, so a verdict points at the file. |
codeowners.owners | Every owner the file names, by owner, with how many rules name them. |
codeowners.matched | Owners GitHub resolves for each path the paths input named, by the path exactly as it was given. A path nobody asked about is absent, so read the ones the guardrail configured rather than iterating. |
codeowners.matched[path].owners | Owners the last rule to match names, because the last match is what GitHub gives the path. Empty when no rule matched, and empty when the last one to match names no owners. |
codeowners.matched[path].rule | Pattern of the last rule that matched, or null when none did. |
codeowners.matched[path].line | Line that rule is declared on, or null when none matched. |
codeowners.unparsed | Every line that is not a rule, in file order. Comments and blank lines are not among them, because GitHub ignores them rather than misreading them. |
codeowners.unparsed[].line | Line that could not be read as a rule. |
codeowners.unparsed[].reason | Why it could not be read as one. |
A collector that cannot finish prints what it had along with an incomplete key saying why, so the facts below the point it stopped are absent. A check that cares reads incomplete before the rest.
Example facts
The example facts collected by github collector:
{
"source": "report",
"reports": [
{
"path": ".github/workflows/build.yml",
"modified": "2026-03-04T10:15:00Z",
"format": "workflow"
},
{
"path": "CODEOWNERS",
"modified": "2026-03-04T10:15:00Z",
"format": "codeowners"
}
],
"workflows": [
{
"path": ".github/workflows/build.yml",
"name": "Build",
"triggers": [
"push",
"pull_request"
],
"permissions": {
"contents": "read"
},
"defaults": null,
"concurrency": false,
"jobs": [
{
"id": "build",
"name": "Build and test",
"runsOn": "ubuntu-latest",
"selfHosted": false,
"permissions": {
"contents": "read"
},
"timeoutMinutes": 15,
"environment": null,
"needs": [],
"if": null,
"uses": null,
"secretsUsed": [],
"steps": [
{
"name": "Check out the repository",
"uses": "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8",
"action": "actions/checkout",
"ref": "08c6903cd8c0fde910a37f88322edcfb5dd907a8",
"pinned": true,
"local": false,
"docker": false,
"run": null,
"shell": null,
"withKeys": [],
"interpolations": []
},
{
"name": "Set up the JDK",
"uses": "actions/setup-java@v4",
"action": "actions/setup-java",
"ref": "v4",
"pinned": false,
"local": false,
"docker": false,
"run": null,
"shell": null,
"withKeys": [
"distribution",
"java-version"
],
"interpolations": []
},
{
"name": "Build",
"uses": null,
"action": null,
"ref": null,
"pinned": false,
"local": false,
"docker": false,
"run": "./gradlew check",
"shell": null,
"withKeys": [],
"interpolations": []
}
]
}
]
}
],
"triggers": [
"push",
"pull_request"
],
"untrustedTriggers": [],
"counts": {
"workflows": 1,
"jobs": 1,
"steps": 3
},
"codeowners": {
"path": "CODEOWNERS",
"rules": [
{
"pattern": "*",
"owners": [
"@acme/platform"
],
"line": 1
},
{
"pattern": "*.md",
"owners": [
"@acme/docs",
"docs@acme.example"
],
"line": 3
},
{
"pattern": "/service/",
"owners": [
"@acme/service-team"
],
"line": 5
},
{
"pattern": "/service/build/",
"owners": [],
"line": 7
}
],
"owners": {
"@acme/platform": 1,
"@acme/docs": 1,
"docs@acme.example": 1,
"@acme/service-team": 1
},
"matched": {
"README.md": {
"owners": [
"@acme/docs",
"docs@acme.example"
],
"rule": "*.md",
"line": 3
},
"LICENSE": {
"owners": [
"@acme/platform"
],
"rule": "*",
"line": 1
}
},
"unparsed": []
},
"unparsed": []
}Collected for
| Guardrail | Category | Inputs |
|---|---|---|
github/actions-pinned-by-digest | github | workflows |
github/codeowners-catch-all | github | workflows |
github/codeowners-no-unowned-rules | github | workflows |
github/codeowners-owners-per-rule | github | workflows |
github/codeowners-parses | github | workflows |
github/codeowners-present | github | workflows |
github/codeowners-team-owned | github | workflows |
github/job-timeout-set | github | workflows |
github/least-privilege-token | github | workflows |
github/no-script-injection | github | workflows |
github/no-untrusted-checkout | github | workflows |