Skip to content

Guardrails / Collectors

Executed commands ​

Every command the build executed, read from the session a running buildnote monitor records on the runner. This is what the job actually ran, not what a pipeline file says it would run.

Facts keycommands
Versionv1
Scriptcommands.py
Timeout60 seconds

Inputs ​

InputDescriptionDefaultEnvironment
sessionsDirDirectory holding the monitor sessions. When unset, the collector reads the CLI's own BUILDNOTE_MONITOR_DIR, which is where buildnote monitor records and which the CLI resolves so this collector does not have to guess. If you run the collector by hand with neither set, it falls back to buildnote-monitor under the temporary directory.``GUARDRAIL_INPUT_SESSIONSDIR
maxExecutionsHow many executions to report before the rest are left out and counted in dropped. The earliest are kept, so a command run near the start of the job is still reported when the build goes past the limit later on.1000GUARDRAIL_INPUT_MAXEXECUTIONS

When a guardrail declares an input with the same name, it passes its value through. That means you set these values in the guardrail's configuration in buildnote.json.

A guardrail asks for these facts by name and reads them back by the same name:

json
{
  "collect": ["commands"]
}
python
commands = guardrail.facts("commands")

Facts ​

These are the fields of the document commands collects. In a path, [] means each entry of the list before it, and [path] means a key of the object before it.

FactMeaning
sourcereport when a monitor session was read, none when there was none.
reasonWhy there was none, present only when source is none.
reportsThe session that was read.
reports[].pathAbsolute path of the session file on the runner. A monitor records outside the checkout, so this is the one path in the facts that is not stated from the directory the CLI runs in.
reports[].modifiedWhen it was last written, ISO 8601. A session still being appended to was written seconds ago.
reports[].formatAlways monitor-session.
monitorThe monitor whose session was read.
monitor.runningWhether the monitor is still recording, meaning the session has no closing record. false means the monitor finished and the commands are the whole of what it traced.
monitor.matchedHow the session was chosen: build when it records the build this run belongs to, which is the case whenever the monitor and the guardrails run in the same job, and recent when no session names this build and the most recent one was read instead.
monitor.sessionsHow many sessions were found in that directory. More than one means another monitor has recorded on this runner.
monitor.pidProcess the monitor is running as.
monitor.startedAtWhen the monitor started recording, epoch milliseconds.
monitor.closedAtWhen it stopped, epoch milliseconds, or null while it is still recording.
monitor.buildBuild the monitor recorded for, which is what matched compares against.
monitor.moduleModule the monitor submits its commands under.
monitor.commandThe command the monitor launched, or null when it attached to a process it did not start.
commandsEvery command that was executed, one entry per execution, in the order they were executed. A command run twice is two entries.
commands[].commandThe command line, exactly as the process was executed, truncated to 2000 characters. Arguments are separated by single spaces and their original quoting is gone: this is the process's argument vector joined, not the shell line that produced it.
commands[].pidProcess the command ran as. A pid is reused within a job, so it identifies an execution only alongside its timestamp.
commands[].startedAtWhen it was executed, epoch milliseconds.
commands[].durationMsHow long it ran, milliseconds, or null when it was not seen to exit.
commands[].exitCodeThe code it exited with, or null when it was not seen to exit, which is a command still running or one the monitor stopped tracing before it finished.
counts.executionsHow many commands were executed in total, including any left out by maxExecutions.
counts.commandsHow many distinct command lines are among the executions reported.
droppedExecutions left out because maxExecutions was reached. Above zero, the list describes only part of the build: a command found here is real, but a command missing from it is not proof the build never ran it, so a check that would otherwise report clean must skip.

If a collector can't finish, it prints what it gathered so far along with an incomplete key that says why. Facts after the point where it stopped are missing, so a check that depends on them should read incomplete first.

Example facts ​

Here are the facts the commands collector gathers from an example project:

json
{
  "source": "report",
  "reports": [
    {
      "path": "/home/dana/widget/monitor-sessions/4242.ndjson",
      "modified": "2026-03-04T10:15:00Z",
      "format": "monitor-session"
    }
  ],
  "monitor": {
    "running": true,
    "matched": "recent",
    "sessions": 1,
    "pid": 4242,
    "startedAt": 1757500000000,
    "closedAt": null,
    "build": "18342119027_1",
    "module": "build",
    "command": null
  },
  "commands": [
    {
      "command": "git rev-parse HEAD",
      "pid": 4310,
      "startedAt": 1757500001000,
      "durationMs": 200,
      "exitCode": 0
    },
    {
      "command": "./gradlew check",
      "pid": 4311,
      "startedAt": 1757500002000,
      "durationMs": null,
      "exitCode": null
    },
    {
      "command": "claude -p --output-format json --allowedTools Read 'review this diff'",
      "pid": 4327,
      "startedAt": 1757500090000,
      "durationMs": 50000,
      "exitCode": 0
    },
    {
      "command": "git rev-parse HEAD",
      "pid": 4402,
      "startedAt": 1757500150000,
      "durationMs": 200,
      "exitCode": 0
    }
  ],
  "counts": {
    "executions": 4,
    "commands": 3
  },
  "dropped": 0
}

Collected for ​

GuardrailCategoryInputs
claude/cli-safe-flagsclaudenone
claude/cli-structured-outputclaudenone
commands/forbidden-invocationcommandsnone

All collectors

Buildnote Limited
Registered in England and Wales, Reg: 16140412