Named pipes, but over the internet. Send data to a path via PUT/POST, receive it via GET — data streams in real-time from sender to receiver(s) with zero server-side storage. Any path. Any content. Up to 256 simultaneous receivers.
Why This Matters
Section titled “Why This Matters”Sharing data between machines has always been painful. FTP servers, SSH tunnels, file upload services, WebSocket boilerplate, signaling servers for WebRTC. Every approach requires setup, authentication libraries, or specialized clients.
hoody-pipe reduces all of it to two curl commands:
# Terminal A: Send a filecurl -T report.pdf https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/my-report
# Terminal B: Receive the file (run this before, during, or after the sender connects)curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/my-report > report.pdfThat’s it. No upload limits. No temporary storage. Data flows directly from sender to receiver the moment both are connected.
No per-pipe accounts — access control is enforced centrally by Hoody Proxy (IP allowlist, passwords, JWT). The pipe service itself has no service-level auth; permissions are configured at the proxy layer, not per-pipe.
How It Works
Section titled “How It Works”- A sender POSTs (or PUTs) data to a path — e.g.
/api/v1/pipe/my-screen-share - A receiver GETs the same path
- Data streams directly from sender to all receivers — no buffering, no temporary files
- Either party can connect first — the server holds the early connection until the counterpart arrives (up to 5-minute TTL)
The path is just a name you choose. /my-file, /demo-stream, /logs/today — anything that isn’t a reserved system path (/, /help, /noscript, /favicon.ico, /robots.txt). Reserved-path matching is normalization-robust: /Help, /help/, /help., /help%2e all fold to /help and are rejected, so attackers can’t register an aliased pipe over a built-in page. The canonical health endpoint is /api/v1/pipe/health; the server also explicitly 404s a bare /health with the hint '/health' is not a valid path. Use '/api/v1/pipe/health'.
Quick Examples
Section titled “Quick Examples”Send and Receive a File
Section titled “Send and Receive a File”# Sender: Upload a filehoody pipe send backup ./backup.tar.gz --container $CONTAINER
# Receiver: Download the filehoody pipe receive backup -o backup.tar.gz --container $CONTAINER# Sender: Upload a file (PUT is natural for curl -T)curl -T ./backup.tar.gz \ https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/backup
# Receiver: Download the filecurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/backup \ -o backup.tar.gzimport { HoodyClient, PipeStream } from '@hoody-ai/hoody-sdk';import { createReadStream, createWriteStream } from 'node:fs';import { Writable } from 'node:stream';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Generic byte-stream API (any source/sink) via PipeStreamconst pipe = PipeStream.fromClient(client, { id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Send: any PipeSource (file stream, Buffer, string, ReadableStream, TCP/Unix socket, AsyncIterable)const send = await pipe.send('backup', createReadStream('./backup.tar.gz'));await send.done;
// Receive: get a Web ReadableStream you can pipe anywhereconst recv = await pipe.receive('backup');await recv.body.pipeTo(Writable.toWeb(createWriteStream('./backup.tar.gz')));Stream Text Between Containers
Section titled “Stream Text Between Containers”# Container A: stream a long-running command's stdouthoody pipe send live-logs --from-cmd "tail -f /var/log/app.log"
# Container B: receive to stdout (the default sink)hoody pipe receive live-logs# Container A: Pipe logs in real-timetail -f /var/log/app.log | curl -T - \ https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/live-logs
# Container B: Watch the logscurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/live-logsimport { PipeStream } from '@hoody-ai/hoody-sdk';
const pipe = PipeStream.fromClient(client, { id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Async-iterable source — perfect for line-by-line producersasync function* tail() { for await (const line of process.stdin) yield line;}await (await pipe.send('live-logs', tail())).done;
// Receiver: pipe Web ReadableStream straight to stdoutconst recv = await pipe.receive('live-logs');await recv.body.pipeTo(Writable.toWeb(process.stdout));Multi-Receiver Fan-Out
Section titled “Multi-Receiver Fan-Out”Send once, stream to multiple receivers simultaneously:
# Sender: Stream to 3 receivershoody pipe send demo ./presentation.webm -n 3
# Receiver 1, 2, 3: Each runs thishoody pipe receive demo -n 3 -o presentation.webm# Sender: Stream to 3 receiverscurl -T ./presentation.webm \ "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/demo?n=3"
# Receiver 1, 2, 3: Each runs this (all get identical copies)curl "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/demo?n=3" \ -o presentation.webm// Sender: Stream to 3 receiversconst send = await pipe.send('demo', createReadStream('./presentation.webm'), { n: 3 });await send.done;
// Each receiverconst recv = await pipe.receive('demo', { n: 3 });await recv.body.pipeTo(Writable.toWeb(createWriteStream('./presentation.webm')));API Endpoints Summary
Section titled “API Endpoints Summary”All endpoints accessed relative to your Pipe service URL:
https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icuData Transfer:
POST /api/v1/pipe/{path}— Send data to a pipe pathPUT /api/v1/pipe/{path}— Send data (alias for POST, natural forcurl -T)GET /api/v1/pipe/{path}— Receive data from a pipe path
Utilities:
GET /api/v1/pipe/health— Service health check (standardized 9-field response:status,service,built,started,memory,fds,pid,ip,userAgent). Build identity is inbuilt(module mtime, ISO 8601);userAgentechoes the requesting client’sUser-Agentheader.GET /api/v1/pipe/help— Usage instructions with curl examplesGET /api/v1/pipe/noscript— JavaScript-free HTML upload form (noscript fallback for the index page; CSP-nonce-restricted styles)GET /api/v1/pipe— Main web UI for browser-based uploads (file or text, with a JS progress bar)
Command-line Interface
Section titled “Command-line Interface”The hoody CLI ships a pipe namespace that wraps every operation above with shell-friendly source/sink flags. Bytes can come from a file, stdin, a literal --text value, a TCP/Unix socket, or any spawned command — and likewise on the receiving side.
# Sources for `send`: file, "-" (stdin), --text, --from-tcp, --from-unix, --from-cmdhoody pipe send <path> [source] [flags]
# Sinks for `receive`: file via -o, "-" (stdout, default), --to-tcp, --to-unix, --to-cmdhoody pipe receive <path> [flags]Commands
Section titled “Commands”| Command | Purpose |
|---|---|
pipe send <path> [source] | Send bytes to a pipe path. Source = file / - / --text / --from-tcp / --from-unix / --from-cmd. Add --put for curl -T parity. |
pipe receive <path> | Receive bytes. Sink = -o file / stdout / --to-tcp / --to-unix / --to-cmd. Honors --download, --inline, --filename. |
pipe progress <path> | Subscribe to live progress via SSE — does not consume a receiver slot. --json emits raw events; --until complete,failed exits on terminal state. |
pipe url <path> | Print the receiver URL for a path (no fetch). Append --video, --progress, --download, --filename, -n N. --video and --progress are mutually exclusive. |
pipe forward-tcp <send-path> <recv-path> | Bidirectional TCP forwarder over two pipes. Pick exactly one of --listen host:port or --connect host:port; the peer must swap the two paths. |
pipe health | Standard 9-field pipe-kit health JSON. |
pipe help-cheatsheet | Server-rendered curl cheatsheet (text/plain). |
Routing flags (all commands)
Section titled “Routing flags (all commands)”| Flag / env | Purpose |
|---|---|
-c <id> / --container <id> / HOODY_CONTAINER | Container to address (resolves the kit URL through the Hoody API). --container-id and --containerId are accepted aliases. |
--pipe-url <url> / HOODY_PIPE_URL | Direct pipe-server URL — bypasses container resolution. Useful for self-hosted pipe servers, tests, or off-network clients. |
Examples
Section titled “Examples”# Bridge a TCP port over a pipe (e.g. expose a localhost service to a peer)# Side A (the side hosting the service)hoody pipe forward-tcp ab ba --connect 127.0.0.1:5432 --container $CTR# Side B (the side dialing in)hoody pipe forward-tcp ba ab --listen 127.0.0.1:15432 --container $CTR# Now `psql -h 127.0.0.1 -p 15432` on side B reaches the Postgres on side A.
# Stream stdout of a long-running commandhoody pipe send build-logs --from-cmd "make -j" --container $CTR
# Watch a transfer's progress as JSON without consuming a slothoody pipe progress big-upload --json --until complete,failed
# Build a shareable URL for an n=10 video stream — no request issuedhoody pipe url my-screen --video -n 10 --container $CTRSDK Streaming Helpers (Node.js)
Section titled “SDK Streaming Helpers (Node.js)”The auto-generated containerClient.pipe.* methods cover the basic JSON-shaped endpoints, but pipe is fundamentally a streaming primitive — the SDK ships a hand-written Node-side companion at @hoody-ai/hoody-sdk’s top level.
import { PipeStream, // class — send / receive / subscribeProgress / forwardTcp coerceToReadableStream, // anything-byte-source → Web ReadableStream parseStatusStream, // [INFO]/[ERROR] line stream parser parseSseStream, // SSE block parser (for /{path}?progress) PipeReceiveEmptyBodyError, // distinguishes intentional empty bodies from network errors validatePipePath, encodePipePath,} from '@hoody-ai/hoody-sdk';PipeStream
Section titled “PipeStream”// Either resolve from the Hoody control plane:const pipe = PipeStream.fromClient(client, { id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Or address a pipe server directly (self-hosted, tests, off-network):const pipe = new PipeStream({ pipeBaseUrl: 'https://...-pipe.containers.hoody.icu' });
// send — accepts ANY PipeSource:// string | Buffer | Uint8Array | ReadableStream | NodeJS.ReadableStream// | AsyncIterable | URL (file:) | { tcp: { host, port } } | { unix: '/path' }const send = await pipe.send('my-path', source, { method: 'POST', // or 'PUT' (curl -T parity) n: 1, // 1..256 — receivers to wait for contentType: 'application/octet-stream', contentLength: stat.size, // optional — when known, enables receiver progress filename: 'report.pdf', // forwarded as Content-Disposition headers: { 'X-Hoody-Pipe': 'kind=audit' }, onStatus: (m) => console.log(m.level, m.message), signal: AbortSignal.timeout(60_000),});await send.done; // resolves when sender's response body fully drains
// receive — gives you a Web ReadableStream<Uint8Array>const recv = await pipe.receive('my-path', { n: 1, download: true });recv.body.pipeTo(Writable.toWeb(createWriteStream('out.bin')));
// subscribeProgress — async-iterable SSE events (does NOT consume a receiver slot)for await (const ev of pipe.subscribeProgress('my-path')) { if (ev.kind === 'progress') console.log(ev.bytesTransferred, ev.speed); if (ev.kind === 'done') break;}PipeStream.forwardTcp
Section titled “PipeStream.forwardTcp”Bidirectional TCP forwarding over two unidirectional pipes — same primitive the CLI’s forward-tcp is built from:
const fwd = pipe.forwardTcp({ sendPath: 'ab', // local → server recvPath: 'ba', // server → local connect: { host: '127.0.0.1', port: 5432 }, // OR listen: { port: 15432 } n: 1,});// fwd.address — Promise<{host, port}> when in listen mode// fwd.done — Promise<void> when fully drained// fwd.close() — stop accepting / tear down active bridgeSender Status Messages
Section titled “Sender Status Messages”When you POST or PUT, the response body streams real-time status messages:
[INFO] Waiting for 1 receiver(s) to connect...[INFO] A receiver connected.[INFO] Streaming to 1 receiver(s)...[INFO] Upload complete.[INFO] Transfer complete.If receivers were already waiting when the sender connects, the first line is replaced by [INFO] N receiver(s) already connected. and streaming begins immediately.
If a receiver disconnects mid-transfer:
[INFO] A receiver disconnected.[INFO] All receivers disconnected before transfer completed.[ERROR] … lines are emitted on failure modes — capacity exceeded, idle timeout, sender error, or wait timeout. The full authoritative vocabulary lives in hoody-pipe’s OpenAPI under StatusMessage.
Receiver Options
Section titled “Receiver Options”Receivers can control download behavior with query parameters:
| Parameter | Effect |
|---|---|
?download | Force browser download (Content-Disposition: attachment) |
?download=false | Suppress Content-Disposition entirely (always display inline) |
?filename=report.pdf | Set a custom download filename (implies ?download) |
?video | Show an HTML video player for WebM/MP4/MPEG-TS streams (browsers only) |
?progress | Watch transfer progress as SSE events or an HTML dashboard |
Video Player
Section titled “Video Player”Append ?video to the receiver URL and open it in a browser — hoody-pipe serves an embedded MSE video player that auto-detects the container/codec from the stream:
# Sender: Stream your screen as WebMffmpeg -f x11grab -i :0 -c:v libvpx -f webm - | \ curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/screen
# Receiver: Open in browser with video player# https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/screen?videoNon-browser clients (VLC, mpv, ffplay) with ?video get the raw stream automatically.
Progress Spectating
Section titled “Progress Spectating”Watch transfer progress without consuming a receiver slot:
# SSE stream (curl, EventSource)curl -H "Accept: text/event-stream" \ "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-transfer?progress"
# HTML dashboard (open in browser)# https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-transfer?progressSSE events include state transitions (idle / waiting / streaming / complete / failed), progress updates (bytes, speed, ETA, active receivers), and a final done event with totals (bytes transferred, duration, average speed).
Real-World Use Cases
Section titled “Real-World Use Cases”Share Your Screen Over HTTP
Section titled “Share Your Screen Over HTTP”Stream your real device screen to anyone with a URL — no WebRTC, no signaling server, no app install:
# On your machine: Capture screen and pipe itffmpeg -f x11grab -i :0 -c:v libvpx-vp9 -f webm - | \ curl -T - "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-screen?n=10"
# Share this URL with up to 10 viewers:# https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-screen?n=10&videoFile Transfer with Progress
Section titled “File Transfer with Progress”Send a large file and let others watch the progress:
# Sendercurl -T ./dataset-50gb.tar.gz \ https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/dataset
# Receivercurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/dataset \ -o dataset-50gb.tar.gz
# Spectator (doesn't consume a receiver slot)# Open in browser: .../api/v1/pipe/dataset?progressLive Event Forwarding
Section titled “Live Event Forwarding”Stream events from one container to another in real-time:
# Container A: Forward nginx access logstail -f /var/log/nginx/access.log | curl -T - \ https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/nginx-logs
# Container B: Process the log streamcurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/nginx-logs | \ grep "500" | tee errors.logSend a Directory
Section titled “Send a Directory”# Sender: Tar and stream a directorytar czf - ./my-project | curl -T - \ https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/project.tar.gz
# Receiver: Download and extractcurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/project.tar.gz | \ tar xzf -End-to-End Encryption
Section titled “End-to-End Encryption”Don’t trust the server? Encrypt before piping:
# Sender: Encrypt and sendopenssl enc -aes-256-cbc -pbkdf2 -pass pass:SECRET < secret.doc | \ curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/encrypted
# Receiver: Download and decryptcurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/encrypted | \ openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:SECRET > secret.docSecurity
Section titled “Security”hoody-pipe takes security seriously:
- Dangerous MIME types rewritten — 21 script-executable types (HTML/XML/SVG plus the WHATWG-canonical JavaScript MIME family —
text/javascript,application/javascript,application/ecmascript,text/javascript1.0–1.5,text/jscript,text/livescript, etc.) are rewritten totext/plainso a browser receiver can’t execute them. InlineContent-Dispositionis also force-coerced toattachmentfor any MIME outside an explicit safelist (image/*,audio/*,video/*,text/plain, …) — defense-in-depth if the dangerous-MIME table misses a niche format. - CRLF injection protection — Forwarded headers (Content-Disposition, X-Piping, X-Hoody-Pipe) are sanitized against header injection.
- CSP nonces — All HTML pages (video player, progress dashboard, noscript upload) use Content-Security-Policy with nonces.
- Path length limit — Max 1024 characters to prevent memory inflation.
- X-Content-Type-Options: nosniff — On every response.
- X-Robots-Tag: none — Prevents indexing of pipe data.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Max concurrent pending (unestablished) pipes | 1000 |
| Max concurrent active (streaming) transfers | 1000 |
| Max receivers per pipe | 256 |
| Max spectators per path | 50 |
| Max spectator groups | 500 |
| Unestablished pipe TTL | 5 minutes (sender or receiver waiting for counterpart; the lonely side is evicted) |
| Active-transfer idle timeout | 5 minutes (no bytes flowing → [ERROR] Transfer aborted — idle timeout exceeded.) |
| Spectator idle TTL | 30 minutes (progress-watcher auto-close on inactivity) |
| Max path length | 1024 characters |
| Max URL length | 4096 characters |
Exceeding limits returns HTTP 429 (Too Many Transfers) or 414 (Path Too Long).
Custom Metadata
Section titled “Custom Metadata”Forward arbitrary metadata from sender to receiver using custom headers:
# Sender: Include metadatacurl -T ./image.png \ -H "X-Hoody-Pipe: source=camera-1;timestamp=2026-03-04T12:00:00Z" \ -H "Content-Disposition: attachment; filename=\"snapshot.png\"" \ https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/snapshot
# Receiver: Gets the metadata headers forwardedcurl -v https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/snapshot \ -o snapshot.png# < X-Hoody-Pipe: source=camera-1;timestamp=2026-03-04T12:00:00Z# < Content-Disposition: attachment; filename="snapshot.png"Both X-Hoody-Pipe and X-Piping headers are forwarded to receivers and exposed via CORS.
Health & Monitoring
Section titled “Health & Monitoring”# Standard 9-field health JSONhoody pipe health --container $CONTAINER# Check service healthcurl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/health{ "status": "ok", "service": "hoody-pipe", "built": "2026-04-14T22:17:54Z", "started": "2026-04-22T09:12:03Z", "memory": { "rss": 44145050, "heap": 29884416 }, "fds": 17, "pid": 1234, "ip": "172.17.0.2", "userAgent": "curl/8.4.0"}userAgent is the requesting client’s User-Agent header, echoed back (useful for debugging client identity through the proxy). Server build identity lives in built (the module’s mtime as an ISO 8601 string).
import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Health check — build identity is in `built` (module mtime, ISO 8601);// `userAgent` echoes whatever User-Agent the SDK's HTTP client sent.const health = await containerClient.pipe.health.check();console.log(health.data.status); // "ok"console.log(health.data.built); // "2026-04-14T22:17:54Z"Troubleshooting
Section titled “Troubleshooting”Transfer Never Starts
Section titled “Transfer Never Starts”Cause: Sender and receiver have mismatched n values, or one party hasn’t connected yet.
Solution: Ensure both sender and receiver use the same ?n= value (or omit it for the default of 1). The connection blocks until the counterpart arrives — check that both parties are running.
HTTP 429 — Too Many Transfers
Section titled “HTTP 429 — Too Many Transfers”Cause: Server has 1000 pending or 1000 active transfers.
Solution: Wait for existing transfers to complete or expire (5-minute TTL for unestablished pipes), then retry. Reduce the number of concurrent transfers.
Receiver Gets text/plain Instead of Expected Content-Type
Section titled “Receiver Gets text/plain Instead of Expected Content-Type”Cause: The sender’s Content-Type is a dangerous MIME type (text/html, application/javascript, etc.) that was rewritten for security.
Solution: This is intentional XSS protection. If you need the original type, the receiver can re-set it based on the filename or use the data as-is.
Path Already Has a Sender
Section titled “Path Already Has a Sender”Cause: Another sender is already connected to the same path.
Solution: Use a different path, or wait for the existing transfer to complete.
What’s Next
Section titled “What’s Next”Data transfer is just a URL now. Send to a path. Receive from the same path. Data flows through. No servers to configure. No clients to install. No files to upload. Just HTTP.