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:

  1. gh-aw reads the front-matter and builds a workflow-level env: block containing DEPLOY_ENV, API_BASE_URL, and the secret reference DEPLOY_TOKEN (handled in pkg/workflow/yaml_generation.go).
  2. The deploy job inherits these variables and adds NODE_VERSION to its local scope (pkg/workflow/yaml.go).
  3. The final step adds DEBUG to its environment while accessing the inherited DEPLOY_TOKEN and DEPLOY_ENV variables (pkg/workflow/prompt_step.go).
  4. 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.go and secret_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →