Winix.HCat
0.4.0
Prefix Reserved
dotnet tool install --global Winix.HCat --version 0.4.0
dotnet new tool-manifest
dotnet tool install --local Winix.HCat --version 0.4.0
#tool dotnet:?package=Winix.HCat&version=0.4.0
nuke :add-package Winix.HCat --version 0.4.0
hcat
Netcat for HTTP — an instant HTTP server in one native binary, no runtime, no daemon. Serve a folder, catch incoming requests, or pipe a command over HTTP. Uses ASP.NET Core's Kestrel under the hood, packaged as a single AOT binary.
Where nc gives you a raw TCP socket, hcat gives you an HTTP one: static-file serving with directory listings, an httpbin-style request echo/recorder, and a CGI-style "run this command per request" pipe. It overlaps with python -m http.server, miniserve, and webhook.site — but it is one cross-platform binary with no Python/Node runtime, LAN-share QR codes, and CI stop conditions built in.
Install
Scoop (Windows)
scoop bucket add winix https://github.com/Yortw/winix
scoop install winix/hcat
Winget (Windows, stable releases)
winget install Winix.HCat
.NET Tool (cross-platform)
dotnet tool install -g Winix.HCat
Direct Download
Download native binaries from GitHub Releases.
Safety: localhost-only unless you opt in
By default hcat binds 127.0.0.1 only — nobody else on your network can reach it. Network exposure is always explicit:
--lanbinds0.0.0.0(every interface) and prints a QR code with the LAN URL so a phone can scan it.--host <addr>binds an explicit address. A non-loopback--hostis also an explicit choice to expose the server.--localforces loopback-only, overriding any earlier--lan/--host.
There is no authentication or IP allowlist in v1 — --lan exposes the server to everyone on your network. Use it on trusted networks only.
Which address does
--lanshow? On a machine with virtual adapters (Hyper-V, WSL, Docker, VPNs),--landeliberately lists only addresses on a gateway-routed interface — the ones a device on your real LAN can actually reach — and hides host-only/virtual addresses to keep the banner and QR unambiguous. If you specifically want to bind a host-only/virtual address (e.g. to reach a VM), pass it with--host <addr>. If no interface has a default gateway, all addresses are shown.
Usage
hcat Serve the current directory on 127.0.0.1 (local preview).
hcat serve [dir] Serve static files with an auto directory listing.
hcat inspect Echo each request back as JSON (httpbin-style).
hcat pipe -- <cmd> Run <cmd> per request (CGI-style): body→stdin, stdout→response.
Serve files (default mode)
# Serve the current directory on http://127.0.0.1:8080 (localhost only)
hcat
# Serving ./myproject
# http://127.0.0.1:8080
# (localhost only — pass --lan to share on your LAN)
# Serve a specific folder
hcat serve ./public
# Share it on your LAN — binds 0.0.0.0 and prints a QR code
hcat serve ./public --lan
serve serves static files and renders an automatic directory listing for folders with no index.html. Path traversal outside the served root is rejected.
Receive uploads
# Enable a POST upload receiver. Files are saved to ./uploads (created, NOT served).
hcat serve ./public --upload
# Relocate the upload target
hcat serve ./public --upload --upload-dir /tmp/incoming
# Escape hatch: point uploads at the served root so they are downloadable
hcat serve . --upload --upload-dir .
With --upload, hcat accepts POST bodies and writes them to a dedicated ./uploads directory that is created but not served — so uploads can't immediately be re-fetched. --upload-dir <path> relocates that target. Pointing it at the served root (--upload-dir .) is the deliberate escape hatch that makes uploads downloadable. Upload paths are sanitised: a request can never write outside the upload directory.
Inspect requests (httpbin-style)
# Echo each incoming request back as JSON
hcat inspect
# Catch webhooks from your LAN
hcat inspect --lan
# Override the response status (e.g. to test a client's 500 handling)
hcat inspect --status 503
# Record every request as a JSONL stream to stdout
hcat inspect --json > requests.jsonl
inspect responds to every request with a JSON object describing that request (method, path, query, headers, body, timestamp, remote address). --status <code> overrides the response code (default 200). With --json, the same record is also written as a JSONL line to stdout, so you can capture a request stream to a file or pipe it to jq.
Pipe a command over HTTP (CGI-style)
# Expose jq over HTTP: POST a body, get the jq output back
hcat pipe -- jq .
# Anything that reads stdin and writes stdout works
hcat pipe -- tr a-z A-Z
pipe runs <cmd> once per request: the request body is fed to the child's stdin, the child's stdout becomes the HTTP response body, and request metadata is exposed as environment variables (CGI-style). The child's exit code maps to the response status — exit 0 → 200, any non-zero → 500. The child's stderr is written to the server console (diagnostic only; it never enters the HTTP response).
Stop conditions for CI
# Exit cleanly after catching 3 requests
hcat inspect --capture 3
# Exit when a specific request arrives
hcat inspect --exit-on path=/done
hcat inspect --exit-on method=POST
hcat inspect --exit-on body~deploy-complete
# Fail (exit 1) if the condition isn't met within 30 seconds
hcat inspect --exit-on path=/done --timeout 30s
--capture <N> stops after N requests, --exit-on <expr> stops on the first matching request (path=, method=, or body~ substring), and --timeout <dur> bounds the wait. If the stop condition is met the tool exits 0; if --timeout elapses first it exits 1.
These flags work in all modes (serve, inspect, pipe) — e.g. hcat serve ./dist --capture 1 serves one request then exits. Note --exit-on body~ is inspect-only: serve never reads the body and pipe streams it to the command, so neither captures it to match against (a body~ predicate there is a usage error rather than a silent never-match).
--capture/--exit-on count completed responses: a request whose client disconnects mid-response (e.g. an aborted download in serve) is not counted and is not logged. Pair the stop condition with --timeout so the run still terminates (exit 1) if the expected request never completes.
Single-page apps
# Serve a built SPA so deep links survive a refresh
hcat serve ./dist --spa
hcat serve ./dist --spa --spa-index app.html # non-standard entry file
With --spa, a request that matches no file is treated as client-side routing: a browser navigation (one whose Accept header includes text/html) gets index.html with 200, so frameworks like React/Vue/Angular keep working when you refresh /users/42. Requests for missing assets or APIs (*/*, application/json, etc.) still return a real 404, and existing files are always served as-is. Directory listing is disabled in --spa mode. --spa-index must be a bare filename in the served root.
HTTPS
# Serve over TLS with an in-memory self-signed certificate
hcat serve ./public --https --lan
--https enables TLS using a self-signed certificate generated in memory. Clients will show a trust warning (it isn't CA-signed) — this is intended for development and LAN use.
Options
| Flag | Argument | Description |
|---|---|---|
--lan |
Bind 0.0.0.0 to share on the local network (prints a QR code). LAN URLs/QR prefer gateway-routed (reachable) addresses; virtual host-only adapters (Hyper-V/WSL/Docker) are skipped from the banner unless none have a gateway. Use --host to pin a specific (incl. host-only) address. |
|
--local |
Force loopback-only binding (overrides --lan / --host). |
|
--host |
ADDR |
Explicit bind address. A non-loopback address exposes the server. |
--port |
N |
Listen port (default 8080). |
--https |
Enable TLS with an in-memory self-signed certificate. | |
--upload |
(serve) Enable the POST upload receiver. | |
--upload-dir |
DIR |
(serve) Upload target directory (default ./uploads). |
--spa |
(serve) SPA fallback: unmatched browser navigations (Accept: text/html) return the index file with 200, so client-side routers survive deep-link refresh. Disables directory listing. Asset/API misses still 404. |
|
--spa-index |
FILE |
(serve, with --spa) Fallback filename (default index.html). |
--status |
CODE |
(inspect) HTTP status to respond with (default 200). |
--capture |
N |
(CI) Exit after capturing N requests. |
--exit-on |
EXPR |
(CI) Exit when a request matches: path=/x, method=POST, or body~text (body~ is inspect-only). |
--timeout |
DUR |
(CI) Fail (exit 1) if the stop condition is not met within DUR (e.g. 30s, 5m). |
--json |
Emit machine-readable JSON: a JSONL request-record stream (inspect/pipe) or per-request access-log lines (serve), to stdout. | |
--describe |
Emit structured JSON metadata for AI discoverability. | |
--help, -h |
Show help and exit. | |
--version, -v |
Show version and exit. | |
--color[=auto\|always\|never] |
Force or suppress coloured output. Bare --color = always. Respects NO_COLOR. |
|
--no-color |
Disable coloured output. Respects NO_COLOR. |
JSON output
Pass --json for machine-readable output on stdout.
In inspect (and pipe) mode, each captured request is written as a JSONL line (one JSON object per line). The request record has these keys (camelCase):
| Key | Description |
|---|---|
method |
HTTP method (GET, POST, …). |
path |
Request path. |
query |
Raw query string. |
headers |
Object of request headers (keys preserved verbatim). |
body |
Request body as text, or null. Truncated to 1 MiB when oversized (see bodyTruncated). In pipe mode the record carries no body (it streams to the child). |
timestamp |
Capture time, UTC ISO 8601. |
remote |
Remote address of the caller. |
bodyTruncated |
true when the body exceeded the 1 MiB cap and was truncated in the record. Absent/false otherwise. |
In serve mode, --json emits a per-request access-log line to stdout instead — one JSON object per request with method, path, and status (the final HTTP status). Without --json, every mode prints a terse human-readable request log to stderr (serve: GET /file 200; inspect/pipe: METHOD /path). The bind banner and all errors also go to stderr, so --json stdout stays clean for piping.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success — clean shutdown (Ctrl+C), or a CI stop condition (--capture / --exit-on) was met. |
| 1 | A CI stop condition was not met before --timeout elapsed. |
| 125 | Usage error — unknown subcommand/flag, bad --exit-on key, non-integer --port / --status / --capture, or pipe with no command. Stderr carries the message. |
| 126 | Startup failure — the port could not be bound (e.g. already in use) or the self-signed certificate could not be created. Stderr carries the message. |
Colour
hcat colours its banner and request-log lines when stderr is a terminal. The --color and --no-color flags control this explicitly, and NO_COLOR is respected (no-color.org).
Known limitations
- Single-command pipe only.
piperuns one command for every request; there is no per-path routing in v1. - Self-signed cert → trust warning.
--httpsuses an in-memory self-signed certificate, so clients show a trust warning. It is for dev / LAN use, not public HTTPS. - No auth or IP allowlist in v1.
--lanexposes the server to everyone on your network; use trusted networks only. - ~12 MB binary. The ASP.NET Core / Kestrel dependency makes the binary larger than the other Winix tools (the price of a real HTTP server).
- Interrupted uploads can leave a partial file (F11). An upload aborted mid-stream may leave a partial file at the target name — there is no atomic write-then-rename in v1.
- Concurrent pipe stderr may interleave (F12). When several pipe-mode requests run concurrently, their children's stderr can interleave on the server console. This is diagnostic output only — it never enters any HTTP response.
- Oversized bodies are truncated. Request bodies larger than 1 MiB are truncated in the inspect record and marked
bodyTruncated. - Streaming pipe output commits the status early. If the child streams stdout, the
200status is sent before the child's exit code is known, so a non-zero exit after output has flushed cannot be downgraded to500(an inherent streaming-CGI constraint).pipe--jsonrecords carry no body (it streams to the child).
Related Tools
nc— the raw-TCP sibling.hcatisncfor HTTP.qr— the QR encoderhcat --lanuses to print the LAN URL.
See Also
man hcat(afterwinix install man)hcat --describefor JSON metadata
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.0 | 151 | 6/13/2026 |
## [0.4.0] - 2026-05-30
- Initial release.
See full changelog at https://github.com/Yortw/winix/blob/main/src/hcat/CHANGELOG.md