How to Pass Environment Variables and Configure Secrets in gh-aw Workflows
You declare environment variables and secrets inside env: maps at the workflow, job, or step level in your markdown front-matter, and gh-aw compiles them into secure GitHub Actions YAML while enforcing that secrets are only referenced via ${{ secrets.NAME }}.
gh-aw is a GitHub CLI extension that transforms markdown-based workflow definitions into production-ready GitHub Actions YAML. During compilation, the tool builds env: blocks for every scope level and injects secret references using strict validation rules defined in the source code. Understanding how to properly pass environment variables and configure secrets ensures your workflows remain secure and maintainable.
Understanding Environment Variable Scopes in gh-aw
gh-aw supports three distinct levels of environment variable scoping, each rendered into specific locations in the generated YAML.
Workflow-Level Environment Variables
Variables defined at the root of your front-matter are available to every job and step in the workflow. The compiler writes these into the top-level env: block in pkg/workflow/yaml_generation.go.
---
name: Production Deploy
env:
NODE_ENV: production
API_VERSION: v2
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
---
Job-Level Environment Variables
Scoped to a specific job, these variables override workflow-level values for that job only. The compiler inserts these under each job definition in pkg/workflow/yaml.go.
---
jobs:
build:
runs-on: ubuntu-latest
env:
BUILD_MODE: debug
CACHE_KEY: ${{ secrets.CACHE_KEY }}
steps:
- uses: actions/checkout@v4
Step-Level Environment Variables
Used for temporary overrides or passing sensitive data to specific commands without exposing them to the entire job. The compiler emits these directly before the run: stanza in pkg/workflow/prompt_step.go.
---
steps:
- name: Deploy to staging
run: ./deploy.sh --env $DEPLOY_ENV
env:
DEPLOY_ENV: staging
AUTH_TOKEN: ${{ secrets.STAGING_TOKEN }}
How to Configure Secrets in gh-aw Workflows
Secrets require special handling to prevent accidental exposure in logs or source control.
Declaring Secrets in Front-Matter
First, create the secret in your GitHub repository under Settings → Secrets → Actions. Then reference it exclusively within an env: block using the GitHub Actions expression syntax:
---
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
---
The compiler validates these references in secret_validation_test.go, ensuring that secrets are only used inside env: blocks and never in plain text or other contexts.
Using Secrets with Safe-Inputs
Safe-inputs expose values to external tools (such as Playwright or HTTP clients) through environment variables without risking template injection. Declare the secret in the tool's env map:
---
tools:
playwright:
version: "v1.41.0"
env:
PLAYWRIGHT_TOKEN: ${{ secrets.PLAYWRIGHT_TOKEN }}
---
The compiler extracts this map, adds it to the step's env: section in the generated YAML, and ensures the secret remains scoped to that specific tool execution according to the safe-inputs specification.
Security Validation and Template Injection Protection
gh-aw implements strict security controls to prevent credential leakage and injection attacks.
The template injection validator in pkg/workflow/template_injection_validation.go ensures that only variables explicitly listed in an env: block are used inside step commands. This prevents attackers from injecting malicious code through environment variable interpolation.
Additionally, the compiler rejects any secret references found outside of env: blocks. If you attempt to use ${{ secrets.NAME }} directly in a run: command without first assigning it to an environment variable, the compilation fails with a validation error defined in the security architecture specification (specs/security-architecture-spec.md).
Complete Example: Environment Variables and Secrets in Practice
Here is a production-ready workflow demonstrating all three scoping levels and proper secret handling:
---
name: Deploy to Production
engine: copilot
env:
DEPLOY_ENV: production
API_BASE_URL: "https://api.example.com"
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_VERSION: "18"
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Deploy application
run: |
echo "Deploying to $DEPLOY_ENV"
./scripts/deploy.sh --token $DEPLOY_TOKEN
env:
DEBUG: "true"
---
Compilation process:
gh-awreads the front-matter and builds a workflow-levelenv:block containingDEPLOY_ENV,API_BASE_URL, and the secret referenceDEPLOY_TOKEN(handled inpkg/workflow/yaml_generation.go).- The
deployjob inherits these variables and addsNODE_VERSIONto its local scope (pkg/workflow/yaml.go). - The final step adds
DEBUGto its environment while accessing the inheritedDEPLOY_TOKENandDEPLOY_ENVvariables (pkg/workflow/prompt_step.go). - The secret remains encrypted in GitHub's secret store and is only expanded at runtime by GitHub Actions, never appearing in the compiled YAML or source markdown.
Summary
- Scope variables appropriately using workflow-level, job-level, or step-level
env:blocks depending on visibility requirements. - Reference secrets exclusively within
env:blocks using the${{ secrets.NAME }}syntax to pass security validation. - Leverage safe-inputs to expose secrets to external tools without risking template injection attacks.
- Trust the compiler to enforce security rules through
template_injection_validation.goandsecret_validation_test.go, rejecting unsafe patterns before they reach production.
Frequently Asked Questions
How do I override a workflow-level environment variable for a specific job?
Define an env: block under the specific job in your front-matter. Variables declared at the job level override workflow-level values for that job only. The compiler handles this scoping in pkg/workflow/yaml.go by inserting the job-specific env: map under the job definition in the generated YAML.
Can I use secrets directly in run commands without assigning them to environment variables?
No. The gh-aw compiler explicitly forbids this pattern. If you attempt to use ${{ secrets.NAME }} directly in a run: command, the validation logic in secret_validation_test.go triggers a compilation error. You must first assign the secret to an environment variable in the step's env: block, then reference that variable in your commands.
What happens if I declare a secret in the workflow-level env block?
The secret reference is propagated to all jobs and steps that inherit that environment block, but the actual secret value remains encrypted in GitHub's secret store. The compiler in pkg/workflow/yaml_generation.go writes the ${{ secrets.NAME }} expression into the top-level env: block of the generated YAML. GitHub Actions only expands this expression at runtime, ensuring the secret never appears in logs or source code.
How does gh-aw protect against template injection attacks when using environment variables?
The tool implements a strict validation layer in pkg/workflow/template_injection_validation.go that ensures only variables explicitly listed in an env: block are used inside step commands. This prevents attackers from injecting malicious code through environment variable interpolation. Additionally, the safe-inputs system isolates secrets to specific tools, further reducing the attack surface.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →