English | 日本語
A Go CLI that bulk-registers (syncs) environment variables declared in an env-sync.yaml definition file to Vercel and/or GitHub Actions.
From a single definition file you can choose the sync target per variable and push to both Vercel and GitHub Actions at once.
- Vercel: Uses the REST API (
POST /v10/projects/{id}/env?upsert=true), so re-running upserts existing variables. - GitHub Actions: Supports both Secrets (sealed-box encrypted) and Variables (plaintext).
secret/environmentsare declared explicitly inenv-sync.yaml— no heuristics based on key names.- Values are never written to
env-sync.yaml(it is committed to git). Values are read from.env(.production). - Keys not declared in the definition are not registered (warned and skipped even if present in
.env). - Keys declared but missing from
.envare also warned and skipped.
brew install ptyhard/tap/env-syncgo install github.com/ptyhard/env-sync/cmd/env-sync@latestDownload the latest binary from GitHub Releases. Archives are provided for darwin/amd64, darwin/arm64, linux/amd64, and linux/arm64.
go build -o env-sync ./cmd/env-sync# 1. Interactively generate auth config (Vercel token / project_id / GitHub token / repo)
env-sync setup
# 2. Generate a definition file scaffold from an existing .env
env-sync init
# 3. Review the generated env-sync.yaml — check secret / environments / provider
# 4. Preview what would be registered without sending (dry-run)
VERCEL_TOKEN=xxxxx env-sync --env .env.production --dry-run
# 5. Deploy for real (prompts y/N only when updates exist)
VERCEL_TOKEN=xxxxx env-sync --env .env.productionInteractively generates an auth config file (.env-sync.config.yaml or ~/.config/env-sync/config.yaml).
# Generate project config (.env-sync.config.yaml)
env-sync setup
# Generate global config (~/.config/env-sync/config.yaml)
env-sync setup --global
# Overwrite existing file (without --force, overwrite is refused with an error)
env-sync setup --force- For each of Vercel / GitHub, prompts whether to configure, project_id, repo, and token input method
- Tokens default to the environment variable reference format
${VERCEL_TOKEN}/${GITHUB_TOKEN}(recommended — avoids writing plaintext tokens to the file) - When writing raw tokens directly or with
--global, the file is created with 0600 permissions - In non-interactive environments (no TTY), exits with an error and instructions for manual configuration
Note: It is recommended to add
.env-sync.config.yamlto.gitignoreto prevent committing this file.
Reads an existing .env and auto-generates an env-sync.yaml scaffold without values.
# Basic (generate env-sync.yaml from .env)
env-sync init
# Specify a different file
env-sync init --env .env.production
# Specify output destination
env-sync init --env .env.production --def env-sync.production.yaml
# Overwrite existing file (without --force, overwrite is refused with an error)
env-sync init --env .env.production --force- Keys with the
NEXT_PUBLIC_prefix default tosecret: false; all others default tosecret: true - This is only a scaffold — always review secrets before deploying
- No values are written (
.envvalues never leak into the yaml) - If
env-sync.yamlalready exists, overwrite is refused without--force
# Link the project (first time only — creates .vercel/project.json)
vercel link
# Issue an access token
# https://vercel.com/account/tokens# dry-run (shows planned registrations with new/update classification — values are not shown)
VERCEL_TOKEN=xxxxx env-sync --env .env.production --dry-run
# Deploy (classifies as new/update and displays — prompts y/N only when updates exist)
VERCEL_TOKEN=xxxxx env-sync --env .env.production
# Skip update confirmation (for CI, etc.)
VERCEL_TOKEN=xxxxx env-sync --env .env.production --yesBefore sending, the tool queries the provider and classifies each key as "+ KEY [new]" or "⟳ KEY [update]".
The y/N confirmation prompt only appears when there are updates (overwrites). New-only registrations proceed without confirmation.
Use --yes (-y) to skip the confirmation. In non-interactive environments (no TTY), if updates exist without --yes, the tool exits with an error for safety.
During --dry-run, new/update classification is shown if the token is set (nothing is sent).
Use --provider github to sync to GitHub Actions Secrets/Variables.
# dry-run (shows planned registrations with new/update classification — values are not shown)
GITHUB_REPO=owner/repo env-sync --provider github --env .env.production --dry-run
# Deploy (repository level — prompts only when updates exist)
GITHUB_TOKEN=xxxxx GITHUB_REPO=owner/repo env-sync --provider github --env .env.production
# Register to a named environment (specified via the environments field in env-sync.yaml)
# Write environments: [production] in the yaml to register to that environment
GITHUB_TOKEN=xxxxx env-sync --provider github --env .env.production
# Skip update confirmation (for CI, etc.)
GITHUB_TOKEN=xxxxx env-sync --provider github --yes --env .env.productionIf GITHUB_REPO is omitted, owner/repo is auto-detected from git remote origin.
defaults:
secret: true
variables:
DATABASE_URL:
secret: true # Repository-level Secret
PUBLIC_FLAG:
secret: false # Repository-level Variable
API_KEY_PROD:
secret: true
environments: [production] # Production environment Secret
FEATURE_FLAG:
secret: false
environments: [production, staging] # Production & staging VariableNote: Named environments specified in
environmentsmust be pre-created on GitHub. Specifying a non-existent environment name results in an error.
- Variables with
secret: trueare encrypted with GitHub's public key using sealed-box encryption before PUT. Plaintext is never sent. - Values are never shown in the registration list, confirmation prompt, or success/failure logs. Same for
--dry-run. - Public key length (32 bytes) is validated; invalid keys abort the operation.
- Public key cache is managed per envScope (environment), as keys differ by scope.
By specifying provider per variable, you can sync to both Vercel and GitHub Actions simultaneously from a single env-sync.yaml.
defaults:
secret: true
# Set defaults.provider to change the default sync target for all variables
# provider: github
variables:
DATABASE_URL:
secret: true
provider: vercel # Sync to Vercel only
GITHUB_ACTIONS_TOKEN:
secret: true
provider: github # Sync to GitHub Actions only
SHARED_SECRET:
secret: true
provider: [vercel, github] # Sync to both
PUBLIC_API_URL:
secret: false # No provider specified → defaults to --provider flag (vercel)# Provide both tokens to sync to Vercel and GitHub in a single run
VERCEL_TOKEN=xxxxx GITHUB_TOKEN=yyyyy env-sync --env .env.productionResolution priority (highest first): per-variable provider → defaults.provider → CLI --provider flag (default vercel)
Invalid values (anything other than vercel / github) cause an error. During --dry-run, the providers column shows the routing for each variable.
Instead of environment variables, you can manage tokens and IDs in a YAML file, eliminating the need to pass VERCEL_TOKEN=... every time.
| Type | Path |
|---|---|
| global | ~/.config/env-sync/config.yaml (respects XDG_CONFIG_HOME) |
| project | .env-sync.config.yaml (current directory) |
If neither file exists, the tool falls back to environment variables and existing fallbacks only (backward compatible).
Environment variables > project config > global config > existing fallbacks (.vercel/project.json / git remote)
Environment variables always take precedence when set.
vercel:
token: <Vercel access token>
project_id: <project ID>
team_id: <team (org) ID>
github:
token: <GitHub access token>
repo: <owner/repo format repository name>For monorepos (e.g., Turborepo) with multiple apps in a single repository, define multiple sync targets using vercel.projects / github.repos arrays. A single run distributes environment variables to all defined targets.
vercel:
token: <shared token> # Fallback when token is omitted per project
team_id: team_xxxxxxxx # Fallback when team_id is omitted per project
projects:
- name: web # Identifier for filtering with --vercel-project
project_id: prj_web
- name: admin
project_id: prj_admin
team_id: team_yyyyyyyy # Different team for this project only
token: ${ADMIN_VERCEL_TOKEN} # Different token for this project only
github:
token: <shared token> # Fallback when token is omitted per repo
repos:
- name: web
repo: myorg/web
- name: infra
repo: myorg/infra
token: ${INFRA_GH_TOKEN} # Different token for this repo only- When
token/team_idis omitted for a target, it falls back to the top-levelvercel.token/vercel.team_id/github.token(and then environment variables). tokenvalues support${VAR}/${VAR:-default}references.- When multiple targets are defined,
project_id/repois required for each (.vercel/project.json/ git remote fallback is only available for single-target configurations).
# Sync to all defined Vercel projects
env-sync --env .env.production
# Filter to a specific project / repo
env-sync --env .env.production --vercel-project admin
env-sync --provider github --env .env.production --github-repo infraAt runtime, each target displays a heading with the target project/repo and a new/update classification list. If one target fails, the remaining targets continue, and the exit code is 1 if any target failed.
Backward compatible: Without
projects/repos, the tool syncs to a single target usingvercel.project_id/github.repo(plus environment variables /.vercel/project.json/ git remote fallback) as before.
The --vercel-project flag narrows the entire run to one project, but you can also specify vercel_project on each variable in env-sync.yaml to target specific Vercel projects per variable by name (requires vercel.projects[] to be defined).
# env-sync.yaml
defaults:
secret: true
# vercel_project: web # Setting in defaults makes it the default target for all variables
variables:
WEB_API_URL: { provider: vercel, vercel_project: web } # Send to web only
ADMIN_API_URL: { provider: vercel, vercel_project: admin } # Send to admin only
SHARED_DB_URL: { provider: vercel, vercel_project: [web, admin] } # Send to both web and admin
COMMON_KEY: { provider: vercel } # No vercel_project → all projects (backward compatible)- Resolution priority: Per-variable
vercel_project>defaults.vercel_project. Variables without it are sent to all resolved Vercel targets (backward compatible). - Combined with CLI
--vercel-project(AND logic): The CLI narrows the target set first, then per-variablevercel_projectfurther filters within that set. - The
nameinvercel_projectrefers tovercel.projects[].name. Specifying a non-existentnamecauses an error. vercel_projectcannot be used in single-resolution mode (withoutvercel.projects[]defined) — it will cause an error.
# Create global config (~/.config/env-sync/config.yaml)
mkdir -p ~/.config/env-sync
cat > ~/.config/env-sync/config.yaml <<'EOF'
vercel:
token: your-vercel-token
github:
token: your-github-token
EOF
chmod 0600 ~/.config/env-sync/config.yaml
# Override with project-specific IDs in project config
cat > .env-sync.config.yaml <<'EOF'
vercel:
project_id: prj_xxxxxxxx
team_id: team_xxxxxxxx
github:
repo: myorg/myrepo
EOF
# Run without environment variables
env-sync --env .env.productionWith a single env-sync.yaml definition file, you can target different environments at sync time using --environments.
# env-sync.yaml
variables:
DATABASE_URL:
secret: true
environments: [production, staging, preview]
API_KEY:
secret: true
environments: [production]
DEBUG_MODE:
secret: false
environments: [staging, preview]# Sync only to production (variables with no 'production' in environments are skipped)
env-sync --env .env.production --def env-sync.yaml --environments production
# Sync only to staging and preview
env-sync --env .env.staging --def env-sync.yaml --environments staging,preview
# Prune + limit deletion scope to staging only
env-sync --env .env.staging --def env-sync.yaml --environments staging --prune- Variables whose declared
environmentshave no overlap with--environmentsare skipped (warning to stderr). - Variables that have no
environmentsdeclared in the definition file are also skipped when--environmentsis specified (they have no declared environments to intersect with). - If all variables are skipped, no changes are sent to any provider and prune is also skipped — the tool exits successfully with code 0.
--environmentsmust contain names that appear inenvironmentsfields of the definition file, or the standard valuesproduction/preview/development. An invalid name causes an error and exits with code 1.- Environment names are case-sensitive — they must match the declaration in
env-sync.yamlexactly. - GCP: GCP Secrets have no per-environment scope. Variables that pass the filter will still have the
managed-by=env-synclabel applied/updated as a side effect of syncing. When--pruneis combined with--environments, GCP prune scope is not narrowed (allmanaged-by=env-syncsecrets are considered for pruning).
Config values can include environment variable references (useful for avoiding plaintext tokens in config files).
| Syntax | Behavior |
|---|---|
${VAR} |
Expands to the value of environment variable VAR. Errors if VAR is unset (including empty) |
${VAR:-default} |
Uses VAR's value if set; otherwise uses default |
vercel:
token: ${MY_VERCEL_TOKEN} # Errors if MY_VERCEL_TOKEN is unset
project_id: ${V_PID:-prj_default} # Falls back to prj_default if V_PID is unset
github:
token: ${GH_TOKEN}
repo: ${GH_REPO:-myorg/myrepo}Note: The existing priority order "environment variables > project config > global config" is maintained. Even with
token: ${VERCEL_TOKEN}in config, if theVERCEL_TOKENenvironment variable is set, the env var takes direct precedence — effectively the same value. This feature is most useful with aliased variables (e.g.,${MY_VERCEL_TOKEN}).
Security: If the global config contains tokens and the file permissions are not
0600, a warning is printed to stderr at runtime. Fix withchmod 0600 ~/.config/env-sync/config.yaml. Encrypted storage and keychain integration are out of scope (future consideration).
defaults:
secret: true
variables:
NEXT_PUBLIC_FIREBASE_API_KEY: { secret: false }
DATABASE_URL: { secret: true, environments: [production] }
API_SECRET: { secret: true }| Field | Type | Description |
|---|---|---|
prune |
bool (top-level) |
true: delete remote variables not declared in variables during sync (default false). Can also be enabled with the --prune flag. See Pruning Undefined Variables |
prune_exclude |
[]string (top-level) |
Glob patterns of key names to never delete during prune (e.g. [BLOB_*, SENTRY_DSN]). Case-insensitive |
secret |
bool |
true (default): register as secret / false: register as plaintext |
environments |
[]string |
Array of target environments. Inherits from defaults.environments if omitted |
provider |
string or []string |
Sync target provider. Falls back to defaults.provider → CLI --provider flag if omitted |
vercel_project |
string or []string |
Target Vercel project name(s) (vercel.projects[].name). Falls back to defaults.vercel_project → all Vercel targets. See Per-Variable Vercel Project Targeting for details |
secret |
Vercel type | Description |
|---|---|---|
true |
sensitive |
For secrets — values cannot be read after saving |
false |
plain |
Plaintext without encryption |
environments accepts production / preview / development. Defaults to production and preview when omitted.
environments also accepts custom environment slugs (e.g. staging). Custom environments must be pre-created in the Vercel project; a non-existent slug results in an error. Standard and custom values can be mixed (e.g. [production, staging]).
secret |
Target | Description |
|---|---|---|
true |
GitHub Actions Secrets | Sealed-box encrypted. Values cannot be viewed after registration |
false |
GitHub Actions Variables | Plaintext. Values are visible in the GitHub UI |
Specifying a named environment in environments registers to that environment scope. When omitted, registers at the repository level.
Note: GitHub named environments must be pre-created. Specifying a non-existent environment name results in an error.
When prune: true is set at the top level of env-sync.yaml (or the --prune flag is passed), variables that exist on the remote but are not declared in variables are deleted during sync.
prune: true
prune_exclude:
- BLOB_* # e.g. keep tokens provisioned by Vercel Blob Store
- SENTRY_DSN
variables:
DATABASE_URL: { secret: true }Safety behavior:
- Deletion targets are listed before sending, and a
y/Nconfirmation is always required when any deletion is planned (--yesskips it;--dry-runonly shows the plan). - Keys declared in
variablesare always kept — even if they have no value in the.envfile. - Keys matching
prune_excludeglob patterns are kept (case-insensitive). - Vercel: system variables and integration-provisioned variables (those with a
configurationId, e.g. created by Blob Store or Marketplace integrations) are automatically excluded. - GitHub: Actions Secrets and Variables are pruned at the repository level and in named environments that appear in the definition file.
- GCP: only Secrets labeled
managed-by=env-syncare pruned. env-sync adds this label automatically when syncing, so Secrets created by anything else are never touched.
| Item | Required | Description |
|---|---|---|
--provider <name> |
– | Sync target (default vercel). Currently vercel / github are available |
--vercel-project <name> |
– | Filter sync to a single vercel.projects[].name from config. All defined projects if unspecified |
--github-repo <name> |
– | Filter sync to a single github.repos[].name from config. All defined repos if unspecified |
--env <file> |
– | Env file to read values from (default .env) |
--def <file> |
– | Definition YAML (default env-sync.yaml) |
--dry-run |
– | Show planned registrations with new/update classification without sending |
--yes / -y |
– | Skip confirmation when updates (overwrites) exist |
--prune |
– | Delete remote variables not in the definition file (also enabled by prune: true in the definition file) |
--environments <list> |
– | Comma-separated list of environments to write to (e.g. staging,preview). Intersects with each variable's declared environments; variables with no overlap are skipped. When combined with --prune, deletion scope is also limited to the specified environments |
--force |
– | Overwrite existing def file during init |
VERCEL_TOKEN |
Yes (Vercel) | Vercel access token (not required for dry-run) |
VERCEL_PROJECT_ID |
Conditional (Vercel) | Project ID. Auto-detected from config file or .vercel/project.json if unset |
VERCEL_TEAM_ID |
– (Vercel) | Team (org) ID. Falls back to config file or .vercel/project.json orgId |
GITHUB_TOKEN |
Yes (GitHub) | GitHub access token (not required for dry-run) |
GITHUB_REPO |
– (GitHub) | owner/repo format. Auto-detected from config file or git remote origin if unset |
--lang <code> |
– | Display language (en / ja). Default: en |
ENV_SYNC_LANG |
– | Display language code (en / ja). Lower priority than --lang |
CLI messages can be displayed in English (default) or Japanese.
--lang <code>flagENV_SYNC_LANGenvironment variablelanguage:field in config file (.env-sync.config.yaml/~/.config/env-sync/config.yaml)- Default:
en
# .env-sync.config.yaml
language: ja
vercel:
token: ${VERCEL_TOKEN}
project_id: <project ID>| Code | Language |
|---|---|
en |
English (default) |
ja |
Japanese |
Invalid or unsupported codes fall back to en.