xenvsync

The Secret-Safe Developer Workflow: Local to CI Without Leaks

Published March 29, 2026 · 9 min read · Developer Workflow

The Problem With How Most Teams Handle Secrets

A 2025 GitHub analysis found that over 10 million secrets were accidentally committed to public repositories in a single year. The vast majority came from developers who copy-pasted a .env file into the wrong place, forgot a .gitignore rule, or pushed a branch without realizing it contained credentials.

The solution is not more careful developers — it is a workflow that makes the insecure path harder than the secure one. When the default action is "encrypt first," accidental exposure becomes structurally unlikely.

This post describes a four-phase workflow using xenvsync that makes the secure path the easy path, from your first git init through production deployment.

Phase 1: Local Setup

Start every new project — or retrofit an existing one — with two commands. xenvsync init generates a 256-bit cryptographically random key and writes it with 0600 permissions. It also updates .gitignore to exclude both the key and your plaintext .env.

Phase 1 — project initializationbash
# Install once per machine
$ npm install -g @nasimstg/xenvsync
# or: brew install nasimstg/tap/xenvsync
# or: go install github.com/nasimstg/xenvsync@latest

# Initialize the project
$ xenvsync init
# ✓ Generated .xenvsync.key (256-bit, mode 0600)
# ✓ Added .xenvsync.key to .gitignore
# ✓ Added .env to .gitignore

# Create your .env file normally
$ cat .env
DATABASE_URL=postgres://localhost:5432/myapp
API_SECRET=dev-secret-key
REDIS_URL=redis://localhost:6379

# Encrypt it
$ xenvsync push
# ✓ Encrypted .env → .env.vault

# Commit the vault (never the plaintext)
$ git add .env.vault
$ git commit -m "add encrypted vault"
Important: The .xenvsync.key file is your decryption material. Back it up somewhere secure (password manager, secrets store) before relying on the vault. If the key is lost, the vault cannot be decrypted.

Phase 2: Daily Development Loop

The daily loop is simple: edit secrets in .env as needed, push to update the vault, and use xenvsync run to start your app. The key insight is that xenvsync run decrypts the vault in memory and injects the variables directly into the child process — plaintext never reaches the filesystem.

Phase 2 — daily workflowbash
# Pull latest vault from repo
$ git pull
$ xenvsync pull     # restores .env from vault

# Edit secrets as needed
$ vim .env

# Re-encrypt after changes
$ xenvsync push
$ git add .env.vault && git commit -m "update secrets"

# Start app — secrets live only in process memory
$ xenvsync run -- npm start
$ xenvsync run -- python manage.py runserver
$ xenvsync run -- go run ./cmd/server

# Check what changed since last push
$ xenvsync diff

# See full sync state
$ xenvsync status

Pro tip: Add the pre-commit hook from examples/hooks/pre-commit to your repo. It blocks commits when the vault is stale or when a plaintext .env file is staged. This turns security into an automatic guard rail rather than a remembered step.

Install pre-commit hookbash
$ cp examples/hooks/pre-commit .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit

Phase 3: CI/CD Integration

CI jobs need secrets but should never store them in pipeline YAML, environment variable UI, or build artifacts. The pattern: store the raw key value as a CI secret, write it to a file at runtime, use xenvsync run to inject secrets in-memory.

GitHub Actions — full workflowyaml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Inject xenvsync key
        run: |
          echo "${{ secrets.XENVSYNC_KEY }}" > .xenvsync.key
          chmod 600 .xenvsync.key

      - name: Audit vault integrity
        run: |
          xenvsync doctor
          xenvsync verify

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

      - name: Build
        run: xenvsync run -- npm run build
GitLab CI — minimalyaml
test:
  script:
    - echo "$XENVSYNC_KEY" > .xenvsync.key && chmod 600 .xenvsync.key
    - xenvsync doctor && xenvsync verify
    - xenvsync run -- npm test
CircleCIyaml
jobs:
  build:
    steps:
      - checkout
      - run:
          name: Inject secrets
          command: |
            echo "$XENVSYNC_KEY" > .xenvsync.key
            chmod 600 .xenvsync.key
      - run: xenvsync run -- npm test

Phase 4: Multiple Environments and Production

For projects with separate staging and production configurations, use named environments. Each gets its own vault file and its own key (or its own team roster slot for V2 vaults). The --env flag is consistent across all commands.

Phase 4 — named environmentsbash
# Push separate vaults per environment
$ xenvsync push --env staging
$ xenvsync push --env production

# Named vault files created:
# .env.staging.vault
# .env.production.vault

# Discover all environments
$ xenvsync envs

# Pull a specific environment
$ xenvsync pull --env staging

# Run against a specific environment
$ xenvsync run --env production -- node server.js

# Audit a specific environment
$ xenvsync verify --env production
$ xenvsync log --env production
Note: Use environment fallback merging to share common variables: put shared values in .env.shared, environment-specific overrides in .env.staging, and local machine overrides in .env.local. xenvsync merges all three when you push.

The Complete Secure Workflow

PhaseCommandWhat it does
Setupxenvsync initGenerate key, update .gitignore
Encryptxenvsync pushEncrypt .env → .env.vault
Run locallyxenvsync run -- <cmd>Inject secrets in-memory
Restorexenvsync pullDecrypt vault → .env
Auditxenvsync doctor + verifyCheck health and integrity
Diffxenvsync diffPreview changes before push
Historyxenvsync logKey-level change history
Rotatexenvsync rotateCycle key material