Skip to content
Latchkey

GitHub Actions Heredoc in a run: Step Breaks or Expands Variables

A heredoc inside a run: script behaves unexpectedly - variables expand when you wanted them literal (or vice versa), or the block never closes because the terminator is indented or misspelled.

What this error means

A heredoc writes the wrong content: $VAR is left literal when you wanted it expanded, expressions are substituted when you wanted them raw, or the step hangs/fails because the closing delimiter was not recognized.

.github/workflows/ci.yml
- run: |
    cat <<EOF > out.txt
    version is $VERSION
    EOF
# EOF (unquoted) expands $VERSION; <<'EOF' would keep it literal

Common causes

Quoted vs unquoted delimiter controls expansion

cat <<EOF expands $VAR and command substitutions; cat <<'EOF' keeps the body literal. Choosing the wrong one writes the wrong content.

Indented or mismatched terminator

The closing delimiter must match exactly and start at column zero unless you use <<-EOF with tabs. A space-indented EOF inside a YAML block scalar is not recognized, so the heredoc never ends.

How to fix it

Pick the delimiter form deliberately

Use an unquoted delimiter to expand variables, a single-quoted delimiter to keep the body literal.

.github/workflows/ci.yml
- run: |
    cat <<'EOF' > template.txt
    keep $THIS and ${{ this }} literal
    EOF

Keep the terminator unambiguous

  1. Put the closing delimiter flush left within the run: block scalar, with no leading spaces.
  2. Use a unique token (e.g. EOF_TEMPLATE) so it cannot appear in the body.
  3. Remember GitHub still interpolates ${{ }} before the shell, so quote the delimiter to keep expressions literal too.

How to prevent it

  • Choose quoted vs unquoted heredoc delimiters based on whether you want expansion.
  • Keep the closing delimiter flush left and unique.
  • Test multiline scripts locally in the same shell GitHub uses.

Related guides

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