Hen
Run API requests as files, from the command line or through MCP.
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 or agent 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, and use response captures to thread values through a collection.
Installation
This installs both hen and hen-mcp.
Container Image
The repository also ships a container image that includes both binaries.
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:
Run the same collection against a selected named environment:
Verify a collection that references local secret providers without loading the secrets:
CLI
Hen has two primary commands:
hen runexecutes a collection or request.hen verifyparses and validates a collection without making requests.
The default hen [PATH] [SELECTOR] form still works for interactive terminal use, but hen run ... is the clearer choice for CI and automation.
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=valueor MCPinputsvalues for prompt placeholders. - Prompt defaults declared with
[[ name = default ]]. - Runtime dependency captures and callback exports for downstream requests.
Local secret providers
Hen supports local secret references inside scalar assignments.
$ API_TOKEN = secret.env("HEN_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
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(...)orsecret.file(...)is intentionally out of scope. - Supported 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.
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 reuse cookies by sharing the same session = ... handle:
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_]*$/)
schema User {
id: UUID
email: EMAIL
handle: HANDLE
}
For the full declaration grammar and more examples, see syntax-reference.md 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.
MCP Server
Hen also ships with hen-mcp, a stdio MCP server for editors and agents. It is intentionally non-interactive:
- use
henfor terminal-first, prompt-driven workflows - use
hen-mcpwhen an MCP client needs structured access - provide any
[[ prompt ]]values explicitly through tool inputs
Running the server
If hen-mcp is on your PATH:
From a checkout of this repository:
For normal MCP client usage, prefer a compiled binary:
Example configuration
VS Code uses .vscode/mcp.json:
Claude Code uses .mcp.json:
If hen-mcp is installed globally, command can simply be hen-mcp.
Exposed MCP surface
run_hen: run a collection or request non-interactivelyverify_hen_syntax: validate a file or inline source without executionget_hen_authoring_guide: return built-in usage or syntax docshen://authoring-guideandhen://readme: built-in resources for clients that read docs directly
run_hen accepts the same explicit environment selection used by the CLI:
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. - 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. - Fragments:
<< file.henreuses shared request snippets or declarations. - Callbacks:
!lines run shell commands after request execution. - Guards:
[predicate]can gate assertions or fragment imports.
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.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"
- 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&&, and must identify exactly one match. - Use
json(...)when a selected field contains stringified JSON that needs another traversal pass. - Hen does not implement full JSONPath or jq features such as wildcards, projections, recursive descent, slices, regex filters, or
||. - 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, 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/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