A command-line tool that decrypts password-protected PDF files using a cascading strategy of tools: qpdf, mutool, and ghostscript. It tries each tool in sequence until one succeeds — so you don't have to remember which tool works for which PDF.
This is not a password cracker. It assumes you already know the password.
The package ships two commands: decrypt-pdf for a single file, and
decrypt-pdfs to bulk-decrypt every encrypted PDF in a directory.
# 1. Install (Homebrew)
brew tap sdaas/tools && brew install decrypt-pdf
# 2. Decrypt with the password on the command line
decrypt-pdf -p 's3cret' document.pdf
# 3. …or pass the password via an environment variable
DECRYPT_PASSWORD='s3cret' decrypt-pdf document.pdfThe decrypted file is written to document_decrypted.pdf by default.
Install via the Homebrew tap:
brew tap sdaas/tools
brew trust sdaas/tools # optional — only on newer Homebrew (see note)
brew install decrypt-pdfWhy
brew trust? Recent versions of Homebrew require you to explicitly trust third-party taps before they will install or run formulae, casks, or commands from them — a supply-chain safety measure, since a tap can ship arbitrary code. Until the tap is trusted, Homebrew ignores it andbrew installwon't finddecrypt-pdf. Trust the whole tap withbrew trust sdaas/tools, or just this formula withbrew trust --formula sdaas/tools/decrypt-pdf. If your Homebrew doesn't require it, the command is a harmless no-op. (To opt out globally — not recommended — setHOMEBREW_NO_REQUIRE_TAP_TRUST=1.)
decrypt-pdf needs at least one of the underlying tools installed (all three
recommended for the best success rate); Homebrew pulls these in as formula
dependencies, but you can also install them directly:
brew install qpdf
brew install mupdf-tools
brew install ghostscriptdecrypt-pdf [OPTIONS] [-p PASSWORD] INPUT_FILE [OUTPUT_FILE]
INPUT_FILE is the encrypted PDF. OUTPUT_FILE is optional — it defaults to
<input>_decrypted.pdf in the same directory.
Specifying the password. The password can be provided two ways, in this order of precedence:
-
-pflag (highest priority) — passed on the command line. Simple, but visible inpsoutput and shell history.decrypt-pdf -p 's3cret' document.pdf -
DECRYPT_PASSWORDenvironment variable — used as a fallback when-pis not given. Keeps the password off the command line.export DECRYPT_PASSWORD='s3cret' decrypt-pdf document.pdf # or inline for a single invocation DECRYPT_PASSWORD='s3cret' decrypt-pdf document.pdf
If neither is provided, the command exits with an error.
Options
| Flag | Description |
|---|---|
-p PASSWORD |
Password for the encrypted PDF |
--verbose |
Show detailed output from each decryption tool |
--quiet, -q |
Suppress all output; rely on exit code and output file |
--cleanup |
Replace the original in place: archive it into originals/ and rename the decrypted file to the original's name. Cannot be combined with an explicit OUTPUT_FILE. |
--version |
Show version information and exit |
-h, --help |
Show help message and exit |
In-place replace (--cleanup). Normally decrypt-pdf writes a new
<input>_decrypted.pdf and leaves the original alone. With --cleanup, on a
successful decryption it instead archives the original into an originals/
folder beside it and renames the decrypted file to the original's name — so the
plain filename ends up holding the decrypted PDF:
decrypt-pdf --cleanup -p 's3cret' document.pdf
# document.pdf -> now the decrypted PDF
# originals/document.pdf -> the untouched encrypted originalThe original is archived, never deleted: --cleanup only runs after the
decryption is verified, refuses to overwrite an existing backup, and orders its
moves so no failure can leave you without a copy of the original.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success (or file is already unencrypted) |
| 1 | Failure |
decrypt-pdfs decrypts every encrypted PDF in a directory in one shot. It
scans for PDFs, uses qpdf to detect which are actually encrypted, and hands
each one to decrypt-pdf. All files are assumed to share a password, but you can
supply several candidates and each file is tried against them in order.
# Decrypt every encrypted PDF in the current directory
decrypt-pdfs -p 's3cret'
# Recurse into sub-folders, try two candidate passwords, replace originals
decrypt-pdfs --recursive -p 'pw1' -p 'pw2' --cleanup ~/statements
# Preview what would happen — writes nothing, needs no password
decrypt-pdfs --dry-run ~/statementsThe decrypted file for name.pdf is written to name_decrypted.pdf. With
--cleanup, the work is delegated to decrypt-pdf --cleanup: the original is
archived into an originals/ folder and the decrypted file takes the original's
name (the original is archived, never deleted). Files already named
*_decrypted.pdf and anything under originals/ are skipped.
Options
| Flag | Description |
|---|---|
-r, --recursive |
Also process PDFs in sub-folders |
-p PW, --password PW |
Candidate password; repeatable, tried in order (or via DECRYPT_PASSWORD) |
--cleanup |
Replace each original in place (archive to originals/, delegated to decrypt-pdf --cleanup) |
-n, --dry-run |
Show what would be decrypted; change nothing; no password required |
-v, --verbose |
Print per-file progress |
--version |
Show version information and exit |
-h, --help |
Show help message and exit |
Exit codes
| Code | Meaning |
|---|---|
| 0 | All encrypted files were decrypted (or nothing to do) |
| 1 | One or more files could not be decrypted (listed on stderr) |
| 2 | Usage or environment error (bad directory, missing dependency, no password) |
Set up a macOS Quick Action to right-click any PDF in Finder and decrypt it — no terminal required.
One-time setup
-
Copy the decryption script into the workflow bundle:
mkdir -p ~/Library/Services/"Decrypt PDF File.workflow"/Contents/ cp "$(command -v decrypt-pdf)" ~/Library/Services/"Decrypt PDF File.workflow"/Contents/
-
Open Automator and select Quick Action as the document type.
-
At the top, set "Workflow receives current PDF files in Finder".
-
From the actions library, drag Run AppleScript into the workflow.
-
Replace the default script with the contents of
automator/decrypt-pdf.applescript. -
File > Save and name it "Decrypt PDF File".
Usage
Right-click a PDF in Finder > Quick Actions > Decrypt PDF File. A dialog
prompts for the password; the decrypted file appears in the same folder as
<filename>_decrypted.pdf.
Note: the Quick Action currently expects the script inside the workflow bundle and runs with a minimal
PATH. Compatibility with a Homebrew install is tracked in the issues.
See design.md for the decryption strategy, tool-specific notes (qpdf, mutool, ghostscript), the cascading workflow, and Homebrew packaging.
| Path | Purpose |
|---|---|
decrypt-pdf |
The main script — cascading PDF decryptor |
decrypt-pdf_test.sh |
Tests for decrypt-pdf (synthetic fixtures) |
decrypt-pdfs |
Batch wrapper — decrypts every encrypted PDF in a directory |
decrypt-pdfs_test.sh |
Tests for decrypt-pdfs (synthetic fixtures) |
release.sh |
Local release pre-flight: bump, gate, tag, push |
release_test.sh |
Characterization tests for release.sh |
run-tests.sh |
Single test runner (discovers every *_test.sh) |
automator/decrypt-pdf.applescript |
Automator Quick Action script |
.githooks/pre-push |
Runs the full suite before every git push |
.github/workflows/ci.yml |
CI — runs the suite on push / PR |
.github/workflows/release.yml |
Renders the Homebrew formula and pushes it to the tap on a tag |
design.md |
Design notes |
homebrew_packaging_guide.md |
How the Homebrew tap / packaging works |
Run the full suite via the single runner (add -v for verbose output):
./run-tests.shTests are any *_test.sh file; the runner discovers them automatically. The
decryption suite is self-contained — it generates its own synthetic encrypted
PDFs at runtime (via qpdf), so no customer files or secret passwords are
needed. Decryption tests are skipped only if qpdf is not installed.
The git tag is the version source of truth — there is no version file to bump. To cut a release, run the local pre-flight:
./release.sh # add --dry-run to preview without tagging/pushingIt suggests a SemVer bump from the commits since the last tag, runs the quality
gates (tests + brew audit/style/install/test), then tags vX.Y.Z and pushes.
That tag push triggers .github/workflows/release.yml,
which renders Formula/decrypt-pdf.rb (stamping the tag version into the
script's __VERSION__ placeholder) and pushes it to the sdaas/tools tap
(repo Sdaas/homebrew-tools). See release.sh and
homebrew_packaging_guide.md for details.
- Pre-push hook —
.githooks/pre-pushruns the full suite before every push and blocks the push if it fails. Enable it once per clone withgit config core.hooksPath .githooks; bypass in an emergency withgit push --no-verify. - CI/CD —
ci.ymlruns the suite on every push and PR (macOS runner, installsqpdf/mupdf-tools/ghostscript).release.ymlpublishes the Homebrew formula on tags (see Release process).
Open items and bug reports live on the GitHub Issues page.