hen 0.19.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# Syntax Reference

## Variables
<!-- markdownlint-disable MD014 -->
```text
$ variable_name = value
$ variable_name = $(command)
$ variable_name = secret.env("NAME")
$ variable_name = secret.file("./path/to/value.txt")
$ variable_name = [[ prompt_name ]]
$ variable_name = [[ prompt_name = default_value ]]
$ variable_name = [foo, bar]
```
<!-- markdownlint-enable MD014 -->

- Array assignments expand the request once per value. When a request references multiple array variables, Hen runs the Cartesian product of those values (up to two variables and 128 total combinations).
- Arrays must contain simple scalar values (no whitespace or nested arrays). Their values can still come from commands or prompts.
- Prompt placeholders may include defaults with `[[ name = default ]]`. Defaults are plain text up to the closing `]]`, so URL-shaped values such as `[[ ws_origin = wss://example.com ]]` are valid.
- Requests generated from arrays are suffixed with the selected values (for example, `[USER=foo]`). A failing iteration aborts the remaining iterations, and exports from each iteration are suffixed with the same label.
- Requests cannot declare dependencies on a mapped request; share setup through an unmapped helper instead.

## Local Secret Providers

<!-- markdownlint-disable MD014 -->
```text
$ API_TOKEN = secret.env("HEN_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
```
<!-- markdownlint-enable MD014 -->

- `secret.env("NAME")` reads the named environment variable when the collection is prepared for execution.
- `secret.file("PATH")` reads a UTF-8 text file relative to the collection working directory and strips one trailing line ending.
- Repeated secret references are cached once per run after the first lookup.
- Secret references are valid anywhere this slice accepts scalar assignments, including collection variables, request variables, and environment overrides.
- Secret reference arguments are string literals in this slice. Interpolation such as `secret.env("{{ NAME }}")` or `secret.file("[[ path ]]")` is rejected.
- `hen verify` parses and validates secret references without reading environment variables or files.
- Supported providers in this slice are `env` and `file`. Remote secret backends are out of scope, and OS keychain is not implemented yet.

## Redaction Rules

```text
redact_header = X-Session-Token
redact_capture = SESSION_ID
redact_body = body.session.accessToken
redact_body = json(body.payload).token
```

- Redaction rules are only valid in the collection preamble before the first `---`.
- Built-in masking already covers `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, and API-key style headers, plus values loaded through `secret.env(...)` and `secret.file(...)`.
- `redact_header = NAME` adds one exact header name to the masked header set for text output, structured reports, transcripts, and retained artifacts.
- `redact_capture = NAME` treats captured or exported values under that name as sensitive in the current request and in downstream dependent requests that reuse the exported value.
- `redact_body = ...` accepts the same body-path syntax used by captures, but it must resolve from the current response body. Valid forms start from `body...` or `json(body...). ...`.
- Redaction rules are additive. They broaden the default safe-output policy and do not disable the built-in masking rules.
- `hen verify` validates the directive syntax and body-path shape without resolving any live values.

## Named Environments

```text
$ API_ORIGIN = https://api.example.com
$ CLIENT_ID = [[ client_id ]]

env local
  $ API_ORIGIN = http://localhost:3000
  $ CLIENT_ID = hen-local

env staging
  $ API_ORIGIN = https://staging.example.com
```

- Environment blocks are only valid in the collection preamble before the first `---`.
- Environment overrides may only target previously declared scalar variables.
- Environment values in this milestone must stay scalar; arrays, shell substitutions, and nested environment syntax are rejected.
- Select an environment with `hen run --env <name>` or with the MCP `run_hen` tool's `environment` argument.
- If no environment is selected, Hen uses the collection defaults.
- `hen verify` reports `availableEnvironments`, and structured run output reports `selectedEnvironment`.

Resolution order is:

1. Collection preamble scalar assignments.
2. The selected named environment.
3. Explicit CLI `--input key=value` or MCP `inputs` values for prompt placeholders.
4. Prompt defaults declared with `[[ name = default ]]`.
5. Runtime dependency captures and callback exports for downstream requests.

## OAuth Profiles

```text
oauth api
  grant = client_credentials
  issuer = https://login.example.com
  client_id = secret.env("HEN_CLIENT_ID")
  client_secret = secret.env("HEN_CLIENT_SECRET")
  scope = read:profile
  param audience = https://api.example.com
  access_token -> $API_ACCESS_TOKEN
  token_type -> $API_TOKEN_TYPE

---

Get profile

auth = api
GET https://api.example.com/me
```

- `oauth <name>` blocks are only valid in the collection preamble before the first `---`.
- `auth = <name>` attaches the named OAuth profile to one request.
- Supported grants in this slice are `client_credentials` and `refresh_token`.
- A profile must define exactly one endpoint source: `issuer = ...` or `token_url = ...`.
- `issuer = ...` performs OIDC discovery at run time and caches the discovered `token_endpoint` for the current run.
- `param name = value` adds an extra form field to the token request.
- `<field> -> $VARIABLE` maps token-response fields into ordinary scalar exports that the current request and downstream dependent requests can reference.
- Hen injects `Authorization: Bearer ...` automatically unless the request already sets `Authorization` explicitly.
- `refresh_token` profiles remember a rotated refresh token returned by the token endpoint and use it for later refreshes in the same run.
- Discovery and token acquisition never run during `hen verify`; verification stays structural.
- Mapped export names participate in the existing built-in sensitive-export masking rules and any configured `redact_capture` rules, while `Authorization` headers are already redacted by default.

## Headers

```text
* header_key = header_value
* header_key = {{ variable_name }}
* header_key = [[ prompt_name ]]
```

Request-level headers override global preamble headers when the header name matches exactly.
Use the same casing when overriding a global header; case-only differences are not a reliable override.

## Query Parameters

```text
? query_key = query_value
? query_key = {{ variable_name }}
? query_key = [[ prompt_name ]]
```

## Multipart Form Data

```text
~ form_key = form_value
~ form_key = {{ variable_name }}
~ form_key = [[ prompt_name ]]
~ form_key = @/path/to/file
```

## Request Body

```text
~~~ [content_type]
body
~~~
```

## Request Reliability

```text
timeout = 30s
poll_every = 1s

---

Wait for export

GET https://example.com/exports/{{ JOB_ID }}
timeout = 5s
poll_until = 2m
poll_every = 2s

^ & status == 200
^ & body.state == "completed"
```

- `timeout = ...` may appear in the preamble or on a request. It applies per attempt and defaults to `30s`.
- `poll_until = ...` may appear in the preamble or on a request. It sets the total polling window across attempts and is disabled by default.
- `poll_every = ...` may appear in the preamble or on a request. It sets the fixed retry interval and defaults to `1s` whenever polling is enabled and no interval is provided.
- Request-level reliability directives override only the fields they declare and inherit the remaining preamble defaults.
- Durations currently accept `ms`, `s`, or `m` suffixes such as `250ms`, `2s`, or `1m`.
- When polling is enabled, Hen reruns the same request and reevaluates its ordinary assertions on each attempt.
- Polling retries only after assertion failures or per-attempt request timeouts. Transport failures remain terminal unless a separate retry policy is added later.
- `within = ...` remains separate from request reliability. Use `within` only for SSE `receive` steps and WebSocket `receive` or `exchange` steps.
- When the polling window expires, Hen reports the last assertion mismatch or timeout detail rather than a generic poll exhaustion message.
- See [examples/request_reliability.hen](examples/request_reliability.hen) for a complete collection that combines preamble defaults, a captured job ID, and a polled follow-up request.

## Assertion Labels

```text
# The page loads
^ & status == 200

# User payload is present
[FEATURE_ENABLED == true] ^ & body.user.id == "123"
```

- A `# ...` comment directly above an assertion attaches a short plain-text label to that assertion only.
- Labels appear in successful text CLI output so passing assertions can describe intent instead of repeating the raw expression.
- Labels are literal text and are not interpolated.
- Blank lines or other request items break the association.

## Protocol Directives

```text
protocol = graphql
operation = GetUser
variables = {"id":"123"}

protocol = mcp
session = app
call = initialize
tool = search
arguments = {"query":"hedgehog"}
protocol_version = 2025-11-25
client_name = hen
client_version = 0.14.0
capabilities = {}

protocol = sse
session = prices
receive
within = 5s

protocol = ws
session = chat
~~~json
{"type":"hello"}
~~~
```

- If `protocol` is omitted, the request is ordinary HTTP.
- `protocol = graphql` enables GraphQL authoring while still executing over HTTP.
- `protocol = mcp` enables MCP-over-HTTP authoring while still executing over HTTP.
- `protocol = sse` enables server-sent event streams with an opening request plus session-backed `receive` steps.
- `protocol = ws` enables session-backed WebSocket open, `send`, `exchange`, and `receive` steps.
- GraphQL requests currently require `POST`.
- MCP requests currently require `POST`.
- SSE opening requests currently require `GET`.
- WebSocket opening requests currently require `GET`.
- `variables = ...` must be valid JSON after interpolation.
- `call = ...` selects the MCP JSON-RPC method for the request.
- `session = ...` names a reusable session handle. For ordinary HTTP requests, steps sharing the same session reuse one cookie jar. When a later session-backed request omits its method/URL line, Hen inherits the most recent method/URL used for that session.
- For `protocol = ws`, a body block implies a send step. Plain `~~~` or `~~~text`/`~~~text/plain` sends text, while `~~~json` or `~~~application/json` sends JSON.
- `receive` is valid for `protocol = sse` and `protocol = ws` steps.
- For `protocol = ws`, adding `within = ...` to a body-backed step turns it into an exchange step.
- `within = ...` is valid for `protocol = sse` receive steps, `protocol = ws` receive steps, and `protocol = ws` exchange steps, and currently accepts `ms`, `s`, or `m` suffixes such as `250ms`, `2s`, or `1m`. Use request-level `timeout`, `poll_until`, and `poll_every` for ordinary execution timeouts and polling.
- `tool = ...` and `arguments = ...` are only valid with `call = tools/call`.
- `protocol_version`, `client_name`, `client_version`, and `capabilities` are only valid with `call = initialize`.
- `arguments = ...` and `capabilities = ...` must be valid JSON objects after interpolation.

## HTTP Session Cookies

```text
Login

session = web
POST https://httpbin.org/cookies/set/session/hen-demo

# Login request succeeds
^ & status == 200

---

Load cookies

session = web
GET https://httpbin.org/cookies

# Reuses the session cookie
^ & body.cookies.session == "hen-demo"
```

- Reusing the same HTTP `session = ...` shares one cookie jar across those steps.
- Session-backed HTTP requests also get the same implicit planner ordering Hen already uses for MCP, SSE, and WebSocket session reuse.
- Cookie values are covered by the built-in redaction rules for `Cookie` and `Set-Cookie`.

## GraphQL Requests

```text
Get User GraphQL

protocol = graphql
POST https://example.com/graphql
operation = GetUser
variables = {"id":"123"}

~~~graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
  }
}
~~~

^ & graphql.errors == null
^ & graphql.data.user.id == "123"
```

- Use `~~~graphql` or `~~~application/graphql` for the document block.
- GraphQL responses still use the normal `body`, `header`, and `status` accessors.
- `graphql.data...` and `graphql.errors...` are aliases for the corresponding JSON body paths.
- Callback environments receive `GRAPHQL_DOCUMENT`, `GRAPHQL_OPERATION`, and `GRAPHQL_VARIABLES` for GraphQL requests.

## MCP Requests

```text
Connect MCP

protocol = mcp
session = app
POST https://example.com/mcp
* Authorization = Bearer [[ mcp_token ]]
call = initialize

^ & body.result.serverInfo.name == "fixture-mcp"

---

List resources

protocol = mcp
session = app
call = resources/list

^ & body.result.resources[0].name == "Fixture Resource"

---

Call tool

protocol = mcp
session = app
call = tools/call
tool = search
arguments = {"query":"hedgehog"}

^ & body.result.content[0].text == "search result"
```

- Supported `call = ...` values in the current slice are `initialize`, `tools/list`, `resources/list`, and `tools/call`.
- `call = initialize` defaults `protocol_version` to Hen's preferred MCP version, `client_name` to `hen`, `client_version` to the running Hen build version, and `capabilities` to `{}` unless you override them.
- MCP request bodies are generated automatically as JSON-RPC envelopes, so explicit body blocks, content-type blocks, and multipart form directives are not allowed.
- MCP authentication currently uses ordinary request headers.
- MCP responses still use the normal `body`, `header`, and `status` accessors, so assertions and captures can target `body.result...` and `body.error...` directly.
- When an MCP tool returns stringified JSON inside a text field, use `json(...)` to decode it before traversing or asserting on nested fields, for example `^ & json(body.result.content[0].text).items[0].id == "123"`.
- Session-backed steps add an implicit dependency on the session-creating request. When a later step omits its method/URL line, Hen inherits the most recent method/URL used for that session.

## SSE Requests

```text
Open price stream

protocol = sse
session = prices
GET https://example.com/prices/stream

^ & status == 200

---

Receive update

session = prices
receive
within = 5s

& sse.id -> $EVENT_ID

^ & sse.event == "price"
^ $EVENT_ID == "evt-1"
^ & body.symbol == "AAPL"
^ & body.price === NUMBER
```

- The opening SSE step establishes the named session and leaves the stream reader running in the background.
- If the opening step omits an explicit `Accept` header, Hen sends `Accept: text/event-stream` automatically.
- `receive` consumes the next event observed for the session within the configured timeout.
- When the event payload is valid JSON, ordinary `body...` JSON paths work for assertions and captures.
- Receive steps can also capture or assert `sse.event` and `sse.id` directly when the server sends them.
- When the payload is plain text, use the normal raw-body comparison or regex assertions.
- Callback environments include `SSE_ACTION`, `SSE_SESSION`, and, for receive steps that observed them, `SSE_EVENT` and `SSE_ID`.
- Structured JSON output preserves SSE protocol context such as `action`, `sessionName`, `within`, `event`, and `id` when available.

## WebSocket Requests

```text
Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
within = 2s
~~~json
{"type":"hello"}
~~~

& ws.kind -> $KIND

^ $KIND == "text"
^ & ws.kind == "text"
^ & body.type == "ack"
```

- The opening WebSocket step establishes the named session.
- A body block makes a WebSocket step a send step. Plain `~~~` or `~~~text`/`~~~text/plain` sends text, while `~~~json` or `~~~application/json` sends JSON.
- Adding `within = ...` to that body-backed step turns it into an exchange step that waits for the next queued message.
- Later session-backed `send`, `exchange`, and `receive` steps inherit the target line from the session-opening step.
- `receive` and `exchange` reuse the shared `within = ...` timeout grammar.
- `ws.kind` can capture or assert the observed reply frame type directly, while `body` contains the reply payload.
- Callback environments include `WS_ACTION`, `WS_SESSION`, and, for send, exchange, and receive steps, `WS_KIND`.
- Structured JSON output preserves WebSocket protocol context such as `action`, `sessionName`, `within`, and `kind` when available.

To run the repository WebSocket example end to end, use `hen ./examples/ws_protocol.hen all` because the file contains both the session-opening step and the exchange step.

## Declarations

Declarations live in the collection preamble before the first `---`. They are collection-local, and imported fragments brought in with `<<` can contribute reusable declarations.

## Fragments

```text
<< ./fragments/common_schema_types.hen
[ FEATURE_ENABLED == true ] << ./fragments/extra_assertions.hen
```

- `<< path.hen` imports another Hen file into the current collection before parsing.
- Fragment paths are resolved relative to the importing file.
- Imports can contribute reusable declarations or request-local snippets.
- Prefix a fragment import with `[predicate]` to include it only when the guard evaluates to true.
- Guarded fragment imports use the same guard rules described below.

```text
scalar Food = enum("pizza", "taco", "salad")
scalar HANDLE = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)

schema Address {
  city: string
  postalCode: string
}

schema User {
  id: UUID
  email: EMAIL
  birthday?: DATE?
  favoriteFood?: Food
  address: Address
}

schema Users = User[]
```

- Built-in scalar targets are always available: `UUID`, `EMAIL`, `NUMBER`, `DATE`, `DATE_TIME`, `TIME`, and `URI`. Those names are reserved and cannot be redefined.
- `scalar` declarations can reference primitive types, built-in scalar targets, or named scalar declarations, then refine them with:
  `enum(value1, value2, ...)` where each value is a quoted string, integer, decimal number, `true`, `false`, or `null`; `format(NAME)` where `NAME` is one of `UUID`, `EMAIL`, `NUMBER`, `DATE`, `DATE_TIME`, `TIME`, or `URI`; `len(min..max)` for string length checks with inclusive integer bounds and optional open-ended forms like `len(3..)` or `len(..24)`; `pattern(/regex/)` for slash-delimited regular-expression checks on strings; and `range(min..max)` for inclusive integer or decimal numeric bounds with optional open-ended forms like `range(0..)` or `range(..10.5)`.
- Scalar and schema references are resolved after the full preprocessed collection is loaded, so forward references are allowed across the collection preamble and imported fragments.
- `schema` object fields are open by default in v1: extra fields are ignored during validation.
- `field?: Type` makes a field optional.
- `Type?` makes the value nullable.
- `schema Name = Type[]` defines a root-array schema.

## JSON Selection

Use JSON selectors anywhere Hen expects a response-body operand, including captures, assertions, redaction rules, and dependency reads.

```text
& body.user.id -> $USER_ID
^ & body.items[0].name == "first"
^ & body.[0].id == 123
^ & body.jobs[? recipient == $RECIPIENT && status != "failed"].status == "succeeded"
^ & json(body.result.content[0].text).items[0].id == "123"
^ &[Create Job].body.result.state == "completed"
```

- `body` starts at the current response JSON value.
- Use dot access for object fields such as `body.user.id`.
- Use `[index]` for arrays nested under an object key, such as `body.items[0].name`.
- Use `body.[index]` when the response body itself is an array.
- Use `[? ...]` to query an array for one matching element, then continue traversing from that element.
- Filter queries evaluate paths relative to each array item, support `==` and `!=`, accept scalar literals or `$VARIABLE` values, and may combine clauses with `&&`.
- Filter queries must resolve to exactly one array element. Zero matches or multiple matches are treated as failures.
- Use `json(...)` when the selected value is a JSON string and you want to decode it before continuing traversal.
- Use `&[Request Name].body...` when selecting from a declared dependency response instead of the current response.
- Hen intentionally keeps selectors narrower than full JSONPath or jq: no wildcards, slices, projections, recursive descent, regex filters, or `||`.

## Response Captures

```text
& body -> $VAR_NAME
&[Dependency Request].body -> $VAR_NAME
& body.json_field -> $VAR_NAME
& body.token -> $TOKEN := fallback
& body.items[0].name -> $VAR_NAME
& body.[0].id -> $VAR_NAME
& body.jobs[? recipient == "alice@example.com"].status -> $VAR_NAME
& json(body.result.content[0].text).items[0].id -> $VAR_NAME
& header.header_name -> $VAR_NAME
& status -> $VAR_NAME
```

Use the JSON selection rules above for `body...`, `json(...)`, and dependency-body paths.
Use `&[Request Name].body...`, `&[Request Name].header...`, or `&[Request Name].status` to read from a declared dependency response.
Dependency response captures require the referenced request to be declared in `> requires:` so the planner can guarantee the upstream result exists.
Append `:= default_value` to use a fallback when the selected capture path is missing.

## Assertions

`val` can be a variable or a response capture.

```text
^ & body.field == 'value'
^ & body.jobs[? recipient == "alice@example.com"].status == 'succeeded'
^ & sse.event == 'price'
^ & json(body.result.content[0].text).items[0].id == '123'
^ & json(body.result.content[0].text).items[0] ~= {"id":"123"}
^ & body.id === UUID
^ & body.total === NUMBER
^ & body === User
^ &[Login].body === LoginResponse

& body.field -> $VAR
^ $VAR == 'value'
```

```text
^ val == value
^ val != value
^ val ~= /regex/
^ val ~= {"id":7}
^ val ~= [{"id":1},{"id":2}]
^ val === SchemaName
^ val > value
^ val >= value
^ val < value
^ val <= value 
```

- Bare `true`, `false`, and `null` are typed literals.
- Quoted values remain strings.
- Prefix an assertion with `[predicate]` to evaluate it conditionally.
- Response body assertions preserve JSON types during evaluation, including arrays, objects, `null`, and missing paths.
- `==` and `!=` compare structured JSON values structurally when both sides resolve to arrays or objects.
- `~=` performs substring or regex matching for string patterns, partial object matching for JSON object literals, unordered membership for array-vs-scalar JSON matches, and unordered subset matching for JSON array literals.
- `===` validates the left-hand JSON value against a built-in scalar, named scalar, or named schema target from the collection preamble.
- The right-hand side of `===` must be a declared target name, not a variable or quoted literal.
- `===` requires a typed JSON left-hand operand, so use response body captures or dependency body captures rather than plain string variables.
- `json(...)` decodes stringified JSON from a response path before the usual JSON traversal and assertion rules apply.
- Use `NUMBER` when you want a concise built-in target for any JSON number. For text validation, the current recommended path is a named scalar built from `string` with `len(...)`, `pattern(...)`, `enum(...)`, or `format(...)` as needed.
- Body capture operands use the same JSON selection rules as response captures, including filtered array queries with `$VARIABLE` values and the same exact-one-match requirement.
- JSON object and array literals must be valid JSON with double-quoted keys and string values.
- Ordering operators only succeed for comparable numeric or text values.

## Conditional Guards

```text
[ USERNAME == "foo" ] ^ & body.username == "foo"
[ FEATURE_ENABLED != true ] << ./fragments/legacy_assertions.hen
```

- Guards apply to assertions and fragment imports.
- The guard predicate must appear in `[...]` immediately before the guarded assertion or `<<` import.
- Guard predicates use the same comparison operators and regex matching as ordinary assertions.
- Guard predicates may reference interpolated variables and typed literals such as `true`, `false`, and `null`.
- `===` schema validation is not supported inside guards.
- A false assertion guard marks that assertion as skipped.
- A false fragment guard omits that fragment import entirely.

## Callbacks

```text
! callback code
! ./path/to/callback_file.sh
! some command -> $VARIABLE_NAME
```

Assignments require whitespace around `->` and suppress the command's stdout.

## Dependencies

```text
> requires: Request Name
```