check-lua
Runs custom validation logic written in Lua (5.4). The escape hatch for domain rules the other validators cannot express.
The script defines a global validate(ctx, content) that returns nil when validation passes, or an error message
string when it fails.
Syntax
Section titled “Syntax”| Attribute | Value | Default |
|---|---|---|
check-lua |
path to a .lua script, relative to the project root |
— |
check-lua-pattern |
regex; every match’s (?P<value>…) group, or whole match |
whole block |
check-lua-timeout |
whole number of seconds (≥ 1) the script may run | 30 |
check-lua-pattern and check-lua-timeout work only next to check-lua. On a block without it, they stop the run,
because they would do nothing there.
The script path must point to a file inside the repository. Paths that escape it — absolute paths outside the project,
../ traversal, or symlinks pointing outward — are rejected.
A leading #! line and a UTF-8 byte order mark are skipped, as the lua interpreter skips them, so a script that runs
standalone runs here unchanged.
Example
Section titled “Example”colors = [ # <block check-lua="scripts/validate_colors.lua"> 'red', 'green', 'blue', # </block>]scripts/validate_colors.lua:
function validate(ctx, content) if content:find("purple") then return "purple is not an allowed color" end return nilendThe validate arguments
Section titled “The validate arguments”ctx— a table with:ctx.file— the source file path, relative to the repository root and always separated by/, on every platform and in every run mode. A script may compare or pattern-match it without normalizing first.ctx.line— the line number of the block’s start tag.ctx.attrs— the block’s own attributes, keyed by attribute name, each value exactly as it was written in the tag. A block tagged<block check-lua="…" name="limits" severity="warning">gives the scriptctx.attrs["check-lua"],ctx.attrs["name"]andctx.attrs["severity"]. Only blockwatch’s own attributes can appear here.ctx.affects— present only when the block also has anaffectsattribute. A 1-based array with one table per target, holdingfile,nameandcontent(trimmed).fileuses the same format asctx.file. Targets that don’t resolve are left out.- For a block,
nameis the block’s name andcontentis its content. - For a whole file,
nameisnilandcontentis the whole file. - For a symbol (
file#/path),nameis the path with its#, such as#/version.contentis the symbol’s value, or its text in the file for a value that holds other values.
- For a block,
content— a string holding the trimmed text content of the block, or, whencheck-lua-patternis set, a 1-based array of the values the pattern extracted.
Narrowing the input with check-lua-pattern
Section titled “Narrowing the input with check-lua-pattern”check-lua-pattern extracts values from the block and passes those to the script instead of the whole content. The
content in the Lua script becomes a 1-based array to accommodate all the matches.
prices = [ # <block check-lua="scripts/check_prices.lua" check-lua-pattern="\$(?P<value>\d+)"> "Item A: $50", "Item B: $150", # </block>]function validate(ctx, content) for _, price in ipairs(content) do -- content is { "50", "150" } if tonumber(price) >= 100 then return "price $" .. price .. " is not under $100" end end return nilendA script that wants a single value reads content[1]:
// <block check-lua="scripts/check_latest_gpt_nano_model.lua" check-lua-pattern='str = "(?P<value>[^"]+)"'>const DEFAULT_MODEL_NAME: &str = "gpt-5.4-nano";// </block>Unlike same-as-pattern, the regex runs against the entire block rather than line by line, so a pattern may span
several lines.
Checking affected blocks
Section titled “Checking affected blocks”Combining affects with check-lua lets a script inspect the blocks it affects through
ctx.affects — with no file IO, so it works in the default sandboxed mode. This keeps two blocks in sync
deterministically:
allowed_colors = [ # <block check-lua="scripts/in_sync.lua" affects=":allowed-colors-docs"> 'blue', 'green', 'red', # </block>]
docs = [ # <block name="allowed-colors-docs"> 'blue', 'green', 'red', # </block>]scripts/in_sync.lua:
function validate(ctx, content) for _, affected in ipairs(ctx.affects) do if affected.content ~= content then return "block '" .. affected.name .. "' in " .. affected.file .. " is out of sync" end end return nilendFor a plain value comparison, same-as does this without a script.
A whole-file affects target reaches the script the same way, which is how a sandboxed script can read a file it could
not otherwise open:
function validate(ctx, content) for _, affected in ipairs(ctx.affects) do if affected.name == nil and not affected.content:find(content, 1, true) then return "the value is missing from " .. affected.file end end return nilendSafety modes
Section titled “Safety modes”By default, Lua scripts run sandboxed with only the coroutine, table, string, utf8, and
math standard libraries available. The io, os, and package libraries are not loaded, preventing file system
access, command execution, and loading of external modules.
Set BLOCKWATCH_LUA_MODE to change the security level:
# Allow IO and OS libraries (memory-safe, but with file/system access)BLOCKWATCH_LUA_MODE=safe blockwatch
# Allow all libraries including C module loading (unsafe)BLOCKWATCH_LUA_MODE=unsafe blockwatchBLOCKWATCH_LUA_MODE |
Libraries available | Security Level |
|---|---|---|
sandboxed (default) |
coroutine, table, string, utf8, math |
Most secure - No file/OS access |
safe |
All memory-safe libraries (including io, os, package) |
Memory-safe - Allows file/OS access |
unsafe |
All Lua standard libraries with no restrictions (including C modules) | Unsafe - Full system access |
An empty value means sandboxed, the same as an unset variable. Upper and lower case do not matter, so Safe works
like safe. Any other value, such as sandbox, stops a run that checks a check-lua block. The error lists the valid
values.
If a script uses a library that is not available in the selected mode, the run stops with a Lua error. The block is not
reported as a violation — the whole run fails. So a script that needs io or os will fail every sandboxed run,
including fork pull requests that changed nothing related to it. Disable check-lua for those runs instead; see
A validator that cannot run fails the check.
← Validators · README