{"id":32792144,"url":"https://github.com/amkisko/opdotenv.rb","last_synced_at":"2026-05-17T19:06:09.294Z","repository":{"id":322582232,"uuid":"1089552018","full_name":"amkisko/opdotenv.rb","owner":"amkisko","description":"Load environment variables from 1Password using the `op` CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.","archived":false,"fork":false,"pushed_at":"2026-02-10T15:07:53.000Z","size":189,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-10T19:44:06.703Z","etag":null,"topics":["1password","dotenv","rails","ruby","secrets"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/opdotenv","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/amkisko.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":"GOVERNANCE.md","roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"amkisko"}},"created_at":"2025-11-04T13:53:09.000Z","updated_at":"2026-02-10T15:08:23.000Z","dependencies_parsed_at":null,"dependency_job_id":"3db92cc6-4537-44f2-a3c4-1e82400538fe","html_url":"https://github.com/amkisko/opdotenv.rb","commit_stats":null,"previous_names":["amkisko/opdotenv.rb"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/amkisko/opdotenv.rb","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amkisko%2Fopdotenv.rb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amkisko%2Fopdotenv.rb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amkisko%2Fopdotenv.rb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amkisko%2Fopdotenv.rb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/amkisko","download_url":"https://codeload.github.com/amkisko/opdotenv.rb/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amkisko%2Fopdotenv.rb/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33151625,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-17T09:28:26.183Z","status":"ssl_error","status_checked_at":"2026-05-17T09:27:52.702Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["1password","dotenv","rails","ruby","secrets"],"created_at":"2025-11-05T15:00:53.469Z","updated_at":"2026-05-17T19:06:09.288Z","avatar_url":"https://github.com/amkisko.png","language":"Ruby","funding_links":["https://github.com/sponsors/amkisko"],"categories":[],"sub_categories":[],"readme":"# opdotenv\n\n[![Gem Version](https://badge.fury.io/rb/opdotenv.svg?v=1.0.3)](https://badge.fury.io/rb/opdotenv) [![Test Status](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/opdotenv.rb/graph/badge.svg?token=U4FMVZGO8R)](https://codecov.io/gh/amkisko/opdotenv.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_opdotenv.rb\u0026metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_opdotenv.rb)\n\nLoad environment variables from 1Password using the `op` CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.\n\nSponsored by [Kisko Labs](https://www.kiskolabs.com).\n\n\u003ca href=\"https://www.kiskolabs.com\"\u003e\n  \u003cimg src=\"kisko.svg\" width=\"200\" alt=\"Sponsored by Kisko Labs\" /\u003e\n\u003c/a\u003e\n\n## Installation\n\nAdd to your Gemfile:\n\n```ruby\ngem \"opdotenv\"\n```\n\n## Requirements\n\nChoose one:\n- **1Password CLI** (`op`) - must be installed and authenticated (`op signin`)\n  - By default, `op` is expected to be in your `PATH`\n  - You can configure a custom path via `OP_CLI_PATH` or `OPDOTENV_CLI_PATH` environment variables, or via Rails config (see below)\n- **1Password Connect Server** - set `OP_CONNECT_URL` and `OP_CONNECT_TOKEN` environment variables\n\nRuby 2.7+ supported.\n\n## Rails\n\nConfigure in `config/application.rb` or environment-specific files:\n\n```ruby\nRails.application.configure do\n  config.opdotenv.sources = [\n    \"op://Vault/.env.development\",  # dotenv format (inferred)\n    \"op://Vault/config.json\",        # json format (inferred from .json extension)\n    \"op://Vault/App\"                 # all fields without parsing\n  ]\nend\n```\n\nFormat is automatically inferred from item name or field name:\n- `.env.*` → dotenv format\n- `*.json` → JSON format\n- `*.yaml` or `*.yml` → YAML format\n- Other items → load all fields without parsing\n\nYou can also specify the field name with extension in the path:\n- `op://Vault/Item Name/config.json` → uses field `config.json` as JSON\n- `op://Vault/Item Name/production.json` → uses field `production.json` as JSON\n- `op://Vault/Item Name/.env.development` → uses field `.env.development` as dotenv\n\n### 1Password Connect\n\n```ruby\nRails.application.configure do\n  config.opdotenv.connect_url = \"https://connect.example.com\"\n  config.opdotenv.connect_token = Rails.application.credentials.dig(:op_connect, :token)\nend\n```\n\n### Configure op CLI path\n\nIf your `op` CLI command is not in your `PATH` or you want to use a custom path:\n\n```ruby\nRails.application.configure do\n  config.opdotenv.cli_path = \"/usr/local/bin/op\"\nend\n```\n\nAlternatively, you can set the `OP_CLI_PATH` or `OPDOTENV_CLI_PATH` environment variable:\n\n```bash\nexport OP_CLI_PATH=/usr/local/bin/op\n```\n\n### Disable automatic loading\n\n```ruby\nRails.application.configure do\n  config.opdotenv.auto_load = false\nend\n\n# Load manually when needed\nOpdotenv::Loader.load(\"op://Vault/Item\")\n```\n\n## Standalone usage\n\n```ruby\nrequire \"opdotenv\"\n\n# Load from dotenv format (format inferred from item name)\nOpdotenv::Loader.load(\"op://Vault/.env.development\")\n\n# Load from JSON format (any item name ending with .json)\nOpdotenv::Loader.load(\"op://Vault/config.json\")\nOpdotenv::Loader.load(\"op://Vault/production.json\")\n\n# Load from field with extension in path\nOpdotenv::Loader.load(\"op://Vault/Item Name/config.json\")\n\n# Load all fields from an item\nOpdotenv::Loader.load(\"op://Vault/App\")\n\n# Don't overwrite existing ENV values\nOpdotenv::Loader.load(\"op://Vault/Item\", overwrite: false)\n```\n\n## Anyway Config integration\n\nAutomatically registers when `anyway_config` is available:\n\n```ruby\nclass AppConfig \u003c Anyway::Config\n  attr_config :api_key, :api_secret\n\n  # Format is inferred from item name\n  loader_options opdotenv: {\n    path: \"op://Vault/.env.development\"  # dotenv format inferred\n  }\nend\n\n# Or load all fields from an item\nclass DatabaseConfig \u003c Anyway::Config\n  attr_config :url, :username, :password\n\n  loader_options opdotenv: {\n    path: \"op://Vault/Database\"  # all fields loaded\n  }\nend\n```\n\n### Conditional Loading (Recommended for Security)\n\nFor better security, only load from 1Password in development/test environments:\n\n```ruby\nclass TestConfig \u003c Anyway::Config\n  config_name :test\n  attr_config :enabled, :sample\n\n  # Only load from 1Password in local/development environments\n  if Rails.env.local?\n    loader_options opdotenv: {\n      path: \"op://Employee/.env.test\"\n    }\n  end\nend\n```\n\nThis ensures that production environments won't attempt to load secrets from 1Password, aligning with the [production recommendations](#environment-recommendation).\n\n### Loading All Fields from an Item\n\nWhen loading all fields from a 1Password item (not a parsed format), field names are automatically normalized to match the `env_prefix`:\n\n```ruby\nclass TestConfig \u003c Anyway::Config\n  config_name :test\n  attr_config :enabled, :sample\n\n  loader_options opdotenv: {\n    path: \"op://Employee/TestConfig\"  # Loads all fields from item\n  }\nend\n```\n\n**Field name matching (strict with case-insensitive prefix):**\n- Fields in 1Password **must** be prefixed with the `env_prefix` (e.g., `TEST_` for `config_name :test`)\n- Matching is **case-insensitive**: `TEST_ENABLED`, `test_enabled`, `Test_Enabled` all work\n- After prefix stripping, `TEST_ENABLED` becomes `enabled` (matching `attr_config :enabled`)\n- Fields without the prefix (e.g., `enabled`, `ENABLED`) are ignored and logged as unmatched\n\n**Debugging field matching:**\n- Enable debug logging by setting `OPDOTENV_DEBUG=true`\n- Check Rails logs for messages like:\n  ```\n  [opdotenv] Available fields from 1Password: enabled, ENABLED, sample, SAMPLE\n  [opdotenv] Matched fields for TEST: enabled, sample\n  [opdotenv] Unmatched fields (must be prefixed with TEST_, case-insensitive): other_field\n  ```\n\n## Using with dotenv\n\nLoad order determines which values take precedence:\n\n```ruby\nrequire \"dotenv\"\nrequire \"opdotenv\"\n\n# Load local files first, then augment from 1Password (1Password values override by default)\nDotenv.load(\".env\", \".env.development\")\nOpdotenv::Loader.load(\"op://Vault/.env.development\")\n\n# Or load from 1Password first, then local files (local values override)\nOpdotenv::Loader.load(\"op://Vault/.env.development\", overwrite: false)\nDotenv.load(\".env\", \".env.development\")\n```\n\n## Export to 1Password\n\n### CLI\n\n```bash\n# Export .env file (format inferred from path)\nopdotenv export --path \"op://Vault/.env.development\" --file .env.development\n\n# Export to item fields\nopdotenv export --path \"op://Vault/App\" --file .env\n\n# Read and print (format inferred from path)\nopdotenv read --path \"op://Vault/.env.development\"\n```\n\n### Ruby API\n\n```ruby\n# Export to Secure Note (format inferred from path)\nOpdotenv::Exporter.export(\n  path: \"op://Vault/.env.development\",\n  data: {\"API_KEY\" =\u003e \"secret\"}\n)\n\n# Export to item fields\nOpdotenv::Exporter.export(\n  path: \"op://Vault/App\",\n  data: {\"API_KEY\" =\u003e \"secret\", \"API_SECRET\" =\u003e \"another\"}\n)\n```\n\n## Supported formats\n\nFormat is automatically inferred from item name or field name:\n- `.env.*` → dotenv format (`KEY=VALUE`)\n- `*.json` → JSON format (nested structures flattened with underscores)\n- `*.yaml` or `*.yml` → YAML format (nested structures flattened with underscores)\n- Other items → load all fields without parsing\n\nField names can be specified with extensions in the path:\n- `op://Vault/Item Name/config.json` → loads field `config.json` as JSON\n- `op://Vault/Item Name/production.json` → loads field `production.json` as JSON\n- `op://Vault/Item Name/.env.development` → loads field `.env.development` as dotenv\n\nFor advanced usage, you can explicitly specify `field_name` and `field_type` in the API.\n\n## Security\n\n### Environment Recommendation\n\n**⚠️ This gem is recommended for development and test environments only.**\n\nFor production environments, we recommend using dedicated secret management solutions that integrate with your infrastructure:\n\n- **Kubernetes**: Use [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) or [External Secrets Operator](https://external-secrets.io/) with providers like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault\n- **AWS**: Use [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)\n- **Azure**: Use [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault)\n- **GCP**: Use [Google Secret Manager](https://cloud.google.com/secret-manager)\n\nProvision secrets through infrastructure-as-code tools:\n- **Helm** (Kubernetes): Use `helm secrets` or external secrets operator\n- **Terraform**: Use `aws_secretsmanager_secret`, `azurerm_key_vault_secret`, or `google_secret_manager_secret`\n- **Bicep** (Azure): Use `Microsoft.KeyVault/vaults` resources\n\nThese solutions provide:\n- Better audit trails and access controls\n- Integration with IAM/RBAC systems\n- Automatic secret rotation\n- Compliance with security standards\n- No dependency on external CLI tools or API servers\n\n### Security Considerations\n\n- **CLI mode**: Secrets fetched via authenticated `op` CLI session.\n- **Connect API mode**: Secrets fetched via HTTPS. Ensure tokens are secure.\n- The library does not persist secrets in memory or on disk.\n- Always verify 1Password CLI or Connect server is up to date and authenticated.\n\n### Code Locations for Security Review\n\nFor security-sensitive applications, developers should review where this gem reads and writes data from 1Password. The following locations handle secret data:\n\n#### Reading Secrets (OpClient - CLI mode)\n\n- **`lib/opdotenv/op_client.rb`**:\n  - `read(path)` - Executes `op read` command to fetch a single field value\n  - `item_get(item, vault:)` - Executes `op item get` command to fetch all item data as JSON\n  - `capture(args)` - Executes shell commands via `IO.popen` (array arguments, no shell interpretation)\n\n#### Reading Secrets (ConnectApiClient - API mode)\n\n- **`lib/opdotenv/connect_api_client.rb`**:\n  - `read(path)` - Makes HTTP GET request to fetch field or notesPlain content\n  - `item_get(item_title, vault:)` - Searches and fetches item data via API\n  - `get_item(vault_name, item_title)` - Fetches full item details including all fields\n  - `item_by_title_in_vault(vault_id, item_title)` - Lists items and fetches by title\n  - `list_vaults()` - Lists all accessible vaults (uses `api_request(:get, \"/v1/vaults\")`)\n  - `vault_name_to_id(vault_name)` - Resolves vault names to IDs (cached)\n  - `api_request(method, path, body)` - All HTTP requests go through this method\n  - `find_field(item, field_name)` - Searches item fields by label, ID, or purpose\n\n#### Main Entry Points\n\n- **`lib/opdotenv/loader.rb`**:\n  - `load(path, ...)` - Main entry point that orchestrates secret fetching\n  - `load_field(client, path, field_name, field_type)` - Loads and parses a single field\n  - `load_all_fields(client, path)` - Loads all fields from an item (skips notesPlain)\n  - `merge_into_env(env, hash, overwrite:)` - Writes secrets to environment hash\n\n#### Rails Integration\n\n- **`lib/opdotenv/railtie.rb`**:\n  - `initializer \"opdotenv.load\"` - Automatically loads secrets during Rails initialization\n  - Reads from `config.opdotenv.sources` array\n  - Sets `OP_CONNECT_URL` and `OP_CONNECT_TOKEN` from Rails config if provided\n\n#### Anyway Config Integration\n\n- **`lib/opdotenv/anyway_loader.rb`**:\n  - `Loader#call(...)` - Loads secrets for Anyway Config classes\n  - Uses `Opdotenv::Loader.load()` internally\n\n#### Parsing and Processing\n\n- **`lib/opdotenv/parsers/dotenv_parser.rb`** - Parses dotenv format strings\n- **`lib/opdotenv/parsers/json_parser.rb`** - Parses and flattens JSON structures\n- **`lib/opdotenv/parsers/yaml_parser.rb`** - Parses YAML (safe_load with aliases: false)\n\n#### Data Flow\n\n1. **Configuration** → Rails config or direct API calls\n2. **Path Parsing** → `SourceParser.parse()` extracts vault/item/field from path\n3. **Client Selection** → `ClientFactory.create()` chooses CLI or API client\n4. **Secret Fetching** → Client reads from 1Password (CLI command or HTTP request)\n5. **Parsing** → Format-specific parser converts to key-value pairs\n6. **Environment Merge** → Secrets merged into `ENV` or provided hash\n\nAll secret data flows through these code paths. No secrets are persisted to disk or logged (except explicit error messages).\n\n## Development\n\n```bash\nbundle install\nbundle exec rspec\nbundle exec rbs validate\nbundle exec standardrb --fix\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/amkisko/opdotenv.rb.\n\nContribution policy:\n- New features are not necessarily added to the gem\n- Pull request should have test coverage for affected parts\n- Pull request should have changelog entry\n\nReview policy:\n- It might take up to 2 calendar weeks to review and merge critical fixes\n- It might take up to 6 calendar months to review and merge pull request\n- It might take up to 1 calendar year to review an issue\n\n## Security\n\nIf you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famkisko%2Fopdotenv.rb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Famkisko%2Fopdotenv.rb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famkisko%2Fopdotenv.rb/lists"}