xenvsync

How a 7-Person Startup Encrypted Their Secrets in One Sprint

Published March 30, 2026 · 7 min read · Case Study

The Starting Point

Picture a typical seed-stage startup: seven engineers, three environments (dev, staging, production), and a secrets strategy that could generously be described as "ad hoc." The staging database password lived in a Slack DM from six months ago. The production API keys were in a Google Doc that three people had bookmarked. Two developers had slightly different local .env files and nobody was quite sure which one was correct.

A contractor who had left two months earlier still theoretically had access to everything they had ever been messaged. Nobody had rotated the keys because nobody knew which keys they had.

This is not unusual. Most early-stage teams prioritize shipping over secret hygiene, and that is a reasonable tradeoff — until it isn't.

The Trigger

A senior engineer noticed that .env files were appearing in branches that contributors had pushed to forks. No secrets were exposed publicly — all the repos were private — but the close call was enough. The team decided to fix the problem properly before their Series A audit.

Requirements they agreed on:

  • Secrets must be encrypted before they touch version control.
  • Each developer should use their own key — no shared team password.
  • CI/CD must work without passing secrets through environment variable UI.
  • When someone leaves, their access must be revocable in under five minutes.
  • The solution cannot require a cloud account or subscription.

Day 1: Initialize and Encrypt

The lead engineer installed xenvsync via npm and ran the first encryption on the staging environment. Total time from reading the docs to a committed vault: eleven minutes.

Day 1 — first vaultbash
# Install
$ npm install -g @nasimstg/xenvsync

# Initialize key for the project
$ xenvsync init
# Generated .xenvsync.key
# Updated .gitignore: added .xenvsync.key, .env

# Encrypt staging secrets
$ xenvsync push --env staging
# Encrypted .env.staging → .env.staging.vault

$ git add .env.staging.vault
$ git commit -m "add encrypted staging vault"
Note: The .xenvsync.key was shared with the rest of the team over a secure channel (1Password) as a temporary measure. The team planned to move to per-member X25519 keys on Day 3.

Day 2: Wire CI to Pull at Runtime

The team stored the staging key value in GitHub Actions Secrets and updated the pipeline to inject it at runtime. The plaintext environment variable blocks in the YAML were removed.

GitHub Actions — runtime key injectionyaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up xenvsync key
        run: |
          echo "${{ secrets.XENVSYNC_STAGING_KEY }}" > .xenvsync.key
          chmod 600 .xenvsync.key

      - name: Run tests with secrets
        run: xenvsync run --env staging -- npm test

      - name: Verify vault integrity
        run: xenvsync verify --env staging
One developer noted that removing the explicit env: block from the pipeline YAML was the most satisfying part of the day. The pipeline went from 14 environment variables to two: the key and the passphrase.

Day 3: Per-Member Keys and the V2 Vault

Each of the seven developers ran xenvsync keygen on their own machine and shared their public key with the lead. The lead added all seven to the team roster and re-pushed the vault. From that point on, everyone used their own identity to decrypt — the shared key from Day 1 was deleted and the locks changed.

Day 3 — V2 team vaultbash
# Each developer (once on their machine)
$ xenvsync keygen
$ xenvsync whoami
# Public key: ABC123...  ← share this

# Lead engineer builds roster
$ xenvsync team add alice   <alice-pubkey>
$ xenvsync team add bob     <bob-pubkey>
$ xenvsync team add carol   <carol-pubkey>
$ xenvsync team add dave    <dave-pubkey>
$ xenvsync team add eve     <eve-pubkey>
$ xenvsync team add frank   <frank-pubkey>
$ xenvsync team add grace   <grace-pubkey>

# Re-push as V2 vault — each member gets their own encrypted slot
$ xenvsync push --env staging
$ xenvsync push --env production
$ git add .env.staging.vault .env.production.vault .xenvsync-team.json
$ git commit -m "migrate to V2 per-member vaults"

# Immediately delete the shared symmetric key
$ rm .xenvsync.key

Week 2: Standardize Across All Services

The team spent the second week rolling the same pattern out to their other three services. Each repo got its own vault. The shared V1 key for CI was replaced with the lead's own identity key injected from GitHub Secrets (since CI acts as a "robot team member" in their roster model).

They also set up the pre-commit hook from examples/hooks/pre-commit to prevent plaintext .env files from being staged accidentally — the problem that had triggered the whole migration.

Pre-commit hook setupbash
$ cp examples/hooks/pre-commit .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit

# Hook now blocks:
# - Staging of plaintext .env files
# - Commits when vault is older than .env

The Offboarding Test

Three weeks after the migration, a contractor finished their engagement. The lead tested the revocation flow in a staging environment first:

Offboarding — revoke and rotatebash
# Revoke contractor access across all environments
$ xenvsync rotate --revoke contractor --env staging
$ xenvsync rotate --revoke contractor --env production

# Confirm they are no longer in the roster
$ xenvsync team list

# Push updated vaults
$ xenvsync push --env staging
$ xenvsync push --env production
Total time: four minutes and twenty seconds. The contractor's previous local identity files were worthless against the re-encrypted vaults.

Three Months Later

The team ran the migration past their Series A security audit. The auditor flagged the use of AES-256-GCM with fresh nonces, Git-committed encrypted vaults, per-member key isolation, and a documented rotation workflow as positives. The pre-existing shared-key approach was noted as resolved.

Developer feedback after three months was uniformly positive. The daily workflow — git pull, xenvsync pull, xenvsync run -- npm start — became muscle memory within a week. The pre-commit hook caught two accidental staging attempts in the first month, both from developers who had forgotten to push their vault after editing .env.

Note: Want to run the same migration? See the Migration Playbook for a phased guide with rollback options.