hen 0.20.2

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
# Hen

Run API requests as files from the command line.

[Read Documentation](https://hen-api.com)

Hen keeps request definitions, assertions, captures, dependencies, and protocol-specific behavior in a single `.hen` file. It works well for local exploration, CI, and editor integrations.

## Table of Contents

- [Quick Example]#quick-example
- [Installation]#installation
- [Quick Start]#quick-start
- [CLI]#cli
- [Authoring Model]#authoring-model
- [Examples]#examples

## Quick Example

```text
name = Test Collection File
description = A collection of mock requests for testing this syntax.

$ API_KEY = $(./get_secret.sh)
$ USERNAME = $(echo $USER)
$ API_ORIGIN = https://lorem-api.com/api
$ ROLE = [admin, user, guest]

---

Some descriptive title for the prompt.

POST {{ API_ORIGIN }}/echo

* Authorization = {{ API_KEY }}
? query_param_1 = value

~~~ application/json
{
  "username": "{{ USERNAME }}",
  "password": "[[ password ]]",
  "role": "{{ ROLE }}"
}
~~~

^ & status == 200

! sh ./callback.sh
```

This single file can define variables, prompt inputs, mapped requests, assertions, and callbacks. Add `> requires:` when later requests depend on earlier ones, including mapped-to-mapped dependencies when the expanded iterations share compatible bindings, and use response captures to thread values through a collection.

## Installation

```bash
cargo install hen
```

This installs the `hen` CLI.

### Container Image

The repository also ships a container image that includes the `hen` CLI.

Build it locally with Docker:

```bash
docker build -t hen:latest .
```

## Quick Start

Verify a collection without executing shell commands or network requests:

```bash
hen verify ./examples/lorem.hen
```

Run every request in a collection non-interactively:

```bash
hen run ./examples/lorem.hen all --non-interactive
```

Emit machine-readable output for scripts or CI:

```bash
hen run ./examples/lorem.hen all --non-interactive --output json
```

Inspect a collection's authoring structure without executing requests:

```bash
hen inspect ./examples/graphql_protocol.hen --output json
```

Import an OpenAPI contract into an editable `.hen` collection:

```bash
hen import ./examples/openapi_import.yaml --output ./examples/openapi_imported.hen
```

Run the same collection against a selected named environment:

```bash
hen run ./examples/environment_overrides.hen 0 --env local --non-interactive
```

Verify a collection that references local secret providers without loading the secrets:

```bash
hen verify ./examples/local_secrets.hen
```

## CLI

Hen has four primary commands:

- `hen run` executes a collection or request.
- `hen verify` parses and validates a collection without making requests.
- `hen inspect` exposes machine-readable authoring structure for editor tooling and analysis.
- `hen import` lowers an OpenAPI 3.x JSON or YAML spec into an editable `.hen` collection.

The default `hen [PATH] [SELECTOR]` form still works for interactive terminal use, but `hen run ...` is the clearer choice for CI and automation.

Use `hen inspect --output json` when another tool needs collection summaries, local declaration and import ranges, request metadata, required inputs, and merged symbol tables without running the collection.

### OpenAPI import

Use `hen import <spec-path> [selector] --output <collection.hen>` when you want a contract-first starting point from an OpenAPI 3.0.x or 3.1.x spec.

- Omit the selector or use `all` to import every safely materializable operation.
- Select one operation with an `operationId`, `METHOD /path`, `tag:name`, or numeric index.
- Supported schema lowering includes plain component aliases, object-shaped `allOf`, named `oneOf(...)` and `anyOf(...)` unions, scalar `const(...)` tags, and `discriminator(...)` unions when the OpenAPI schema shape is representable in Hen.
- Import fails when the spec is invalid, references cannot be resolved, or the selected operation set cannot be emitted as valid `.hen`.
- Successful imports still print warnings for approximations such as reduced auth flows or omitted unsupported schema details.

See [examples/openapi_import.yaml](examples/openapi_import.yaml) for the real API source contract and [examples/openapi_imported.hen](examples/openapi_imported.hen) for its generated collection. See [examples/openapi_union_import.yaml](examples/openapi_union_import.yaml) and [examples/openapi_union_imported.hen](examples/openapi_union_imported.hen) for a focused union and discriminator import example.

### Selection and prompts

- If a directory contains one `.hen` file, Hen selects it automatically.
- If a collection contains multiple requests, provide an index or `all` to bypass the picker.
- The text CLI prompts for unresolved `[[ prompt ]]` placeholders.
- `--non-interactive` disables selection prompts and fails when required prompt values are missing.
- Use repeated `--input key=value` flags to provide prompt values up front.
- Use `--env <name>` to apply a named collection environment before request execution.

### Named environments

Collections can declare named environment overlays in the preamble to swap scalar variable values without editing requests.

```text
name = Example Collection

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

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

---

Get profile

GET {{ API_ORIGIN }}/profile

* X-Client-Id = {{ CLIENT_ID }}
```

- Environment blocks are only valid in the preamble before the first `---`.
- Environment overrides can only target previously declared scalar variables in this milestone.
- If no environment is selected, Hen keeps the collection-level defaults.
- `hen verify` reports available environment names without selecting one.
- Structured run output reports the selected environment so CI artifacts show which overlay was used.

Resolution order is:

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

### Local secret providers

Hen supports local secret references inside scalar assignments.

<!-- 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 one environment variable at run time.
- `secret.file("PATH")` reads one 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 only resolved during runs, not during `hen verify`.
- Secret reference arguments are string literals in this slice; interpolation inside `secret.env(...)` or `secret.file(...)` is intentionally out of scope.
- Supported providers in this slice are `env` and `file`. OS keychain remains a stretch goal and is not implemented.

### Safe output redaction

Hen automatically redacts values loaded through `secret.*(...)` plus built-in sensitive header names such as `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, and API-key style headers across text, JSON, NDJSON, JUnit, transcripts, and retained artifacts.

Use preamble redaction rules to broaden that default policy without changing request execution:

```text
redact_header = X-Session-Token
redact_capture = SESSION_ID
redact_body = body.session.accessToken
```

- `redact_header = NAME` adds one header name to the built-in masked header set.
- `redact_capture = NAME` treats captured or exported values under that name as sensitive in the current request and in downstream dependent requests that reuse them.
- `redact_body = body.path` or `redact_body = json(body.payload).token` masks a specific response-body value even when you do not export it.
- Redaction rules are additive, only valid in the collection preamble before the first `---`, and are validated structurally by `hen verify`.
- See [examples/redaction_rules.hen]examples/redaction_rules.hen for a concrete collection.

### HTTP session cookies

Ordinary HTTP requests can author explicit outbound cookies with `@ ...` and can reuse response-set
cookies by sharing the same `session = ...` handle.

```text
name = Cookie Example

@ region = us-east-1

---

Load dashboard

session = web
GET https://example.com/dashboard
@ session = [[ session_id ]]
```

- `@ cookie_name = value` is valid in the collection preamble and inside individual requests.
- Request-level cookies override preamble cookies when the cookie name matches exactly.
- Hen serializes structured `@ ...` cookie directives into one outbound `Cookie` header.
- Structured `@ ...` cookies cannot be mixed with a manual `* Cookie = ...` header on the same
  request.
- When a request also uses `session = ...`, explicit `@ ...` cookies merge with the session cookie
  jar by name, and explicit request cookies win on conflicts.

Session-backed requests still reuse the same cookie jar for the duration of the run:

```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"
```

- Requests that share the same HTTP session reuse one cookie jar for that run.
- Session reuse also adds the same implicit ordering Hen already uses for other session-backed
  protocols, so login-before-profile flows stay deterministic.
- Built-in redaction already masks `Cookie` and `Set-Cookie` headers across text, JSON, NDJSON,
  JUnit, transcripts, and retained artifacts.
- See [examples/http_cookie_session.hen]examples/http_cookie_session.hen for a concrete
  collection.

### OAuth profiles

Hen can acquire and reuse OAuth tokens from named preamble profiles.

```text
name = OAuth Client Credentials

$ API_ORIGIN = https://api.example.com

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 = {{ API_ORIGIN }}
  access_token -> $API_ACCESS_TOKEN
  token_type -> $API_TOKEN_TYPE

---

Get profile

auth = api
GET {{ API_ORIGIN }}/me
* X-Token-Type = {{ API_TOKEN_TYPE }}
```

- `oauth <name>` blocks are only valid in the collection preamble before the first `---`.
- Requests attach a profile with `auth = <name>` and Hen lazily acquires or reuses the token immediately before dispatch.
- Supported grants in this slice are `client_credentials` and `refresh_token`.
- Use exactly one endpoint source per profile: `issuer = ...` for OIDC discovery or `token_url = ...` for a direct token endpoint.
- `param name = value` adds extra token form fields such as `audience` or `resource`.
- `<field> -> $VARIABLE` maps token-response fields into ordinary scalar exports that the current request and downstream dependent requests can reuse.
- Hen injects `Authorization: Bearer ...` automatically unless the request already sets `Authorization` explicitly.
- Discovery and token acquisition only happen during `hen run`, never during `hen verify`.
- Access tokens, refresh tokens, and injected authorization headers stay redacted across text output, structured reports, transcripts, and retained artifacts.
- See [examples/oauth_client_credentials.hen](examples/oauth_client_credentials.hen) and [examples/oauth_refresh_token.hen](examples/oauth_refresh_token.hen) for concrete collections.

### Conditional guards

Use a bracketed predicate to conditionally run an assertion or include a fragment.

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

- Guards can prefix assertions or `<<` fragment imports.
- A false assertion guard skips that assertion without failing the request.
- A false fragment guard skips that import entirely.
- Guard predicates use the same comparison and regex operators as ordinary assertions, except schema validation with `===` is not supported in guards.
- See [examples/conditionals.hen]examples/conditionals.hen for a runnable assertion-guard example.

### Useful options

- `--body none|selected|failed|all` controls how much of the request and response bodies appear in text output and retained artifacts.
- `--output text|json|ndjson|junit` selects human or machine-readable output.
- `--parallel` runs independent requests concurrently.
- `--max-concurrency N` throttles parallel execution.
- `--continue-on-error` keeps unaffected dependency branches running.
- `--benchmark N` benchmarks a request instead of running it once.
- `--export` renders the request as a curl command.
- `--verbose` includes more detail in text output and enables debug logging.

### Schema validation

Hen supports collection-local `scalar` and `schema` declarations plus built-in targets such as
`UUID`, `EMAIL`, `NUMBER`, `DATE`, `DATE_TIME`, `TIME`, and `URI`.

```text
scalar HANDLE = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)
scalar CardKind = const("card")

schema User {
  id: UUID
  email: EMAIL
  handle: HANDLE
}

schema Users = User[]
schema Contact = anyOf(EMAIL, URI)
schema Checkout = discriminator(method,
  "card": CardCheckout,
  "bank": BankCheckout
)
```

- `scalar` declarations can start from a primitive type, a built-in scalar target, or another
  named scalar, then refine that value with `const(...)`, `enum(...)`, `format(...)`, `len(...)`,
  `pattern(...)`, and `range(...)` predicates.
- `schema` declarations support object shapes, plain aliases, root-array aliases, and combinators
  such as `allOf(...)`, `oneOf(...)`, `anyOf(...)`, `not(...)`, and `discriminator(...)`.
- Object schema fields are open by default in v1. Use `field?: Type` for optional fields and
  `Type?` for nullable values.
- Scalar and schema references are resolved after the full preprocessed collection is loaded, so
  forward references are valid across the preamble and imported fragments.
- `===` validates a typed JSON value against a built-in scalar target, named scalar declaration, or
  named schema declaration.

For the full declaration grammar and more examples, see [syntax-reference.md](syntax-reference.md),
[examples/openapi_union_imported.hen](examples/openapi_union_imported.hen), and the schema examples
under [examples/](examples/).

### Assertion labels

Use a `# ...` comment directly above an assertion to annotate it with a short intent label:

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

- Successful text output prints the label, for example `✅ [Fetch fixture] [The page loads]`.
- The comment labels only the assertion on the very next line.
- Blank lines or other request items break the association.
- Labels are literal text and are not interpolated.

## Authoring Model

Hen files are plain text collections made of a preamble and one or more requests separated by `---`.

### Core concepts

- Variables: `$ NAME = value`, shell substitutions with `$(...)`, local secret references such as `secret.env("NAME")` and `secret.file("./path")`, prompt placeholders with `[[ name ]]`, and simple arrays for mapped requests.
- Named environments: `env <name>` blocks in the preamble override previously declared scalar variables for a selected run.
- OAuth profiles: preamble `oauth <name>` blocks plus request-level `auth = <name>` provide lazy token acquisition and mapped token exports.
- Redaction rules: preamble-level `redact_header`, `redact_capture`, and `redact_body` extend the default safe-output masking policy without changing request behavior.
- Request inputs: headers with `*`, cookies with `@`, query parameters with `?`, form data with `~`, and fenced body blocks.
- Requests: HTTP by default, plus explicit `protocol = graphql`, `protocol = mcp`, `protocol = sse`, and `protocol = ws`.
- Reliability: preamble and request-level `timeout`, `poll_until`, and `poll_every` directives control per-attempt timeouts and eventual-consistency polling.
- Captures: `& body.token -> $TOKEN` stores response data for later requests, assertions, and callbacks.
- Assertions: `^` lines validate status, headers, body fields, structural JSON matches, and schema targets.
- Dependencies: `> requires: Request Name` creates a DAG so setup requests run before dependents, including matching mapped iterations when producer and consumer share compatible bindings.
- Fragments: `<< file.hen` reuses shared request snippets or declarations.
- Callbacks: `!` lines run shell commands after request execution.
- Guards: `[predicate]` can gate assertions or fragment imports, including typed schema checks with `===` when the left-hand side resolves to JSON.

### JSON selection and queries

Hen uses a small, explicit JSON selector syntax for captures, assertions, dependency reads, and body redaction.

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

- Use `body.field` for object-root responses and `body.[0]` when the root is an array.
- Use `[index]` to step into arrays and `[? ...]` to query an array by field values.
- Filter queries are evaluated relative to each array item, support `==`, `!=`, scalar literals, `$VARIABLE` values, `&&`, `||`, and parentheses, and must identify exactly one match.
- Use `json(...)` when a selected field contains stringified JSON that needs another traversal pass.
- Captures may include a fallback with `:=`, for example `& body.token -> $TOKEN := fallback`, which is used when the selected path is missing.
- Hen does not implement full JSONPath or jq features such as wildcards, projections, recursive descent, slices, nested filters, regex filters, or structural filter RHS values.
- See [examples/json_response_captures.hen]examples/json_response_captures.hen and [examples/filtered_selector_variables.hen]examples/filtered_selector_variables.hen for runnable examples.

### Request reliability

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

---

Create export

POST https://api.example.com/exports
timeout = 20s

^ & status == 202
& body.jobId -> $JOB_ID

---

Wait for export

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

^ & status == 200
^ & body.state == "completed"
^ & body.downloadUrl != null
```

- `timeout` applies to each attempt and defaults to `30s`.
- `poll_until` is off by default. When set, Hen reruns the same request until its assertions pass or the poll window expires.
- `poll_every` controls the fixed retry interval and defaults to `1s` whenever polling is enabled.
- Preamble directives provide collection-wide defaults; a request overrides only the fields it sets.
- Polling retries only assertion failures and per-attempt timeouts. Transport failures stay terminal.
- Durations use `ms`, `s`, or `m` suffixes such as `250ms`, `2s`, and `1m`.
- `within = ...` remains the protocol-specific receive or exchange timeout for SSE and WebSocket steps; use `timeout = ...` for ordinary request execution.
- See [examples/request_reliability.hen]examples/request_reliability.hen for a complete authoring example.

### Protocol support

- HTTP: ordinary request and response workflows.
- GraphQL: GraphQL-over-HTTP with `operation`, `variables`, and `~~~graphql` documents.
- MCP: MCP-over-HTTP authoring with generated JSON-RPC envelopes and reusable sessions.
- SSE: named streaming sessions with `receive` steps and `within` timeout windows.
- WebSocket: `open`, `send`, `exchange`, and `receive` flows over a named session.

### Full syntax guide

The complete authoring grammar lives in [syntax-reference.md](syntax-reference.md). That file covers:

- variables and prompts
- headers, cookies, query parameters, form data, and body blocks
- JSON selection, filtered array queries, and decoded `json(...)` traversal
- reliability directives and protocol-specific directives
- declarations, fragments, captures, assertions, guards, callbacks, and dependencies

## Examples

The fastest way to learn the format is to run the included examples:

- [examples/lorem.hen]examples/lorem.hen: basic HTTP requests
- [examples/environment_overrides.hen]examples/environment_overrides.hen: named environment overlays
- [examples/oauth_client_credentials.hen]examples/oauth_client_credentials.hen: client-credentials auth with mapped token fields
- [examples/oauth_refresh_token.hen]examples/oauth_refresh_token.hen: refresh-token auth with downstream reuse
- [examples/conditionals.hen]examples/conditionals.hen: conditional assertions with guard predicates
- [examples/local_secrets.hen]examples/local_secrets.hen: local env and file secret providers
- [examples/redaction_rules.hen]examples/redaction_rules.hen: additive safe-output redaction rules
- [examples/request_reliability.hen]examples/request_reliability.hen: per-attempt timeouts and poll-until authoring
- [examples/openapi_import.yaml]examples/openapi_import.yaml: source OpenAPI contract for import workflows
- [examples/openapi_imported.hen]examples/openapi_imported.hen: generated `.hen` output from the OpenAPI import example
- [examples/openapi_union_import.yaml]examples/openapi_union_import.yaml: source OpenAPI contract showing `oneOf` and `discriminator` import
- [examples/openapi_union_imported.hen]examples/openapi_union_imported.hen: generated `.hen` output for union and discriminator import
- [examples/json_response_captures.hen]examples/json_response_captures.hen: captures and JSON paths
- [examples/filtered_selector_variables.hen]examples/filtered_selector_variables.hen: selector variables in captures and assertions
- [examples/schema_scalar_checks.hen]examples/schema_scalar_checks.hen: scalar validation
- [examples/schema_object_validation.hen]examples/schema_object_validation.hen: object schema validation
- [examples/schema_root_array_validation.hen]examples/schema_root_array_validation.hen: root-array schemas
- [examples/schema_fragment_reuse.hen]examples/schema_fragment_reuse.hen: declaration reuse through fragments
- [examples/structural_json_matching.hen]examples/structural_json_matching.hen: structural JSON assertions
- [examples/graphql_protocol.hen]examples/graphql_protocol.hen: GraphQL authoring
- [examples/mcp_protocol.hen]examples/mcp_protocol.hen: MCP-over-HTTP sessions
- [examples/sse_protocol.hen]examples/sse_protocol.hen: server-sent events
- [examples/ws_protocol.hen]examples/ws_protocol.hen: WebSocket sessions