Library / SDK
FiloSottile/age avatar
FiloSottile/age

FiloSottile/age: file encryption with small keys and UNIX pipes

A simple, modern and secure encryption tool (and Go library) with small explicit keys, no config options, and UNIX-style composability.

24,309 stars681 forksGoBSD-3-Clause

At a glance

What is it?
age is a file encryption tool, format and Go library with explicit keys, no config file and a v1.3.0 release that adds post-quantum recipients. It fits pipelines and key-based automation, and it is the wrong tool when you need per-file ACLs or a key server.
Who is it for?
Adopt age when your encryption step should be one command inside a shell pipeline and your recipients are identified by a short public key you can paste into a config or a recipients file. Do not adopt it as a replacement for a secrets manager that enforces access control, rotation and audit, and do not adopt it expecting a config file to exist, because age has no config options.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 76 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • FiloSottile/age24,309 ★ · +638
Star history
Repository2026-09-222026-11-13
FiloSottile/age23.7k24.3k

638 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

What age is for, and who ends up using it

The problem age addresses is narrow: you have bytes, you want them encrypted to one or more recipients, and you want that to happen inside a shell command rather than inside an application. The README frames this as UNIX-style composability, and the first example shows the shape of it: tar writes a stream, age encrypts it, the shell redirects the result to a file. Nothing in that chain needs a configuration directory or a daemon.

The audience follows from that. Build and release engineers encrypting artifacts before uploading them. Operators moving a backup off a host without installing a backup product. Go developers who want the same format available as a library, since the module path is filippo.io/age and the repository ships age.go, parse.go, primitives.go and x25519.go at the top level alongside cmd/. Anyone who has tried to explain a GPG keyring to a colleague is the target reader.

The design constraints are stated plainly in the README: small explicit keys, no config options. A public key looks like age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p. That string is short enough to paste into a chat message or a CI variable, which is the whole point. There is no trust model to configure, because there is no web of trust. You encrypt to a key you already have, or you encrypt with a passphrase.

How age encryption works: recipients, identities, one stream

The mechanism is a recipient and identity model. A recipient is a public key, such as an age key generated by age-keygen. An identity is a secret key, stored in a file. Encryption takes one or more recipients; decryption takes one or more identities. The command line mirrors that asymmetry exactly, with -r and -R on the encrypt side and -i on the decrypt side.

Recipient files are the part that matters for automation. The README states that recipients can be listed one per line in files passed with -R, shows # comment lines in its example recipients.txt, and says that an argument of - reads the file from standard input. That last detail is what lets you generate a recipient list in a pipeline and encrypt to it without writing a temporary file.

The format is specified separately at age-encryption.org/v1, and the README links the specification badge to the same place. That separation matters more than it looks: the tool, the format and the Go library are three things, and the Rust implementation rage and the TypeScript implementation Typage exist because the format is documented independently of this codebase. If you need a second implementation to read what age wrote, the specification is where you check, not the README.

Version v1.3.0, released 2025-12-27, is titled post-quantum (and more). The repository has a pq.go file at the top level and depends on filippo.io/hpke and filippo.io/nistec in go.mod, which is consistent with a hybrid post-quantum recipient type. The README documents that mode: age-keygen -pq generates hybrid post-quantum keys, identities start with AGE-SECRET-KEY-PQ-1... and recipients with age1pq1..., and the recipients are about 2000 characters long.

Installing age and encrypting your first file

Installation is a package manager line on most systems. On macOS or Linux with Homebrew it is brew install age; on Debian 12 and Ubuntu 22.04 or later it is apt install age; on Fedora it is dnf install age; on Windows it is winget install --id FiloSottile.age. Debian 11 users need the bullseye-backports archive to get age v1.0.0 or newer. The README also lists pre-built binaries under dl.filippo.io for linux, darwin and other targets, with Sigsum proofs described in SIGSUM.md for verifying them. If you have a supported Go toolchain, the source install is:

bash
go install filippo.io/age/cmd/...@latest

That installs the commands in the cmd/ tree, which is where age and age-keygen live. After that, generate a key pair. The README's own example is:

bash
age-keygen -o key.txt

The tool writes the identity to key.txt and prints the public key to the terminal. You should see a line beginning with Public key: followed by a string starting with age1. Keep key.txt out of version control; the public key is the part you distribute.

Encrypting a directory tree to that recipient uses a pipe, exactly as the README demonstrates:

bash
tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age

Nothing appears on standard output except the encrypted stream, which is redirected to data.tar.gz.age. Decryption reads the identity file and writes the archive back:

bash
age --decrypt -i key.txt data.tar.gz.age > data.tar.gz

Two flags are worth knowing before you build anything on top. -a, or --armor, makes age emit PEM-encoded output, which is what you want when the ciphertext has to travel through a channel that mangles binary. -p, or --passphrase, switches from recipient-based encryption to a passphrase, which is the right choice for a one-off file you are sending to someone who has no key yet.

Where age is the wrong tool

age has no key server, no revocation and no access control layer. If a recipient's key is compromised you cannot un-encrypt what was already written, and you cannot remove that recipient from an existing file. There is no notion of a group, a role or an expiry. For a team that needs to answer who could read a given object six months ago, age gives you the recipient list you passed on the command line and nothing more.

Key management is entirely yours. If the identity file is lost, the ciphertext is gone. That is the correct property for an encryption tool and a real operational hazard for anyone who has been trained by systems that offer account recovery.

age has no config options by design. Hardware token support exists but lives outside this repository, in the age-plugin-yubikey project that the README links. If your requirement is that the private key never leaves a PIV token, you are adopting a plugin and its own release cycle, not just age.

Finally, age is a file and stream encryption tool, not a disk encryption tool and not a transport protocol. Encrypting a file at rest does not protect a machine that is running and unlocked, and piping through age does not authenticate the endpoints on either side of the pipe.

age vs GPG, and the other implementations

The comparison people reach for is age vs GPG, and the difference is in the key model rather than the cipher. GPG carries a web of trust, key servers, subkeys, expiry and a large option surface. age carries a public key string and a secret key file. The README's own framing is no config options, which is a description of the trade: you lose the ability to express policy and you gain the ability to read the entire command in one line. For a CI job that encrypts one artifact to one key, that trade is easy. For an organization that needs signatures, revocation lists and a documented chain of custody, GPG or a dedicated secrets platform is the more appropriate layer, and age will feel like it is missing pieces.

Among implementations of the same format, rage is a Rust implementation that the README calls interoperable, and Typage is a TypeScript implementation that the README says works in the browser, Node.js, Deno and Bun. The difference from this repository is the runtime and the packaging, not the format. A browser-side TypeScript implementation changes what is possible: you can decrypt in a web page without a native binary, which the Go command cannot do. Conversely, the Go library is what you import when your service is written in Go, via the filippo.io/age module path.

SSH keys are the other practical alternative to generating a new key pair at all.

Maintenance, versioning and licence

The repository is not archived, and the last push was on 2026-08-29, which is also the date of the v1.3.2 release. The preceding releases were v1.3.1 on 2025-12-28 and v1.3.0 on 2025-12-27. That cadence suggests a project that ships when there is something to ship rather than on a schedule, and the gap between v1.3.0 and v1.3.1 is one day, which is consistent with a follow-up fix rather than a feature release.

Upgrade cost is low on the surface because there is no configuration to migrate. The real cost is in the toolchain: go.mod declares go 1.25.0 and a toolchain of go1.27.0 for release builds, so building from source requires a recent Go. If you consume age as a library rather than a binary, your dependency graph picks up filippo.io/edwards25519, filippo.io/hpke, filippo.io/nistec, golang.org/x/crypto, golang.org/x/sys and golang.org/x/term.

The licence is BSD-3-Clause, stated in the repository and in the LICENSE file at the top level. That is a permissive licence, which generally means you can use the code in closed products provided you keep the copyright notice and the disclaimer, but the exact obligations depend on how you redistribute it and this is not legal advice. If you vendor the code or ship a binary, read LICENSE and your own counsel's guidance rather than treating the identifier as sufficient.

Editorial conclusion

Adopt age when your encryption step should be one command inside a shell pipeline and your recipients are identified by a short public key you can paste into a config or a recipients file. Do not adopt it as a replacement for a secrets manager that enforces access control, rotation and audit, and do not adopt it expecting a config file to exist, because age has no config options. Before rolling it out, verify three things on your own machines: that the package your distribution ships is at least v1.3.0 if you need the post-quantum recipients introduced in that release, that your recipients file parses with comments as in the README example, and that your decryption path has the identity file available, since age does not recover a lost identity for you.

Frequently asked questions

How does age encryption work?

You encrypt to one or more recipients, such as age public keys beginning with age1, and you decrypt with one or more identity files containing the matching secret keys. The command line reflects this with -r and -R for recipients and -i for identities, and the format itself is specified separately at age-encryption.org/v1.

What is age-keygen used for?

age-keygen generates a key pair. The README example runs age-keygen -o key.txt, which writes the identity to key.txt and prints the public key, a string starting with age1, for you to distribute to anyone who wants to encrypt to you.

What does AgeKey or an age key mean?

In age there are two key forms: a recipient, which is the public key printed by age-keygen and begins with age1, and an identity, which is the secret key stored in an identity file and written as AGE-SECRET-KEY-1. Encryption takes recipients, decryption takes identities.

Official sources

  1. FiloSottile/age on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Community

Where developers are discussing it

Posts on Hacker News, dev.to and Lobsters that link to this repository, found by our scan of those communities. 0 in the last 30 days; newest first.

Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/filosottile-age.svg)](https://hysenlabs.com/projects/filosottile-age)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/filosottile-age.svg)](https://hysenlabs.com/projects/filosottile-age)

Was this useful?