A command line for any HTTP API that publishes an OpenAPI 3 description.
Point ezu at a spec and every operation becomes a command, with its own help, shell completion, and checks that catch mistakes before anything is sent. No code generation: the spec is read when you run the command.
- Every operation, at your fingertips. Path parameters as arguments,
--q./--h./--b.for query, headers and body fields, typed from the spec. - Output you can use. Pretty JSON or YAML, tables and CSV,
--selectto pick and filter what you need, exit codes that tell 4xx from 5xx. - Profiles and authentication. Bearer, Basic, API keys, OAuth 2.0 (authorization code with PKCE, client credentials, device flow), external signing tools, mutual TLS.
- Any URL, too.
ezu http GET /zones --profile exosends a request without a spec, with the profile's authentication; a profile's credentials only go to its own API. - Ready for AI agents.
ezu mcp exoserves an API to Claude, Cursor and other MCP clients: operations become tools, authenticated with your profile, read-only unless you allow more. - Workflows. Run multi-step Arazzo workflows: chain calls, poll until a job finishes, branch on results.
- Safe by default. Verified TLS, credentials never follow a redirect to another host, secrets redacted from logs and
--dry-run. - One small static binary for macOS and Linux, about 2 MB, with no runtime dependencies. Also a Fennel/Lua library.
With Homebrew, on macOS or Linux:
brew install mpenet/tap/ezuThe full name is needed: Homebrew's own ezu is another tool. It installs the release binary below, with shell completion.
Or download the binary for your platform from the releases page (ezu-macos-arm64, ezu-macos-x86_64, ezu-linux-x86_64 or ezu-linux-arm64), then:
install -m 755 ezu-macos-arm64 /usr/local/bin/ezu
ezu --versionEvery release carries SHA256SUMS and a build provenance attestation:
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify ezu-macos-arm64 --repo mpenet/ezuOther ways to install (from source, with luarocks) are in Installing ezu.
The documentation has an index of every page. Most used:
- Installing ezu, with shell completion.
- Guide: adding an API, credentials, environments, scripting, debugging.
- Commands, Requests, Output and Configuration: every option and setting.
- Moving from Restish:
ezu profile import-restishturns a Restish configuration into profiles. - Changelog.
The tour uses the public Swagger Petstore. It is a shared demo, so the pets you see will differ.
export PETSTORE=https://petstore3.swagger.io/api/v3/openapi.jsonList what the API can do:
$ ezu op $PETSTORE --list
add-pet Add a new pet to the store.
create-user Create user.
delete-pet Deletes a pet.
find-pets-by-status Finds Pets by status.
get-pet-by-id Find pet by ID.
...Operation names come from the spec's operationIds (getPetById → get-pet-by-id).
Read an operation's help:
$ ezu op $PETSTORE get-pet-by-id --help
GET /pet/{petId}
Find pet by ID.
Returns a single pet.
Usage: ezu op <profile-or-schema> get-pet-by-id <petId>
Path params:
petId integer [required] — ID of pet to return
Responses:
200 successful operation
400 Invalid ID supplied
404 Pet not found
default Unexpected errorCall it. Path parameters are positional:
$ ezu op $PETSTORE get-pet-by-id 7
{
"category": {
"id": 4,
"name": "Lions"
},
"id": 7,
"name": "Lion 1",
"photoUrls": [
"url1",
"url2"
],
"status": "available",
"tags": [ ... ]
}Query parameters, selection and formats:
$ ezu op $PETSTORE find-pets-by-status --q.status=sold --select '[].{id,name}' --output yaml
- id: 100001
name: doggie-updated
- id: 54
name: Roki
...--select takes .field, [N], [] (every element), {a,b} (pick fields), filters like [?status==sold] and ..field at any depth. --output is json (the default), yaml, raw, table, csv, tsv, status or headers; see Output. Colours are used on a terminal only.
Send a body. Fields are typed from the request schema; -d takes JSON or YAML, inline, from a file (@pet.json) or from stdin (@-). Check what would be sent with --dry-run, or get a curl command with --curl:
$ ezu op $PETSTORE add-pet --b.name=Rex --b.status=available --b.photoUrls='["rex.jpg"]' --dry-run
POST https://petstore3.swagger.io/api/v3/pet
accept: application/json, application/xml
content-type: application/json
{"name":"Rex","photoUrls":["rex.jpg"],"status":"available"}ezu checks the request against the spec first, and sends nothing when something is missing:
$ ezu op $PETSTORE add-pet --b.name=Rex
body.photoUrls: required
(use --no-validate to send anyway)Errors go to stderr, and the exit code tells them apart: 4 for a 4xx response, 5 for a 5xx, 1 for anything else that went wrong.
$ ezu op $PETSTORE get-pet-by-id 999999999
HTTP 404 0.495s
Pet not found
$ echo $?
4-v shows the request and the response headers on stderr, with credentials redacted.
Name your APIs in ~/.config/ezu/config.json, with their settings and credentials:
{
"profiles": {
"petstore": {
"schema": "https://petstore3.swagger.io/api/v3/openapi.json",
"auth": {"name": "bearer", "params": {"token": "env:PETSTORE_TOKEN"}}
},
"petstore-yaml": {
"extends": "petstore",
"output": "yaml"
}
}
}Then use the profile's name instead of the spec's URL:
$ ezu op petstore-yaml get-pet-by-id 7 --select '{name,status}'
name: Lion 1
status: availableenv:NAME reads a secret from the environment. Authentication runs only when a request is actually sent, never for --help, --list or completion. OAuth tokens are cached and refreshed. See Configuration, Authentication and the guide.
An Arazzo document describes a sequence of calls. doc/examples/petstore.arazzo.yaml finds a pet by status, then fetches it:
$ ezu az doc/examples/petstore.arazzo.yaml
first-pet Find the first pet with a given status and fetch its details.
$ ezu az doc/examples/petstore.arazzo.yaml first-pet status=sold --output yaml
name: doggie-updated
status: sold--arazzo-output progress shows each step as it runs. Workflows can poll with goto and retry, branch on success criteria and call other workflows; they use your profiles for authentication. See Arazzo workflows.
ezu mcp serves an API's operations as tools to Claude, Cursor and other MCP clients, authenticated with your profile:
claude mcp add petstore -- ezu mcp petstoreRead-only operations by default; see MCP server.
The same engine is a Fennel/Lua library: point it at a spec and call operations as functions.
(local ezu (require :ezu))
(local petstore (ezu.client "https://petstore3.swagger.io/api/v3/openapi.json"))
(petstore.get-pet-by-id.invoke {:path {:petId 7}})
;; => {:status 200 :headers {...} :body {:id 7 :name "Lion 1" ...}}Install it with luarocks install ezu. See the library documentation.
make deps
make test
make binarySee Development for the requirements, the release process and the other targets.
Copyright © 2026 Max Penet Distributed under the Mozilla Public License Version 2.0