GitHub Actions Linter (Workflow Security and Expression Checks)
Expressions, jobs, matrices, schedules and security checks for workflow files.
Try before you buy.
- Free preview: every count, the jobs, matrices and schedules of each workflow, with the first problems of the report (up to 10).
- Locked until you unlock it: download and copy.
- Unlock: Pro pass, ₹179 for 30 days, a one-time payment that never renews.
Ways to unlock shows how to get the full result.
Printing this result is locked in the free preview.
Workflow files
uses: ./.github/workflows/build.yml are matched by this path. Checked as you type, in your browser. Ctrl/⌘ + Enter checks now.
Start from a starter workflow
Pinned to commit SHAs, least-privilege and with a timeout. It opens as a new file that you can change; the linter checks it like any other.
Problems
Locked in the free preview. Opens the ways to unlock this result.
Locked in the free preview. Batch runs unlock with a pass.
Locked in the free preview. Query results unlock with a pass.
About the GitHub Actions Linter (Workflow Security and Expression Checks)
Paste one or more GitHub Actions workflow files and get what a schema check cannot tell you, each problem with its file, line and a link to GitHub’s documentation:
- Expressions: the syntax of every
${{ }}andif:condition, unknown functions and contexts, contexts used where GitHub does not provide them (secretsin a job’sif,hashFiles()outside steps), and references to step ids, needed jobs, matrix variables and inputs that do not exist. - Jobs:
needsthat point nowhere or form a circle, the order the jobs run in, and each matrix expanded withincludeandexcludeinto its combinations and job count. The newbackground,wait,wait-all,cancelandparallelsteps are understood. - Reusable workflows: give the called file too, and the inputs, secrets and outputs of each call are checked against its
workflow_call. - Triggers: unknown events and activity types, filters that cannot be combined, and every schedule in plain English with its next runs.
- Security, after GitHub’s secure-use reference: untrusted event data such as pull-request titles and branch names pasted into scripts,
pull_request_targetworkflows that check out the pull request’s code, missing least-privilegepermissions, actions not pinned to a full commit SHA, and secrets pasted into or printed by scripts.
Your files stay in your browser. Without a pass the result is a free preview: every count and the jobs, matrices and schedules of each workflow, with the first problems of the report. The whole report, copying it and downloading it unlock with a Pro pass. To check a file against the workflow schema, use the YAML Validator.
How to use it
- Paste a workflow into the first file, or open
.ymlfiles. Use Add file for each file a workflow calls (./.github/workflows/build.yml), and give each file its path so calls can be matched. - Or start from a starter workflow for Node.js, Python or a Docker build: it is already pinned and least-privilege, and loads into the editor for you to change.
- Read the report: errors are what GitHub refuses or what breaks, warnings are risks, notes are advice. Show selects the line; each finding links GitHub’s page about it. Below the problems, each workflow’s jobs, matrices and schedules are listed.
- Fix the YAML and the report updates as you type. With a pass, or once the report is unlocked, copy it as Markdown or download it as Markdown or JSON; the free preview shows its first problems and locks both.
Examples
on: pull_request_target
…
- run: echo "Checking ${{ github.event.pull_request.title }}"Error (Security), line 9: github.event.pull_request.title is written by whoever opens the issue, pull request or commit, and ${{ }} pastes it into the script before it runs… Pass it through env (TITLE: ${{ github.event.pull_request.title }}) and use "$TITLE" in the script.In a pull_request_target workflow the injected code would run with the repository’s secrets, so this is an error; in a pull_request workflow it is a warning.
matrix:
os: [ubuntu-latest, windows-latest]
node: [22, 24]
include:
- os: windows-latest
node: 24
experimental: true4 jobs: os: ubuntu-latest, node: 22 · os: ubuntu-latest, node: 24 · os: windows-latest, node: 22 · os: windows-latest, node: 24, experimental: true
An include that matches existing combinations adds its extra values to them; one that matches none becomes a job of its own.
on:
schedule:
- cron: '30 5 * * 1-5'
timezone: Asia/KolkataAt 05:30, Monday through Friday (Asia/Kolkata) — with the next five run times.
The security checks
- Untrusted input in scripts. Titles, bodies, commit messages, author names and branch names are written by whoever opens the issue, pull request or commit.
${{ }}pastes them into arunscript (or agithub-scriptscript) before it runs, soa"; curl …; "becomes a command. Pass such values throughenv:and quote the variable (script injections). - Pwn requests.
pull_request_targetandworkflow_runrun with the base repository’s secrets and a write token; checking out the pull request’s head or merge commit and then building it runs the contributor’s code with them (securely using pull_request_target). - Least privilege. Without
permissions:, theGITHUB_TOKENgets the repository’s default, which may be write access to everything; setcontents: readat the top and grant more per job (permissions). - Pinning. A tag can be moved by the action’s owner or an attacker who takes over the repository; a full-length commit SHA cannot (using third-party actions).
- Secrets in logs. GitHub masks a secret’s exact value, not parts or encoded copies of it, so printing secrets or pasting them into script text is reported (secrets).
Where contexts can be used
GitHub evaluates expressions at different times: a job’s if runs before the job is sent to a runner, so env, steps, runner and secrets do not exist there yet; hashFiles() needs the files, so it only works in steps. The linter applies GitHub’s context availability table key by key, and checks that steps.<id> names an earlier step, needs.<job> a job in needs, and matrix.<name> a variable of the job’s matrix.
Limitations
- The linter reads the workflow files; it does not know your repository’s secrets, variables, environments, rulesets or the actions’ own inputs, and it does not run anything.
- Schema checks (every allowed key and type) stay with the YAML Validator, which checks workflow files against the SchemaStore schema.
- Matrices built from an expression (
fromJSON(…)) and labels of larger or self-hosted runners are only known when the workflow runs. - A finding is advice to look at the line, not proof of an exploit: a value that looks untrusted may come from a step that already sanitised it.
Privacy
Everything happens in your browser. What you enter or open here is not uploaded or stored by MySmartCoPilot.
Frequently asked questions
What do I get without a pass?
Without a pass, GitHub Actions Linter (Workflow Security and Expression Checks) shows every count, the jobs, matrices and schedules of each workflow, with the first problems of the report (up to 10). Until you unlock it, the result can’t be downloaded or copied. A Pro, Premium or Ultimate pass, a one-time payment that never renews, unlocks the full result. The pricing page lists the passes and their prices.
Is this like actionlint or zizmor?
It does much of the same work in one report in your browser: expression, context, needs, matrix and cron checks like actionlint’s, and injection, pull_request_target, permission and pinning checks like zizmor’s. It does not run shellcheck on scripts or check the inputs of each action. The rules are written from GitHub’s documentation; nothing to install.
Why is secrets not allowed in my job’s if?
A job’s if is evaluated before the job starts, where GitHub only provides github, needs, vars and inputs. Put the secret in a job-level env and test the variable in a step, or check it in an earlier job and pass a boolean output through needs.
Do I have to pin every action to a SHA?
GitHub recommends a full-length commit SHA as the only immutable reference, especially for third-party actions. The linter warns about third-party tags and branches and only notes tags of GitHub’s own actions. Tools such as Dependabot update pinned SHAs for you.
Which time zone does a schedule use?
UTC, unless the schedule sets timezone: to an IANA name such as America/New_York. Times a daylight-saving change skips run at the next valid time. Scheduled runs can also be delayed when GitHub is busy, especially at the start of an hour.
Are my workflow files uploaded?
No. They are read and checked in your browser only.