Two small bash functions that wrap age — a modern,
audited file-encryption tool from Filippo Valsorda — into a one-shot
tar + encrypt and decrypt + extract workflow against a
passphrase. They live in your shell config (.bashrc or .zshrc)
and give you portable, password-protected archives without dragging in GPG or a key
management story.
Threat model: at-rest protection for directories you carry around on laptops, USB sticks, or sync to consumer cloud storage (Dropbox, iCloud, Google Drive). Attacker has the encrypted file but not the passphrase. The passphrase strength is the whole game — use a long, random one (a passphrase manager entry or a 6+ word diceware string).
What it produces: a single <dir>.tar.gz.age file.
The tar.gz step keeps directory structure and permissions; the .age
layer is the X25519 / scrypt-passphrase encryption envelope.
age:# macOS
brew install age
# Debian / Ubuntu
sudo apt install age
# Verify
age --version
~/.bashrc (or ~/.zshrc):Click Copy in the top-right of the block, then paste the whole thing
at the bottom of your shell config. Open a new shell or run
source ~/.bashrc and you'll have en and de
available everywhere.
#--------------------------------------------------------------------------------------------------------#
# Encrypt a directory with age (passphrase) -> <dir>.tar.gz.age
# Usage: en Notes/
#--------------------------------------------------------------------------------------------------------#
en() {
if [ -z "$1" ]; then
echo "Usage: en <directory>"
return 1
fi
if ! command -v age >/dev/null; then
echo "age not installed. Run: brew install age"
return 1
fi
local src="${1%/}"
if [ ! -d "$src" ]; then
echo "Error: '$src' is not a directory"
return 1
fi
local out="${src}.tar.gz.age"
if [ -e "$out" ]; then
echo "Error: '$out' already exists"
return 1
fi
echo "Encrypting '$src' -> '$out'"
echo " $ tar czf - $src | age -p -o $out"
if tar czf - "$src" | age -p -o "$out"; then
echo "✓ Done: $(ls -lh "$out" | awk '{print $5" "$NF}')"
else
echo "✗ Failed"
rm -f "$out"
return 1
fi
}
#--------------------------------------------------------------------------------------------------------#
# Decrypt an age file and extract its tar -> current directory
# Usage: de notes.tar.gz.age
#--------------------------------------------------------------------------------------------------------#
de() {
if [ -z "$1" ]; then
echo "Usage: de <file.tar.gz.age>"
return 1
fi
if ! command -v age >/dev/null; then
echo "age not installed. Run: brew install age"
return 1
fi
if [ ! -f "$1" ]; then
echo "Error: '$1' not found"
return 1
fi
echo "Decrypting '$1' -> current directory"
echo " $ age -d $1 | tar xzf -"
if age -d "$1" | tar xzf -; then
echo "✓ Extracted"
else
echo "✗ Failed"
return 1
fi
}
#--------------------------------------------------------------------------------------------------------#
# Encrypt a directory (you'll be prompted for a passphrase, twice)
en Notes/
# Encrypting 'Notes' -> 'Notes.tar.gz.age'
# $ tar czf - Notes | age -p -o Notes.tar.gz.age
# Enter passphrase (leave empty to autogenerate a secure one):
# Confirm passphrase:
# ✓ Done: 142K Notes.tar.gz.age
# Decrypt + extract back into current directory
de Notes.tar.gz.age
# Decrypting 'Notes.tar.gz.age' -> current directory
# $ age -d Notes.tar.gz.age | tar xzf -
# Enter passphrase:
# ✓ Extracted
age binary: early exit with an install hint — no half-encrypted output.en requires a directory; de requires an existing file.en refuses to write if <dir>.tar.gz.age already exists.tar | age fails mid-run, the partial output file is removed and the function returns non-zero (so chained scripts fail loudly).${1%/} strips it so en Notes/ and en Notes both produce Notes.tar.gz.age.age -p prompts on the controlling terminal — works in interactive shells, not in non-interactive scripts. For unattended encryption, switch to recipient-key mode (age -r <pubkey>) and store the matching identity file under ~/.config/age/keys.txt.age derives a key with scrypt (work factor 18 by default) and encrypts with ChaCha20-Poly1305. There is no key-rotation primitive — rotate by re-encrypting with a new passphrase.tar czf step gzip-compresses before encrypting. Encrypted output is high-entropy, so compressing after encryption is a no-op — always compress first, encrypt last.Notes.tar.gz.age); rename if that matters.de extracts into the current working directory. cd to wherever you want the tree to land before running it.local, ${var%/}, command -v) all work in zsh too, so the same block is fine in ~/.zshrc.This is layer 4 (Data Protection — At Rest) of the defense-in-depth model: a passphrase-protected envelope around files you don't trust the storage layer (laptop, USB, consumer cloud) to guard for you. It is intentionally not a substitute for KMS-backed envelope encryption when the data lives inside a cloud account — for that, see AWS KMS, Snowflake's encryption key hierarchy, or Databricks CMKs.