Skip to content
Latchkey

GitHub Actions ${{ }} Eaten by the Shell in a run: Step

A run: script that needs a literal ${{ ... }} sequence, or a value containing characters the shell treats specially, gets rewritten by GitHub expression interpolation or by the shell before your command sees it.

What this error means

A run step prints the wrong text, or fails with a shell parse error, because GitHub substituted an expression you meant to keep literal, or the shell expanded a value that contained quotes, backticks, or a dollar sign.

.github/workflows/ci.yml
- run: echo "template uses ${{ '${{' }} version ${{ '}}' }}"
# intended to print the literal "${{ version }}", but interpolation rewrote it

Common causes

GitHub interpolates ${{ }} before the shell runs

Every ${{ ... }} in a run: block is evaluated by GitHub first, then the result is handed to the shell. A literal ${{ you wanted to keep is consumed unless you escape it.

Interpolated values are pasted unquoted

An expression result containing spaces, quotes, backticks, or $ is spliced raw into the script. The shell then re-parses it, which can break the command or run unexpected substitutions.

How to fix it

Pass values through env, not inline

Bind the expression to an env var and reference $VAR in the script, so the shell receives a single safe value instead of spliced text.

.github/workflows/ci.yml
- run: echo "Title is: $TITLE"
  env:
    TITLE: ${{ github.event.issue.title }}

Escape a literal ${{ sequence

  1. To emit a literal ${{ , write ${{ '${{' }} so GitHub outputs the braces as a string.
  2. Quote interpolated values inside the script ("$VAR") so the shell does not re-split them.
  3. Prefer the env approach for any untrusted input to avoid script injection.

How to prevent it

  • Bind context and event values to env vars, then use $VAR in run scripts.
  • Never paste untrusted ${{ github.event.* }} directly into a shell command.
  • Quote shell variables to stop word-splitting and globbing.

Frequently asked questions

Why is passing inputs through env safer than inlining ${{ }}?
Inlined expression results are spliced into the script before the shell parses it, so a crafted value can inject commands. An env var is delivered as data and never re-parsed as script.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →