A GitHub Action that loads secrets from Proton Pass vaults into your GitHub Actions workflows using pass:// URI references.
Works like 1Password's load-secrets-action, but backed by Proton Pass.
- name: Load secrets
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
env:
DATABASE_URL: "pass://Production/Database/connection_string"
STRIPE_KEY: "pass://Production/Stripe/secret_key"
- name: Deploy
run: ./deploy.sh # DATABASE_URL and STRIPE_KEY are in envEach pass://vault/item/field value is replaced with the real secret and exported as a regular environment variable for every subsequent step in the job.
- A Proton Pass Plus+ subscription (required for CLI access)
- The Proton Pass CLI installed locally (you only need it on your own machine to mint the token — the action installs it on the runner automatically)
Log in to pass-cli on your local machine, then create a scoped, expiring token for CI:
# Create a named token (90-day expiration in this example)
pass-cli pat create --name "github-actions" --expiration 90d
# Grant it read-only access to each vault it should be able to see.
# IMPORTANT: `pat create` alone gives the token zero vault access — you must
# run this grant for every vault the action needs to read from.
pass-cli pat access grant --pat-name "github-actions" --vault-name "Production" --role viewerThe create command prints the token in the format pst_xxxx::TOKENKEY. Copy it now — it is shown only once.
Token tips:
- Scope per-vault with
--role viewerso the token can read but never write. - Use short expirations (
30d,90d) and rotate. - Revoke any time with
pass-cli pat delete --name "github-actions".
In your repository, go to Settings → Secrets and variables → Actions and add:
| GitHub Secret | Description |
|---|---|
PROTON_PASS_PERSONAL_ACCESS_TOKEN |
The full pst_xxxx::TOKENKEY value from step 2 |
Define each secret as an environment variable on the action step using a pass:// URI:
pass://vault-name/item-name/field-name
- vault-name — name of the Proton Pass vault
- item-name — name of the item in the vault
- field-name —
password,username, or any custom field name
Every pass:// env var on the action step is resolved and re-exported as a regular env var available to all subsequent steps in the same job:
- name: Load secrets
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
env:
DB_PASSWORD: "pass://Production/Database/password"
- name: Run migrations
run: ./migrate.sh # DB_PASSWORD is in envWhen an item carries several related fields (a database item with host, port, password, database_name), use * in the field segment to pull all of them with one entry:
- name: Load secrets
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
env:
DB: "pass://Production/Database/*"
- name: Connect
run: psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME"The env-var name on the left becomes the prefix. Each field is exported as <PREFIX>_<FIELD>, where <FIELD> is sanitized: non-alphanumeric characters collapse to _, leading/trailing _ is trimmed, and the result is uppercased. api-key and API Key both become API_KEY.
Restrictions:
- Wildcards are only valid in the field segment.
pass://Vault/*/fieldandpass://*/item/fieldare rejected. - An item with zero fields fails the step (a warning instead when
strict: false). - Two field names that sanitize to the same suffix (e.g.
api-keyandapi_key) fail the step with both raw names listed. Rename the field or use explicitpass://URIs. - Adding a new field to a globbed item adds a new env var on the next run. Keep that in mind when sharing vaults across workflows.
By default the step fails when any pass:// URI cannot be resolved, ending with a report that lists every failing variable (names and URIs only — never secret values):
::error::Failed to resolve 1 secret(s):
::error:: BOGUS -> pass://Prod/Does-Not-Exist/x (Error: Could not find item by name 'Does-Not-Exist')
Keeping strict: true (the default) is strongly recommended — a missing secret that silently becomes an empty string tends to surface later as a confusing failure (empty DB_PASSWORD, 401s from an empty API_KEY) far from the real cause.
If some secrets are genuinely optional, set strict: false: failures are reported as warnings, the affected variables are left unset, and the step succeeds:
- name: Load secrets (best effort)
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
strict: false
env:
OPTIONAL_TOKEN: "pass://CI/Optional-Service/token"For applications that read a .env file, render one from a template with {{ pass://vault/item/field }} placeholders:
- name: Render .env from template
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
env-template: ".env.production.template"Template (.env.production.template):
DB_HOST=db.example.com
DB_PASSWORD={{ pass://Production/Database/password }}
REDIS_URL={{ pass://Production/Redis/url }}
Output (.env.production):
DB_HOST=db.example.com
DB_PASSWORD=actual-resolved-password
REDIS_URL=redis://actual-url:6379
With an explicit output path:
- name: Render .env from template with custom output
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
env-template: ".env.production.template"
output-path: ".env.production"| Input | Required | Default | Description |
|---|---|---|---|
personal-access-token |
Yes | Proton Pass PAT (pst_xxxx::TOKENKEY) |
|
env-template |
No | '' |
Path to a template file with pass:// references |
pass-cli-version |
No | 2.1.0 |
Pinned for reproducibility. Override with latest or any version listed at proton.me/download/pass-cli/versions.json |
mask-values |
No | true |
Mask resolved values in workflow logs |
strict |
No | true |
Fail the step when any pass:// URI cannot be resolved. Set false for best-effort mode: failures become warnings and the step continues |
output-path |
No | '' |
Where to write the rendered template. Defaults to stripping .template/.tpl, else <input>.resolved. |
| Output | Description |
|---|---|
resolved-keys |
Comma-separated, sorted list of env var names the action populated (e.g. API_KEY,DB_PASSWORD). Names only — values never appear. Empty string when nothing resolved. |
Use it to gate downstream steps on what was actually loaded, without touching values:
- name: Load secrets
id: secrets
uses: gizmodlabs/load-secrets-proton-pass@v1
with:
personal-access-token: ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}
env:
DB_PASSWORD: "pass://Prod/DB/password"
- name: Run migrations only if DB_PASSWORD loaded
if: contains(steps.secrets.outputs.resolved-keys, 'DB_PASSWORD')
run: ./run-migrations.shReady-to-copy workflow files live in examples/:
| Example | Description |
|---|---|
with-pat.yml |
Generate a PAT locally, load one secret in CI |
basic-usage.yml |
Load a couple of secrets and use them |
multi-service.yml |
Load secrets for multiple services in one step |
env-template.yml |
Inject secrets into a .env template file |
The action is bash on top of pass-cli. Three commands cover the local loop:
# 1. Lint
shellcheck scripts/*.sh tests/*.sh
# 2. Unit tests against the mock pass-cli (no Proton account needed)
bash tests/run-local-tests.sh
# 3. Full workflow simulation using the official GitHub Actions runner
npx @redwoodjs/agent-ci run --workflow tests/test-workflow.ymlagent-ci wraps the official actions/runner binary, so what passes locally is what runs in CI.
tests/test-real.yml (gitignored) runs the action end-to-end against your own Proton Pass account. Set up:
# 1. Put your PAT in .env.agent-ci (also gitignored) — agent-ci picks up
# secrets from this file automatically.
echo 'PROTON_PASS_PERSONAL_ACCESS_TOKEN=pst_xxxx::TOKENKEY' > .env.agent-ci
# 2. Edit the pass:// URIs in tests/test-real.yml to point at items you own.
# 3. Run it.
npx @redwoodjs/agent-ci run --workflow tests/test-real.ymlThe workflow references the PAT as ${{ secrets.PROTON_PASS_PERSONAL_ACCESS_TOKEN }}, so the token never lives in the YAML.
- A Proton Pass Plus+ subscription (required for CLI access)
- The Proton Pass CLI — installed automatically on the runner by this action; needed locally only to mint the PAT
This is an independent, community-maintained GitHub Action. It is not affiliated with, endorsed by, or sponsored by Proton AG. "Proton" and "Proton Pass" are trademarks of Proton AG and are used here only to describe what the action talks to.
The action is a thin wrapper around Proton's public pass-cli — the same binary anyone can install and run. It uses only documented commands, accesses no private APIs, bypasses no auth, and does not reuse Proton branding beyond naming the integration.
Open source under MIT. Contributions welcome — bug reports, fixes, docs, new examples, dependency bumps, anything.
- File issues and feature requests in the Issues tab.
- Before opening a PR, run
shellcheck scripts/*.sh tests/*.shandbash tests/run-local-tests.shlocally. Both should pass. - Keep PRs focused; one concern per branch.
- See Local development for the full dev loop.
Created by Martin at Gizmodlabs LLC, with contributions from the community.
MIT — see LICENSE