hen 0.18.1

Run protocol-aware API request collections from the command line or through MCP.
Documentation

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

cargo install hen

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:

docker build -t hen:latest .

Quick Start

Verify a collection without executing shell commands or network requests:

hen verify ./examples/lorem.hen

Run every request in a collection non-interactively:

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

Emit machine-readable output for scripts or CI:

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

Run the same collection against a selected named environment:

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

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

hen verify ./examples/local_secrets.hen

CLI

Hen has two primary commands:

  • hen run executes a collection or request.
  • hen verify parses 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 .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.

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 or MCP inputs 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.

$ 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(...) 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:

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 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 Cookie and Set-Cookie headers 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_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 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|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.

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 hen for terminal-first, prompt-driven workflows
  • use hen-mcp when an MCP client needs structured access
  • provide any [[ prompt ]] values explicitly through tool inputs

Running the server

If hen-mcp is on your PATH:

hen-mcp

From a checkout of this repository:

cargo run --bin hen-mcp

For normal MCP client usage, prefer a compiled binary:

cargo build --release
./target/release/hen-mcp

Example configuration

VS Code uses .vscode/mcp.json:

{
  "servers": {
    "hen": {
      "type": "stdio",
      "command": "/absolute/path/to/hen-mcp"
    }
  },
  "inputs": []
}

Claude Code uses .mcp.json:

{
  "mcpServers": {
    "hen": {
      "type": "stdio",
      "command": "/absolute/path/to/hen-mcp"
    }
  }
}

If hen-mcp is installed globally, command can simply be hen-mcp.

Exposed MCP surface

  • run_hen: run a collection or request non-interactively
  • verify_hen_syntax: validate a file or inline source without execution
  • get_hen_authoring_guide: return built-in usage or syntax docs
  • hen://authoring-guide and hen://readme: built-in resources for clients that read docs directly

run_hen accepts the same explicit environment selection used by the CLI:

{
  "path": "./examples/environment_overrides.hen",
  "selector": "0",
  "environment": "staging",
  "inputs": {
    "client_id": "ci-client"
  }
}

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.
  • 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.
  • 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.

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.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 &&, 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
  • 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 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. 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: