🔐 env2op ⇄ op2env
Push .env files to 1Password and pull them back with two simple commands.



Installation
Homebrew (macOS/Linux)
brew tap tolgamorf/tap
brew install env2op-cli
# or in a single command:
brew install tolgamorf/tap/env2op-cli
Scoop (Windows)
scoop bucket add tolgamorf https://github.com/tolgamorf/scoop-bucket
scoop install env2op-cli
Chocolatey (Windows)
choco install env2op-cli
WinGet (Windows)
winget install tolgamorf.env2op-cli
Package Managers (macOS/Linux/Windows)
Global installation
# Using bun
bun add -g @tolgamorf/env2op-cli
# Using npm
npm install -g @tolgamorf/env2op-cli
# Using pnpm
pnpm add -g @tolgamorf/env2op-cli
Running directly
# Using bun
bunx @tolgamorf/env2op-cli .env Personal "MyApp"
# Using npm
npx @tolgamorf/env2op-cli .env Personal "MyApp"
# Using pnpm
pnpm dlx @tolgamorf/env2op-cli .env Personal "MyApp"
Prerequisites
Commands
This package provides two commands:
| Command | Description |
|---|
env2op | Push .env to 1Password, generate .env.tpl template |
op2env | Pull secrets from 1Password using .env.tpl template |
env2op (Push)
Push environment variables to 1Password and generate a template file.
env2op [options]
A variable with an empty value (KEY=) is written into the template as KEY=, not as a
reference: 1Password stores no empty field, and an empty value is not a secret. op2env writes it
back unchanged, so "empty" survives the round trip. That matters where the file overrides another,
as .env.development.local does .env: there KEY= and a missing KEY mean different things.
A quoted value keeps its quotes in the template (KEY="op://…"), so op2env writes it back quoted.
A value such as "a # b" or " padded " then reads the same on the next push. Inline comments
(KEY=value # note) are kept too; a reference followed by one is always quoted, so op2env writes
that line back as KEY="value" # note.
Examples for env2op
# Basic usage - creates a Secure Note and generates .env.tpl
env2op .env.production Personal "MyApp - Production"
# Custom output path for template
env2op .env Personal "MyApp" -o secrets.tpl
# Preview what would happen without making changes
env2op .env.production Personal "MyApp" --dry-run
# Store all fields as password type (hidden in 1Password)
env2op .env.production Personal "MyApp" --secret=password
# Hide only the fields whose name or value looks secret (API_KEY, DB_PASSWORD, ...)
env2op .env.production Personal "MyApp" --secret=auto
# Skip confirmation prompts (useful for scripts/CI)
env2op .env.production Personal "MyApp" -f
Options for env2op
| Flag | Description |
|---|
-o, --output | Output template path (default: .tpl) |
-f, --force | Skip confirmation prompts |
--dry-run | Preview actions without executing |
--secret= | Field type: text (default), password, or auto |
--verbose | Show op CLI output (item values are never printed) |
--update | Check for and install updates |
-v, --version | Show version |
-h, --help | Show help |
op2env (Pull)
Pull secrets from 1Password to generate a .env file.
op2env [options]
Examples for op2env
# Basic usage - generates .env from .env.tpl
op2env .env.tpl
# Custom output path
op2env .env.tpl -o .env.local
# Preview without making changes
op2env .env.tpl --dry-run
# Overwrite existing .env without prompting
op2env .env.tpl -f
Options for op2env
| Flag | Description |
|---|
-o, --output | Output .env path (default: template without .tpl) |
-f, --force | Overwrite without prompting |
--dry-run | Preview actions without executing |
--verbose | Show op CLI output |
--update | Check for and install updates |
-v, --version | Show version |
-h, --help | Show help |
Both commands check for a new version at most once a day and mention it when one is out. Set
ENV2OP_NO_UPDATE_CHECK=1 to turn that off (no request, no notice), for example where env2op is
bundled at a pinned version. --update still works.
Exit codes
| Code | Meaning |
|---|
0 | Done: the push or pull completed |
1 | Failed |
2 | Declined: a confirmation prompt was answered No, or there was no terminal to answer it. Nothing was written. Pass -f/--force to skip the prompts in scripts |
130 | Cancelled: Escape or Ctrl-C at a prompt (nothing written), or Ctrl-C while the command runs (it stops at once; an op call already under way may still complete) |
A script running env2op over several files can skip a file on 2 and stop the whole run on 130.
SIGTERM ends the command with 143.
How It Works
- env2op parses your
.env file, creates a 1Password Secure Note, and generates a .tpl template
- op2env reads the template and pulls current values from 1Password to create a
.env file
You can also use the op run command to run processes with secrets injected:
op run --env-file .env.tpl -- npm start
Field Types
By default, all fields are stored as text type (visible in 1Password). Use --secret to change that:
| Value | Behaviour |
|---|
--secret=text | All fields are text (visible). The default. |
--secret=password | All fields are password (hidden by default, revealed on click). |
--secret=auto | Fields whose name or value looks secret are stored as password; the rest as text. See below. |
Bare --secret is accepted as a shorthand for --secret=password. A type is given only with =:
--secret auto is the bare flag followed by an argument.
--secret=auto hides a field when either of these is true:
- Its name has a secret-looking part. Names are split into parts at
_ and at camelCase, so
API_KEY, apiKey, DB_PASS, SENTRY_DSN, SSH_PRIVATE_KEY and ACCESSTOKEN are hidden,
while MONKEY_MODE, KEYBOARD_LAYOUT and PASSENGER_COUNT are not.
- Its value is a URL with a password in it, such as
DATABASE_URL=postgres://app:s3cret@db/app.
DATABASE_URL=postgres://localhost/app stays visible.
Hidden or not, every field is encrypted in 1Password; the type only decides whether it shows on screen.
Use --dry-run to see which fields would be hidden.
Example
Given this .env file:
DATABASE_URL=postgres://localhost/myapp
API_KEY=sk-1234567890
DEBUG=true
Running:
env2op .env Personal "MyApp Secrets"
Creates a 1Password Secure Note with fields:
DATABASE_URL (text)
API_KEY (text)
DEBUG (text)
And generates .env.tpl with UUID-based references (avoids naming conflicts):
DATABASE_URL=op://abc123vaultid/xyz789itemid/def456fieldid
API_KEY=op://abc123vaultid/xyz789itemid/ghi012fieldid
DEBUG=op://abc123vaultid/xyz789itemid/jkl345fieldid
Programmatic Usage
You can also use env2op as a library:
import { parseEnvFile, createSecureNote, generateTemplateContent } from "@tolgamorf/env2op-cli";
const result = await parseEnvFile(".env");
console.log(result.variables);
The parser and the comment masking are exported on their own, and importing the package has no
side effects (no CLI start, no update check, no prompts):
| Export | What it does |
|---|
parseEnvText(content) | Synchronous parseEnvFile for text you already hold: strips a BOM and any env2op header, then parses each line |
parseValue(raw) | Parses the text after KEY= into { value, quote?, commentStart? } (ParsedValue) |
maskSecretRefsInComments(template) | Masks op:// in full-line and inline comments so op inject does not read them as references; returns a MaskedTemplate |
unmaskSecretRefs(output, mask) | Restores the masked text in op inject's output |
The parser follows dotenv's quoting rules. A quoted value ends at the first matching quote followed
only by whitespace or a comment, so "{"x":1}" is {"x":1}. Unquoted, # starts a comment only
after whitespace, so #336699 is a value.
License
MIT