Runs local commands over HTTP and describes itself with OpenAPI, so an API client — or an Arazzo workflow — can shell out without leaving the description.
Commands are executed directly, not interpreted. Shell syntax — redirection,
pipes, globs, &&, variable expansion — is passed through as literal text:
Ask for a shell when you want one:
{ "command": "sh", "args": ["-c", "echo hello > world.txt"] }
// → { "stdout": "", "exitCode": 0 }, and the file existsPass untrusted values as arguments rather than splicing them into the command, so they cannot escape it:
{ "command": "sh", "args": ["-c", "printf %s \"$1\" > out", "sh", "«value»"] }This server runs whatever it is told to run, and has no authentication. Anything that can reach the port has the privileges of the user running it.
It binds localhost on a random port by default. Do not change that unless you
have put authentication in front of it. --hostname 0.0.0.0 publishes remote
code execution to your network.
npx httpexec --port 8080| Flag | Default | |
|---|---|---|
--port, -p |
0 |
0 picks a free port. |
--hostname, -h |
localhost |
See above before changing. |
curl -X POST http://localhost:8080/ \
-H 'content-type: application/json' \
-d '{"command":"echo","args":["hello"]}'{ "stdout": "hello\n", "stderr": "", "exitCode": 0 }The OpenAPI description is served from /openapi.json, with servers set to
the address it is actually reachable at.
The response reports what happened; it is not itself the verdict. A command that
ran and exited non-zero returns 200 with that exitCode. Only a malformed
request body is a 400.
So check exitCode, not just the status:
successCriteria:
- condition: $statusCode == 200
- condition: $response.body#/exitCode == 0Node 22.18 or newer — the binary is TypeScript, run through Node's built-in type stripping.
MIT
{ "command": "echo", "args": ["hello > world.txt"] } // → { "stdout": "hello > world.txt\n", "exitCode": 0 }, and no file