Hen
Run API requests as files from the command line.
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
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
This installs the hen CLI.
Container Image
The repository also ships a container image that includes the hen CLI.
Build it locally with Docker:
Quick Start
Verify a collection without executing shell commands or network requests:
Run every request in a collection non-interactively:
Emit machine-readable output for scripts or CI:
Inspect a collection's authoring structure without executing requests:
Import an OpenAPI contract into an editable .hen collection:
Run the same collection against a selected named environment:
Verify a collection that references local secret providers without loading the secrets:
CLI
Hen has four primary commands:
hen runexecutes a collection or request.hen verifyparses and validates a collection without making requests.hen inspectexposes machine-readable authoring structure for editor tooling and analysis.hen importlowers an OpenAPI 3.x JSON or YAML spec into an editable.hencollection.
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
allto 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, namedoneOf(...)andanyOf(...)unions, scalarconst(...)tags, anddiscriminator(...)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 for the real API source contract and examples/openapi_imported.hen for its generated collection. See examples/openapi_union_import.yaml and examples/openapi_union_imported.hen for a focused union and discriminator import example.
Selection and prompts
- If a directory contains one
.henfile, Hen selects it automatically. - If a collection contains multiple requests, provide an index or
allto bypass the picker. - The text CLI prompts for unresolved
[[ prompt ]]placeholders. --non-interactivedisables selection prompts and fails when required prompt values are missing.- Use repeated
--input key=valueflags 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.
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 verifyreports available environment names without selecting one.- Structured run output reports the selected environment so CI artifacts show which overlay was used.
Resolution order is:
- Collection preamble scalar assignments.
- The selected named environment.
- Explicit
--input key=valuevalues for prompt placeholders. - Prompt defaults declared with
[[ name = default ]]. - Runtime dependency captures and callback exports for downstream requests.
Dotenv overlays
Collections can also preload dotenv files in the preamble and then bind selected keys explicitly
through secret.env(...).
dotenv .env
$ API_ORIGIN = secret.env("API_ORIGIN")
env local
dotenv .env.local
env staging
dotenv .env.staging
dotenv PATHis only valid in the collection preamble before the first---.- Top-level dotenv directives apply to every run. The selected named environment can add more dotenv directives inside its
env ...block. - Dotenv directives may use the existing guard prefix, for example
[ PROFILE == "local" ] dotenv .env.local. - Dotenv paths are plain string-like values in this slice. Interpolation such as
dotenv {{ path }}ordotenv [[ path ]]is rejected. hen verifyvalidates dotenv syntax and guards, and reports duplicate-path or missing-file warnings from resolved paths, without reading dotenv contents.- During runs,
env("NAME")andsecret.env("NAME")both check the real process environment first and then fall back to loaded dotenv values. Later dotenv directives override earlier ones. - Missing dotenv files are skipped with a warning instead of failing the run.
- Dotenv files are not inherently secret. Values bound through
env(...)stay ordinary scalars, while values consumed throughsecret.env(...)are auto-redacted. - See examples/dotenv_support.hen for a concrete runnable collection.
Local value providers
Hen supports both non-secret environment lookups and secret-backed local references inside scalar assignments.
$ API_ORIGIN = env("API_ORIGIN")
$ API_TOKEN = secret.env("HEN_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
env("NAME")reads one value from the process environment at run time and falls back to loaded dotenv values when the process environment does not define that key. Values loaded this way are not auto-redacted.secret.env("NAME")reads from the same lookup stack but marks the resolved value sensitive for reports, transcripts, and retained artifacts.secret.file("PATH")reads one UTF-8 text file relative to the collection working directory and strips one trailing line ending.- Repeated environment-backed and secret-backed references are cached once per run after the first lookup.
- These references are only resolved during runs, not during
hen verify. - Reference arguments are string literals in this slice; interpolation inside
env(...),secret.env(...), orsecret.file(...)is intentionally out of scope. - Supported secret providers in this slice are
envandfile. 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.
That includes values resolved through secret.env(...) when the source value came from a dotenv file. Dotenv entries that are never consumed through secret.env(...) stay ordinary non-secret values.
Use preamble redaction rules to broaden that default policy without changing request execution:
redact_header = X-Session-Token
redact_capture = SESSION_ID
redact_body = body.session.accessToken
redact_header = NAMEadds one header name to the built-in masked header set.redact_capture = NAMEtreats captured or exported values under that name as sensitive in the current request and in downstream dependent requests that reuse them.redact_body = body.pathorredact_body = json(body.payload).tokenmasks 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 byhen verify. - See 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.
name = Cookie Example
@ region = us-east-1
---
Load dashboard
session = web
GET https://example.com/dashboard
@ session = [[ session_id ]]
@ cookie_name = valueis 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 outboundCookieheader. - 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:
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
CookieandSet-Cookieheaders across text, JSON, NDJSON, JUnit, transcripts, and retained artifacts. - See examples/http_cookie_session.hen for a concrete collection.
OAuth profiles
Hen can acquire and reuse OAuth tokens from named preamble profiles.
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_credentialsandrefresh_token. - Use exactly one endpoint source per profile:
issuer = ...for OIDC discovery ortoken_url = ...for a direct token endpoint. param name = valueadds extra token form fields such asaudienceorresource.<field> -> $VARIABLEmaps 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 setsAuthorizationexplicitly. - Discovery and token acquisition only happen during
hen run, never duringhen 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 and examples/oauth_refresh_token.hen for concrete collections.
Conditional guards
Use a bracketed predicate to conditionally run an assertion or include a fragment.
[ 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 for a runnable assertion-guard example.
Useful options
--body none|selected|failed|allcontrols how much of the request and response bodies appear in text output and retained artifacts.--output text|json|ndjson|junitselects human or machine-readable output.--parallelruns independent requests concurrently.--max-concurrency Nthrottles parallel execution.--continue-on-errorkeeps unaffected dependency branches running.--benchmark Nbenchmarks a request instead of running it once.--exportrenders the request as a curl command.--verboseincludes 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.
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
)
scalardeclarations can start from a primitive type, a built-in scalar target, or another named scalar, then refine that value withconst(...),enum(...),format(...),len(...),pattern(...), andrange(...)predicates.schemadeclarations support object shapes, plain aliases, root-array aliases, and combinators such asallOf(...),oneOf(...),anyOf(...),not(...), anddiscriminator(...).- Object schema fields are open by default in v1. Use
field?: Typefor optional fields andType?for nullable values. - Schema field names may be bare identifiers,
$-prefixed names such as$metadata, or quoted strings such as"request-id"when a JSON object key is not a plain identifier. - Schema object fields may optionally end with a trailing comma, so JSON-style layouts like
field: Type,are valid. - 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, examples/openapi_union_imported.hen, and the schema examples under examples/.
Assertion labels
Use a # ... comment directly above an assertion to annotate it with a short intent label:
# 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 assecret.env("NAME")andsecret.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-levelauth = <name>provide lazy token acquisition and mapped token exports. - Redaction rules: preamble-level
redact_header,redact_capture, andredact_bodyextend the default safe-output masking policy without changing request behavior. - Request inputs: headers with
*, cookies with@, query parameters with?, form data with~, fenced body blocks, and raw file-backed bodies withbody_file = @.... - Requests: HTTP by default, plus explicit
protocol = graphql,protocol = mcp,protocol = sse, andprotocol = ws. - Reliability: preamble and request-level
timeout,poll_until, andpoll_everydirectives control per-attempt timeouts and eventual-consistency polling. - Captures:
& body.token -> $TOKENstores response data for later requests, assertions, and callbacks. - Assertions:
^lines validate status, headers, body fields, structural JSON matches, and schema targets. - Dependencies:
> requires: Request Namecreates a DAG so setup requests run before dependents, including matching mapped iterations when producer and consumer share compatible bindings. - Fragments:
<< file.henreuses 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.
Request bodies
~~~ [content_type]
body
~~~
body_file = @/path/to/file
- Use fenced body blocks for inline text payloads such as JSON, GraphQL, or plain text.
- Use
body_file = @/path/to/fileto send raw bytes from disk, similar tocurl --data-binary @file. body_filepaths resolve relative to the.henfile.- Set
* Content-Type = ...explicitly when the server expects a specific media type such asapplication/octet-stream. - See examples/binary_request_body.hen for a runnable file-backed body example.
JSON selection and queries
Hen uses a small, explicit JSON selector syntax for captures, assertions, dependency reads, and body redaction.
& 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.fieldfor object-root responses andbody.[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,$VARIABLEvalues,&&,||, 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 and examples/filtered_selector_variables.hen for runnable examples.
Request reliability
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
timeoutapplies to each attempt and defaults to30s.poll_untilis off by default. When set, Hen reruns the same request until its assertions pass or the poll window expires.poll_everycontrols the fixed retry interval and defaults to1swhenever 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, ormsuffixes such as250ms,2s, and1m. within = ...remains the protocol-specific receive or exchange timeout for SSE and WebSocket steps; usetimeout = ...for ordinary request execution.- See 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~~~graphqldocuments. - MCP: MCP-over-HTTP authoring with generated JSON-RPC envelopes and reusable sessions.
- SSE: named streaming sessions with
receivesteps andwithintimeout windows. - WebSocket:
open,send,exchange, andreceiveflows over a named session.
Full syntax guide
The complete authoring grammar lives in 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: basic HTTP requests
- examples/environment_overrides.hen: named environment overlays
- examples/oauth_client_credentials.hen: client-credentials auth with mapped token fields
- examples/oauth_refresh_token.hen: refresh-token auth with downstream reuse
- examples/conditionals.hen: conditional assertions with guard predicates
- examples/local_secrets.hen: local env and file secret providers
- examples/redaction_rules.hen: additive safe-output redaction rules
- examples/request_reliability.hen: per-attempt timeouts and poll-until authoring
- examples/openapi_import.yaml: source OpenAPI contract for import workflows
- examples/openapi_imported.hen: generated
.henoutput from the OpenAPI import example - examples/openapi_union_import.yaml: source OpenAPI contract showing
oneOfanddiscriminatorimport - examples/openapi_union_imported.hen: generated
.henoutput for union and discriminator import - examples/json_response_captures.hen: captures and JSON paths
- examples/filtered_selector_variables.hen: selector variables in captures and assertions
- examples/schema_scalar_checks.hen: scalar validation
- examples/schema_object_validation.hen: object schema validation
- examples/schema_root_array_validation.hen: root-array schemas
- examples/schema_fragment_reuse.hen: declaration reuse through fragments
- examples/structural_json_matching.hen: structural JSON assertions
- examples/graphql_protocol.hen: GraphQL authoring
- examples/mcp_protocol.hen: MCP-over-HTTP sessions
- examples/sse_protocol.hen: server-sent events
- examples/ws_protocol.hen: WebSocket sessions