push
Write 1Password values to every target of the selected services.
env-sync push (--service <name> | --all) --env <env> [--apply] [--yes]
| Flag | Meaning |
|---|---|
--service <name> / --all | Which services to push. Exactly one is required. |
--env <env> | Required. The environment to push. |
--apply | Actually write. Without it, push is a dry run. |
--yes | Skip the confirmation gate (see below). |
Dry run (the default)
Without --apply, push writes nothing. It prints what would change:
$ env-sync push --service web --env dev
[web] Dry-run (no --apply given) — showing what would change:
[web] vercel:myapp-web[development,preview]: drift on [API_KEY]
[web] dotenv:apps/web/.env.local: no drift
Like diff, a dry run exits 1 if it finds drift.
Apply
Targets are written one after another. The first failure stops the remaining targets of that service. Other services still run.
$ env-sync push --service api --env dev --apply
[api] Succeeded: ssm:/myapp/api, github-actions:myorg/myapp:dev
Read-only targets are never written. CapRover targets are read-only by default:
[api] SKIPPED (read_only): caprover:myapp-api@captain.example.com
Confirmation gate
This safety net is on by default. No flag turns it on. A push --apply runs without a prompt only if both of these are true:
--envis dev-tier:dev,development,test, ortesting(case-insensitive).- Every non-read-only target is structurally provable as scoped to that environment.
| Platform | Auto-approvable when |
|---|---|
ssm | Always. The live path is <path>/<env>, derived from --env itself. |
github-actions | Only if github_env is set and is itself dev-tier. |
vercel, render, dotenv, caprover | Never. No manifest field proves where the live write lands. |
If the gate applies, env-sync explains why:
$ env-sync push --service web --env dev --apply
Protected push:
[web] vercel:myapp-web[development,preview] is not structurally scoped to a dev-tier env
[web] dotenv:apps/web/.env.local is not structurally scoped to a dev-tier env
Type 'y' to continue:
- TTY: only an explicit
y/yesproceeds. Any other answer aborts withAborted: nothing was written.(exit1). - No TTY (CI, pipes): fails closed immediately with exit
1. It never waits for input. --yes: proceeds without asking.
With --all, the check runs across every target of every selected service before any write, and it asks at most once. A safe service is never applied before an unsafe service fails closed, so a partial apply cannot happen.
The Architecture page explains why Vercel is never auto-approved, even for vercel_targets: [development].
Audit log
Every push --apply attempt appends one JSON line per target to .env-sync/audit-YYYY-MM-DD.jsonl in the current directory. The file mode is 0600. Secret values are redacted from every field.
{"ts":"2026-09-28T12:00:00.000Z","service":"api","env":"dev","target":"ssm:/myapp/api","outcome":"success","actor":"alice"}
outcome is success, failure, or aborted (the gate declined). Add .env-sync/ to your .gitignore.