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 -ooverwrites 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,-ior-a, useagedirectly.
cage writes ordinary age files. Plain age -d reads them.