Using `act --var` and `--var-file` to Pass Workflow Variables in Local GitHub Actions

The act CLI accepts workflow variables via --var KEY=VALUE flags or --var-file path files, merging them into a Vars map that workflows access via ${{ vars.KEY }} syntax.

Running GitHub Actions locally with nektos/act often requires injecting dynamic values without hardcoding them into workflow YAML. The --var and --var-file flags solve this by piping custom variables into the runner's expression engine, letting you test workflows against different environments or secrets managers without committing sensitive data.

How act Processes Variable Flags Internally

The implementation spans the command layer and the runner engine. Here is the exact path your variables take from CLI argument to workflow expression.

Flag Registration and Input Handling

The root command defines the interface in cmd/root.go. Line 79 registers --var as a string array, while line 109 registers --var-file as a single string path.

// cmd/root.go:79
rootCmd.PersistentFlags().StringArrayVarP(&input.vars, "var", "", []string{}, "")

// cmd/root.go:109
rootCmd.PersistentFlags().StringVarP(&input.varfile, "var-file", "", "", "")

These values populate the Input struct defined in cmd/input.go:18-20, which holds the raw slices before validation.

type Input struct {
    vars     []string
    varfile  string
    // ...
}

When the CLI resolves the file path, the Varfile() method (lines 94-96) interprets relative paths against the working directory, ensuring the subsequent loader finds the file regardless of where you invoke act.

Parsing and Loading Variable Definitions

act reuses existing environment-parsing machinery for variable files. The readEnvs function (called at cmd/root.go:445-447) ingests the file specified by --var-file, supporting both .env style (KEY=VALUE) and YAML formats, and returns a map[string]string.

Individual --var arguments are processed by newSecrets in cmd/secrets.go:14-36. Despite the name, this helper parses both secrets and variables. It splits each NAME[=VALUE] entry, upper-cases the key, and—if the value is omitted—reads from the host environment or prompts interactively.

Merging CLI and File Variables

Precedence is determined by load order. First, readEnvs loads the file content into a map. Then, newSecrets iterates over the --var slice and writes into the same map. Because the CLI processing happens second, command-line variables override file definitions when keys collide.

Runtime Exposure via the Vars Map

Once merged, the map is stored in runner.Config.Vars. The Config struct definition in pkg/runner/runner.go:38-41 includes:

type Config struct {
    Vars map[string]string
    // ...
}

During workflow execution, the expression evaluator calls getWorkflowVars (located at pkg/runner/expression.go:92-94), which returns rc.Config.Vars. This makes every entry available to the workflow expression engine as ${{ vars.NAME }}.

Practical Examples for Local Testing

Pass a Single Variable Inline

Supply a build flag without touching the workflow file:

act -j build --var BUILD_TYPE=debug

Inside the workflow, reference it via the expression context:

- run: echo "Building ${{ vars.BUILD_TYPE }} image"

Load Multiple Variables from a File

Create staging.vars:

API_ENDPOINT=https://staging.example.com
TIMEOUT_SECONDS=30
DEBUG_MODE=true

Execute the job with the file path:

act -j deploy --var-file staging.vars

The runner loads all three entries into the Vars map, making ${{ vars.API_ENDPOINT }} and siblings available immediately.

Override File Values with CLI Flags

When you need to tweak one value without editing the file, combine both flags. The CLI value wins:


# staging.vars contains LOG_LEVEL=info

act -j test --var-file staging.vars --var LOG_LEVEL=debug

LOG_LEVEL resolves to debug because the --var assignment overwrites the file entry during the merge step.

Access Variables in Workflow Expressions and Shell

Variables appear in both the expression context and the shell environment:

jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - name: Via expression syntax
        run: echo "Endpoint is ${{ vars.API_ENDPOINT }}"
      - name: Via shell environment
        run: echo "Timeout is $TIMEOUT_SECONDS"

Both methods work because the runner injects the Vars map into the step's environment variables in addition to the expression evaluator.

Summary

  • --var accepts individual KEY=VALUE pairs and can be repeated; missing values fall back to host environment variables or interactive prompts.
  • --var-file points to a .env or YAML file containing multiple definitions, resolved relative to the working directory.
  • Precedence: CLI flags override file entries when names conflict, as implemented by sequential map writes in cmd/root.go.
  • Access: Variables surface in workflows as ${{ vars.NAME }} via getWorkflowVars in pkg/runner/expression.go, and also propagate to shell environments for $NAME access.

Frequently Asked Questions

Does act --var support reading values from the host environment?

Yes. If you omit the value in a --var argument (e.g., --var MY_TOKEN), the newSecrets parser in cmd/secrets.go:14-36 looks for MY_TOKEN in the host environment. If it is not found and stdin is a TTY, act prompts you interactively to enter the value.

Can I use YAML format instead of .env style in var files?

Yes. The readEnvs function (used at cmd/root.go:445-447) accepts both formats. You can use traditional KEY=VALUE lines or a YAML mapping structure; the parser normalizes either into the internal map[string]string required by the runner configuration.

What happens if the same variable is defined in both --var-file and --var?

The --var value takes precedence. The loading sequence in cmd/root.go first populates the map from the file via readEnvs, then processes the CLI slice via newSecrets. Because the latter writes after the former, duplicate keys are overwritten by the command-line specification.

How do I list or debug which variables are actually loaded?

act does not print the Vars map by default. To verify injection, add a step that dumps the context:

- run: echo '${{ toJson(vars) }}'

This outputs the entire Vars object as JSON during the run, letting you confirm that act --var and --var-file merged correctly before your production logic executes.

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 →