You spent an hour writing a GitHub Actions workflow. You push it, and it fails immediately. A syntax error. A misspelled action input. A reference to a secret that does not exist. Those errors could have been caught before the push.
actionlint is a static linter that analyses your workflow files and catches errors before they happen on GitHub.
What you will learn
- Install actionlint on Linux, macOS and Windows
- Validate your workflows and read the error messages
- Wire actionlint into VS Code and a pre-commit hook
- Add a lint workflow that validates pull requests
- Configure the rules and the variables specific to your organisation
What is actionlint?
actionlint is a command-line tool that statically analyses your GitHub Actions workflows. It checks the YAML syntax, the action references, the expressions, the permissions and much more, all without running the workflow.
Concretely, actionlint catches:
- YAML syntax errors (indentation, structure)
- Unknown or misspelled properties, including invalid
with:inputs of popular actions - Invalid context references (
${{ github.sha }}) - Actions that do not exist or are badly referenced
- Permissions declared with an unknown scope or value
- Type problems in expressions
- Invalid glob patterns
- And many other categories, including script injection and hardcoded credentials
Why use actionlint?
The table below contrasts the usual fix loop with the one actionlint brings. The heaviest line is the second: without a linter, every typo in a workflow costs a full round trip with the GitHub runner, several tens of seconds at best, and that cycle repeats until the error is found. actionlint brings that loop down to a local validation in under a second, before the commit even happens.
| Without actionlint | With actionlint |
|---|---|
| The error is found after the push | The error is caught before the commit |
| Waiting for the runner (30s to several minutes) | Instant validation (< 1s) |
| History polluted with failed runs | Cleaner runs |
| Debugging by trial and error | Clear, precise error messages |
Installation
With Homebrew:
brew install actionlintWith Homebrew:
brew install actionlintFrom the GitHub releases, with a checksum verification. The project publishes a
checksums.txt file for every version; you download the archive and that
file, then refuse to extract until sha256sum --check answers OK:
ACTIONLINT_VERSION=1.7.12BASE="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}"curl -sSLO "${BASE}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"curl -sSLO "${BASE}/actionlint_${ACTIONLINT_VERSION}_checksums.txt"sha256sum --ignore-missing --check "actionlint_${ACTIONLINT_VERSION}_checksums.txt"tar -xzf "actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" actionlintsudo install -m 0755 actionlint /usr/local/bin/actionlintWith Go:
go install github.com/rhysd/actionlint/cmd/actionlint@latestWith Chocolatey:
choco install actionlintWith Scoop:
scoop install actionlintChecking the installation:
actionlint --versionThe first line prints the installed version (for example 1.7.12), followed by
two lines about the installation method and the Go compiler used.
Basic usage
Validating every workflow
At the root of your project:
actionlintactionlint finds the files in .github/workflows/ automatically and analyses
all of them.
A sample output: every error shows the file, the line, the column, then the
category in square brackets ([action], [expression], and so on). The
category is what tells you which rule you are facing:
.github/workflows/ci.yml:11:11: input "node-verion" is not defined in action "actions/setup-node@v4". available inputs are "always-auth", "architecture", "cache", ..., "node-version", ... [action] |11 | node-verion: 20 | ^~~~~~~~~~~~Here, actionlint recognises actions/setup-node, knows its valid inputs and
spots the typo node-verion instead of node-version. Without that linter,
the error would only show up when running on GitHub.
Validating a specific workflow
Passing a path as an argument narrows the analysis to that single file, which
helps when you work on one precise workflow in a repository holding several.
actionlint always expects to find the root of the Git repository above the
file, because it detects the project through the .github/workflows/ directory:
run outside a repository, it stops with no project was found.
actionlint .github/workflows/ci.ymlValidating from stdin
Useful for scripts and pipelines:
cat .github/workflows/ci.yml | actionlint -Reading the error messages
actionlint gives precise messages with the line number, the column, and an explanation. Here are the main categories:
Syntax errors
.github/workflows/ci.yml:1:1: "on" section is missing in workflow [syntax-check]The workflow has no on: section defining when it triggers.
Property errors
.github/workflows/ci.yml:11:11: input "node-verion" is not defined in action "actions/setup-node@v4" [action]A typo: node-verion instead of node-version. The [action] category says
actionlint validated the with: key against the real list of the action's
inputs.
Runner errors
.github/workflows/ci.yml:5:14: label "123" is unknown. available labels are "windows-latest", ..., "ubuntu-24.04", ... [runner-label]The runs-on value matches no known runner label. A custom self-hosted runner
label is declared in the actionlint.yaml file to avoid that false positive.
Security errors
.github/workflows/ci.yml:7:24: "github.event.issue.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable [expression]actionlint detects script injection: a piece of data a third party controls
(github.event.issue.title) interpolated straight into a run: block. The
counter is to pass it through an env: block. The [credentials] category
covers container passwords written in clear text. actionlint does not,
however, report a missing SHA pin: see
SHA pinning and the
dedicated scanners.
Expression errors
.github/workflows/ci.yml:9:18: property "secret" is not defined in object type {action: string; ...} [expression]The github.secret context does not exist (secrets.X is what you want).
Useful options
Output format
The default format is meant to be read by a human in the terminal. The other
three exist to plug actionlint into another tool: json for a script that
parses the results, sarif to push the alerts into the GitHub Security tab
through Code Scanning, and the ::error template so GitHub Actions highlights
the error directly in the run interface. Choose the format according to the
consumer, not to your taste.
# Default format (readable)actionlint
# JSON format (for CI integration)actionlint -format json
# SARIF format (for GitHub Code Scanning)actionlint -format sarif > results.sarif
# Format compatible with GitHub Actions problem matchersactionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}'Ignoring certain errors
The -ignore flag takes a regular expression matched against the error
message, not a rule identifier. It helps to silence a known, accepted
shellcheck warning: the SC2086 and SC2129 codes come from the shellcheck
integration that analyses your run: blocks.
# Silence one precise shellcheck warning (unquoted words)actionlint -ignore 'SC2086'
# Silence severalactionlint -ignore 'SC2086' -ignore 'SC2129'You can also neutralise a category on one precise block, with a comment inside the workflow file:
# actionlint: ignore=expression- name: Step with ignored warning run: echo "${{ github.secret }}"Verbose mode
actionlint -verbosePrints the files analysed and the run time.
Checking popular actions, without network access
actionlint validates the with: inputs of about a hundred popular actions
(actions/checkout, actions/setup-node, actions/cache, and so on) thanks to
a database embedded in the binary at compile time. It therefore never
needs network access for those checks, which makes it usable offline and in an
isolated runner. That database only covers the major-version form
(actions/checkout@v4); a version frozen at the patch level (@v4.0.1) or a
@main reference are not validated.
VS Code integration
actionlint integrates with editors to show errors in real time.
The VS Code extension
- Install GitHub's GitHub Actions extension
- It uses actionlint automatically when it is installed
Or use the dedicated actionlint extension:
- Open VS Code
- Extensions, then search for "actionlint"
- Install the extension
Errors then appear directly in the editor, underlined.
VS Code configuration
In .vscode/settings.json:
{ "actionlint.executable": "/usr/local/bin/actionlint", "yaml.schemas": { "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml" }}CI integration
A validation workflow
Create a workflow that validates your other workflows:
name: Lint Workflows
on: push: paths: - '.github/workflows/**' pull_request: paths: - '.github/workflows/**'
# No rights by default: the job asks for the minimumpermissions: {}
jobs: actionlint: runs-on: ubuntu-24.04 permissions: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Install actionlint env: ACTIONLINT_VERSION: 1.7.12 run: | BASE="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}" curl -sSLO "${BASE}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" curl -sSLO "${BASE}/actionlint_${ACTIONLINT_VERSION}_checksums.txt" sha256sum --ignore-missing --check "actionlint_${ACTIONLINT_VERSION}_checksums.txt" tar -xzf "actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" actionlint sudo install -m 0755 actionlint /usr/local/bin/actionlint
- name: Validate the workflows run: actionlint -colorWith the official action
name: Lint Workflows
on: pull_request: paths: - '.github/workflows/**'
# No rights by default: the job asks for the minimumpermissions: {}
jobs: actionlint: runs-on: ubuntu-24.04 permissions: contents: read pull-requests: write # So reviewdog can comment on the pull request steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- uses: reviewdog/action-actionlint@50842263c20a7c46bd0065b9e624d3c569db061e # v1.73.0 with: reporter: github-pr-reviewThat action uses reviewdog to comment the detected errors directly on the pull requests.
A pre-commit hook
Validate before every commit with pre-commit:
repos: - repo: https://github.com/rhysd/actionlint rev: v1.7.12 hooks: - id: actionlintThen:
pip install pre-commitpre-commit installThe validation rules
actionlint checks many rules. The word in square brackets at the end of each
error message is the category of the rule triggered, and that category is
what the -ignore flag or the # actionlint: ignore= comment targets. The
tables below group the most frequent categories by theme.
Syntax and structure
These are the errors actionlint catches first, before it even understands the
logic of the workflow: badly indented YAML, a malformed ${{ }} expression, or
a workflow command GitHub has removed. The deprecated-commands category is
useful during migrations, as it spots the old ::set-output:: calls GitHub no
longer supports.
| Category | Description |
|---|---|
syntax-check | Basic YAML errors and missing keys |
expression | Invalid ${{ }} expressions or incompatible types |
deprecated-commands | Obsolete workflow commands (set-output, save-state) |
Security
These three categories cover what actionlint can detect on the security side
without overlapping the supply chain scanners. Script injection has no
category of its own: it is reported under expression, with an explicit message
inviting you to pass the data through env:. As a reminder, SHA pinning is not
part of it.
| Category | Description |
|---|---|
permissions | Unknown permissions: scope or value |
credentials | Container password written in clear text |
expression | Untrusted data interpolated into run: (script injection) |
Actions and references
actionlint knows the expected structure of events and of inter-job
dependencies, which lets it spot a needs: pointing at a job that does not
exist or a misspelled event name. The action category covers the invalid
with: inputs of the popular actions seen above.
| Category | Description |
|---|---|
action | Invalid with: inputs, obsolete action (runner too old) |
events | Invalid trigger events |
job-needs | Incorrect needs: dependencies |
Types and values
These categories check that values match what GitHub really expects: a runner
label that exists, a valid glob pattern in a paths: filter, a well-formed
matrix. The runner-label check is the one that produces the most false
positives on self-hosted runners, hence the configuration file described below.
| Category | Description |
|---|---|
runner-label | Unknown runner labels |
glob | Invalid glob patterns in paths: |
matrix | Errors in the matrix definition |
Combining with act
For a complete validation of your workflows:
- actionlint: static validation (syntax, types, references)
- act: local execution (logic, scripts, behaviour)
#!/usr/bin/env bash# Complete validation scriptset -euo pipefail
echo "Static validation with actionlint..."actionlint
echo "Dry run with act..."act -n
echo "Workflows valid."With set -euo pipefail, the script stops at the first command that fails: no
need to test $? after each step, a failing actionlint or act -n
interrupts everything and returns a non-zero code to the CI.
Advanced configuration
The configuration file
Create a .github/actionlint.yaml file:
# actionlint configurationself-hosted-runner: labels: - my-runner - gpu-runner
config-variables: - MY_ORG_VAR - DEPLOYMENT_ENV
paths: ignore: - '.github/workflows/deprecated-*.yml'Organisation variables
If you use organisation-level variables (${{ vars.ORG_VAR }}), declare them to
avoid false positives:
config-variables: - ORG_CONFIG - COMPANY_NAMETroubleshooting
"command not found: actionlint"
Symptom: the terminal cannot find the command.
Fixes:
# Check the installationwhich actionlint
# If installed through Go, add it to the PATHexport PATH="$PATH:$(go env GOPATH)/bin"False positives on custom actions
Symptom: actionlint reports local actions as non-existent.
Fix: local actions (./actions/my-action) are validated if they exist in
the repository. Check the path.
"no project was found"
Symptom: actionlint stops with no project was found in any parent directories.
Cause: actionlint looks for the root of a Git repository above the files it
analyses. Run outside a repository (or in a folder extracted without .git), it
finds no project.
Fix: run it from the repository root, or pass the workflow through stdin:
cat ci.yml | actionlint -Key points
Instant validation
Catch errors in under a second, before the push.
50+ checks
Syntax, security, types, references: everything is verified.
IDE integration
Errors shown directly in VS Code while you write.
CI/CD ready
JSON and SARIF formats, plus reviewdog integration for pull requests.
The main points:
- Install actionlint and use it before every push
- Wire it into VS Code for real-time validation
- Add a lint workflow to validate pull requests automatically
- Combine it with act for a complete validation
- Configure the rules and variables specific to your organisation
The actionlint repository documents every rule, and the online playground lets you test a workflow in the browser.
Next steps
- zizmor: the security audit that takes over where the linter stops, on the workflow itself.
- poutine: the same check extended to a whole organisation and to several CI platforms.
- OpenSSF Scorecard: the posture score that measures what a linter cannot see, from branch protection to release signing.