Pipelines chain transformations with |. Labels track data provenance and sensitivity. Guards inspect and control operations at runtime. Hooks observe lifecycle events for logging and telemetry.
Pipelines
Chain stages with |. Each stage receives the previous stage output as a single value.
Built-in transformers available in pipelines include:
@parse@trim@pretty@sort
var @users = cmd {cat users.json} | @parse
>> Built-ins and custom stages can be mixed
var @msg = " hello pipeline " | @trim
exe @double(n) = js { return Number(n) * 2 }
var @x = cmd {echo "5"} | @double
>> JSON parsing modes
var @relaxed = @input | @parse.loose >> single quotes, trailing commas
var @strict = @input | @parse.strict >> strict JSON only
var @extracted = @llmResponse | @parse.llm >> extract from LLM response
>> Stages receive whole values (no implicit array auto-map)
var @items = [" beta ", " alpha "]
var @whole = @items | @trim
var @each = for @item in @items => @item | @trim
>> Handle failures with retry + fallback in a stage
exe @source() = "not-json"
exe @parseOrFallback(input) = when [
@input.startsWith("{") => @input | @parse
@mx.try < 2 => retry "expected JSON object"
* => { ok: false, error: "invalid-json", raw: @input }
]
var @result = @source() | @parseOrFallback
Pipeline Context
Pipeline context:
@mx.try- current attempt number@mx.stage- current 1-based stage index (1,2,3, ...)@p[-1]- previous stage output (same value as current stage input)
exe @trace(input) = [
show `stage=@mx.stage value=@input`
=> @input
]
show " hello " | @trace | @trim | @trace
>> Output:
>> stage=1 value= hello
>> stage=3 value=hello
Retry in Pipelines
Retry in pipelines:
exe @validator(input) = when [
@input.valid => @input
@mx.try < 3 => retry "need more detail"
* => "fallback"
]
var @result = @raw | @validator
Parallel Pipeline Groups
Parallel groups:
>> Two transforms run concurrently
var @results = || @fetchA() || @fetchB() || @fetchC()
>> With concurrency cap
var @capped = || @a() || @b() || @c() (2, 100ms) >> cap=2, 100ms pacing
Labels
Labels are strings attached to values that track what data IS and where it CAME FROM. They're the foundation of mlld's security model.
The core insight:
You cannot prevent LLMs from being tricked by prompt injection. But you CAN prevent the consequences of being tricked from manifesting.
Labels make this possible. When an operation is attempted, mlld checks whether the labels on the input data are allowed to flow to that operation. The LLM may have been tricked into trying something dangerous, but labels block it.
Label categories:
| Category | Examples | Applied How |
|---|---|---|
| Trust | trusted, untrusted |
Policy defaults, explicit declaration |
| Sensitivity | secret, sensitive, pii |
Explicit declaration, keychain |
| Source | src:mcp, src:cmd, src:js, src:file |
Auto-applied by system |
| Operation | op:cmd:git:status, op:sh |
Auto-applied during execution |
| Custom | internal, redacted |
User-defined |
Declaring labels on variables:
var secret @customerList = <internal/customers.csv>
var pii @patientRecords = <clinic/patients.csv>
var untrusted @externalData = "from outside"
Labels propagate through transformations:
var secret @customerList = <internal/customers.csv>
var @summary = @customerList | @summarize
show @summary.mx.labels
The @summary value still carries the secret label because labels propagate through all transformations (result: ["secret"]).
The security check:
When an operation is attempted:
- What labels does the input data have?
- What labels does the operation have?
- Does policy allow this flow?
var secret @customerList = <internal/customers.csv>
>> Guard on the data label — fires when secret data flows to any operation
guard @noSecretExfil before secret = when [
@mx.op.labels.includes("net:w") => deny "Secret data cannot flow to network operations"
* => allow
]
exe net:w @postToWebhook(data) = run cmd { curl -d "@data" https://hooks.example.com/ingest }
show @postToWebhook(@customerList)
The @customerList has label secret. The operation @postToWebhook has label net:w. The guard blocks the flow: Guard blocked operation: Secret data cannot flow to network operations.
You can guard from either direction — guard before secret (check the operation) or guard before net:w (check the data). See guards-basics for the matching model.
Label context (@mx):
Every value carries label metadata accessible via @mx:
var secret @key = "abc"
show @key.mx.labels
show @key.mx.taint
show @key.mx.sources
labels- User-declared sensitivity labelstaint- Union of labels plus source markers (for provenance)sources- Source references (file paths and guard names), not a full transformation history
Why labels work:
Labels are enforced by the mlld runtime, not by LLM reasoning. A tricked LLM can try to send your customer list to an attacker's webhook, but:
- The data still has its
secretlabel - The network operation still has its
net:wlabel - Policy or guards say
secret → net:w = DENY - The operation is blocked regardless of LLM intent
This is the fundamental security guarantee: labels track facts about data that cannot be changed by prompt injection.
Sensitivity Labels
Sensitivity labels classify what data IS: whether it contains secrets, personal information, or other confidential content. Unlike source labels (which track provenance automatically), sensitivity labels are explicitly declared by developers.
The three sensitivity labels:
| Label | Meaning | Common Use |
|---|---|---|
secret |
Confidential secrets, credentials, proprietary data | Customer lists, credentials, trade secrets |
sensitive |
Confidential but not cryptographic | Business data, internal configs |
pii |
Personally identifiable information | Email addresses, names, SSNs |
Declaring sensitivity labels:
var secret @customerList = <internal/customers.csv>
var pii @patientRecords = <clinic/patients.csv>
var sensitive @internalConfig = <./company-config.json>
The label appears before the variable name when you declare it.
Auto-applied secret label:
Values retrieved from the keychain automatically receive the secret label and src:keychain source taint:
var @key = keychain.get("api-token")
show @key.mx.labels
show @key.mx.taint
Output: ["secret"] and ["secret", "src:keychain"]
This is the ONLY case where sensitivity labels are auto-applied. All other sensitivity labels must be declared explicitly.
How sensitivity labels differ from trust labels:
Trust labels (trusted/untrusted) track whether a source is trustworthy. Sensitivity labels track what the data contains:
var untrusted secret @leakedKey = <./found-on-internet.txt>
This data is BOTH untrusted (came from unreliable source) AND secret (contains a credential). The two classifications are independent.
Sensitivity labels propagate:
Like all labels, sensitivity markers flow through transformations:
var secret @customerList = <internal/customers.csv>
var @parsed = @customerList | @parse
var @firstTen = @parsed.slice(0, 10)
var @summary = `Top customers: @firstTen`
show @summary.mx.labels
Output: ["secret"]
The secret label propagates through the parse, the slice operation, and the template interpolation. This is critical: you cannot accidentally remove sensitivity by transforming data.
Security rules for sensitivity labels:
Policy defines built-in rules that block dangerous flows:
| Rule | Behavior |
|---|---|
no-secret-exfil |
Blocks secret data from flowing to operations labeled exfil |
no-sensitive-exfil |
Blocks sensitive data from flowing to exfil operations |
These rules are opt-in via policy configuration:
policy @p = {
defaults: {
rules: [
"no-secret-exfil",
"no-sensitive-exfil"
]
}
}
What counts as exfil?
exfil is a risk classification you apply to your semantic operation labels via policy.operations. You label exe functions with semantic labels describing what they do (e.g. net:w), then policy maps those to risk categories:
>> Semantic label describes what the operation does
exe net:w @sendToServer(data) = run cmd {
curl -d "@data" https://example.com/collect
}
>> Policy groups semantic labels under risk categories
policy @p = {
defaults: { rules: ["no-secret-exfil"] },
operations: { exfil: ["net:w"] }
}
Blocked flow example:
policy @p = {
defaults: { rules: ["no-secret-exfil"] },
operations: { exfil: ["net:w"] }
}
var secret @customerList = <internal/customers.csv>
exe net:w @postToWebhook(data) = run cmd {
curl -d "@data" https://hooks.example.com/ingest
}
show @postToWebhook(@customerList)
Error: the secret label on @customerList cannot flow to the exfil-classified operation per the no-secret-exfil rule.
Alternative — direct risk labeling: You can skip the two-step pattern and label operations directly as exe exfil @sendToServer(...). This works but couples the exe definition to the risk category. See policy-operations for details.
Using sensitivity in guards:
Guards can check for sensitivity labels and enforce custom rules:
guard before op:show = when [
@input.any.mx.labels.includes("secret") => deny "Cannot display secrets"
* => allow
]
var secret @recipe = <vault/secret-recipe.txt>
show @recipe
This blocks showing any secret-labeled data.
Why sensitivity labels work:
Sensitivity labels are enforced by the mlld runtime, not by LLM reasoning. Even if an LLM is tricked via prompt injection:
- The secret data still carries its
secretlabel - The operation still has its risk labels (
exfil,network, etc.) - Policy rules block the dangerous combination
- The operation fails regardless of LLM intent
This is defense in depth: the LLM may try to exfiltrate a secret, but the label system prevents it from succeeding.
Trust Labels
Trust labels classify data reliability: trusted or untrusted.
>> Declare untrusted variable
var untrusted @payload = "user input"
>> Semantic label on operation, mapped to destructive via policy
exe fs:w @wipe(data) = run cmd { rm -rf "@data" }
policy @p = {
defaults: { rules: ["no-untrusted-destructive"] },
operations: { destructive: ["fs:w"] }
}
Trust asymmetry: untrusted is sticky. Adding trusted to untrusted data creates a conflict (both labels are kept). Removing untrusted requires privilege via => trusted! @var.
Built-in rules: Enable in policy defaults:
policy @p = {
defaults: { rules: ["no-untrusted-destructive", "no-untrusted-privileged"] }
}
| Rule | Blocks |
|---|---|
no-untrusted-destructive |
untrusted → destructive operations |
no-untrusted-privileged |
untrusted → privileged operations |
Flow blocked:
policy @p = {
defaults: { rules: ["no-untrusted-destructive"] },
operations: { destructive: ["fs:w"] }
}
var untrusted @payload = "data"
exe fs:w @wipe(data) = run cmd { echo "@data" }
show @wipe(@payload)
Error: Rule 'no-untrusted-destructive': label 'untrusted' cannot flow to 'destructive'
The two-step flow: fs:w on exe → policy maps to destructive → no-untrusted-destructive rule blocks untrusted data.
Alternative: Label exe directly as exe destructive @wipe(...) to skip the mapping step. See policy-operations.
Opt-in auto-labeling: Instead of labeling every variable manually, defaults.unlabeled in policy config automatically labels all data that has no user-assigned labels:
policy @p = {
defaults: {
unlabeled: "untrusted",
rules: ["no-untrusted-destructive"]
},
operations: { destructive: ["fs:w"] }
}
var @data = <./input.txt>
exe fs:w @wipe(data) = run cmd { echo "@data" }
show @wipe(@data)
Error: Rule 'no-untrusted-destructive': label 'untrusted' cannot flow to 'destructive' -- file-loaded data has no user labels, so defaults.unlabeled: "untrusted" applies the untrusted label automatically.
This is opt-in via policy config, not default behavior. Data with explicit labels (e.g., var trusted @clean = ...) is unaffected.
Influenced Label
Mark LLM outputs as influenced when they process untrusted data.
policy @p = {
defaults: {
rules: ["untrusted-llms-get-influenced"]
}
}
var untrusted @task = "Review this external input"
exe llm @process(input) = run cmd { claude -p "@input" }
var @result = @process(@task)
show @result.mx.labels >> ["llm", "untrusted", "influenced"]
The rule only auto-applies the label. Enforcement comes from policy.labels.influenced.
Restrict influenced outputs:
labels: {
influenced: {
deny: ["destructive", "exfil"]
}
}
Requirements for label application:
- Policy rule
untrusted-llms-get-influencedenabled - Executable labeled
llm - Input contains
untrustedlabel
Notes:
- Label propagates through interpolation
- Trusted inputs don't trigger the label
- Defense in depth against prompt injection
- See
labels-overviewfor label system basics
Automatic Source Labels
Source labels are automatically applied by the system to track where data originates. Unlike user-declared labels (like secret or pii), you don't add these manually—they're applied when data enters the system.
Available source labels:
| Label | Applied When |
|---|---|
src:cmd |
Output from cmd { } blocks |
src:sh |
Output from sh { } blocks |
src:js |
Output from js { } blocks |
src:py |
Output from py { } blocks |
src:template |
Output from template executables |
src:exe |
Output from pure mlld executables (no code block) |
src:file |
Content loaded from files |
src:user |
User input (via @input resolver) |
src:mcp |
Data from MCP tool calls |
src:network |
Network fetches |
src:keychain |
Values retrieved from the keychain |
src:dynamic |
Runtime-injected modules |
src:env:<provider> |
Output from environment providers |
Directory labels:
When loading files, directory labels (dir:/path) are also applied for each parent directory. This enables location-based security rules.
File load example:
var @config = <@root/config.txt>
show @config.mx.taint | @parse
Output includes ["src:file", "dir:/path/to/parent", ...] - the file source plus all parent directories. Note: @root resolves from the project root location, ensuring paths work from any working directory.
Code block examples:
Each code block type adds its own source label. Input labels from arguments are preserved alongside the source taint.
exe @fromCmd(val) = cmd { printf "%s" "@val" }
exe @fromSh(val) = sh { echo "$val" }
exe @fromJs(val) = js { return val; }
exe @fromPy(val) = py { print(val) }
var pii @name = "Alice"
var @cmdResult = @fromCmd(@name)
show @cmdResult.mx.taint | @parse
>> ["pii", "src:cmd"]
var @jsResult = @fromJs(@name)
show @jsResult.mx.taint | @parse
>> ["pii", "src:js"]
Source taint stacks with input labels: the pii label from @name is preserved, and the execution medium is recorded.
Why source labels matter:
Source labels enable provenance-based security. You can write guards that restrict what external data can do:
guard before op:exe = when [
@input.any.mx.taint.includes("dir:/tmp/uploads") => deny "Cannot execute uploaded files"
* => allow
]
Using source labels in policy:
Policy can set label flow rules for source labels:
policy @p = {
labels: {
"src:mcp": {
deny: ["destructive"]
}
}
}
This prevents MCP-sourced data from flowing to operations labeled destructive.
Source labels vs user labels:
- Source labels (
src:*,dir:*) - factual provenance, auto-applied, cannot be removed - User labels (
secret,pii,untrusted) - semantic classification, manually declared, can be modified
Both flow through the taint field. Guards typically check @mx.taint to see the full picture:
show @data.mx.taint
show @data.mx.labels
The taint array contains both source markers and user labels. The labels array contains only user-declared labels.
Automatic Labels
| Label | Applied To |
|---|---|
src:cmd |
Output from cmd { } blocks |
src:sh |
Output from sh { } blocks |
src:js |
Output from js { } blocks |
src:py |
Output from py { } blocks |
src:template |
Output from template executables |
src:exe |
Output from pure mlld executables (no code block) |
src:file |
File loads |
src:mcp |
MCP tool call results |
src:user |
User input (via @input resolver) |
src:network |
Network fetches |
src:keychain |
Values from the keychain |
src:dynamic |
Dynamic module imports |
src:env:<provider> |
Environment provider outputs |
dir:/path |
File directories (all parents) |
Example directory guards:
guard before op:run = when [
@input.any.mx.taint.includes('dir:/tmp/uploads') =>
deny "Cannot execute uploaded files"
* => allow
]
Label Tracking
Labels propagate through all transformations automatically.
>> Method calls preserve labels
var secret @data = <internal/customers.csv>
var @trimmed = @data.trim()
show @trimmed.mx.labels // ["secret"]
Templates: Interpolated values carry labels to the result.
var secret @recipe = <vault/secret-recipe.txt>
var @msg = `Recipe: @recipe`
show @msg.mx.labels // ["secret"]
Collections: Items retain labels; collection has union.
var secret @data = <internal/customers.csv>
var @arr = [@data, "public"]
show @arr.mx.labels // ["secret"]
Pipelines: Labels accumulate through stages.
var secret @financials = <internal/q4-earnings.txt>
var @result = @financials | @transform | @process
show @result.mx.labels // ["secret"]
Expressions: Ternary/conditional expressions, nullish coalescing, and object spread all preserve labels from their inputs.
var pii @name = "Alice"
var @flag = true
var @result = @flag ? @name : "anon"
show @result.mx.labels // ["pii"]
var @other = null
var @fallback = @other ?? @name
show @fallback.mx.labels // ["pii"]
var secret @creds = { key: "sk-123" }
var @copy = { ...@creds, extra: "x" }
show @copy.mx.labels // ["secret"]
When-expressions: Labels from the matched branch propagate to the result.
var pii @name = "Alice"
var @result = when [
true => @name
* => "anonymous"
]
show @result.mx.labels // ["pii"]
For-loops: Source labels propagate through iteration results.
var secret @items = ["alpha", "beta"]
var @results = for @item in @items => @item.toUpperCase()
show @results.mx.labels // ["secret"]
Code blocks: Labels on arguments survive round-trips through js, sh, py, and cmd blocks. The block type also adds its own source taint.
var pii @name = "Alice"
exe @process(val) = js { return val.toUpperCase(); }
var @result = @process(@name)
show @result.mx.labels // ["pii"]
show @result.mx.taint // ["pii", "src:js"]
File I/O: When labeled data is written to disk, the audit log records the taint. Reading the file restores it.
var secret @records = <internal/patients.csv>
output @records to "@root/tmp/export.txt"
var @loaded = <@root/tmp/export.txt>
show @loaded.mx.labels // ["secret"]
The audit log stores a write event with the taint set. On subsequent reads, mlld consults the log and applies the recorded labels. See audit-log for the ledger format.
Note: If @loaded.mx.labels shows [], check that you declared the sensitivity label on the original variable (e.g., var secret @records). Labels are not inferred from content—they must be declared explicitly.
Label Modification
Label modification syntax applies security labels to return values.
Add labels:
exe @classify(data) = [
let @processed = @data | @transform
=> pii @processed
]
exe @markMultiple(data) = [
=> pii,internal @data
]
Multiple labels separated by commas.
Trust modification:
>> Downgrade trust (always allowed)
exe @taint(data) = [
=> untrusted @data
]
>> Add trusted (warning if already untrusted)
exe @suggest(data) = [
=> trusted @data
]
Adding untrusted replaces any existing trusted label. Adding trusted to already untrusted data triggers a warning (configurable via policy.defaults.trustconflict).
Privileged operations:
Privileged guards can remove protected labels:
guard privileged @bless after secret = when [
* => allow with { addLabels: ["trusted"], removeLabels: ["untrusted", "secret"] }
]
Policy guards are automatically privileged. User-defined guards are privileged when declared with the privileged prefix or with { privileged: true }. Privilege applies to guard declarations; exe functions do not have a privileged mode.
| Privileged action | Syntax | Effect |
|---|---|---|
| Blessing | => trusted! @var |
Remove untrusted, add trusted |
| Label removal | => !pii @var |
Remove specific label |
| Multi-label removal | => !pii,!internal @var |
Remove multiple labels |
| Clear labels | => clear! @var |
Remove all non-factual labels |
The shorthand syntax (trusted!, !label, clear!) ONLY works inside privileged guards (including guard when [...] actions). It does not work in exe blocks or non-privileged guards. Attempting to use it outside a privileged guard context throws a privilege error.
>> Privileged guard — shorthand works here
guard privileged @sanitize after secret = when [
@output.verified => trusted! @output
@output.public => !secret @output
* => deny "Unverified secret data"
]
>> Non-privileged guard — shorthand does NOT work
guard @attempt after secret = when [
* => trusted! @output
]
>> Error: LABEL_PRIVILEGE_REQUIRED — trusted! requires privileged guard context
>> Exe block — shorthand does NOT work
exe @tryBless(data) = [
=> trusted! @data
]
>> Error: LABEL_PRIVILEGE_REQUIRED — trusted! requires privileged guard context
Guards also support allow with { ... } action syntax for privileged label modifications:
| Guard action | Privilege? | Effect |
|---|---|---|
allow with { addLabels: ["trusted"] } |
No | Adds trusted (trust conflict policy still applies) |
allow with { removeLabels: ["untrusted"] } |
Yes | Removes untrusted |
allow with { addLabels: ["trusted"], removeLabels: ["untrusted"] } |
Yes | Blessing: removes untrusted, adds trusted |
allow with { removeLabels: ["secret"] } |
Yes | Removes protected label |
Trust label asymmetry:
| Syntax | Privilege? | Effect |
|---|---|---|
=> untrusted @var |
No | Replaces trusted (taint flows down) |
=> trusted @var |
No | Adds trusted; warning if conflict |
=> trusted! @var |
Yes | Blessing: removes untrusted |
=> !label @var |
Yes | Removes specific label |
=> clear! @var |
Yes | Removes all non-factual labels |
Protected labels:
These labels require privilege to remove:
secret- Prevents self-blessing of sensitive datauntrusted- Tracks trust statesrc:*(all source labels:src:mcp,src:cmd,src:sh,src:js,src:py,src:file,src:network,src:keychain, etc.) - Provenance tracking
Attempting to remove protected labels without privilege throws PROTECTED_LABEL_REMOVAL error.
Factual labels:
Labels starting with src: are factual provenance labels. They are part of .mx.taint and may not appear in .mx.labels. Use .mx.taint for source checks such as src:mcp and src:cmd. clear! does not remove factual labels.
Guard context:
After guards receive @output with the operation result:
guard @validateMcp after src:mcp = when [
@output.data?.valid => allow @output
* => deny "Invalid MCP response"
]
The guard uses after timing to process output. Blessing and label removal require privileged guards using allow with { addLabels, removeLabels } syntax.
Trust conflict behavior:
Controlled by policy.defaults.trustconflict:
warn(default) - Log warning, keep both labels, treat as untrustederror- Throw errorsilent- No warning, keep both labels
Guards
Guards block labeled data at trust boundaries:
guard before secret = when [
@mx.op.labels.includes("net:w") => deny "Secrets cannot flow to network operations"
* => allow
]
Guard syntax:
guard [@name] TIMING TRIGGER = when [...]
TIMING:before,after, oralways(foris shorthand forbefore)TRIGGER: a label — matches wherever that label appears (on input data, on operations, or both). Use theop:prefix to narrow to operation-only matching.
How triggers match:
A guard trigger is a label. It matches wherever that label appears:
| Match source | Scope | @input |
When it fires |
|---|---|---|---|
| Data label on an input | per-input | The individual labeled variable | Each input with that label |
| Operation label (exe label) | per-operation | Array of all operation inputs | Once per matching operation |
>> Matches input data with the 'secret' label AND exes labeled 'secret'
guard before secret = when [...]
>> Matches ONLY exes/operations labeled 'exfil' (narrowed with op:)
guard before op:exfil = when [...]
The op: prefix is for disambiguation — use it when you want operation-only matching. For most guards, bare labels are simpler and match both contexts.
Security context in guards:
All guards have access to the full operation context:
@mx.labels- semantic classification (what it is):secret,pii,untrusted@mx.taint- provenance (where it came from):src:mcp,src:cmd,src:js,src:file@mx.sources- transformation trail (how it got here):mcp:createIssue,command:curl@mx.op.labels- operation labels, including exe labels likedestructiveornet:w
Guard Context Reference:
| Guard scope | @input |
@output |
@mx highlights |
|---|---|---|---|
| per-operation | Array of operation inputs | String view of the first input | @mx.op.type, @mx.op.name, @mx.op.labels, @mx.guard.try |
| per-operation (after) | Array of operation outputs in the current guard scope | String view of the current output | @mx.op.*, @mx.guard.try, @mx.guard.reasons, @mx.guard.hintHistory |
| per-input | The current labeled value (string, object, array, etc.) |
String view of the current value | @mx.op.*, @mx.labels, @mx.taint, @mx.sources, @mx.guard.try |
Per-operation guard inputs expose helper metadata for aggregate checks:
@input.any.mx.labels.includes("secret")@input.all.mx.taint.includes("src:file")@input.none.mx.labels.includes("pii")@input.mx.labels,@input.mx.taint,@input.mx.sources@input.any.text.includes("SSN")for content-level text inspection
Two ways to guard the same flow:
You can guard from the data side or the operation side — both work:
>> Approach 1: Guard on the data label, check the operation
guard before secret = when [
@mx.op.labels.includes("net:w") => deny "Secrets cannot flow to network operations"
* => allow
]
>> Approach 2: Guard on the operation label, check the data
guard before net:w = when [
@input.any.mx.labels.includes("secret") => deny "Secrets cannot flow to network operations"
* => allow
]
Both prevent secret data from reaching net:w operations. Choose whichever reads more naturally for your use case.
Hierarchical operation matching:
Operation type matching with op: is hierarchical: before op:cmd:git matches op:cmd:git:push, op:cmd:git:status, etc.
Per-input validation and transformation:
Per-input guards can validate or sanitize data by label:
guard @validateSecret before secret = when [
@input.length < 8 => deny "Secret is too short"
* => allow
]
guard @sanitize before untrusted = when [
* => allow @input.trim().slice(0, 100)
]
Per-input guards run in full operation context — use @mx.op.type, @mx.op.labels, etc. to check what operation the labeled data is flowing into:
guard @redact before secret = when [
@mx.op.type == "show" => allow @redact(@input)
* => allow
]
After guards:
After guards validate or transform operation output:
guard @validateJson after op:exe = when [
@isValidJson(@output) => allow
* => deny "Invalid JSON"
]
After-guard transforms chain sequentially in declaration order — each matching guard receives the output from the previous guard. See guard-composition for the full resolution model.
Guard Composition
These rules apply to all guards (per-operation and per-input):
- Guards run top-to-bottom in declaration order.
alwaystiming participates in both phases (beforeandafter).- Decision precedence is
deny>retry>allow @value>allow. - Before-phase transforms are last-wins: each guard evaluates against the original input independently, and the last guard's replacement becomes the operation input.
- After-phase transforms chain sequentially; each guard receives the previous guard's output.
retryactions apply only in retryable operation contexts (for example pipeline stages). In non-retryable contexts, retry resolves as a deny.before op:exetransforms run before executable evaluation, so guard logic reads@inputin this phase.@outputis available inafterphase only.
Guards are non-reentrant (won't trigger on their own operations).
Per-input guards (matched via data labels) fire once per matching input and participate in the same decision pipeline — their deny/allow/retry results compose with per-operation guard results using the same precedence rules above.
Privileged Guards
Privileged guards cannot be bypassed with { guards: false } and can remove protected labels.
>> Mark a user-defined guard as privileged (prefix form)
guard privileged @blocker before op:run = when [
* => deny "blocked"
]
>> Equivalent with-clause form
guard @blocker before op:run = when [
* => deny "blocked"
] with { privileged: true }
>> Policy rules create privileged guards automatically
policy @p = {
defaults: { rules: ["no-secret-exfil"] },
operations: { exfil: ["net:w"] }
}
var secret @customerList = <internal/customers.csv>
exe net:w @send(data) = run cmd { printf "%s" "@data" }
>> Privileged guard still blocks — { guards: false } only disables user guards
show @send(@customerList) with { guards: false }
>> Error: Rule 'no-secret-exfil': label 'secret' cannot flow to 'exfil'
What privileged guards can do:
| Action | Syntax | Effect |
|---|---|---|
| Bless | => trusted! @var |
Remove untrusted, add trusted |
| Remove label | => !label @var |
Remove specific label |
| Clear labels | => clear! @var |
Remove all non-factual labels |
Non-privileged guards cannot remove ANY labels. Protected labels (secret, untrusted, src:*) get PROTECTED_LABEL_REMOVAL; other labels get LABEL_PRIVILEGE_REQUIRED. This ensures label removal is always a privilege escalation.
Contrast — non-privileged guard cannot remove labels:
>> User-defined guard — NOT privileged
guard @bless after secret = when [
* => allow with { removeLabels: ["secret"] }
]
>> PROTECTED_LABEL_REMOVAL: Cannot remove protected label 'secret' without privilege
How guards become privileged:
Policy-generated guards are privileged. User-defined guards are privileged when declared with the privileged prefix or with { privileged: true }.
Notes:
with { guards: false }disables user guards but privileged guards still runwith { guards: only(...) }andexcept(...)also preserve privileged guards- See
label-modificationfor privilege syntax details
Transform with Allow
guard @redact before secret = when [
@mx.op.type == "show" => allow @redact(@input)
* => allow
]
allow @value replaces the guarded value:
- In
beforephase, replacements read from@input. - In
afterphase, replacements read from@output.
For before op:exe, write transforms against @input (for example method calls or helper executables) so input conversion stays explicit.
When multiple before transforms match, the last replacement becomes operation input. after transforms chain in declaration order.
Denied Handlers
The denied keyword is a when-condition that tests if we're in a denied context. Use it to handle guard denials gracefully.
deny "reason"— guard action that blocks an operationdenied— when-condition that matches inside a denied handler
guard before op:run = when [
@input.any.mx.labels.includes("secret") => deny "Secrets blocked from shell"
* => allow
]
exe @safe(value) = when [
denied => `[blocked] @mx.guard.reason`
* => @value
]
denied handlers catch denials from guards in both per-operation and per-input scope. When a guard denies an operation, the exe's when block can match denied and provide a fallback value.
Accessing guard context:
exe @handler(value) = when [
denied => show "Blocked: @mx.guard.reason"
denied => show "Guard: @mx.guard.name"
denied => show "Labels: @mx.labels.join(', ')"
* => show @value
]
Negating denied:
exe @successOnly(value) = when [
!denied => @value
]
Hooks
hook registers user lifecycle hooks that run before or after operations. Hooks observe, transform, or log — they do not abort (use guards for that).
Syntax: hook [<@name>] <before|after> <filter> = <body>
>> Named hook on a function (after)
hook @logger after @review = [
append `reviewed: @mx.op.name` to "audit.log"
]
>> Before hook on an operation type
hook before op:run = [
log `running: @mx.op.name`
]
>> After hook with when body
hook @router after op:exe = when [
@mx.op.name == "deploy" => log "deployed"
* => log "other exe"
]
Three filter types:
| Filter | Example | Matches |
|---|---|---|
| Function | @review |
Calls to that executable |
| Function + prefix | @review("src/") |
Calls where first arg starts with "src/" |
| Operation | op:run, op:exe, op:var |
All operations of that type |
| Data label | untrusted |
Operations with labeled inputs |
Supported operation filters: op:var, op:run, op:exe, op:show, op:output, op:log, op:append, op:stream, op:for, op:for:iteration, op:for:batch, op:loop, op:import.
op:loop fires per iteration (each call to loop()), not once for the whole /loop directive. op:log matches /log directives specifically (not /output).
Hook body context:
| Variable | Timing | Description |
|---|---|---|
@input |
both | Operation inputs (function args for before function hooks) |
@output |
after |
Operation result |
@mx.op.name |
both | Operation or executable name |
@mx.op.type |
both | Operation type (exe, run, show, etc.) |
@mx.op.labels |
both | Labels on the operation |
@mx.hooks.errors |
both | Errors from earlier hooks in the chain |
Behavior:
- Hooks run in declaration order
- Return values chain — a later hook receives the previous hook's transformed value
- Body errors are isolated and collected in
@mx.hooks.errors(the parent operation continues) - Hooks are non-reentrant — nested operations inside a hook body skip user hooks
>> Observability: telemetry + error tracking
hook @telemetry after @emit = [
output `telemetry:@mx.op.name` to "state://telemetry"
]
hook @errors after @emit = [
append `errors:@mx.hooks.errors.length` to "hook-errors.log"
]
Hooks vs guards: Hooks observe and transform. Guards enforce security policy and can deny or retry operations. Use hooks for logging, telemetry, and light transforms. Use guards when you need to block operations based on labels or content.