A TIL post about a decades-old Bash feature just hit 373 points on Hacker News — and the reason it resonated so hard in mid-2026 is more interesting than the trick itself. Marek Šuppa's short post showing how to make a raw HTTP request using nothing but bash and its built-in /dev/tcp pseudo-device tapped into something that's been building quietly across the DevOps community: the tension between minimal container images, locked-down CI environments, and the gnawing suspicion that your "hardened" build infrastructure might not be as tool-free as you think. Here's what this actually means for small teams, freelancers, and agencies who are running lean cloud infrastructure and writing the shell scripts that hold everything together.

My sharp take: this is not just a neat trick. It's a window into a systemic assumption that a lot of teams have made — that removing curl from a container buys you meaningful security — and that assumption deserves to be challenged right now.

What is this actually?

At its core, the trick exploits a feature that has been baked into GNU Bash since the early 2.x days: Bash will intercept any file redirection that points to the magic path /dev/tcp/<host>/<port> and instead of opening an actual file, it opens a TCP socket to that host and port. The kernel never sees this path. Bash handles it entirely in userspace, inside the shell interpreter itself.

The basic pattern looks like this:

exec 3<>/dev/tcp/example.com/80
printf "GET / HTTP/1.0\r\nHost: example.com\r\nConnection: close\r\n\r\n" >&3
cat <&3
exec 3>&-

Breaking that down: exec 3<> opens file descriptor 3 for both reading and writing — <> is the bidirectional redirect operator. The /dev/tcp/example.com/80 path is parsed by Bash's internal redirect logic, which intercepts it before any filesystem lookup happens and dials a TCP connection. Then you write a raw HTTP/1.0 request to file descriptor 3, and read the response back with cat. Finally you close the descriptor.

It is genuinely that simple. There are no compiled binaries involved beyond bash itself. No shared libraries for HTTP. No system calls you wouldn't make in any other TCP connection. Bash is acting as a minimal TCP client using the same POSIX socket API that curl uses, just exposed through a different surface.

The feature also has a UDP sibling at /dev/udp/<host>/<port>, which works identically for datagram sockets. Both are documented — if briefly — in the official Bash manual under the Redirections section. This is not some obscure hack or undocumented behavior; it has been intentionally supported and shipped in every mainstream Linux distribution for over twenty years.

There are important limitations to understand clearly. First: this does not handle TLS. /dev/tcp gives you a raw TCP socket. If you try to point it at port 443 and send an HTTP request, you'll get back a TLS ClientHello handshake that you cannot parse. HTTPS is simply not supported natively. Second: HTTP/1.1 persistent connections require you to handle chunked transfer encoding and Content-Length headers yourself if you want to read the full response — using HTTP/1.0 with Connection: close sidesteps most of this complexity, which is why tutorials default to it. Third: this is a Bash-specific feature. It does not work in /bin/sh unless that shell is actually symlinked to Bash. It does not work in dash, ash (the shell used in Alpine's default /bin/sh), or fish. Zsh supports it only on some builds. If your script has a #!/bin/sh shebang, this will silently fail rather than open a socket.

The feature is also configurable: system administrators can disable it at compile time via --disable-net-redirections when building Bash. Most distributions ship with it enabled, but security-hardened builds — some enterprise Linux configurations, some custom embedded toolchains — may have it compiled out.

Despite being two decades old, the reason this blew up on HN right now is not nostalgia. It's that the use cases for it have quietly multiplied as the industry shifted toward containers, minimal base images, and security-hardened CI pipelines. The trick looks more useful than ever against that backdrop.

Why this matters right now

Twelve months ago, this would have been a nice-to-know curiosity. Today it's operationally relevant for a large and growing fraction of the teams reading this.

The container ecosystem in 2026 has fully bifurcated. On one side, you have developer-facing images — Ubuntu-based, Debian-slim, with a full suite of CLI utilities including curl, wget, jq, and whatever else developers need to feel productive. On the other side, you have production and CI images increasingly built on Alpine, Wolfi, Chainguard's distroless variants, or from-scratch builds that contain exactly the runtime binary and nothing else. The driving forces here are image size (faster pulls, faster cold starts), attack surface reduction (fewer binaries means fewer vulnerabilities), and supply chain security (fewer packages means fewer CVEs to track).

The result is that teams are regularly operating in shell environments where curl isn't available — and where installing it mid-script would require a package manager call that might fail, might be slow, or might be explicitly blocked by policy. Small teams shipping on a tight budget feel this acutely because they're often doing more with fewer custom base images: you build one Alpine-based image and use it everywhere from CI health checks to production sidecars.

Meanwhile, the other half of the equation: more small-team infrastructure than ever is glued together with shell scripts. Webhook calls in CI. Health checks in Docker Compose. Readiness probes in Kubernetes manifests that run shell commands. Notification scripts in cron jobs. These scripts often need to make exactly one HTTP request — check if a service is up, fire a webhook, call a simple API. Installing curl just for that feels heavy. Importing Python just for requests feels heavier. /dev/tcp suddenly looks like a legitimate option.

What changed compared to five or even three years ago is the combination: minimal images became the default rather than the exception, at the same time that shell-scripted HTTP calls became a routine part of every team's infrastructure plumbing. The feature hasn't changed. The environment around it has shifted to make it matter.

There's also a red-teaming dimension that's newly relevant. As more organizations adopt "no curl, no wget" as a security control in containers (particularly in environments running untrusted or semi-trusted code), the security community has been quietly noting for a while that this control is much weaker than it looks if bash is still present. That awareness is becoming mainstream — which means both defenders and people writing shell scripts for legitimate purposes need to understand the actual threat model.

Practical implications for small teams

Let me walk through four concrete scenarios where this knowledge changes what you'd actually do.

Scenario 1: Health checks in minimal Docker containers

You're running a side-project or client project on a small VPS, using Docker Compose. Your service container is built on node:20-alpine — no curl, and you don't want to add it just for a health check. Your Docker Compose healthcheck currently looks like this:

healthcheck:
 test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
 interval: 10s
 timeout: 5s
 retries: 3

This fails silently if curl isn't in the image. The /dev/tcp alternative:

healthcheck:
 test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/3000 && echo -e 'GET /health HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '200 OK'"]

This works with zero additional packages, as long as your base image ships bash (alpine's default /bin/sh is ash, so you'd need to either change the shebang to /bin/bash or install bash in the image). It's a little verbose, but it's dependency-free and it works. For teams who've already added bash to an Alpine image (common for scripting needs), this is a genuinely useful pattern.

Scenario 2: Webhook calls in CI pipelines

You're running GitHub Actions or a self-hosted Gitea/Forgejo instance. Your CI pipeline needs to fire a Slack webhook or a custom deployment webhook at the end of a job. Your runner image is minimal and curl isn't available. Rather than bloating the image or adding a run: apt-get install -y curl step (which adds latency and a network dependency to every CI run), you can inline the webhook call with /dev/tcp.

For HTTP webhooks this works perfectly. For HTTPS, you hit the TLS limitation — but here's where the practical workaround lives: openssl s_client is installed in many more minimal images than curl, because it's part of the OpenSSL package that's already present for TLS certificate validation. The pattern becomes:

echo -e "POST /services/YOUR_WEBHOOK_PATH HTTP/1.1\r\nHost: hooks.slack.com\r\nContent-Type: application/json\r\nContent-Length: ${#PAYLOAD}\r\n\r\n${PAYLOAD}" \
 | openssl s_client -quiet -connect hooks.slack.com:443 2>/dev/null

This is the HTTPS-capable version of the same concept: raw HTTP over a TLS tunnel provided by openssl. It's not pretty. But when you're in a stripped CI environment and need one webhook call, it removes a dependency entirely.

Scenario 3: Scripted readiness checks in Kubernetes

Kubernetes liveness and readiness probes can run exec commands inside containers. Many teams configure these with a shell script that checks whether a service's internal endpoint is responding. If your container image doesn't include curl, you've historically had three options: add curl to the image, use a TCP-only check (which doesn't verify the HTTP response), or use a language-specific health check binary baked into the image.

The /dev/tcp approach adds a fourth option: a bash-native check that speaks HTTP without any additional binary. For teams running bash-based container images (common in automation, data pipeline, and scripting-heavy workloads), this is worth knowing about. The key advantage is that you're verifying an actual HTTP 200 response, not just TCP connectivity — which catches application-level failures that a pure TCP check misses.

Scenario 4: Minimal server-side scripting and automation

You're a freelancer or solo founder with a cron job running on a cheap VPS or even a shared hosting environment. The job needs to poll an external API to check whether something has changed, then trigger a secondary action. Installing curl isn't hard, but the shared environment might restrict package management. Python might not be available in the version you need. What's almost certainly available is bash.

In these constrained environments, knowing that bash can natively speak HTTP means you can write self-contained shell scripts that don't rely on the environment having the right tools pre-installed. The script becomes more portable across different Linux environments — which matters when you're managing scripts across multiple cheap VPSes, clients' servers, or environments where you don't control the base configuration.

The flip side of all four scenarios: every one of them involves HTTP, not HTTPS, unless you bring in openssl. For any communication that involves credentials, API keys, or sensitive data, you should be using the openssl tunnel pattern or finding a way to get curl into the environment. Raw /dev/tcp over HTTP in 2026 is appropriate only for internal service-to-service communication within a trusted network boundary, or for completely public endpoints where privacy and integrity aren't concerns.

How to respond and act on this

Here's how I'd actually think about incorporating this knowledge into your workflow, depending on where you sit.

Step 1: Audit what your scripts actually depend on. Go through your Dockerfiles, docker-compose files, CI YAML files, and cron scripts and catalog every place you call curl, wget, or a Python one-liner just to make an HTTP request. For each one, ask: is this over HTTP or HTTPS? Is this inside a trusted network? Does the environment have bash? This gives you a prioritized list of where /dev/tcp is actually applicable versus where it's the wrong tool.

Step 2: Add bash to your Alpine images if scripting is already happening there. If your Alpine-based images already contain shell scripts (and most do), the marginal cost of apk add bash is minimal. Once bash is available, the /dev/tcp pattern becomes available for all your internal health checks and probes. This is a one-time image change that pays off across multiple scripts.

Step 3: Use the openssl pattern for HTTPS calls in minimal CI environments. OpenSSL is present in almost every Linux environment that does anything with TLS certificates. The openssl s_client pattern is ugly but functional for simple POST requests to webhooks. Wrap it in a shell function so you write it once and reuse it. Something like:

https_post {
 local host="$1" path="$2" payload="$3"
 local len=${#payload}
 printf "POST %s HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" \
 "$path" "$host" "$len" "$payload" \
 | openssl s_client -quiet -connect "$host:443" 2>/dev/null
}

Step 4: Revisit your security assumptions about "no curl" controls. If you have containers or environments where you've removed curl and wget as a security measure, check whether bash is still present. If it is, your HTTP egress control is weaker than you thought. In that context, you have three options: also remove bash (use sh/dash/ash only), use network-level egress controls (iptables rules, Kubernetes NetworkPolicies, or a service mesh) that block outbound TCP regardless of how it's initiated, or accept the residual risk and focus controls elsewhere. Network-level controls are more reliable than binary removal in any case, because /dev/tcp is just one of many ways to open a socket if you have arbitrary code execution.

Step 5: Don't reach for /dev/tcp in production application code. This is a scripting and operations tool. If your application code needs to make HTTP calls, use a proper HTTP library for your language. The operational costs of maintaining raw-HTTP-over-bash in a production code path — debugging malformed headers, dealing with HTTP/1.1 chunked responses, handling redirects — are not worth it when curl or a language SDK exists.

Step 6: Document the pattern for your team if you use it. Raw /dev/tcp scripts look unusual to engineers who haven't seen the trick before. If you're introducing this into shared infrastructure scripts, add a comment explaining what it's doing and why. "This uses bash's built-in TCP socket support to make an HTTP request without requiring curl in this minimal container image" is thirty words that saves your future self or a colleague twenty minutes of confusion.

Comparison: Ways to make HTTP requests in constrained environments

Method Shell/Tool Required HTTPS Support Difficulty Key Differentiator
Bash /dev/tcp bash only No (HTTP only) Medium Zero external dependencies if bash present
curl curl binary Yes, native Low Most capable; handles auth, redirects, all methods
wget wget binary Yes, native Low Built-in recursion; good for file download
HTTPie / http Python + httpie Yes Low Human-friendly output; great for interactive use
xh Rust binary Yes Low curl-like but friendlier; ~$0, single static binary
netcat (nc) nc binary No Medium TCP-only; similar to /dev/tcp but needs install
socat socat binary Partial (with openssl) High Most flexible socket tool; overkill for HTTP
openssl s_client openssl binary Yes (it IS TLS) High Best HTTPS option when curl unavailable
Python one-liner python3 Yes Medium Works if Python present; urllib.request is stdlib

In my view, the practical hierarchy for constrained environments goes like this: if you can use curl, use curl — it handles edge cases in HTTP correctly and everyone knows how to maintain it. If curl isn't available but Python is, a python3 -c "import urllib.request;..." one-liner is more correct and more maintainable than raw /dev/tcp. If neither is available but bash and openssl are, the openssl s_client pattern handles HTTPS properly. /dev/tcp over plain HTTP lands last on the list for most real scenarios because TLS is non-negotiable for anything that touches credentials or external APIs in 2026. Where /dev/tcp genuinely wins is internal service health checks within a trusted cluster network, or as a lightweight connectivity test that verifies TCP + HTTP-layer response rather than just TCP open.

What the HN community is saying

The 168-comment thread produced a notably signal-dense discussion — rare for a TIL post, which suggests the topic hit a real nerve rather than just being a clever trick.

The dominant reaction from practitioners was something along the lines of "I knew this existed but forgot about it until now." A substantial number of commenters were experienced sysadmins and DevOps engineers who had encountered this feature years ago in a specific context (usually: debugging in a minimal container, or reading the bash manual out of curiosity) and then not thought about it again. The post served as a retrieval cue for a useful piece of knowledge that tends to fall out of working memory.

The security angle generated some of the most substantive discussion. Several red teamers and security engineers explicitly noted that /dev/tcp is well-known in the offensive security community as a means of establishing outbound connections from "hardened" environments. One recurring theme: teams that remove curl from containers but leave bash present have achieved a false sense of security. The consensus in that sub-thread was that network-level egress controls (firewall rules, Kubernetes NetworkPolicies) are the only reliable way to prevent unauthorized outbound HTTP, because any sufficiently capable scripting language or shell can open a socket if the kernel allows it.

The HTTPS limitation prompted a lot of discussion about the openssl s_client workaround, with several people sharing refined versions of the pattern and others pointing out that even openssl s_client has edge cases (SNI handling, certificate verification flags). A common practical note was that in CI environments specifically, you're often behind a TLS-terminating proxy anyway, so plain HTTP to localhost or a sidecar is perfectly appropriate.

Some skeptics pushed back on the practical value: "just install curl" was a sentiment expressed more than once, along with observations that any image complex enough to need an HTTP client is probably complex enough to have one installed. This is a reasonable position for production application containers, but it misses the operational scripting use case, where you're writing the scaffolding around containers rather than the containers themselves.

The most interesting technical thread was about whether this feature can be disabled, with several people confirming the --disable-net-redirections compile flag and a few noting that some hardened Linux distributions (and some embedded environments) do ship with this compiled out. The existence of the disable flag suggests the bash maintainers have always been aware that this feature has a security footprint.

Risks and things to watch

The TLS gap is real and underappreciated. If you adopt /dev/tcp in your scripts and then later someone uses those scripts as a template for a context where the endpoint is HTTPS, the request will silently fail or produce garbage output. The failure mode is not obvious if you're not watching stderr carefully. Build TLS checking into any script that uses /dev/tcp: verify at the start of the script that the target is HTTP, or explicitly document that the script only works for plain HTTP.

Shell portability is a constant hazard. The single biggest risk with /dev/tcp in production scripts is that someone changes the shebang from #!/bin/bash to #!/bin/sh, or runs the script in a Dockerfile RUN command that uses sh rather than bash as the default shell. This breaks silently or with a cryptic "No such file or directory" error on the /dev/tcp path. Always explicitly invoke bash: use #!/bin/bash or bash script.sh, never rely on the environment's default shell being bash.

Response parsing is fragile. Raw HTTP responses include headers, status codes, and body all in one stream. If you're checking for a 200 response, you need to reliably parse the status line, handle cases where the server returns a 301 redirect, deal with gzip-encoded responses, and so on. curl handles all of this for you. With /dev/tcp you're handling it yourself, and the edge cases are numerous. For simple "is this endpoint up?" checks, checking that the response contains "200" is usually sufficient. For anything more complex, you're better served by a tool with proper HTTP parsing.

The security implications run both ways. If you're operating environments where you're trying to prevent outbound HTTP (data exfiltration controls, sandbox environments, developer environments with access to production data), understanding that /dev/tcp bypasses binary-level controls is critical. Don't assume that removing curl and wget closes the outbound HTTP channel. Assume it doesn't, and implement network-level controls if outbound HTTP truly needs to be blocked.

Bash version fragmentation. While /dev/tcp has been in bash for a very long time, extremely old or custom-compiled bash builds may have it disabled. Don't assume it's available on every bash installation without testing — especially in embedded environments, macOS (which ships an older bash due to licensing), or any system where bash was compiled from source with custom flags.

Cost and complexity creep. For small teams, the risk here is different from enterprise: it's using /dev/tcp as a clever trick and then having a new team member spend two hours debugging why a health check isn't working because ash doesn't support it. The complexity cost of clever shell tricks compounds over time. Use them where they genuinely save meaningful dependencies, not for the aesthetic pleasure of fewer binary dependencies.

Frequently asked questions

Does this work on macOS? Yes, with a catch. macOS ships bash 3.2 due to licensing (Apple won't distribute GPL v3 software as a default tool). Bash 3.2 does support /dev/tcp, so the feature works. However, if you're using the system /bin/bash on macOS, you're on a version that's many years behind current. Most developers on macOS have installed a more recent bash via Homebrew (/opt/homebrew/bin/bash), and that version works fully. The practical advice: if you're writing scripts intended to run on both macOS and Linux, test on both, and be explicit about the bash path in your shebang.

Can I use this for WebSocket connections? In theory, the WebSocket handshake starts as an HTTP Upgrade request, and you could send that via /dev/tcp. In practice, implementing a WebSocket client in pure bash is a significant undertaking — the protocol involves a base64 key exchange, frame masking, and a binary framing format that's painful to handle in shell. For any real WebSocket use, use a proper WebSocket client. /dev/tcp is strictly appropriate for plain HTTP request-response patterns.

What about rate limiting and connection timeouts? Bash's file descriptor redirections don't have a built-in timeout mechanism for TCP connections. If the remote host doesn't respond, your script will hang. The fix is to use the timeout command: timeout 5 bash -c 'exec 3<>/dev/tcp/example.com/80;...'. The timeout utility is available on virtually all Linux systems and is the standard way to add connection timeouts to shell TCP operations. Rate limiting is a non-issue at the shell level — you're just controlling how fast your loop runs.

Is /dev/udp useful for anything practical? Yes, though the use cases are narrower. The most common practical application is sending UDP syslog messages, which is a one-way fire-and-forget protocol that maps perfectly to the /dev/udp interface. DNS queries are also technically possible but extremely painful to implement in bash (the binary packet format requires careful shell manipulation). For DNS lookups in minimal environments, the host, dig, or nslookup commands are almost always available even when other tools aren't, and they're far more practical.

Will this work in a Docker RUN command or CMD? Docker's default shell for RUN commands is /bin/sh, not bash. If your image uses Alpine, that's ash, which doesn't support /dev/tcp. If your image uses Debian/Ubuntu, /bin/sh is dash, which also doesn't support it. To use /dev/tcp in a Dockerfile instruction, either change to bash explicitly: RUN ["/bin/bash", "-c", "exec 3<>/dev/tcp/..."] or use the form SHELL ["/bin/bash", "-c"] at the top of your Dockerfile to change the default shell for subsequent RUN commands. Don't forget to ensure bash is installed in the image first.

Is there a performance advantage to using /dev/tcp over curl? Marginally, in startup time — bash doesn't need to fork a new process to run curl. For a single HTTP request, the difference is negligible (single-digit milliseconds). For a loop making hundreds of requests, the process-fork overhead for curl can add up, but at that point you should be questioning whether shell scripting is the right tool at all. Don't choose /dev/tcp for performance reasons; choose it for dependency reasons.

Does this feature have any official status — can I rely on it being maintained? It's documented in the official GNU Bash manual under "Redirections," which means it's part of the defined behavior of the tool, not an undocumented implementation detail. The bash maintainers have explicitly included the --disable-net-redirections compile flag, which means they've consciously chosen to make it configurable rather than remove it. In my view, this is as close to "officially supported" as a shell feature gets. It's been present and documented since bash 2.04 in the late 1990s, and there's no indication of plans to remove it. That said: always test your assumptions about whether it's compiled in for any specific environment.

What's the right way to handle the HTTP response if I need to parse the body? The response stream from /dev/tcp includes both headers and body in a single stream, separated by a blank line (\r\n\r\n). The cleanest approach is to pipe through awk or sed to split on the blank line and extract the body. For example: cat <&3 | awk 'BEGIN{body=0} /^\r?$/{body=1; next} body{print}' will print everything after the first blank line. If you need to parse JSON from the body, pipe through jq if it's available. But if you're doing anything this complex with the response, I'd seriously reconsider whether a simple Python one-liner using urllib.request would be cleaner to maintain.

Final verdict

Here's what I think the /dev/tcp revival moment actually means for the audience at Opsvoro.

If you run any infrastructure that uses bash (and almost everyone does, because Dockerfiles, CI YAML files, and cron jobs all tend to involve bash at some level), this is genuinely useful knowledge to carry. Not because you should rewrite your curl calls, but because it fills a specific gap: the moment when you need to make a single HTTP request in a minimal environment and adding a dependency feels disproportionate. Health checks in lean containers, smoke tests in stripped CI images, quick connectivity verification in automation scripts — these are real use cases where knowing about /dev/tcp removes friction.

Who should act on this right now: teams that are actively working with minimal container images (Alpine, distroless, Chainguard) and find themselves adding curl just for health checks or CI notification webhooks. If that's a pattern you recognize, spend an hour auditing your scripts and converting the right ones to /dev/tcp or the openssl s_client HTTPS pattern. You'll reduce your image sizes, remove a package from your vulnerability surface, and write scripts that work across more environments.

Who should wait or skip this entirely: teams whose infrastructure uses standard Debian/Ubuntu-based images that already have curl, and teams whose scripts make HTTP calls with any complexity beyond a simple request and status check. If you need to handle redirects, authentication, cookies, or response parsing beyond a grep for "200," curl is simply the better tool and the complexity tax of raw bash HTTP is not worth paying.

The security implication is the one I'd leave ringing in your head regardless of which camp you're in: "no curl in this container" is not a meaningful security boundary. If your threat model includes malicious code running inside a container (supply chain attacks, compromised dependencies — both real concerns in 2026), network-level egress controls are the only controls that actually hold. /dev/tcp makes this concrete: a one-line bash command can open an outbound TCP connection to anywhere the kernel allows. If the kernel allows it — if there are no iptables rules, no NetworkPolicy, no service mesh egress control — then removing curl accomplished very little.

For small teams, the actionable summary is this: add /dev/tcp to your mental toolkit for operational scripting in minimal environments, layer it with openssl s_client for HTTPS where you need it, and treat this moment as a prompt to verify that your actual outbound network controls are working at the right layer. The bash trick is useful. The security implication is more important.