cage

August 23, 2026 ยท View on GitHub

One command for age. It encrypts a plain file with a passphrase, decrypts an age file, and works out which from the file's own header.

cage secret.pdf                 # -> secret.pdf.age
cage secret.pdf.age             # -> secret.pdf
cage ~/Downloads/report.pdf     # -> ~/Downloads/report.pdf.age

The name is the input path with .age added or removed, so the result lands next to the input rather than in your current directory.

No -e, no -d, no -o. Pass a second argument to choose the name yourself:

cage secret.pdf backup.age      # -> backup.age
cage backup.age restored.pdf    # -> restored.pdf

A relative name resolves against your current directory. The second argument is always a filename, never a destination folder.

- is the one name that isn't a file. It prints the decrypted contents to your terminal instead, and writes nothing to disk:

cage secret.txt.age -           # contents, straight to the terminal
cage secret.txt.age - | less    # or into another command

That's plain stdout, so it pipes and redirects like any other command's output. Decryption only: cage secret.pdf - is an error rather than binary age output all over your terminal. The Decrypting: ... -> (stdout) line goes to stderr, so it never mixes into the data.

An encrypted file that doesn't end in .age decrypts to <input>.decrypted, since age doesn't record the original name.

Works anywhere zsh and age do, including macOS, Linux and BSD.

Install

Requires zsh and age.

Don't have age yet?
brew install age     # macOS
apt install age      # Debian, Ubuntu

Everything else is in age's installation docs.

antidote

antidote install matijaoe/cage.plugin.zsh

If you bundle statically, add matijaoe/cage.plugin.zsh to ~/.zsh_plugins.txt instead.

zinit

zinit light matijaoe/cage.plugin.zsh

oh-my-zsh

git clone https://github.com/matijaoe/cage.plugin.zsh ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/cage

Then add cage to plugins=(...).

No plugin manager

It's a single file.

curl -fsSL -o ~/.cage.zsh https://raw.githubusercontent.com/matijaoe/cage.plugin.zsh/main/cage.plugin.zsh
echo 'source ~/.cage.zsh' >> ~/.zshrc

Or paste the function straight into your .zshrc.

Any other plugin manager works the same way. Point it at matijaoe/cage.plugin.zsh.

Prefer a shorter name? Add alias ag=cage. The function name is only a default.

How it decides

The file's header decides the direction, either age-encryption.org/v1 or -----BEGIN AGE ENCRYPTED FILE-----. The .age suffix only affects the default output name, so a plain file that happens to be called notes.age still gets encrypted.

What it won't do

  • Overwrite an existing output. age -o overwrites silently. cage stops first.
  • Delete the input, in either direction.
  • Anything age does itself. The passphrase prompt, armor detection, recipient handling, error messages and exit status all belong to age. Encryption is always age -p. For -r, -i or -a, use age directly.

cage writes ordinary age files. Plain age -d reads them.