opdotenv
Load environment variables from 1Password using the op CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.
Sponsored by Kisko Labs.
Installation
Add to your Gemfile:
gem "opdotenv"Requirements
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.
Rails
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 JSON -
op://Vault/Item Name/production.json→ uses fieldproduction.jsonas JSON -
op://Vault/Item Name/.env.development→ uses field.env.developmentas dotenv
1Password Connect
Rails.application.configure do
config.opdotenv.connect_url = "https://connect.example.com"
config.opdotenv.connect_token = Rails.application.credentials.dig(:op_connect, :token)
endConfigure op CLI path
If 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/opDisable automatic loading
Rails.application.configure do
config.opdotenv.auto_load = false
end
# Load manually when needed
Opdotenv::Loader.load("op://Vault/Item")Standalone usage
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)Anyway Config integration
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
}
endConditional Loading (Recommended for Security)
For 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.
Loading All Fields from an Item
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
Using with dotenv
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 to 1Password
CLI
# 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"Ruby API
# 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"}
)Supported formats
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 JSON -
op://Vault/Item Name/production.json→ loads fieldproduction.jsonas JSON -
op://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.
Security
Environment Recommendation
⚠️ This gem is recommended for development and test environments only.
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
Security Considerations
-
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.
Code Locations for Security Review
For security-sensitive applications, developers should review where this gem reads and writes data from 1Password. The following locations handle secret data:
Reading Secrets (OpClient - CLI mode)
-
lib/opdotenv/op_client.rb:-
read(path)- Executesop readcommand to fetch a single field value -
item_get(item, vault:)- Executesop item getcommand to fetch all item data as JSON -
capture(args)- Executes shell commands viaIO.popen(array arguments, no shell interpretation)
-
Reading Secrets (ConnectApiClient - API mode)
-
lib/opdotenv/connect_api_client.rb:-
read(path)- Makes HTTP GET request to fetch field or notesPlain content -
item_get(item_title, vault:)- Searches and fetches item data via API -
get_item(vault_name, item_title)- Fetches full item details including all fields -
item_by_title_in_vault(vault_id, item_title)- Lists items and fetches by title -
list_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 method -
find_field(item, field_name)- Searches item fields by label, ID, or purpose
-
Main Entry Points
-
lib/opdotenv/loader.rb:-
load(path, ...)- Main entry point that orchestrates secret fetching -
load_field(client, path, field_name, field_type)- Loads and parses a single field -
load_all_fields(client, path)- Loads all fields from an item (skips notesPlain) -
merge_into_env(env, hash, overwrite:)- Writes secrets to environment hash
-
Rails Integration
-
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
-
Anyway Config Integration
-
lib/opdotenv/anyway_loader.rb:-
Loader#call(...)- Loads secrets for Anyway Config classes - Uses
Opdotenv::Loader.load()internally
-
Parsing and Processing
-
lib/opdotenv/parsers/dotenv_parser.rb- Parses dotenv format strings -
lib/opdotenv/parsers/json_parser.rb- Parses and flattens JSON structures -
lib/opdotenv/parsers/yaml_parser.rb- Parses YAML (safe_load with aliases: false)
Data Flow
- 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).
Development
bundle install
bundle exec rspec
bundle exec rbs validate
bundle exec standardrb --fixContributing
Bug 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
Security
If you discover a security vulnerability, please report it responsibly. See SECURITY.md for details.
License
The gem is available as open source under the terms of the MIT License.