Skip to main content

Secret env sync for GCP teams

Project description

senzu

PyPI version PyPI status CI Python License: MIT PyPI downloads codecov built with nix

Stop manually copy-pasting secrets from GCP Secret Manager into .env files like an animal. Senzu syncs secrets between GCP Secret Manager and local .env files, tracks where every key came from, and won't let you blow up production by pushing stale local changes over remote ones.

It's a CLI + Python library for teams using GCP Secret Manager who need their secrets to actually stay in sync — across multiple environments, multiple secrets, multiple people.


Why it's actually sick

Most teams end up with a shared .env in a private Slack channel, a Notion doc, or some other nightmare. If you're using Secret Manager, you're at least in the right place — but the workflow is still garbage. You open the GCP console, copy values one by one, paste them into a file you hope you don't accidentally commit.

Senzu fixes this:

  • senzu pull — fetches all your configured secrets into a local .env file in one command. Handles JSON and dotenv formats automatically. Works across multiple secrets per environment. If your local file already has keys that aren't in remote yet, they're preserved — remote wins on conflicts, but local-only keys survive. Use --overwrite if you want the remote to fully replace your local file.

  • senzu push — pushes local changes back to Secret Manager. But here's the thing: it actually checks if someone else changed the remote since you last pulled. If they did, it blocks you.

  • senzu diff — see exactly what's different between your local file and what's in Secret Manager, without touching anything. Pipe it into CI, use it in code review, whatever.

  • Lock file — after a pull, Senzu writes senzu.lock which tracks which key came from which secret and which project. This is what makes push safe. It knows exactly where to send each key back, even if you're pulling from 5 different secrets into one .env.

  • senzu import — already have a .env file and want to get into Secret Manager without touching the GCP console? senzu import dev --from .env creates the secret if it doesn't exist, pushes the keys, and writes senzu.lock so you're immediately ready to pull/push. If the secret already has data, it merges — your local keys win. Before confirming, it shows you exactly which keys are new, which are changed, and which are unchanged, so you know what you're actually pushing. If nothing has changed, it exits early.

  • Multiple environmentsdev, staging, prod, whatever you want. Each one can have its own GCP project, its own secrets, its own local file. senzu pull dev or senzu pull prod, no config flags needed.

  • SenzuSettings — drop-in Pydantic BaseSettings subclass. Automatically reads the right .env file based on your ENV var, parses nested JSON objects into proper Python dicts/lists, and falls back to reading directly from Secret Manager in Cloud Run by just setting SENZU_USE_SECRET_MANAGER=true.

  • senzu generate — auto-generates a typed Pydantic settings class from your actual secrets. You never have to manually write api_key: str for every field again.


Install

pip install senzu
# or
uv add senzu

Requires Python 3.10+. You'll need GCP credentials set up — either gcloud auth application-default login locally or a service account in prod.


Setup

Run the init wizard in your project root:

senzu init

# or skip the prompts entirely with flags
senzu init --project my-gcp-project --env dev --file .env.dev --secret app-env

This creates senzu.toml and updates .gitignore to exclude your .env.* files. You don't want those committed. The --env flag controls the environment name (defaults to dev) — useful if you want to scaffold a non-dev env first.

Or write senzu.toml yourself:

[envs.dev]
project = "my-gcp-project-dev"
file    = ".env.dev"
secrets = [
  { secret = "app-env" },
  { secret = "db-creds", format = "json" },
]

[envs.prod]
project = "my-gcp-project-prod"
file    = ".env.prod"
secrets = [
  { secret = "app-env-prod" },
]

Each secret in the secrets array is fetched and merged into the local file. If you want the entire secret stored as a single env var rather than expanded into individual keys, use type = "raw". env_var is required — it sets the env var name the value is written under:

secrets = [
  # Scalar value (e.g. a webhook secret string)
  { secret = "stripe-webhook-secret", type = "raw", env_var = "STRIPE_WEBHOOK_SECRET" },

  # JSON object stored whole — useful when you want one dict, not individual keys
  { secret = "firebase-sdk", type = "raw", env_var = "FIREBASE_CREDS" },
]

For the JSON case, Senzu stores it single-quoted in the .env file (FIREBASE_CREDS='{"type":"service_account",...}'). Declare the field as dict in SenzuSettings and it's automatically deserialized.

Cross-project secrets

A secret can pull from a different GCP project than the environment default. This is useful for shared infrastructure secrets owned by a central project:

[envs.prod]
project = "my-app-prod"
file    = ".env.prod"
secrets = [
  { secret = "app-env-prod" },
  { secret = "datadog-api-key", project = "shared-infra" },
]

When multiple secrets share a key name, the last secret listed wins. Senzu will emit a warning so you know it happened.


Usage

# Bootstrap — import an existing .env into Secret Manager for the first time
senzu import dev --from .env
senzu import dev --from .env --secret app-env          # skip interactive routing, send all keys to this secret
senzu import dev --from .env --keys DB_URL,DB_PASSWORD # specific keys only
senzu import dev --from .env --format json             # write as JSON instead of dotenv

# Pull secrets to local .env files
senzu pull              # all environments defined in senzu.toml
senzu pull dev          # specific environment only
senzu pull dev --overwrite  # fully replace local file with remote (discards local-only keys)

# See what's different between local and remote
senzu diff         # all environments
senzu diff dev     # specific environment only

# Push local changes back to Secret Manager
senzu push         # all environments (prompts for confirmation per env)
senzu push dev     # specific environment
senzu push dev --force  # skip confirmation even if remote has unretrieved changes

# Show all configured environments, their secrets, GCP projects, and local file status
senzu status

# Generate a typed Pydantic settings class from your secrets
senzu generate dev --out settings.py

Using in Python

If you have a Python app and want type-safe settings without the manual config:

from senzu import SenzuSettings

class Settings(SenzuSettings):
    database_url: str
    api_key: str
    firebase_creds: dict  # see note on nested JSON below

settings = Settings()

GCP secrets often store nested JSON objects (service account keys, connection configs, etc.) that can't be written as raw dotenv values. Senzu encodes these as single-quoted strings in the .env file:

// Secret Manager — app-env secret value
{
  "database_url": "postgres://...",
  "api_key": "sk-...",
  "firebase_creds": { "type": "service_account", "project_id": "my-app" }
}
# .env.dev — what Senzu writes after senzu pull
DATABASE_URL=postgres://...
API_KEY=sk-...
FIREBASE_CREDS='{"type":"service_account","project_id":"my-app"}'

When you declare the field as dict (or list) in SenzuSettings, Senzu automatically deserializes it — so settings.firebase_creds is already a Python dict, not a string.

Senzu reads ENV or SENZU_ENV to figure out which environment you're in, finds the right .env file from senzu.toml, and loads it. In Cloud Run or any environment where you don't have a file, set SENZU_USE_SECRET_MANAGER=true and it reads directly from Secret Manager.


The lock file

After senzu pull, you'll have a senzu.lock file. This is how Senzu knows which of your 40 env vars came from which of your 5 secrets. Don't delete it — push won't work without it. Commit it — it contains no secret values, just routing metadata (which key lives in which secret), and your teammates need it to push without doing a redundant pull first.


Auth

Senzu uses the standard GCP auth chain via google-cloud-secret-manager. Locally, run:

gcloud auth application-default login

In CI/CD or Cloud Run, use a service account with Secret Manager Secret Accessor role on the relevant secrets.


Development

With Nix (recommended)

The repo uses uv2nix to provide a fully reproducible dev environment. Dependencies are pinned in uv.lock.

nix develop

This drops you into a shell with the right Python version, all deps installed, and senzu itself available as an editable install — changes to the source are reflected immediately without reinstalling. gcloud is also available.

To run the CLI directly without entering a shell:

nix run

Without Nix

uv sync
uv run senzu --help

Or with a standard virtualenv:

python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'

Running tests

pytest
# or inside nix develop:
pytest tests/

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

senzu-0.3.1.tar.gz (86.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

senzu-0.3.1-py3-none-any.whl (28.7 kB view details)

Uploaded Python 3

File details

Details for the file senzu-0.3.1.tar.gz.

File metadata

  • Download URL: senzu-0.3.1.tar.gz
  • Upload date:
  • Size: 86.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for senzu-0.3.1.tar.gz
Algorithm Hash digest
SHA256 350a20cd9e71832845282df08dcae099b9a89d0d922060414710be8481eeb26d
MD5 d24bbf614f68de13651c46ba13f8f6c8
BLAKE2b-256 0871475d3a343cceb3b255a8c5b5759fda0c2296e0b9359b80d4c61d88bb2388

See more details on using hashes here.

Provenance

The following attestation bundles were made for senzu-0.3.1.tar.gz:

Publisher: publish.yml on philip-730/senzu

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file senzu-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: senzu-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 28.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for senzu-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a4d630c88c2c179b03fbaa06526bfda74009c814549d4eee1017af2392e5799e
MD5 5cde2628f6828f89d32993bdea0f0876
BLAKE2b-256 2bd1ffbfaac168e3f7d3d8cf4ee66af6dc1b1131e55b9f6a177b481a71065b36

See more details on using hashes here.

Provenance

The following attestation bundles were made for senzu-0.3.1-py3-none-any.whl:

Publisher: publish.yml on philip-730/senzu

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page