↓ Skip to main content

A Safer curl | bash

·5 mins
Andrew Walsh
Author
Andrew Walsh
Coder with a passion for most all things technological.

I’ve had to fetch a file and run it a few times. For example, Toggling Bluetooth on Linux Login Status used to tell readers to do this, and a repo I maintain did it at runtime (since fixed by bundling the file instead).

Piping curl straight into bash is how a lot of software asks to be installed (e.g. Homebrew, nvm, Bun, rustup). It’s convenient and a simple two-liner, but you’re running whatever the server sends at that moment, and you can’t see it first. A dropped connection can even run half a script. At best it stops partway; at worst it leaves your system half-changed.

This isn’t hypothetical either. Attackers altered the Codecov Bash Uploader, which CI pipelines ran with curl | bash, and it quietly sent their secrets out for two months before a customer’s hash check caught it.

I started writing a small wrapper to address this, then realized it was mostly re-implementing two tools that are already on almost every Linux machine: curl and sha256sum (from coreutils). They’re packaged, maintained, and audited by far more people than any wrapper I’d write, so this post uses them directly.

Download, Check, Then Run
#

The pattern is three steps chained with &&, so each one only happens if the previous one worked.

PUB_SHA256="<the authoritative hash for the script from the published source>"
curl -q --proto '=https' --tlsv1.2 -fsSLO https://example.com/install.sh &&
printf '%s  %s\n' "$PUB_SHA256" 'install.sh' | sha256sum --check &&
bash install.sh

Here is what each part does:

  • -q - ignores ~/.curlrc, so a modified config can’t change the download. It has to be the first option.
  • --proto '=https' --tlsv1.2 - HTTPS only (including after redirects), with TLS 1.2 or newer.
  • -f - fails on an HTTP error instead of saving the error page as the script.
  • -sSLO - quiet but still shows errors, follows redirects, and saves under the remote file name.
  • PUB_SHA256 - the script’s hash as its publisher lists it (e.g. on their release page), copied in by you rather than fetched with the script.
  • printf '%s %s\n' "$PUB_SHA256" 'install.sh' - writes the line sha256sum expects: the hash, two spaces, and the file name. macOS’s shasum rejects the line without the second space, and the format string makes it easy to see.
  • sha256sum --check - compares the file to the hash and fails on a mismatch.
  • bash install.sh - only runs once the whole file is on disk and matches, so a dropped connection never runs half a script.

On macOS, use shasum -a 256 --check in place of sha256sum --check.

Example
#

Posts on this site, that install something, show a command like the one below. The hash is computed from the file when the site is built, so it always matches what the site serves. Try it; the script only prints its arguments.

curl -q --proto '=https' --tlsv1.2 -fsSLO http://awalsh128.com/safer-curl-bash/hello.sh &&
printf '%s  %s\n' '293c047844c69759f4e12fb413523424755b5d38a76923cb7515cd4dbe009e7f' 'hello.sh' | sha256sum --check &&
bash hello.sh --name you
hello.shshDownload
#!/usr/bin/env bash
# Example script for "A Safer curl | bash". It only prints the arguments it was given.
echo "Hello from a verified script. Arguments: $*"

To read a script before running it, leave off the last line (and the && before it), then open the file after the check passes.

less hello.sh

Checking It Came From This Site’s Build
#

The hash above lives on the same site as the file, so if someone could change one, they could change the other. For a check that doesn’t depend on this site, every build signs the published scripts with a GitHub artifact attestation. It ties the file to the commit and workflow that built it, signed through Sigstore.

gh attestation verify hello.sh --repo awalsh128/awalsh128.github.io

What About Other Tools?
#

I looked for an existing tool before settling on plain coreutils. None of them was both pinned to a hash and still maintained when I checked (November 2026).

  • preflight - pipes through preflight run sha256=<hash> and stops on a mismatch, which is the closest match. No release since 2022.
  • hashpipe - passes the data through only if the hash matches. No commits since 2022, and it defaults to base58 multihashes rather than the hex hashes projects publish.
  • vet - shows the script, a diff since your last run, and shellcheck output, then asks before running. It’s the most active of the three, but it doesn’t check a hash, so it protects you only as well as your review does.

None of them is in Ubuntu’s repositories either, so you’d be installing a tool from a third party to avoid trusting a third party.

Caveats
#

  • It’s not a sandbox. The script runs as you, with everything you can touch. A hash only proves you got the bytes you expected, not that they’re safe. Read the script first.
  • The hash is only as good as where it came from. If the hash and the script come from the same site, someone who can change one can usually change both. The attestation check above is the way around that for this site.
  • Changing a script breaks old commands. That’s on purpose: a command you copied last year fails instead of running something you never saw.
  • It can’t see past the first script. If that script downloads and runs something else, that part is up to the script. Playwright’s install scripts (CVE-2025-59288) are a real example: they downloaded packages with TLS certificate checks turned off, so a verified install script would still have pulled in whatever an attacker served.

That’s it. It won’t make an untrustworthy script safe, but it does take the guesswork out of what actually runs.

References
#

Related

Get Windows Service name from executable in PowerShell.

I was recently putting some PowerShell scripts together for deployment and maintenance of software to our machine instances. One of the requirements was to be able to discover the service name from a Windows Service executable that uses ServiceInstaller. I needed to be able to extract this value in a generic way in order to query the service and stop it if running. Here is what I was able to put together.

Licensing using XML digital signing.

·6 mins
Motivation # One of the main concerns in any licensing implementation is that authored license files are coming from the trusted authority (ie. your company) and not some third party. This can be accomplished in software by encrypting and decrypting the license with a single key, but symmetric key storage can be cumbersome and have added complexity.

Automate IIS enable on Windows 7.

·3 mins
Background # There are plenty of sites that will tell you how to enable IIS through the Control Panel. Although, if you are tasked with enabling IIS automatically (eg. for an installer), it can be a nightmare. This is only really needed though in special cases where the installer has more control over the hardware specifications. Determining an upgrade or uninstall scenario with IIS can be very tough since you are not installing your own software but rather enabling a feature of Windows. With that in mind, we can propose a solution for automated enabling of IIS.

comments powered by Disqus