Load environment variables from 1Password using the op CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.
Sponsored by Kisko Labs.
Add to your Gemfile:
gem "opdotenv"Choose one:
- 1Password CLI (
op) - must be installed and authenticated (op signin)- By default,
opis expected to be in yourPATH - You can configure a custom path via
OP_CLI_PATHorOPDOTENV_CLI_PATHenvironment variables, or via Rails config (see below)
- By default,
- 1Password Connect Server - set
OP_CONNECT_URLandOP_CONNECT_TOKENenvironment variables
Ruby 2.7+ supported.
Configure in config/application.rb or environment-specific files:
Rails.application.configure do
config.opdotenv.sources = [
"op://Vault/.env.development", # dotenv format (inferred)
"op://Vault/config.json", # json format (inferred from .json extension)
"op://Vault/App" # all fields without parsing
]
endFormat is automatically inferred from item name or field name:
.env.*→ dotenv format*.json→ JSON format*.yamlor*.yml→ YAML format- Other items → load all fields without parsing
You can also specify the field name with extension in the path:
op://Vault/Item Name/config.json→ uses fieldconfig.jsonas JSONop://Vault/Item Name/production.json→ uses fieldproduction.jsonas JSONop://Vault/Item Name/.env.development→ uses field.env.developmentas dotenv
Rails.application.configure do
config.opdotenv.connect_url = "https://connect.example.com"
config.opdotenv.connect_token = Rails.application.credentials.dig(:op_connect, :token)
endIf your op CLI command is not in your PATH or you want to use a custom path:
Rails.application.configure do
config.opdotenv.cli_path = "/usr/local/bin/op"
endAlternatively, you can set the OP_CLI_PATH or OPDOTENV_CLI_PATH environment variable:
export OP_CLI_PATH=/usr/local/bin/opRails.application.configure do
config.opdotenv.auto_load = false
end
# Load manually when needed
Opdotenv::Loader.load("op://Vault/Item")require "opdotenv"
# Load from dotenv format (format inferred from item name)
Opdotenv::Loader.load("op://Vault/.env.development")
# Load from JSON format (any item name ending with .json)
Opdotenv::Loader.load("op://Vault/config.json")
Opdotenv::Loader.load("op://Vault/production.json")
# Load from field with extension in path
Opdotenv::Loader.load("op://Vault/Item Name/config.json")
# Load all fields from an item
Opdotenv::Loader.load("op://Vault/App")
# Don't overwrite existing ENV values
Opdotenv::Loader.load("op://Vault/Item", overwrite: false)Automatically registers when anyway_config is available:
class AppConfig < Anyway::Config
attr_config :api_key, :api_secret
# Format is inferred from item name
loader_options opdotenv: {
path: "op://Vault/.env.development" # dotenv format inferred
}
end
# Or load all fields from an item
class DatabaseConfig < Anyway::Config
attr_config :url, :username, :password
loader_options opdotenv: {
path: "op://Vault/Database" # all fields loaded
}
endFor better security, only load from 1Password in development/test environments:
class TestConfig < Anyway::Config
config_name :test
attr_config :enabled, :sample
# Only load from 1Password in local/development environments
if Rails.env.local?
loader_options opdotenv: {
path: "op://Employee/.env.test"
}
end
endThis ensures that production environments won't attempt to load secrets from 1Password, aligning with the production recommendations.
When loading all fields from a 1Password item (not a parsed format), field names are automatically normalized to match the env_prefix:
class TestConfig < Anyway::Config
config_name :test
attr_config :enabled, :sample
loader_options opdotenv: {
path: "op://Employee/TestConfig" # Loads all fields from item
}
endField name matching (strict with case-insensitive prefix):
- Fields in 1Password must be prefixed with the
env_prefix(e.g.,TEST_forconfig_name :test) - Matching is case-insensitive:
TEST_ENABLED,test_enabled,Test_Enabledall work - After prefix stripping,
TEST_ENABLEDbecomesenabled(matchingattr_config :enabled) - Fields without the prefix (e.g.,
enabled,ENABLED) are ignored and logged as unmatched
Debugging field matching:
- Enable debug logging by setting
OPDOTENV_DEBUG=true - Check Rails logs for messages like:
[opdotenv] Available fields from 1Password: enabled, ENABLED, sample, SAMPLE [opdotenv] Matched fields for TEST: enabled, sample [opdotenv] Unmatched fields (must be prefixed with TEST_, case-insensitive): other_field
Load order determines which values take precedence:
require "dotenv"
require "opdotenv"
# Load local files first, then augment from 1Password (1Password values override by default)
Dotenv.load(".env", ".env.development")
Opdotenv::Loader.load("op://Vault/.env.development")
# Or load from 1Password first, then local files (local values override)
Opdotenv::Loader.load("op://Vault/.env.development", overwrite: false)
Dotenv.load(".env", ".env.development")# Export .env file (format inferred from path)
opdotenv export --path "op://Vault/.env.development" --file .env.development
# Export to item fields
opdotenv export --path "op://Vault/App" --file .env
# Read and print (format inferred from path)
opdotenv read --path "op://Vault/.env.development"# Export to Secure Note (format inferred from path)
Opdotenv::Exporter.export(
path: "op://Vault/.env.development",
data: {"API_KEY" => "secret"}
)
# Export to item fields
Opdotenv::Exporter.export(
path: "op://Vault/App",
data: {"API_KEY" => "secret", "API_SECRET" => "another"}
)Format is automatically inferred from item name or field name:
.env.*→ dotenv format (KEY=VALUE)*.json→ JSON format (nested structures flattened with underscores)*.yamlor*.yml→ YAML format (nested structures flattened with underscores)- Other items → load all fields without parsing
Field names can be specified with extensions in the path:
op://Vault/Item Name/config.json→ loads fieldconfig.jsonas JSONop://Vault/Item Name/production.json→ loads fieldproduction.jsonas JSONop://Vault/Item Name/.env.development→ loads field.env.developmentas dotenv
For advanced usage, you can explicitly specify field_name and field_type in the API.
For production environments, we recommend using dedicated secret management solutions that integrate with your infrastructure:
- Kubernetes: Use Kubernetes Secrets or External Secrets Operator with providers like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault
- AWS: Use AWS Secrets Manager or AWS Systems Manager Parameter Store
- Azure: Use Azure Key Vault
- GCP: Use Google Secret Manager
Provision secrets through infrastructure-as-code tools:
- Helm (Kubernetes): Use
helm secretsor external secrets operator - Terraform: Use
aws_secretsmanager_secret,azurerm_key_vault_secret, orgoogle_secret_manager_secret - Bicep (Azure): Use
Microsoft.KeyVault/vaultsresources
These solutions provide:
- Better audit trails and access controls
- Integration with IAM/RBAC systems
- Automatic secret rotation
- Compliance with security standards
- No dependency on external CLI tools or API servers
- CLI mode: Secrets fetched via authenticated
opCLI session. - Connect API mode: Secrets fetched via HTTPS. Ensure tokens are secure.
- The library does not persist secrets in memory or on disk.
- Always verify 1Password CLI or Connect server is up to date and authenticated.
For security-sensitive applications, developers should review where this gem reads and writes data from 1Password. The following locations handle secret data:
lib/opdotenv/op_client.rb:read(path)- Executesop readcommand to fetch a single field valueitem_get(item, vault:)- Executesop item getcommand to fetch all item data as JSONcapture(args)- Executes shell commands viaIO.popen(array arguments, no shell interpretation)
lib/opdotenv/connect_api_client.rb:read(path)- Makes HTTP GET request to fetch field or notesPlain contentitem_get(item_title, vault:)- Searches and fetches item data via APIget_item(vault_name, item_title)- Fetches full item details including all fieldsitem_by_title_in_vault(vault_id, item_title)- Lists items and fetches by titlelist_vaults()- Lists all accessible vaults (usesapi_request(:get, "/v1/vaults"))vault_name_to_id(vault_name)- Resolves vault names to IDs (cached)api_request(method, path, body)- All HTTP requests go through this methodfind_field(item, field_name)- Searches item fields by label, ID, or purpose
lib/opdotenv/loader.rb:load(path, ...)- Main entry point that orchestrates secret fetchingload_field(client, path, field_name, field_type)- Loads and parses a single fieldload_all_fields(client, path)- Loads all fields from an item (skips notesPlain)merge_into_env(env, hash, overwrite:)- Writes secrets to environment hash
lib/opdotenv/railtie.rb:initializer "opdotenv.load"- Automatically loads secrets during Rails initialization- Reads from
config.opdotenv.sourcesarray - Sets
OP_CONNECT_URLandOP_CONNECT_TOKENfrom Rails config if provided
lib/opdotenv/anyway_loader.rb:Loader#call(...)- Loads secrets for Anyway Config classes- Uses
Opdotenv::Loader.load()internally
lib/opdotenv/parsers/dotenv_parser.rb- Parses dotenv format stringslib/opdotenv/parsers/json_parser.rb- Parses and flattens JSON structureslib/opdotenv/parsers/yaml_parser.rb- Parses YAML (safe_load with aliases: false)
- Configuration → Rails config or direct API calls
- Path Parsing →
SourceParser.parse()extracts vault/item/field from path - Client Selection →
ClientFactory.create()chooses CLI or API client - Secret Fetching → Client reads from 1Password (CLI command or HTTP request)
- Parsing → Format-specific parser converts to key-value pairs
- Environment Merge → Secrets merged into
ENVor provided hash
All secret data flows through these code paths. No secrets are persisted to disk or logged (except explicit error messages).
bundle install
bundle exec rspec
bundle exec rbs validate
bundle exec standardrb --fixBug reports and pull requests are welcome on GitHub at https://github.com/amkisko/opdotenv.rb.
Contribution policy:
- New features are not necessarily added to the gem
- Pull request should have test coverage for affected parts
- Pull request should have changelog entry
Review policy:
- It might take up to 2 calendar weeks to review and merge critical fixes
- It might take up to 6 calendar months to review and merge pull request
- It might take up to 1 calendar year to review an issue
If you discover a security vulnerability, please report it responsibly. See SECURITY.md for details.
The gem is available as open source under the terms of the MIT License.