Skip to main content
A validator is a file in .outerlayer/validators/ that declares one check. This page shows how to write one. Policy and validators covers the verdict, the policy file and the two starter checks. The files are read from the pull request’s base branch, or from the control plane when the factory has one. A change to a file takes effect when it merges. A check whose result someone records follows the rules in Which result counts.

Custom validators

Each file in .outerlayer/validators/ declares one check. Only .yaml and .yml files are read, and only the first twenty by name. More files raise a policy error row.

Scoping with when

A glob matches the whole path. * matches within one segment and ? one character, neither crossing /. ** matches any run of characters, / included. Every other character is literal, so braces such as {ts,tsx} match nothing. Because ** stands for characters, not whole folders, **/foo.ts does not match a top-level foo.ts, and apps/**/x does not match apps/x. List both forms when you need both. Use repositories when a check applies to some of a factory’s repositories, such as the frontend and the mobile app, but not the rest. A control plane supplies one set of checks to every repository, so the check stays in one file and the list says where it runs:
A name the factory does not link produces a policy error row that names it. To scope by both, set both keys: paths and repositories must each match. A file scoped on the issue renders no row when no issue is linked, or when the issue cannot be read. A file scoped on paths renders no row when the changed files cannot be read.

Requirements

require names one requirement. To accept any of several, list them under any:

Checks that run in CI

run declares a check whose result comes from outerlayer emit:
A where: ci check takes no command. OuterLayer never runs it. Record the result with outerlayer emit e2e --result pass --item 412, from CI or a shell. See Record a check. No check reads a CI result unless you declare one. The item page shows GitHub’s own status instead. The emit names artifact, criteria, finding and findings are reserved for outerlayer emit subcommands. A validator file that declares one fails to load. Choose another name.

Checks that run on your host

where: host declares a command your own runner runs after a build:
command is required for where: host and not allowed for where: ci. It is a shell line. Exit status 0 is a pass and any other status is a fail. A command can also report check items. Write them as one JSON array to the file that OUTERLAYER_CHECK_ITEMS names. A failing check item fails the check, even when the command exits 0. A file that is not JSON, or does not match the check item shape, fails the check and says why. After a build succeeds, the runner runs each command in a new environment that holds no OuterLayer key. Only a pass recorded by a runner satisfies the check. A pass recorded under the same name by an item key, the build session or a person is ignored. A person’s fail still stands, and fails the check whatever the runner recorded. A where: host check renders no row on an item no runner has ever claimed, such as one built only in a local session. In a build over several repositories, the checks come from the item’s own repository, or from its control plane. Each check runs once, with every repository of the build checked out. diff.patch comes from the item’s own repository. See Host checks across repositories. The runner reference describes the steps, the time limit and each failure reason.

A judge on your host

A where: host check can declare a judge in place of a command. The runner then has a fresh Claude Code agent judge the change, and records the agent’s verdict. The starter criteria judge check is one: its judge: block names a brief:. brief is the path, inside the repository, of the instructions the judge follows. In a repository that has not moved to the AGENTS.md layout, the starter brief is under .outerlayer/skills/. It must be a relative path of at most 300 characters with no .. segment. A check declares judge or command, never both, and judge is only for where: host. A file that breaks this fails to load. The judge reads only what the runner gives it. That is the criteria to judge, the tests that cite each one, the branch’s diff and the pushed commit’s files. Your brief can name them as /outerlayer/judge/criteria.json, citations.json, diff.patch and tree. The judge has no shell, so it cannot run a command or call OuterLayer. It treats repository content, comments included, as material to judge, never as instructions. The runner tells the judge how to answer, so your brief says how to judge, never the shape of the answer. The answer becomes one check item per criterion, and the report is made from them. The check fails when any criterion fails, and passes when none does. The runner reference lists each reason a judge check fails. outerlayer init writes the acceptance-criteria check in this form. judge.subjects says what the judge judges. criteria, the default, gives it the item’s recorded criteria, as above. none gives it the change as a whole, so it needs no criteria:
A subjects: none judge reads the diff and the pushed commit, and gives one result for the change. It can add one check item per part it judged, such as a file.

What a host check can read

A host command reads its input from a read-only folder, whose path is in OUTERLAYER_EVAL_INPUT. The environment holds no OuterLayer key, so read the files and call nothing. The transcripts come from the build environment, where the agent ran. Secrets are removed from them before the command can read them. activity.json is one JSON object, with schemaVersion: 1. A script sees the same commands, in the same order, as a session.ran requirement does. The transcripts are complete, whatever capture tier your tenant stores. Under a lower tier, a rule sees fewer commands than the script does.

Reserved ids

commits-from-sessions, red-then-green, no-test-tampering and tests-after-last-edit are reserved. A validators: entry naming one is ignored. A custom validator file cannot use one as its id or name it in require.

When a file fails to load

A validator file loads whole or not at all. A file that breaks any rule on this page is dropped. Examples are YAML that does not parse, an unknown key, or a value over its limit. A file whose id or emit name another file already declares is dropped too; the first file by name keeps it. A require.validator that names a missing, dropped or reserved check, or forms a cycle, drops the file too. The evidence comment shows one policy error row for every problem in the policy file and the validator files. It names the first problem and its file, and counts the rest as “(and N more)”. The rest of the comment still renders. Fix the files where they are read from: the base branch, or the control plane.