•
8 min read
  • TypeScript
  • Clack
  • Age Encryption
  • Github API

nspt is a secure, serverless .env and secret-file sharing tool for teams, synced through Git.

It was built to solve a simple but annoying problem: sharing .env files through chat. You send a secret once, and a few weeks later someone needs it again — now you’re scrolling through old messages trying to find the right .env, wondering whether it’s still up to date, and potentially leaving sensitive configuration sitting in your chat history.

nspt keeps the secrets where your team already works — your Git repository — while keeping the actual contents encrypted. End-to-end encrypted: secrets can only be opened by members you’ve explicitly added; not even the Git host can read the contents.

No keys to exchange either: member keys are fetched automatically from GitHub and everything is decrypted locally. No public keys to copy. No secrets to paste into chat.

🎬 See It In Action

Initialization process

Add a teammate

nspt init demo

nspt add teammate demo

Once they’re added, sharing is just a regular Git push. The teammate pulls and decrypts locally:

git pull
nspt pull <groupName>

The encrypted files come down through Git, but the .env files are only decrypted on their own machine — so the Git host never sees the secrets.

How it works

If you’re curious about what actually happens behind the scenes, here’s the short version.

1. Every group has a file-encryption key

When a group is created, nspt generates a cryptographically random 256-bit key for that group.

This key is used to encrypt the files tracked by the group.

Group
 │
 └── 256-bit file key
          │
          ├── encrypts .env
          ├── encrypts .env.local
          └── encrypts other tracked files

The key itself is never stored in plaintext inside the repository.


2. Files are encrypted locally

When you run nspt push, the plaintext files never need to leave your machine.

The flow is essentially:

             Your machine
                  │
                  ▼
             frontend/.env
                  │
                  ▼
           encrypt with group key
                  │
                  ▼
      nspt/<group>/encfiles/...
                  │
                  ▼
                Git

Only the encrypted version is written to the nspt directory.

The original file stays where it was.


3. So how can multiple people use the same key?

This is where public-key encryption comes in.

The group has one symmetric file-encryption key, but every member has their own public/private key pair.

Instead of putting the group key directly in Git, nspt encrypts (wraps) the group key separately for every member using their public key.

For example:

                    Group file key
                         │
             ┌───────────┼───────────┐
             │           │           │
             ▼           ▼           ▼
        Alice's key   Bob's key   Charlie's key
             │           │           │
             ▼           ▼           ▼
       wrapped key   wrapped key   wrapped key

The repository can contain all of those wrapped keys.

But only the corresponding private key can unwrap each one.


4. Where do public keys come from?

nspt uses GitHub as the public-key directory.

When a user is added, nspt fetches their SSH public keys from:

https://github.com/<username>.keys

This means there is no key exchange between teammates.

You don’t need to:

❌ copy a public key
❌ send a key over Discord
❌ paste a key into a config file

However, there was an interesting cryptography problem to solve here.

Ed25519 → X25519

GitHub commonly provides Ed25519 SSH keys. Ed25519 is primarily designed for digital signatures and authentication, while age uses X25519 for public-key encryption.

So I couldn’t simply give an Ed25519 public key directly to age.

Fortunately, Ed25519 and X25519 are mathematically related: both are based on the Curve25519 family. This allows the Ed25519 key material to be deterministically converted into the corresponding X25519 representation.

The flow looks like:

GitHub
  │
  ▼
Ed25519 SSH public key
  │
  │ convert
  ▼
X25519 public key
  │
  ▼
age recipient
  │
  ▼
wrapped group key

The important part is that the user doesn’t need another key pair.

On the recipient’s machine, the corresponding Ed25519 private key can be converted/derived into the X25519 private-key representation needed to decrypt the age-wrapped group key:

             Bob's machine

          Ed25519 private key
                  │
                  │ derive/convert
                  ▼
          X25519 private key
                  │
                  ▼
           age decrypts
                  │
                  ▼
           group file key
                  │
                  ▼
              decrypt .env

So the same SSH identity that Bob already has on his machine can be used to access the group without requiring Bob to generate or manually exchange a separate encryption key.

The private key itself never needs to leave Bob’s machine. GitHub only provides the public key.

This gives nspt a convenient way to discover a user’s public key while keeping the actual decryption capability local to that user.


5. What gets stored in Git?

Conceptually, the repository contains something like:

nspt/
└── my-team/
    ├── config.toml
    ├── user_keys.toml
    └── encfiles/
        ├── frontend-env-987af88e.enc
        └── backend-envb-99b7057.enc

The encrypted files use automatically generated names, while the original file paths are tracked separately.

In simplified terms:

plaintext .env       → stays on your machine
encrypted .env       → goes into Git
group key            → never stored directly
wrapped group keys   → stored for individual members
private keys         → stay on member machines

So the Git host doesn’t need to be trusted with the actual secrets.


6. What happens when someone runs nspt pull?

The process works in reverse.

Git repository
      │
      ▼
encrypted file + wrapped group key
      │
      ▼
member's private key
      │
      ▼
unwrap group key
      │
      ▼
decrypt encrypted file
      │
      ▼
original filepath

For example:

nspt/<group>/encfiles/frontend-env-987af88e.enc
                         │
                         ▼
                      decrypt
                         │
                         ▼
                   frontend/.env

The original path is tracked alongside the encrypted file, allowing nspt to restore the file where it originally came from.


7. Why not just encrypt every file directly with each member’s public key?

Because a person can have multiple public keys, encrypting every file directly with their public keys would be storage-inefficient.

For example, if Bob has 5 public keys registered on GitHub, we would need to store 5 encrypted copies of every file for Bob.

Instead, nspt uses a hybrid encryption approach.

The actual files are encrypted once using symmetric encryption:

                  .env
                   │
                   ▼
          symmetric encryption
                   │
                   │
            256-bit group key
                   │
                   ▼
             encrypted file

Then the small group key is protected using public-key encryption:

              256-bit group key
                     │
       ┌─────────────┼─────────────┐
       ▼             ▼             ▼
   Alice's pub    Bob's pub    Charlie's pub
       │             │             │
       ▼             ▼             ▼
   wrapped key   wrapped key   wrapped key

This gives you the efficiency of symmetric encryption for the actual files while still allowing different people to securely access the same data.


8. What happens when a member is removed?

Simply deleting their wrapped key isn’t enough.

They may already have a copy of the old group key.

So when a member is removed, nspt performs key rotation:

                Old group key
                     │
          ┌──────────┼──────────┐
          ▼          ▼          ▼
        Alice      Bob        Charlie
                    ❌
                 removed

                     │
                     ▼
                new group key
                     │
          ┌──────────┴──────────┐
          ▼                     ▼
        Alice                Charlie

The files are re-encrypted using the new group key, and the new key is wrapped for the remaining members.

Bob can still possess the old key, but the newly encrypted files require the new one.


9. What if someone’s GitHub SSH key changes?

nspt can re-fetch the public keys for the group’s members.

The important distinction is that GitHub only provides the public side of the key.

The private key used to decrypt the wrapped group key remains on the member’s machine.


10. What if someone gets the entire repository?

They can see the encrypted files and the encrypted/wrapped group keys.

They cannot simply read the .env files from the repository.

For the actual file-encryption key, nspt uses cryptographically secure random generation rather than deriving it from a human password.

So an attacker can certainly try to brute-force the key, but a 256-bit random key has 2²⁵⁶ possible combinations. Even with an absurd amount of computing power—trillions of machines working continuously—the keyspace is so enormous that brute-forcing it is astronomically impractical.

The security model therefore assumes:

Git repository      → potentially public
Git host            → untrusted
Encrypted files     → safe to store in Git

Member private key  → secret
Member machine      → trusted
Plaintext .env      → local

That’s the basic idea behind nspt:

Git handles synchronization; cryptography handles confidentiality.