Secrets with env:// — complete example
Inject secrets from host environment variables into Loom jobs using the env:// provider. This tutorial walks through file injection, direct injection, and optional secrets in a single workflow you can copy and run.
What you will learn
- How
env://resolves secrets from host environment variables - The difference between file injection (default) and direct injection (
file: false) - How optional secrets (
required: false) let jobs run when a value is absent - How Loom redacts secret values from console output and runtime logs
Prerequisites
- Loom CLI installed and on your
PATH - The environment variables referenced by
env://exported in your shell before running
Workflow
version: v1
stages: [ci]
deploy:
stage: ci
target: linux
variables:
DEPLOY_ENV: "staging"
secrets:
API_TOKEN:
ref: env://DEPLOY_API_TOKEN
DB_PASSWORD:
ref: env://DB_PASSWORD
file: false
SLACK_WEBHOOK:
ref: env://SLACK_WEBHOOK_URL
required: false
script:
- echo "Deploying to $DEPLOY_ENV"
- echo "API token file is at $API_TOKEN"
- echo "DB password is available as direct env var"
- |
if [ -n "$SLACK_WEBHOOK" ]; then
echo "Slack notification configured (file at $SLACK_WEBHOOK)"
else
echo "Slack webhook not configured, skipping"
fi
Run it
Export the secret values, then execute the workflow:
export DEPLOY_API_TOKEN="my-api-token"
export DB_PASSWORD="s3cret"
loom run --local --workflow .loom/workflow.yml
SLACK_WEBHOOK_URL is intentionally omitted — it is marked required: false, so the job runs without it and the script handles the missing case gracefully.
What each secret demonstrates
API_TOKEN — file injection (default)
API_TOKEN:
ref: env://DEPLOY_API_TOKEN
| Field | Default | Behavior |
|---|---|---|
file | true | Loom writes the resolved value to a temp file with 0600 permissions and sets $API_TOKEN to the file path. |
required | true | If DEPLOY_API_TOKEN is not set in the host environment, the job fails with SECRETS_REQUIRED_MISSING before script execution. |
Read the value in scripts with cat $API_TOKEN.
DB_PASSWORD — direct injection
DB_PASSWORD:
ref: env://DB_PASSWORD
file: false
file: false means $DB_PASSWORD contains the raw secret value, not a file path. Use direct injection when a tool reads the secret from an environment variable directly (for example, a database client that expects $DB_PASSWORD).
Tradeoff: direct injection is more exposed to shell tracing (set -x). If CI_DEBUG_TRACE=true and any secret uses file: false, Loom hard-fails with SECRETS_UNSAFE_DEBUG_TRACE to prevent accidental leakage.
SLACK_WEBHOOK — optional secret
SLACK_WEBHOOK:
ref: env://SLACK_WEBHOOK_URL
required: false
required: false means the job continues when SLACK_WEBHOOK_URL is not set. The secret is silently omitted from injection. Your script should check whether $SLACK_WEBHOOK is set before using it.
Optional secrets are useful for notifications, telemetry, or other non-critical integrations.
Expected output
When the workflow runs successfully:
- The
deployjob starts. Loom resolves all three secrets (or skipsSLACK_WEBHOOKif absent). - Console output shows the
echostatements. Any line that would reveal a resolved secret value is redacted to[REDACTED:SECRET_<NAME>]. - Runtime logs under
.loom/.runtime/logs/<run_id>/contain the redacted output — raw values never appear in logs.
If a required secret is missing, the job fails before script execution with a SECRETS_REQUIRED_MISSING error.
Adapting this example
Add more secrets
Add entries under the secrets block. Each entry needs at minimum a ref:
secrets:
NEW_SECRET:
ref: env://MY_NEW_SECRET
Switch between file and direct injection
Change file: false to file: true (or remove file entirely — true is the default) and update scripts to read from the file:
# Direct injection (file: false)
echo "Password: $DB_PASSWORD"
# File injection (file: true, default)
echo "Password: $(cat $DB_PASSWORD)"
Use with Docker jobs
Add image: to the job. File-injected secrets are bind-mounted read-only into the container. The environment variable inside the container points to the container-local path.
The container mount path follows the pattern /tmp/loom-secret-<normalized-name>-<ordinal>, where the name is lowercased with underscores and uppercase letters replaced by dashes.
deploy:
stage: ci
target: linux
image: loom:nix-local
secrets:
API_TOKEN:
ref: env://DEPLOY_API_TOKEN
script:
- cat $API_TOKEN
Graduate to a vault provider
When you outgrow env://, switch to a vault-backed provider without changing your workflow structure — only the ref URI changes:
| Provider | URI format | Guide |
|---|---|---|
| KeePass | keepass://<alias>#<path>:<field> | KeePass secrets |
| 1Password | op://<vault>/<item>/<field> | 1Password secrets |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
SECRETS_REQUIRED_MISSING | Required env var not exported before loom run | export VAR_NAME="value" in your shell |
SECRETS_REF_INVALID | Malformed ref URI | Verify the format: env://<VAR_NAME> |
SECRETS_REF_NOT_FOUND | Provider resolved but the env var does not exist | Export the variable or set required: false |
SECRETS_PROVIDER_UNAVAILABLE | Provider adapter not available or misconfigured | Check that the env:// scheme is spelled correctly |
SECRETS_UNSAFE_DEBUG_TRACE | CI_DEBUG_TRACE=true with file: false secrets | Disable debug trace or switch to file: true |
| Script error reading secret | Using $VAR as a raw value but file: true is set | Read with cat $VAR, or set file: false |
For detailed triage, follow the Diagnostics ladder and check the job's events.jsonl for the specific error event.
Related docs
- Concepts — Secrets — mental model, redaction, vault auth contract
- Workflows — Secrets — full YAML syntax and provider URIs
- Getting started with secrets — create a vault, add a secret, use it in a workflow
- Stage 0 example — workspace gate workflow